├── .github ├── badges │ ├── coverage-summary.json │ ├── branches.svg │ └── jacoco.svg ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── maven.yml │ └── maven-settings.xml ├── bash.lnk ├── deploy ├── scripts │ ├── run_client.sh │ └── run_server.sh ├── common │ ├── 02_deploy_servers.sh │ ├── 03_deploy_clients.sh │ ├── core_functions.sh │ ├── 04_start_servers.sh │ ├── main.sh │ ├── core_util.sh │ └── 01_create_cluster.sh ├── azure-singleregion.sh ├── gce-singleregion-eu.sh ├── aws-multiregion-eu-us.sh ├── gce-singleregion-us.sh ├── gce-multiregion-us.sh ├── aws-multiregion-eu.sh ├── aws-singleregion-eu.sh ├── gce-multiregion-eu.sh └── README.md ├── bank-server └── src │ ├── main │ ├── resources │ │ ├── db │ │ │ └── common │ │ │ │ └── V1_2__load_metadata.sql │ │ ├── static │ │ │ ├── favicon.ico │ │ │ ├── images │ │ │ │ ├── flags │ │ │ │ │ ├── ARG.png │ │ │ │ │ ├── AUS.png │ │ │ │ │ ├── AUT.png │ │ │ │ │ ├── BEL.png │ │ │ │ │ ├── BGR.png │ │ │ │ │ ├── BRA.png │ │ │ │ │ ├── BRL.png │ │ │ │ │ ├── CAN.png │ │ │ │ │ ├── CHE.png │ │ │ │ │ ├── CHL.png │ │ │ │ │ ├── CHN.png │ │ │ │ │ ├── CZE.png │ │ │ │ │ ├── DEU.png │ │ │ │ │ ├── DNK.png │ │ │ │ │ ├── ESP.png │ │ │ │ │ ├── EST.png │ │ │ │ │ ├── FIN.png │ │ │ │ │ ├── FRA.png │ │ │ │ │ ├── GBR.png │ │ │ │ │ ├── GEN.png │ │ │ │ │ ├── GRC.png │ │ │ │ │ ├── HKG.png │ │ │ │ │ ├── HUN.png │ │ │ │ │ ├── IDN.png │ │ │ │ │ ├── IRL.png │ │ │ │ │ ├── ITA.png │ │ │ │ │ ├── JPN.png │ │ │ │ │ ├── KAZ.png │ │ │ │ │ ├── LTH.png │ │ │ │ │ ├── LVA.png │ │ │ │ │ ├── MEX.png │ │ │ │ │ ├── MLT.png │ │ │ │ │ ├── NLD.png │ │ │ │ │ ├── NOR.png │ │ │ │ │ ├── NZL.png │ │ │ │ │ ├── PHL.png │ │ │ │ │ ├── POL.png │ │ │ │ │ ├── PRT.png │ │ │ │ │ ├── ROU.png │ │ │ │ │ ├── RUS.png │ │ │ │ │ ├── SGP.png │ │ │ │ │ ├── SRB.png │ │ │ │ │ ├── SVK.png │ │ │ │ │ ├── SVN.png │ │ │ │ │ ├── SWE.png │ │ │ │ │ ├── USA.png │ │ │ │ │ └── ZCE.png │ │ │ │ ├── logo_mark_black.png │ │ │ │ ├── logo_mark_white.png │ │ │ │ └── logo_white-32px.png │ │ │ └── js │ │ │ │ └── color-modes.js │ │ ├── application-pgjdbc-dev.yml │ │ ├── application-crdb-dev.yml │ │ ├── application-crdb-local.yml │ │ ├── application-psql-dev.yml │ │ ├── application-psql-local.yml │ │ ├── banner.txt │ │ ├── application-demo.yml │ │ └── logback-spring.xml │ └── java │ │ └── io │ │ └── roach │ │ └── bank │ │ ├── AdvisorOrder.java │ │ ├── repository │ │ ├── ReportingRepository.java │ │ ├── MultiRegionRepository.java │ │ ├── jpa │ │ │ ├── TransactionJpaRepository.java │ │ │ ├── TransactionItemJpaRepository.java │ │ │ ├── JpaTransactionRepositoryAdapter.java │ │ │ └── AccountJpaRepository.java │ │ ├── RegionRepository.java │ │ ├── TransactionRepository.java │ │ └── AccountRepository.java │ │ ├── service │ │ ├── InfrastructureException.java │ │ ├── NoSuchTransactionException.java │ │ ├── BusinessException.java │ │ ├── NoSuchAccountException.java │ │ ├── BadRequestException.java │ │ ├── NegativeBalanceException.java │ │ ├── AccountClosedException.java │ │ ├── TransactionService.java │ │ └── AccountService.java │ │ ├── web │ │ ├── push │ │ │ ├── TopicNames.java │ │ │ ├── AccountPayload.java │ │ │ └── BalanceUpdateAspect.java │ │ ├── support │ │ │ ├── ConnectionPoolSizeFactory.java │ │ │ ├── FollowLocation.java │ │ │ ├── ConnectionPoolConfigFactory.java │ │ │ ├── ResponseHeaderFilter.java │ │ │ └── ZoomExpression.java │ │ ├── config │ │ │ ├── RegionResourceAssembler.java │ │ │ └── ConfigurationController.java │ │ ├── transaction │ │ │ ├── TransactionResourceAssembler.java │ │ │ ├── TransactionItemResourceAssembler.java │ │ │ └── TransactionItemController.java │ │ ├── ViewModel.java │ │ ├── account │ │ │ └── AccountResourceAssembler.java │ │ └── IndexController.java │ │ ├── util │ │ ├── AsciiArt.java │ │ ├── MetadataUtils.java │ │ └── TimeUtils.java │ │ ├── health │ │ └── DBHealthIndicator.java │ │ ├── config │ │ ├── TransactionConfig.java │ │ ├── MicrometerConfig.java │ │ ├── JpaTransactionManagerConfig.java │ │ ├── WebSocketConfig.java │ │ ├── CacheConfig.java │ │ ├── ThymeleafConfig.java │ │ ├── NoRetryConfig.java │ │ ├── DriverSideRetryConfig.java │ │ ├── SavepointRetryConfig.java │ │ ├── JdbcTransactionManagerConfig.java │ │ └── ClientSideRetryConfig.java │ │ ├── domain │ │ ├── AbstractEntity.java │ │ └── AccountTypeConverter.java │ │ ├── AccountPlan.java │ │ └── ProfileNames.java │ └── test │ ├── resources │ ├── application-integrationtest.yml │ ├── db │ │ ├── clear.sql │ │ ├── cdc.sql │ │ └── etc.sql │ └── logback-test.xml │ └── java │ └── io │ └── roach │ └── bank │ ├── service │ └── TransactionServiceFacade.java │ └── AdhocTest.java ├── docs ├── diagram_schema.png ├── diagram_frontend.png ├── deploy_multiregion.png ├── deploy_singleregion.png └── diagram_architecture.png ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── bank-api ├── src │ └── main │ │ └── java │ │ └── io │ │ └── roach │ │ └── bank │ │ └── api │ │ ├── SurvivalGoal.java │ │ ├── support │ │ ├── WeightedItem.java │ │ ├── MoneySerializer.java │ │ ├── MoneyDeserializer.java │ │ ├── LocalDateDeserializer.java │ │ ├── LocalDateTimeDeserializer.java │ │ ├── LocalDateSerializer.java │ │ ├── LocalDateTimeSerializer.java │ │ ├── EnumPattern.java │ │ ├── EnumPatternValidator.java │ │ └── CockroachFacts.java │ │ ├── AccountStatus.java │ │ ├── CurrencyMismatchException.java │ │ ├── MessageModel.java │ │ ├── AccountType.java │ │ ├── ReportUpdate.java │ │ ├── ConnectionPoolSize.java │ │ ├── TransactionItemModel.java │ │ ├── ConnectionPoolConfig.java │ │ ├── AccountBatchForm.java │ │ ├── AccountSummary.java │ │ ├── TransactionSummary.java │ │ ├── Region.java │ │ ├── AccountForm.java │ │ ├── TransactionModel.java │ │ └── LinkRelations.java └── pom.xml ├── run-client.sh ├── .gitignore ├── bank-client ├── src │ ├── main │ │ ├── java │ │ │ └── io │ │ │ │ └── roach │ │ │ │ └── bank │ │ │ │ └── client │ │ │ │ ├── event │ │ │ │ ├── ClearErrorsEvent.java │ │ │ │ ├── ExecutionErrorEvent.java │ │ │ │ └── ConnectionUpdatedEvent.java │ │ │ │ ├── config │ │ │ │ ├── AppConfig.java │ │ │ │ ├── ConcurrencyConfig.java │ │ │ │ └── HypermediaConfig.java │ │ │ │ ├── support │ │ │ │ ├── TableUtils.java │ │ │ │ ├── ThreadPoolStats.java │ │ │ │ ├── Console.java │ │ │ │ └── DurationFormat.java │ │ │ │ ├── AbstractCommand.java │ │ │ │ ├── Cancel.java │ │ │ │ ├── RegionProvider.java │ │ │ │ ├── Constants.java │ │ │ │ ├── ClientApplication.java │ │ │ │ ├── Balance.java │ │ │ │ └── Report.java │ │ └── resources │ │ │ ├── logback-spring.xml │ │ │ ├── banner.txt │ │ │ └── application.yml │ └── test │ │ └── java │ │ └── io │ │ └── roach │ │ └── bank │ │ └── client │ │ └── util │ │ └── DurationFormatTest.java ├── README.md └── pom.xml ├── LICENSE.txt ├── release.sh └── run-server.sh /.github/badges/coverage-summary.json: -------------------------------------------------------------------------------- 1 | {"branches": 0.0, "coverage": 0.0} -------------------------------------------------------------------------------- /bash.lnk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bash.lnk -------------------------------------------------------------------------------- /deploy/scripts/run_client.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | java -jar bank-client.jar -------------------------------------------------------------------------------- /bank-server/src/main/resources/db/common/V1_2__load_metadata.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- Bank metadata 3 | -- 4 | -------------------------------------------------------------------------------- /docs/diagram_schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/docs/diagram_schema.png -------------------------------------------------------------------------------- /docs/diagram_frontend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/docs/diagram_frontend.png -------------------------------------------------------------------------------- /docs/deploy_multiregion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/docs/deploy_multiregion.png -------------------------------------------------------------------------------- /docs/deploy_singleregion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/docs/deploy_singleregion.png -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /docs/diagram_architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/docs/diagram_architecture.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/favicon.ico -------------------------------------------------------------------------------- /bank-api/src/main/java/io/roach/bank/api/SurvivalGoal.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.api; 2 | 3 | public enum SurvivalGoal { 4 | ZONE, 5 | REGION 6 | } 7 | -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/ARG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/ARG.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/AUS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/AUS.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/AUT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/AUT.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/BEL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/BEL.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/BGR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/BGR.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/BRA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/BRA.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/BRL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/BRL.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/CAN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/CAN.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/CHE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/CHE.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/CHL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/CHL.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/CHN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/CHN.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/CZE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/CZE.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/DEU.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/DEU.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/DNK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/DNK.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/ESP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/ESP.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/EST.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/EST.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/FIN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/FIN.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/FRA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/FRA.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/GBR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/GBR.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/GEN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/GEN.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/GRC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/GRC.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/HKG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/HKG.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/HUN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/HUN.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/IDN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/IDN.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/IRL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/IRL.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/ITA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/ITA.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/JPN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/JPN.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/KAZ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/KAZ.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/LTH.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/LTH.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/LVA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/LVA.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/MEX.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/MEX.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/MLT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/MLT.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/NLD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/NLD.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/NOR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/NOR.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/NZL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/NZL.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/PHL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/PHL.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/POL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/POL.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/PRT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/PRT.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/ROU.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/ROU.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/RUS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/RUS.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/SGP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/SGP.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/SRB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/SRB.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/SVK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/SVK.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/SVN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/SVN.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/SWE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/SWE.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/USA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/USA.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/flags/ZCE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/flags/ZCE.png -------------------------------------------------------------------------------- /bank-api/src/main/java/io/roach/bank/api/support/WeightedItem.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.api.support; 2 | 3 | public interface WeightedItem { 4 | double getWeight(); 5 | } 6 | -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/logo_mark_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/logo_mark_black.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/logo_mark_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/logo_mark_white.png -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/images/logo_white-32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kai-niemi/roach-bank/HEAD/bank-server/src/main/resources/static/images/logo_white-32px.png -------------------------------------------------------------------------------- /run-client.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | FILE=bank-client/target/bank-client.jar 4 | if [ ! -f "$FILE" ]; then 5 | chmod +x mvnw 6 | ./mvnw clean install 7 | fi 8 | 9 | java -jar $FILE "$@" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .data/ 2 | .idea/ 3 | .log/ 4 | target/ 5 | *.iml 6 | *.ipr 7 | *.iws 8 | .DS_Store 9 | .DS_Store? 10 | Thumbs.db 11 | *.csv 12 | *.log 13 | *.tmp 14 | *.gz 15 | *.history 16 | -------------------------------------------------------------------------------- /bank-api/src/main/java/io/roach/bank/api/AccountStatus.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.api; 2 | 3 | /** 4 | * Enumeration of different account status codes. 5 | */ 6 | public enum AccountStatus { 7 | OPEN, 8 | CLOSED 9 | } 10 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /bank-client/src/main/java/io/roach/bank/client/event/ClearErrorsEvent.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.client.event; 2 | 3 | import org.springframework.context.ApplicationEvent; 4 | 5 | public class ClearErrorsEvent extends ApplicationEvent { 6 | public ClearErrorsEvent(Object source) { 7 | super(source); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/AdvisorOrder.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank; 2 | 3 | import org.springframework.core.Ordered; 4 | 5 | /** 6 | * Ordering constants for transaction advisors. 7 | */ 8 | public interface AdvisorOrder { 9 | int OUTBOX_ADVISOR = Ordered.LOWEST_PRECEDENCE - 1; 10 | 11 | int TRANSACTION_ADVISOR = Ordered.LOWEST_PRECEDENCE - 2; 12 | } 13 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/repository/ReportingRepository.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.repository; 2 | 3 | import io.roach.bank.api.AccountSummary; 4 | import io.roach.bank.api.TransactionSummary; 5 | 6 | public interface ReportingRepository { 7 | AccountSummary accountSummary(String city); 8 | 9 | TransactionSummary transactionSummary(String city); 10 | } 11 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/service/InfrastructureException.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.service; 2 | 3 | /** 4 | * Base type for unrecoverable infrastructure exceptions. 5 | */ 6 | public class InfrastructureException extends RuntimeException { 7 | public InfrastructureException(String message, Throwable cause) { 8 | super(message, cause); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /bank-server/src/test/resources/application-integrationtest.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | main: 3 | allow-bean-definition-overriding: true 4 | banner-mode: off 5 | datasource: 6 | url: jdbc:postgresql://localhost:26257/roach_bank?sslmode=disable 7 | # url: jdbc:postgresql://192.168.1.99:26257/roach_bank?sslmode=disable 8 | driver-class-name: org.postgresql.Driver 9 | username: root 10 | password: 11 | -------------------------------------------------------------------------------- /deploy/common/02_deploy_servers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z "${CLUSTER}" ]; then 4 | fn_echo_warning "No \$CLUSTER id variable set!" 5 | export CLUSTER="your-cluster-id" 6 | fi 7 | 8 | for c in "${clients[@]}" 9 | do 10 | fn_failcheck roachprod put ${CLUSTER}:${c} ../bank-server/target/bank-server.jar 11 | fn_failcheck roachprod put ${CLUSTER}:${c} scripts/run_server.sh 12 | 13 | i=($i+1) 14 | done 15 | -------------------------------------------------------------------------------- /deploy/common/03_deploy_clients.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z "${CLUSTER}" ]; then 4 | fn_echo_warning "No \$CLUSTER id variable set!" 5 | export CLUSTER="your-cluster-id" 6 | fi 7 | 8 | for c in "${clients[@]}" 9 | do 10 | fn_failcheck roachprod put ${CLUSTER}:${c} ../bank-client/target/bank-client.jar 11 | fn_failcheck roachprod put ${CLUSTER}:${c} scripts/run_client.sh 12 | 13 | i=($i+1) 14 | done 15 | -------------------------------------------------------------------------------- /bank-server/src/test/resources/db/clear.sql: -------------------------------------------------------------------------------- 1 | drop table if exists transaction_item cascade; 2 | drop table if exists transaction cascade; 3 | drop table if exists account cascade; 4 | drop table if exists region cascade; 5 | drop table if exists outbox cascade; 6 | 7 | TRUNCATE TABLE transaction_item CASCADE; 8 | TRUNCATE TABLE transaction CASCADE; 9 | TRUNCATE TABLE account CASCADE; 10 | TRUNCATE TABLE region CASCADE; 11 | 12 | -------------------------------------------------------------------------------- /bank-server/src/main/resources/application-pgjdbc-dev.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | jpa: 3 | properties: 4 | hibernate: 5 | dialect: org.hibernate.dialect.CockroachDialect 6 | flyway: 7 | enabled: true 8 | locations: classpath:db/crdb,classpath:db/common 9 | datasource: 10 | url: jdbc:postgresql://192.168.1.99:26257/roach_bank?sslmode=disable 11 | driver-class-name: org.postgresql.Driver 12 | username: root 13 | -------------------------------------------------------------------------------- /bank-api/src/main/java/io/roach/bank/api/CurrencyMismatchException.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.api; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(value = HttpStatus.BAD_REQUEST) 7 | public class CurrencyMismatchException extends IllegalArgumentException { 8 | public CurrencyMismatchException(String s) { 9 | super(s); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /deploy/scripts/run_server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | db_url="jdbc:postgresql://localhost:26257/roach_bank?sslmode=disable" 4 | spring_profile="retry-client,pgjdbc-local" 5 | 6 | nohup java -jar bank-server.jar \ 7 | --spring.datasource.url="${db_url}" \ 8 | --spring.datasource.username=root \ 9 | --spring.datasource.password= \ 10 | --spring.profiles.active="${spring_profile}" \ 11 | --roachbank.default-account-limit=10 \ 12 | > /dev/null 2>&1 & 13 | -------------------------------------------------------------------------------- /bank-server/src/main/resources/application-crdb-dev.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | jpa: 3 | properties: 4 | hibernate: 5 | dialect: org.hibernate.dialect.CockroachDialect 6 | flyway: 7 | enabled: true 8 | locations: classpath:db/crdb,classpath:db/common 9 | datasource: 10 | url: jdbc:cockroachdb://192.168.1.99:26257/roach_bank?sslmode=disable 11 | driver-class-name: io.cockroachdb.jdbc.CockroachDriver 12 | username: root 13 | -------------------------------------------------------------------------------- /bank-server/src/main/resources/application-crdb-local.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | jpa: 3 | properties: 4 | hibernate: 5 | dialect: org.hibernate.dialect.CockroachDialect 6 | flyway: 7 | enabled: true 8 | locations: classpath:db/crdb,classpath:db/common 9 | datasource: 10 | url: jdbc:cockroachdb://localhost:26257/roach_bank?sslmode=disable 11 | driver-class-name: io.cockroachdb.jdbc.CockroachDriver 12 | username: root 13 | password: 14 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/service/NoSuchTransactionException.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.service; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "No such transaction") 7 | public class NoSuchTransactionException extends BusinessException { 8 | public NoSuchTransactionException(String id) { 9 | super("No such transaction: " + id); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /bank-server/src/main/resources/application-psql-dev.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | jpa: 3 | properties: 4 | hibernate: 5 | dialect: org.hibernate.dialect.PostgreSQLDialect 6 | flyway: 7 | enabled: true 8 | locations: classpath:db/psql,classpath:db/common 9 | datasource: 10 | url: jdbc:postgresql://192.168.1.99:5432/roach_bank 11 | driver-class-name: org.postgresql.Driver 12 | username: postgres 13 | password: root 14 | hikari: 15 | transaction-isolation: TRANSACTION_READ_COMMITTED -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/service/BusinessException.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.service; 2 | 3 | /** 4 | * Base type for unrecoverable business exceptions. 5 | */ 6 | public abstract class BusinessException extends RuntimeException { 7 | protected BusinessException() { 8 | } 9 | 10 | protected BusinessException(String message) { 11 | super(message); 12 | } 13 | 14 | public BusinessException(String message, Throwable cause) { 15 | super(message, cause); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /bank-server/src/main/resources/application-psql-local.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | jpa: 3 | properties: 4 | hibernate: 5 | dialect: org.hibernate.dialect.PostgreSQLDialect 6 | flyway: 7 | enabled: true 8 | locations: classpath:db/psql,classpath:db/common 9 | datasource: 10 | url: jdbc:postgresql://localhost:5432/roach_bank 11 | driver-class-name: org.postgresql.Driver 12 | username: postgres 13 | password: root 14 | hikari: 15 | transaction-isolation: TRANSACTION_READ_COMMITTED 16 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/service/NoSuchAccountException.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.service; 2 | 3 | import java.util.UUID; 4 | 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.web.bind.annotation.ResponseStatus; 7 | 8 | @ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "No such account") 9 | public class NoSuchAccountException extends BusinessException { 10 | public NoSuchAccountException(UUID id) { 11 | super("No such account with id: " + id); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /bank-client/src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /bank-client/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ${AnsiColor.BRIGHT_YELLOW} C O C K R O A C H D B 2 | ──▄──▄────▄▀ ___ __ ___ __ 3 | ───▀▄─█─▄▀▄▄▄ / _ \___ ___ _____/ / / _ )___ ____ / /__ 4 | ▄██▄████▄██▄▀█▄ / , _/ _ \/ _ `/ __/ _ \ / _ / _ `/ _ \/ '_/ 5 | ─▀▀─█▀█▀▄▀███▀ /_/|_|\___/\_,_/\__/_//_/ /____/\_,_/_//_/_/\_\ 6 | ──▄▄▀─█──▀▄▄ 7 | ${AnsiColor.BRIGHT_CYAN}${application.title}${application.formatted-version} powered by Spring Boot${spring-boot.formatted-version} 8 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/web/push/TopicNames.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.web.push; 2 | 3 | public abstract class TopicNames { 4 | private TopicNames() { 5 | } 6 | 7 | public static final String TOPIC_ACCOUNT_SUMMARY = "/topic/account-summary"; 8 | 9 | public static final String TOPIC_TRANSACTION_SUMMARY = "/topic/transaction-summary"; 10 | 11 | public static final String TOPIC_REPORT_UPDATE = "/topic/report-update"; 12 | 13 | public static final String TOPIC_ACCOUNT_UPDATE = "/topic/account-update"; 14 | } 15 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/util/AsciiArt.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.util; 2 | 3 | public abstract class AsciiArt { 4 | private AsciiArt() { 5 | } 6 | 7 | public static String happy() { 8 | return "(ʘ‿ʘ)"; 9 | } 10 | 11 | public static String shrug() { 12 | return "¯\\_(ツ)_/¯"; 13 | } 14 | 15 | public static String flipTableGently() { 16 | return "(╯°□°)╯︵ ┻━┻"; 17 | } 18 | 19 | public static String flipTableRoughly() { 20 | return "(ノಠ益ಠ)ノ彡┻━┻"; 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /bank-api/src/main/java/io/roach/bank/api/support/MoneySerializer.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.api.support; 2 | 3 | import java.io.IOException; 4 | 5 | import com.fasterxml.jackson.core.JsonGenerator; 6 | import com.fasterxml.jackson.databind.JsonSerializer; 7 | import com.fasterxml.jackson.databind.SerializerProvider; 8 | 9 | public class MoneySerializer extends JsonSerializer { 10 | @Override 11 | public void serialize(Money value, JsonGenerator gen, SerializerProvider sp) throws IOException { 12 | gen.writeString(value.toString()); 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /bank-api/src/main/java/io/roach/bank/api/support/MoneyDeserializer.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.api.support; 2 | 3 | import java.io.IOException; 4 | 5 | import com.fasterxml.jackson.core.JsonParser; 6 | import com.fasterxml.jackson.databind.DeserializationContext; 7 | import com.fasterxml.jackson.databind.JsonDeserializer; 8 | 9 | public class MoneyDeserializer extends JsonDeserializer { 10 | @Override 11 | public Money deserialize(JsonParser jp, DeserializationContext ctxt) 12 | throws IOException { 13 | return Money.parse(jp.readValueAs(String.class)); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /bank-client/src/main/java/io/roach/bank/client/config/AppConfig.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.client.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | import io.roach.bank.client.RegionProvider; 7 | import io.roach.bank.client.support.CallMetrics; 8 | 9 | @Configuration 10 | public class AppConfig { 11 | @Bean 12 | public RegionProvider regionProvider() { 13 | return new RegionProvider(); 14 | } 15 | 16 | @Bean 17 | public CallMetrics callMetrics() { 18 | return new CallMetrics(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /deploy/common/core_functions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | core_util.sh(){ 4 | source "${functionsdir}/core_util.sh" 5 | } 6 | 7 | 01_create_cluster.sh(){ 8 | source "${functionsdir}/01_create_cluster.sh" 9 | } 10 | 11 | 02_deploy_servers.sh(){ 12 | source "${functionsdir}/02_deploy_servers.sh" 13 | } 14 | 15 | 03_deploy_clients.sh(){ 16 | source "${functionsdir}/03_deploy_clients.sh" 17 | } 18 | 19 | 04_start_servers.sh(){ 20 | source "${functionsdir}/04_start_servers.sh" 21 | } 22 | 23 | 05_partition.sh(){ 24 | source "${functionsdir}/05_partition.sh" 25 | } 26 | 27 | main.sh(){ 28 | source "${functionsdir}/main.sh" 29 | } 30 | 31 | -------------------------------------------------------------------------------- /bank-api/src/main/java/io/roach/bank/api/support/LocalDateDeserializer.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.api.support; 2 | 3 | import java.io.IOException; 4 | import java.time.LocalDate; 5 | 6 | import com.fasterxml.jackson.core.JsonParser; 7 | import com.fasterxml.jackson.databind.DeserializationContext; 8 | import com.fasterxml.jackson.databind.JsonDeserializer; 9 | 10 | public class LocalDateDeserializer extends JsonDeserializer { 11 | @Override 12 | public LocalDate deserialize(JsonParser jp, DeserializationContext ctxt) 13 | throws IOException { 14 | return LocalDate.parse(jp.readValueAs(String.class)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /bank-server/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ${AnsiColor.BRIGHT_YELLOW} C O C K R O A C H D B 2 | ──▄──▄────▄▀ ___ __ ___ __ 3 | ───▀▄─█─▄▀▄▄▄ / _ \___ ___ _____/ / / _ )___ ____ / /__ 4 | ▄██▄████▄██▄▀█▄ / , _/ _ \/ _ `/ __/ _ \ / _ / _ `/ _ \/ '_/ 5 | ─▀▀─█▀█▀▄▀███▀ /_/|_|\___/\_,_/\__/_//_/ /____/\_,_/_//_/_/\_\ 6 | ──▄▄▀─█──▀▄▄ 7 | ${AnsiColor.BRIGHT_GREEN}${application.title}${application.formatted-version} powered by Spring Boot${spring-boot.formatted-version} 8 | Active profiles: ${spring.profiles.active} 9 | Datasource URL: ${spring.datasource.url}${AnsiColor.DEFAULT} 10 | -------------------------------------------------------------------------------- /bank-api/src/main/java/io/roach/bank/api/support/LocalDateTimeDeserializer.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.api.support; 2 | 3 | import java.io.IOException; 4 | import java.time.LocalDateTime; 5 | 6 | import com.fasterxml.jackson.core.JsonParser; 7 | import com.fasterxml.jackson.databind.DeserializationContext; 8 | import com.fasterxml.jackson.databind.JsonDeserializer; 9 | 10 | public class LocalDateTimeDeserializer extends JsonDeserializer { 11 | @Override 12 | public LocalDateTime deserialize(JsonParser jp, DeserializationContext ctxt) 13 | throws IOException { 14 | return LocalDateTime.parse(jp.readValueAs(String.class)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /bank-client/src/main/java/io/roach/bank/client/event/ExecutionErrorEvent.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.client.event; 2 | 3 | import org.springframework.context.ApplicationEvent; 4 | 5 | public class ExecutionErrorEvent extends ApplicationEvent { 6 | private final String message; 7 | 8 | private final Throwable cause; 9 | 10 | public ExecutionErrorEvent(Object source, String message, Throwable cause) { 11 | super(source); 12 | this.message = message; 13 | this.cause = cause; 14 | } 15 | 16 | public String getMessage() { 17 | return message; 18 | } 19 | 20 | public Throwable getCause() { 21 | return cause; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/service/BadRequestException.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.service; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | /** 7 | * Exception thrown when a monetary transaction request 8 | * is illegal, i.e unbalanced or mixes currencies. 9 | */ 10 | @ResponseStatus(value = HttpStatus.BAD_REQUEST) 11 | public class BadRequestException extends BusinessException { 12 | public BadRequestException(String message) { 13 | super(message); 14 | } 15 | 16 | public BadRequestException(String message, Throwable cause) { 17 | super(message, cause); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /bank-api/src/main/java/io/roach/bank/api/support/LocalDateSerializer.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.api.support; 2 | 3 | import java.io.IOException; 4 | import java.time.LocalDate; 5 | import java.time.format.DateTimeFormatter; 6 | 7 | import com.fasterxml.jackson.core.JsonGenerator; 8 | import com.fasterxml.jackson.databind.JsonSerializer; 9 | import com.fasterxml.jackson.databind.SerializerProvider; 10 | 11 | public class LocalDateSerializer extends JsonSerializer { 12 | @Override 13 | public void serialize(LocalDate value, JsonGenerator gen, SerializerProvider sp) throws IOException { 14 | gen.writeString(value.format(DateTimeFormatter.ISO_LOCAL_DATE)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/service/NegativeBalanceException.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.service; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | /** 7 | * Business exception thrown if an account has insufficient funds. 8 | */ 9 | @ResponseStatus(value = HttpStatus.CONFLICT, reason = "Negative balance violation") 10 | public class NegativeBalanceException extends BadRequestException { 11 | public NegativeBalanceException(String message) { 12 | super(message); 13 | } 14 | 15 | public NegativeBalanceException(String message, Throwable cause) { 16 | super(message, cause); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /bank-client/src/main/java/io/roach/bank/client/support/TableUtils.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.client.support; 2 | 3 | import org.springframework.shell.table.BorderStyle; 4 | import org.springframework.shell.table.TableBuilder; 5 | import org.springframework.shell.table.TableModel; 6 | 7 | public abstract class TableUtils { 8 | private TableUtils() { 9 | } 10 | 11 | public static String prettyPrint(TableModel model) { 12 | TableBuilder tableBuilder = new TableBuilder(model); 13 | tableBuilder.addInnerBorder(BorderStyle.fancy_light); 14 | tableBuilder.addHeaderBorder(BorderStyle.fancy_double); 15 | return tableBuilder 16 | .build().render(120); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /bank-api/src/main/java/io/roach/bank/api/MessageModel.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.api; 2 | 3 | import org.springframework.hateoas.RepresentationModel; 4 | 5 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 6 | 7 | @JsonPropertyOrder({"links"}) 8 | public class MessageModel extends RepresentationModel { 9 | private String message; 10 | 11 | public MessageModel() { 12 | } 13 | 14 | public MessageModel(String message) { 15 | this.message = message; 16 | } 17 | 18 | public String getMessage() { 19 | return message; 20 | } 21 | 22 | public MessageModel setMessage(String message) { 23 | this.message = message; 24 | return this; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /bank-api/src/main/java/io/roach/bank/api/support/LocalDateTimeSerializer.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.api.support; 2 | 3 | import java.io.IOException; 4 | import java.time.LocalDateTime; 5 | import java.time.format.DateTimeFormatter; 6 | 7 | import com.fasterxml.jackson.core.JsonGenerator; 8 | import com.fasterxml.jackson.databind.JsonSerializer; 9 | import com.fasterxml.jackson.databind.SerializerProvider; 10 | 11 | public class LocalDateTimeSerializer extends JsonSerializer { 12 | @Override 13 | public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider sp) throws IOException { 14 | gen.writeString(value.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); 15 | } 16 | } 17 | 18 | 19 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/health/DBHealthIndicator.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.health; 2 | 3 | import javax.sql.DataSource; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.boot.actuate.jdbc.DataSourceHealthIndicator; 8 | import org.springframework.stereotype.Component; 9 | 10 | @Component 11 | public class DBHealthIndicator extends DataSourceHealthIndicator { 12 | public DBHealthIndicator(@Autowired DataSource dataSource, 13 | @Value("${spring.datasource.hikari.connection-init-sql}") String validationQuery) { 14 | super(dataSource, validationQuery); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/config/TransactionConfig.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.config; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.transaction.PlatformTransactionManager; 7 | import org.springframework.transaction.support.TransactionTemplate; 8 | 9 | @Configuration 10 | public class TransactionConfig { 11 | @Autowired 12 | private PlatformTransactionManager transactionManager; 13 | 14 | @Bean 15 | public TransactionTemplate transactionTemplate() { 16 | return new TransactionTemplate(transactionManager); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /bank-client/src/main/java/io/roach/bank/client/AbstractCommand.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.client; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.shell.Availability; 7 | 8 | import io.roach.bank.client.support.Console; 9 | 10 | public abstract class AbstractCommand { 11 | protected final Logger logger = LoggerFactory.getLogger(getClass()); 12 | 13 | @Autowired 14 | protected Console console; 15 | 16 | public Availability connectedCheck() { 17 | return Connect.isConnected() 18 | ? Availability.available() 19 | : Availability.unavailable("You are not connected"); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/repository/MultiRegionRepository.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.repository; 2 | 3 | import java.util.List; 4 | 5 | import io.roach.bank.api.Region; 6 | import io.roach.bank.api.SurvivalGoal; 7 | 8 | public interface MultiRegionRepository { 9 | void addDatabaseRegions(List regions); 10 | 11 | void dropDatabaseRegions(List regions); 12 | 13 | void setPrimaryRegion(Region region); 14 | 15 | void setSecondaryRegion(Region region); 16 | 17 | void dropSecondaryRegion(); 18 | 19 | void setSurvivalGoal(SurvivalGoal survivalGoal); 20 | 21 | void setGlobalTable(String table); 22 | 23 | void setRegionalByRowTable(String table); 24 | 25 | void setRegionalByTable(String table); 26 | } 27 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/config/MicrometerConfig.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.config; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | import io.micrometer.core.instrument.MeterRegistry; 9 | 10 | @Configuration 11 | public class MicrometerConfig { 12 | @Bean 13 | public MeterRegistryCustomizer metricsCommonTags( 14 | @Value("${spring.application.name}") String applicationName) { 15 | return registry -> registry.config() 16 | .commonTags("application", applicationName); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/repository/jpa/TransactionJpaRepository.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.repository.jpa; 2 | 3 | import java.util.Optional; 4 | import java.util.UUID; 5 | 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor; 8 | import org.springframework.transaction.annotation.Propagation; 9 | import org.springframework.transaction.annotation.Transactional; 10 | 11 | import io.roach.bank.domain.Transaction; 12 | 13 | @Transactional(propagation = Propagation.MANDATORY) 14 | public interface TransactionJpaRepository extends JpaRepository, 15 | JpaSpecificationExecutor { 16 | Optional findByIdAndCity(UUID id, String city); 17 | } 18 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/service/AccountClosedException.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.service; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | /** 7 | * Exception thrown when a closed account is referenced in a monetary transaction request. 8 | */ 9 | @ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "Account closed") 10 | public class AccountClosedException extends BusinessException { 11 | private String accountName; 12 | 13 | public AccountClosedException(String accountName) { 14 | super("Account is closed '" + accountName + "'"); 15 | this.accountName = accountName; 16 | } 17 | 18 | public String getAccountName() { 19 | return accountName; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/util/MetadataUtils.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.util; 2 | 3 | import org.springframework.dao.DataAccessException; 4 | import org.springframework.jdbc.core.JdbcTemplate; 5 | 6 | import javax.sql.DataSource; 7 | 8 | public abstract class MetadataUtils { 9 | private MetadataUtils() { 10 | } 11 | 12 | public static boolean isCockroachDB(DataSource dataSource) { 13 | return databaseVersion(dataSource).contains("CockroachDB"); 14 | } 15 | 16 | public static String databaseVersion(DataSource dataSource) { 17 | try { 18 | return new JdbcTemplate(dataSource).queryForObject("select version()", String.class); 19 | } catch (DataAccessException e) { 20 | return "unknown"; 21 | } 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/web/support/ConnectionPoolSizeFactory.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.web.support; 2 | 3 | import com.zaxxer.hikari.HikariPoolMXBean; 4 | 5 | import io.roach.bank.api.ConnectionPoolSize; 6 | 7 | public abstract class ConnectionPoolSizeFactory { 8 | private ConnectionPoolSizeFactory() { 9 | } 10 | 11 | public static ConnectionPoolSize from(HikariPoolMXBean bean) { 12 | ConnectionPoolSize instance = new ConnectionPoolSize(); 13 | instance.activeConnections = bean.getActiveConnections(); 14 | instance.idleConnections = bean.getIdleConnections(); 15 | instance.threadsAwaitingConnection = bean.getThreadsAwaitingConnection(); 16 | instance.totalConnections = bean.getTotalConnections(); 17 | return instance; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /bank-client/src/main/java/io/roach/bank/client/event/ConnectionUpdatedEvent.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.client.event; 2 | 3 | import java.net.URI; 4 | 5 | import org.springframework.context.ApplicationEvent; 6 | import org.springframework.http.HttpStatusCode; 7 | 8 | public class ConnectionUpdatedEvent extends ApplicationEvent { 9 | private final URI baseUri; 10 | 11 | private final HttpStatusCode httpStatus; 12 | 13 | public ConnectionUpdatedEvent(Object source, URI baseUri, HttpStatusCode httpStatus) { 14 | super(source); 15 | this.baseUri = baseUri; 16 | this.httpStatus = httpStatus; 17 | } 18 | 19 | public URI getBaseUri() { 20 | return baseUri; 21 | } 22 | 23 | public HttpStatusCode getHttpStatus() { 24 | return httpStatus; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/service/TransactionService.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.service; 2 | 3 | import java.util.UUID; 4 | 5 | import org.springframework.data.domain.Page; 6 | import org.springframework.data.domain.Pageable; 7 | 8 | import io.roach.bank.api.TransactionForm; 9 | import io.roach.bank.domain.Transaction; 10 | import io.roach.bank.domain.TransactionItem; 11 | 12 | public interface TransactionService { 13 | Transaction createTransaction(UUID id, TransactionForm transactionForm); 14 | 15 | Transaction findById(UUID id); 16 | 17 | TransactionItem findItemById(UUID transactionId, UUID accountId); 18 | 19 | Page find(Pageable page); 20 | 21 | Page findItemsByTransactionId(UUID transactionId, Pageable page); 22 | 23 | void deleteAll(); 24 | } 25 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/web/support/FollowLocation.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.web.support; 2 | 3 | import org.springframework.web.bind.ServletRequestUtils; 4 | import org.springframework.web.context.request.RequestContextHolder; 5 | import org.springframework.web.context.request.ServletRequestAttributes; 6 | 7 | import jakarta.servlet.http.HttpServletRequest; 8 | 9 | public abstract class FollowLocation { 10 | private FollowLocation() { 11 | } 12 | 13 | public static boolean ofCurrentRequest() { 14 | HttpServletRequest currentRequest = 15 | ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()) 16 | .getRequest(); 17 | return ServletRequestUtils.getBooleanParameter(currentRequest, "followLocation", false); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /bank-api/src/main/java/io/roach/bank/api/AccountType.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.api; 2 | 3 | import java.util.EnumSet; 4 | 5 | public enum AccountType { 6 | EXPENSE("Expense"), 7 | ASSET("Asset"), 8 | REVENUE("Revenue"), 9 | LIABILITY("Liability"), 10 | EQUITY("Equity"); 11 | 12 | private String code; 13 | 14 | AccountType(String code) { 15 | this.code = code; 16 | } 17 | 18 | public static AccountType of(String code) { 19 | for (AccountType accountType : EnumSet.allOf(AccountType.class)) { 20 | if (accountType.code.equals(code)) { 21 | return accountType; 22 | } 23 | } 24 | throw new IllegalArgumentException("No such type: " + code); 25 | } 26 | 27 | public String getCode() { 28 | return code; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/repository/RegionRepository.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.repository; 2 | 3 | import io.roach.bank.api.Region; 4 | 5 | import java.util.Collection; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.Optional; 9 | import java.util.Set; 10 | 11 | public interface RegionRepository { 12 | List listRegions(Collection regions); 13 | 14 | Set listCities(Collection regions); 15 | 16 | Region getRegionByName(String region); 17 | 18 | String getGatewayRegion(); 19 | 20 | Optional getPrimaryRegion(); 21 | 22 | Optional getSecondaryRegion(); 23 | 24 | Boolean hasExistingAccountPlan(); 25 | 26 | void createRegion(Region region); 27 | 28 | void createRegionMappings(Map mappings); 29 | } 30 | -------------------------------------------------------------------------------- /bank-client/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | output: 3 | ansi: 4 | enabled: always 5 | shell: 6 | interactive: 7 | enabled: true 8 | version: 9 | enabled: true 10 | showBuildName: true 11 | showGitBranch: true 12 | history: 13 | name: client.history 14 | logging: 15 | pattern: 16 | console: "%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:%5p}) [%t] %clr([%logger{39}]){cyan} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}" 17 | ############################# 18 | # Bank client config 19 | ############################# 20 | roachbank: 21 | default-url: http://localhost:8090/api 22 | http: 23 | # Total concurrent http connections 24 | maxTotal: -1 25 | # Total concurrent http connections per route (endpoint) 26 | maxConnPerRoute: -1 27 | -------------------------------------------------------------------------------- /bank-api/src/main/java/io/roach/bank/api/support/EnumPattern.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.api.support; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.Target; 6 | 7 | import jakarta.validation.Constraint; 8 | import jakarta.validation.Payload; 9 | 10 | import static java.lang.annotation.ElementType.*; 11 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 12 | 13 | @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) 14 | @Retention(RUNTIME) 15 | @Documented 16 | @Constraint(validatedBy = EnumPatternValidator.class) 17 | public @interface EnumPattern { 18 | String regexp(); 19 | 20 | String message() default "must match \"{regexp}\""; 21 | 22 | Class[] groups() default {}; 23 | 24 | Class[] payload() default {}; 25 | } -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/domain/AbstractEntity.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.domain; 2 | 3 | import java.io.Serializable; 4 | 5 | import org.springframework.data.domain.Persistable; 6 | 7 | import com.fasterxml.jackson.annotation.JsonIgnore; 8 | 9 | import jakarta.persistence.PostLoad; 10 | import jakarta.persistence.PrePersist; 11 | import jakarta.persistence.Transient; 12 | 13 | public abstract class AbstractEntity implements Persistable { 14 | @Transient 15 | private boolean isNew = true; 16 | 17 | @Override 18 | @JsonIgnore 19 | public boolean isNew() { 20 | return isNew; 21 | } 22 | 23 | @PrePersist 24 | @PostLoad 25 | void markNotNew() { 26 | this.isNew = false; 27 | onCreate(); 28 | } 29 | 30 | protected void onCreate() { 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/repository/TransactionRepository.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.repository; 2 | 3 | import java.util.UUID; 4 | 5 | import org.springframework.data.domain.Page; 6 | import org.springframework.data.domain.Pageable; 7 | 8 | import io.roach.bank.domain.Transaction; 9 | import io.roach.bank.domain.TransactionItem; 10 | 11 | public interface TransactionRepository { 12 | Transaction createTransaction(Transaction transaction); 13 | 14 | Transaction findTransactionById(UUID id); 15 | 16 | Transaction findTransactionById(UUID id, String city); 17 | 18 | TransactionItem findTransactionItemById(TransactionItem.Id id); 19 | 20 | Page findTransactions(Pageable pageable); 21 | 22 | Page findTransactionItems(UUID transactionId, Pageable pageable); 23 | 24 | void deleteAll(); 25 | } 26 | -------------------------------------------------------------------------------- /bank-server/src/main/resources/application-demo.yml: -------------------------------------------------------------------------------- 1 | ############################# 2 | # Bank server config 3 | ############################# 4 | bank: 5 | name: "Demo" 6 | default-account-limit: 10 7 | report-query-timeout: 60 8 | select-for-update: true 9 | clear-at-startup: false 10 | account-plan: 11 | accounts-per-city: 5000 12 | initial-balance: "10000.00" 13 | currency: USD 14 | regions: 15 | - name: eu-central-1 16 | primary: true 17 | cities: "london,amsterdam,rotterdam,berlin,hamburg,frankfurt" 18 | - name: eu-north-1 19 | cities: "stockholm,copenhagen,helsinki,oslo,riga,tallinn" 20 | - name: us-east-1 21 | cities: "new york,boston,washington dc,miami,charlotte,atlanta" 22 | region-mapping: 23 | aws-eu-central-1: eu-central-1 24 | aws-eu-north-1: eu-north-1 25 | aws-us-east-1: us-east-1 26 | -------------------------------------------------------------------------------- /bank-api/src/main/java/io/roach/bank/api/ReportUpdate.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.api; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | public class ReportUpdate { 6 | private LocalDateTime lastUpdatedAt; 7 | 8 | private int numCities; 9 | 10 | private String message; 11 | 12 | public LocalDateTime getLastUpdatedAt() { 13 | return lastUpdatedAt; 14 | } 15 | 16 | public void setLastUpdatedAt(LocalDateTime lastUpdatedAt) { 17 | this.lastUpdatedAt = lastUpdatedAt; 18 | } 19 | 20 | public int getNumCities() { 21 | return numCities; 22 | } 23 | 24 | public void setNumCities(int numCities) { 25 | this.numCities = numCities; 26 | } 27 | 28 | public String getMessage() { 29 | return message; 30 | } 31 | 32 | public void setMessage(String message) { 33 | this.message = message; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /bank-client/src/main/java/io/roach/bank/client/Cancel.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.client; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.shell.standard.ShellCommandGroup; 5 | import org.springframework.shell.standard.ShellComponent; 6 | import org.springframework.shell.standard.ShellMethod; 7 | 8 | import io.roach.bank.client.support.AsyncHelper; 9 | import io.roach.bank.client.support.CallMetrics; 10 | 11 | @ShellComponent 12 | @ShellCommandGroup(Constants.WORKLOAD_COMMANDS) 13 | public class Cancel extends AbstractCommand { 14 | @Autowired 15 | private AsyncHelper asyncHelper; 16 | 17 | @Autowired 18 | private CallMetrics callMetrics; 19 | 20 | @ShellMethod(value = "Cancel all workers", key = {"cancel", "x"}) 21 | public void cancel() { 22 | callMetrics.clear(); 23 | asyncHelper.cancelFutures(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /bank-server/src/test/resources/db/cdc.sql: -------------------------------------------------------------------------------- 1 | SET CLUSTER SETTING kv.rangefeed.enabled = true; 2 | SET CLUSTER SETTING kv.range_merge.queue_enabled = false; 3 | 4 | INSERT INTO account (id,city,balance,currency,name,type,closed,allow_negative,updated_at) VALUES 5 | (gen_random_uuid(), 'stockholm', '100.00', 'SEK', 'test', 'A', false, 0, clock_timestamp()); 6 | -- delete from account where id='18955dc6-400d-4bb9-96c0-125bbe95e4ab'; 7 | 8 | CANCEL JOBS (SELECT job_id FROM [SHOW JOBS] where job_type='CHANGEFEED'); 9 | 10 | INSERT INTO outbox (id, aggregate_type, aggregate_id, event_type, payload) 11 | VALUES (gen_random_uuid(), 'transaction', gen_random_uuid()::string, 'TransactionCreatedEvent', '[ 12 | { 13 | "abc": false 14 | }, 15 | { 16 | "def": true 17 | } 18 | ]'); 19 | 20 | ALTER TABLE outbox SET (ttl_expire_after = '1 hour'); 21 | 22 | SELECT * FROM [SHOW JOBS] WHERE job_type = 'ROW LEVEL TTL'; -------------------------------------------------------------------------------- /bank-server/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /deploy/common/04_start_servers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | fn_start_server(){ 4 | local c=$1 5 | 6 | fn_echo_info_nl "Starting bank server $c.." 7 | 8 | fn_failcheck roachprod run $CLUSTER:$c 'chmod +x *.sh' 9 | fn_failcheck roachprod run $CLUSTER:$c './run_server.sh > /dev/null 2>&1 &' 10 | 11 | local ip 12 | ip=$(roachprod ip $CLUSTER:$c --external) 13 | 14 | local url 15 | url="http://$ip:8090" 16 | 17 | fn_echo_info_nl "Waiting for server to start: $url" 18 | 19 | until curl --output /dev/null --silent --head --fail "$url"; do 20 | printf '.' 21 | sleep 5 22 | done 23 | 24 | fn_open_url "$url" 25 | } 26 | 27 | ############################################################# 28 | 29 | if [ -z "${CLUSTER}" ]; then 30 | fn_echo_warning "No \$CLUSTER id variable set!" 31 | export CLUSTER="your-cluster-id" 32 | fi 33 | 34 | i=0; 35 | for c in "${clients[@]}" 36 | do 37 | fn_start_server $c 38 | i=($i+1) 39 | done 40 | 41 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Java CI 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Set up JDK 17 20 | uses: actions/setup-java@v3 21 | with: 22 | java-version: '17' 23 | distribution: 'temurin' 24 | cache: maven 25 | 26 | - name: Build with Maven 27 | run: mvn -s $GITHUB_WORKSPACE/.github/workflows/maven-settings.xml -B package --file pom.xml 28 | env: 29 | USER_NAME: ${{ secrets.USER_NAME }} 30 | ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} 31 | 32 | -------------------------------------------------------------------------------- /deploy/azure-singleregion.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Configuration 4 | ######################## 5 | 6 | title="CockroachDB single region deployment (AZ)" 7 | # CRDB release version 8 | releaseversion="v23.2.2" 9 | # Number of node instances in total including clients 10 | nodes="7" 11 | # Nodes hosting CRDB 12 | crdbnodes="1-6" 13 | # Array of client nodes (must match size of regions) 14 | clients=(7) 15 | # Array of regions localities (must match zone names) 16 | regions=('westeurope' 'westeurope' 'westeurope') 17 | # AWS/GCE/AZ cloud (aws|gce) 18 | cloud="azure" 19 | # AWS/GCE/AZ region zones (must align with nodes size) 20 | zones="\ 21 | westeurope,\ 22 | westeurope,\ 23 | westeurope,\ 24 | westeurope,\ 25 | westeurope,\ 26 | westeurope,\ 27 | westeurope" 28 | # Machine type 29 | machinetypes="Standard_D8_v4" 30 | 31 | # DO NOT EDIT BELOW THIS LINE 32 | ############################# 33 | 34 | functionsdir="./common" 35 | 36 | source "${functionsdir}/core_functions.sh" 37 | 38 | main.sh 39 | 40 | exit 0 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/domain/AccountTypeConverter.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.domain; 2 | 3 | import java.util.stream.Stream; 4 | 5 | import io.roach.bank.api.AccountType; 6 | import jakarta.persistence.AttributeConverter; 7 | import jakarta.persistence.Converter; 8 | 9 | @Converter(autoApply = true) 10 | public class AccountTypeConverter implements AttributeConverter { 11 | @Override 12 | public String convertToDatabaseColumn(AccountType accountType) { 13 | if (accountType == null) { 14 | return null; 15 | } 16 | return accountType.getCode(); 17 | } 18 | 19 | @Override 20 | public AccountType convertToEntityAttribute(String code) { 21 | if (code == null) { 22 | return null; 23 | } 24 | return Stream.of(AccountType.values()) 25 | .filter(c -> c.getCode().equals(code)) 26 | .findFirst() 27 | .orElseThrow(IllegalArgumentException::new); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/service/AccountService.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.service; 2 | 3 | import io.roach.bank.api.support.Money; 4 | import io.roach.bank.domain.Account; 5 | import org.springframework.data.domain.Page; 6 | import org.springframework.data.domain.Pageable; 7 | 8 | import java.util.Collection; 9 | import java.util.List; 10 | import java.util.UUID; 11 | import java.util.function.Supplier; 12 | 13 | public interface AccountService { 14 | Account createAccount(Account account); 15 | 16 | List createAccountBatch(Supplier factory, int batchSize); 17 | 18 | List findTopAccountsByCity(Collection cities, int limit); 19 | 20 | Page findAll(Collection cities, Pageable page); 21 | 22 | Account getAccountById(UUID id); 23 | 24 | Money getBalance(UUID id); 25 | 26 | Money getBalanceSnapshot(UUID id); 27 | 28 | Account openAccount(UUID id); 29 | 30 | Account closeAccount(UUID id); 31 | 32 | void deleteAll(); 33 | } 34 | -------------------------------------------------------------------------------- /bank-api/src/main/java/io/roach/bank/api/ConnectionPoolSize.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.api; 2 | 3 | import org.springframework.hateoas.RepresentationModel; 4 | 5 | import com.fasterxml.jackson.annotation.JsonInclude; 6 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 7 | 8 | @JsonPropertyOrder({"links"}) 9 | @JsonInclude(JsonInclude.Include.NON_NULL) 10 | public class ConnectionPoolSize extends RepresentationModel { 11 | public int activeConnections; 12 | 13 | public int idleConnections; 14 | 15 | public int threadsAwaitingConnection; 16 | 17 | public int totalConnections; 18 | 19 | public int getActiveConnections() { 20 | return activeConnections; 21 | } 22 | 23 | public int getIdleConnections() { 24 | return idleConnections; 25 | } 26 | 27 | public int getThreadsAwaitingConnection() { 28 | return threadsAwaitingConnection; 29 | } 30 | 31 | public int getTotalConnections() { 32 | return totalConnections; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /deploy/gce-singleregion-eu.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Script for setting up a multi-region Roach Bank cluster using roachprod in either AWS or GCE. 3 | 4 | # Configuration 5 | ######################## 6 | 7 | title="CockroachDB single-region EU deployment" 8 | # CRDB release version 9 | releaseversion="v23.2.2" 10 | # Number of node instances in total including clients 11 | nodes="4" 12 | # Nodes hosting CRDB 13 | crdbnodes="1-3" 14 | # Array of client nodes (must match size of regions) 15 | clients=(4) 16 | # Array of regions localities (must match zone names) 17 | regions=('europe-west3') 18 | # AWS/GCE cloud (aws|gce) 19 | cloud="gce" 20 | # AWS/GCE region zones (must align with nodes count) 21 | zones="\ 22 | europe-west3-a,\ 23 | europe-west3-b,\ 24 | europe-west3-c,\ 25 | europe-west3-a" 26 | # AWS/GCE machine types 27 | machinetypes="n2d-standard-8" 28 | 1 29 | # DO NOT EDIT BELOW THIS LINE 30 | ############################# 31 | 32 | functionsdir="./common" 33 | 34 | source "${functionsdir}/core_functions.sh" 35 | 36 | main.sh 37 | 38 | exit 0 -------------------------------------------------------------------------------- /.github/badges/branches.svg: -------------------------------------------------------------------------------- 1 | branches0% -------------------------------------------------------------------------------- /.github/badges/jacoco.svg: -------------------------------------------------------------------------------- 1 | coverage0% -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/web/support/ConnectionPoolConfigFactory.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.web.support; 2 | 3 | import com.zaxxer.hikari.HikariConfigMXBean; 4 | 5 | import io.roach.bank.api.ConnectionPoolConfig; 6 | 7 | public abstract class ConnectionPoolConfigFactory { 8 | private ConnectionPoolConfigFactory() { 9 | } 10 | 11 | public static ConnectionPoolConfig from(HikariConfigMXBean bean) { 12 | ConnectionPoolConfig instance = new ConnectionPoolConfig(); 13 | instance.connectionTimeout = bean.getConnectionTimeout(); 14 | instance.poolName = bean.getPoolName(); 15 | instance.idleTimeout = bean.getIdleTimeout(); 16 | instance.leakDetectionThreshold = bean.getLeakDetectionThreshold(); 17 | instance.maximumPoolSize = bean.getMaximumPoolSize(); 18 | instance.maxLifetime = bean.getMaxLifetime(); 19 | instance.minimumIdle = bean.getMinimumIdle(); 20 | instance.validationTimeout = bean.getValidationTimeout(); 21 | return instance; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /bank-api/src/main/java/io/roach/bank/api/support/EnumPatternValidator.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.api.support; 2 | 3 | import java.util.regex.Matcher; 4 | import java.util.regex.Pattern; 5 | import java.util.regex.PatternSyntaxException; 6 | 7 | import jakarta.validation.ConstraintValidator; 8 | import jakarta.validation.ConstraintValidatorContext; 9 | 10 | public class EnumPatternValidator implements ConstraintValidator> { 11 | private Pattern pattern; 12 | 13 | @Override 14 | public void initialize(EnumPattern annotation) { 15 | try { 16 | pattern = Pattern.compile(annotation.regexp()); 17 | } catch (PatternSyntaxException e) { 18 | throw new IllegalArgumentException("Invalid regex", e); 19 | } 20 | } 21 | 22 | @Override 23 | public boolean isValid(Enum value, ConstraintValidatorContext context) { 24 | if (value == null) { 25 | return true; 26 | } 27 | 28 | Matcher m = pattern.matcher(value.name()); 29 | return m.matches(); 30 | } 31 | } -------------------------------------------------------------------------------- /deploy/aws-multiregion-eu-us.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Configuration 4 | ######################## 5 | 6 | title="CockroachDB 3-region EU-US deployment" 7 | # CRDB release version 8 | releaseversion="v23.2.2" 9 | # Number of node instances in total including clients 10 | nodes="12" 11 | # Nodes hosting CRDB 12 | crdbnodes="1-9" 13 | # Array of client nodes (must match size of regions) 14 | clients=(10 11 12) 15 | # Array of regions localities (must match zone names) 16 | regions=('us-east-1' 'eu-central-1' 'eu-north-1') 17 | # AWS/GCE cloud (aws|gce) 18 | cloud="aws" 19 | # AWS/GCE region zones (must align with nodes count) 20 | zones="\ 21 | us-east-1a,\ 22 | us-east-1b,\ 23 | us-east-1c,\ 24 | eu-central-1a,\ 25 | eu-central-1b,\ 26 | eu-central-1c,\ 27 | eu-north-1a,\ 28 | eu-north-1b,\ 29 | eu-north-1c,\ 30 | us-east-1a,\ 31 | eu-central-1a,\ 32 | eu-north-1a" 33 | # AWS/GCE machine types 34 | machinetypes="m6i.2xlarge" 35 | 36 | # DO NOT EDIT BELOW THIS LINE 37 | ############################# 38 | 39 | functionsdir="./common" 40 | 41 | source "${functionsdir}/core_functions.sh" 42 | 43 | main.sh -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/config/JpaTransactionManagerConfig.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.Profile; 6 | import org.springframework.data.cockroachdb.aspect.TransactionAttributesAspect; 7 | import org.springframework.jdbc.core.JdbcTemplate; 8 | import org.springframework.transaction.annotation.EnableTransactionManagement; 9 | 10 | import io.roach.bank.AdvisorOrder; 11 | import io.roach.bank.ProfileNames; 12 | 13 | @Configuration 14 | @EnableTransactionManagement(order = AdvisorOrder.TRANSACTION_ADVISOR) 15 | @Profile(ProfileNames.JPA) 16 | public class JpaTransactionManagerConfig { 17 | @Bean 18 | @Profile({ 19 | ProfileNames.PGJDBC_DEV, 20 | ProfileNames.CRDB_LOCAL, 21 | ProfileNames.CRDB_DEV}) 22 | public TransactionAttributesAspect transactionAttributesAspect(JdbcTemplate jdbcTemplate) { 23 | return new TransactionAttributesAspect(jdbcTemplate); 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /bank-server/src/test/java/io/roach/bank/service/TransactionServiceFacade.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.service; 2 | 3 | import java.util.UUID; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.data.cockroachdb.annotations.Retryable; 7 | import org.springframework.data.cockroachdb.annotations.TransactionBoundary; 8 | import org.springframework.stereotype.Service; 9 | import org.springframework.transaction.support.TransactionSynchronizationManager; 10 | import org.springframework.util.Assert; 11 | 12 | import io.roach.bank.api.TransactionForm; 13 | import io.roach.bank.domain.Transaction; 14 | 15 | @Service 16 | public class TransactionServiceFacade { 17 | @Autowired 18 | private TransactionService transactionService; 19 | 20 | @TransactionBoundary 21 | @Retryable(retryAttempts = 30) 22 | public Transaction createTransaction(UUID id, TransactionForm transactionForm) { 23 | Assert.isTrue(TransactionSynchronizationManager.isActualTransactionActive(), "Expected transaction"); 24 | return transactionService.createTransaction(id, transactionForm); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2024 Kai Niemi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /deploy/gce-singleregion-us.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Script for setting up a single-region Roach Bank cluster using roachprod in either AWS or GCE. 3 | 4 | # Configuration 5 | ######################## 6 | 7 | title="CockroachDB single region deployment (GCE)" 8 | # CRDB release version 9 | releaseversion="v23.2.2" 10 | # Number of node instances in total including clients 11 | nodes="4" 12 | # Nodes hosting CRDB 13 | crdbnodes="1-3" 14 | # Array of client nodes (must match size of regions) 15 | clients=(4) 16 | # Array of regions localities (must match zone names) 17 | regions=('us-east4') 18 | # AWS/GCE cloud (aws|gce) 19 | cloud="gce" 20 | # AWS/GCE region zones (must align with nodes size) 21 | zones="\ 22 | us-east4-a,\ 23 | us-east4-a,\ 24 | us-east4-a,\ 25 | us-east4-b,\ 26 | us-east4-b,\ 27 | us-east4-b,\ 28 | us-east4-c,\ 29 | us-east4-c,\ 30 | us-east4-c,\ 31 | us-east4-a,\ 32 | us-east4-b,\ 33 | us-east4-c" 34 | # AWS/GCE machine types 35 | machinetypes="n2-standard-8" 36 | 37 | # DO NOT EDIT BELOW THIS LINE 38 | ############################# 39 | 40 | functionsdir="./common" 41 | 42 | source "${functionsdir}/core_functions.sh" 43 | 44 | main.sh 45 | 46 | exit 0 -------------------------------------------------------------------------------- /bank-api/src/main/java/io/roach/bank/api/TransactionItemModel.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.api; 2 | 3 | import org.springframework.hateoas.RepresentationModel; 4 | import org.springframework.hateoas.server.core.Relation; 5 | 6 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 7 | 8 | import io.roach.bank.api.support.Money; 9 | 10 | import static io.roach.bank.api.LinkRelations.CURIE_PREFIX; 11 | 12 | /** 13 | * Describes a transaction item leg resource representation. 14 | */ 15 | @Relation(value = CURIE_PREFIX + LinkRelations.TRANSACTION_ITEM_REL, 16 | collectionRelation = CURIE_PREFIX + LinkRelations.TRANSACTION_ITEMS_REL) 17 | @JsonPropertyOrder({"links"}) 18 | public class TransactionItemModel extends RepresentationModel { 19 | private Money amount; 20 | 21 | private String note; 22 | 23 | public Money getAmount() { 24 | return amount; 25 | } 26 | 27 | public void setAmount(Money amount) { 28 | this.amount = amount; 29 | } 30 | 31 | public String getNote() { 32 | return note; 33 | } 34 | 35 | public void setNote(String note) { 36 | this.note = note; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/config/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.messaging.simp.config.MessageBrokerRegistry; 5 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; 6 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry; 7 | import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; 8 | 9 | @Configuration 10 | @EnableWebSocketMessageBroker 11 | public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { 12 | @Override 13 | public void registerStompEndpoints(StompEndpointRegistry registry) { 14 | registry.addEndpoint("/roach-bank") 15 | .withSockJS() 16 | .setHttpMessageCacheSize(128) 17 | .setDisconnectDelay(15000) 18 | .setClientLibraryUrl("/webjars/sockjs-client/1.5.1/sockjs.min.js"); 19 | } 20 | 21 | @Override 22 | public void configureMessageBroker(MessageBrokerRegistry registry) { 23 | registry.enableSimpleBroker("/topic"); 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /deploy/gce-multiregion-us.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Script for setting up a multi-region Roach Bank cluster using roachprod in either AWS or GCE. 3 | 4 | # Configuration 5 | ######################## 6 | 7 | title="CockroachDB 3-region US deployment" 8 | # CRDB release version 9 | releaseversion="v23.2.2" 10 | # Number of node instances in total including clients 11 | nodes="12" 12 | # Nodes hosting CRDB 13 | crdbnodes="1-9" 14 | # Array of client nodes (must match size of regions) 15 | clients=(10 11 12) 16 | # Array of regions localities (must match zone names) 17 | regions=('us-east1' 'us-central1' 'us-west1') 18 | # AWS/GCE cloud (aws|gce) 19 | cloud="gce" 20 | # AWS/GCE region zones (must align with nodes count) 21 | zones="\ 22 | us-east1-b,\ 23 | us-east1-c,\ 24 | us-east1-d,\ 25 | us-central1-a,\ 26 | us-central1-b,\ 27 | us-central1-c,\ 28 | us-west1-a,\ 29 | us-west1-b,\ 30 | us-west1-c,\ 31 | us-east1-b,\ 32 | us-central1-a,\ 33 | us-west1-a" 34 | # AWS/GCE machine types 35 | machinetypes="n2-standard-16" 36 | 37 | # DO NOT EDIT BELOW THIS LINE 38 | ############################# 39 | 40 | functionsdir="./common" 41 | 42 | source "${functionsdir}/core_functions.sh" 43 | 44 | main.sh -------------------------------------------------------------------------------- /deploy/aws-multiregion-eu.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Script for setting up a multi-region Roach Bank cluster using roachprod in either AWS or GCE. 3 | 4 | # Configuration 5 | ######################## 6 | 7 | title="CockroachDB 3-region EU deployment" 8 | # CRDB release version 9 | releaseversion="v23.2.2" 10 | # Number of node instances in total including clients 11 | nodes="12" 12 | # Nodes hosting CRDB 13 | crdbnodes="1-9" 14 | # Array of client nodes (must match size of regions) 15 | clients=(10 11 12) 16 | # Array of regions localities (must match zone names) 17 | regions=('eu-west-1' 'eu-central-1' 'eu-north-1') 18 | # AWS/GCE cloud (aws|gce) 19 | cloud="aws" 20 | # AWS/GCE region zones (must align with nodes count) 21 | zones="\ 22 | eu-west-1a,\ 23 | eu-west-1b,\ 24 | eu-west-1c,\ 25 | eu-central-1a,\ 26 | eu-central-1b,\ 27 | eu-central-1c,\ 28 | eu-north-1a,\ 29 | eu-north-1b,\ 30 | eu-north-1c,\ 31 | eu-west-1a,\ 32 | eu-central-1a,\ 33 | eu-north-1a" 34 | # AWS/GCE machine types 35 | machinetypes="m6i.2xlarge" 36 | 37 | # DO NOT EDIT BELOW THIS LINE 38 | ############################# 39 | 40 | functionsdir="./common" 41 | 42 | source "${functionsdir}/core_functions.sh" 43 | 44 | main.sh -------------------------------------------------------------------------------- /deploy/aws-singleregion-eu.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Script for setting up a single-region Roach Bank cluster using roachprod in either AWS or GCE. 3 | 4 | # Configuration 5 | ######################## 6 | 7 | title="CockroachDB single region deployment (AWS)" 8 | # CRDB release version 9 | releaseversion="v23.2.2" 10 | # Number of node instances in total including clients 11 | nodes="12" 12 | # Nodes hosting CRDB 13 | crdbnodes="1-9" 14 | # Array of client nodes (must match size of regions) 15 | clients=(10 11 12) 16 | # Array of regions localities (must match zone names) 17 | regions=('eu-central-1') 18 | # AWS/GCE cloud (aws|gce) 19 | cloud="aws" 20 | # AWS/GCE region zones (must align with nodes size) 21 | zones="\ 22 | eu-central-1a,\ 23 | eu-central-1b,\ 24 | eu-central-1c,\ 25 | eu-central-1a,\ 26 | eu-central-1b,\ 27 | eu-central-1c,\ 28 | eu-central-1a,\ 29 | eu-central-1b,\ 30 | eu-central-1c,\ 31 | eu-central-1a,\ 32 | eu-central-1b,\ 33 | eu-central-1c" 34 | 35 | # AWS/GCE machine types 36 | machinetypes="m6i.2xlarge" 37 | 38 | # DO NOT EDIT BELOW THIS LINE 39 | ############################# 40 | 41 | functionsdir="./common" 42 | 43 | source "${functionsdir}/core_functions.sh" 44 | 45 | main.sh -------------------------------------------------------------------------------- /deploy/gce-multiregion-eu.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Script for setting up a multi-region Roach Bank cluster using roachprod in either AWS or GCE. 3 | 4 | # Configuration 5 | ######################## 6 | 7 | title="CockroachDB 3-region EU deployment" 8 | # CRDB release version 9 | releaseversion="v23.2.2" 10 | # Number of node instances in total including clients 11 | nodes="12" 12 | # Nodes hosting CRDB 13 | crdbnodes="1-9" 14 | # Array of client nodes (must match size of regions) 15 | clients=(10 11 12) 16 | # Array of regions localities (must match zone names) 17 | regions=('europe-west1' 'europe-west2' 'europe-west3') 18 | # AWS/GCE cloud (aws|gce) 19 | cloud="gce" 20 | # AWS/GCE region zones (must align with nodes count) 21 | zones="\ 22 | europe-west1-b,\ 23 | europe-west1-c,\ 24 | europe-west1-d,\ 25 | europe-west2-a,\ 26 | europe-west2-b,\ 27 | europe-west2-c,\ 28 | europe-west3-a,\ 29 | europe-west3-b,\ 30 | europe-west3-c,\ 31 | europe-west1-b,\ 32 | europe-west2-a,\ 33 | europe-west3-a" 34 | # AWS/GCE machine types 35 | machinetypes="n2-standard-4" 36 | 37 | # DO NOT EDIT BELOW THIS LINE 38 | ############################# 39 | 40 | functionsdir="./common" 41 | 42 | source "${functionsdir}/core_functions.sh" 43 | 44 | main.sh 45 | 46 | exit 0 -------------------------------------------------------------------------------- /bank-client/src/main/java/io/roach/bank/client/support/ThreadPoolStats.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.client.support; 2 | 3 | import java.util.concurrent.ThreadPoolExecutor; 4 | 5 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 6 | 7 | public class ThreadPoolStats { 8 | public int maximumPoolSize; 9 | 10 | public int poolSize; 11 | 12 | public int activeCount; 13 | 14 | public long corePoolSize; 15 | 16 | public long taskCount; 17 | 18 | public int largestPoolSize; 19 | 20 | public long completedTaskCount; 21 | 22 | public static ThreadPoolStats from(ThreadPoolTaskExecutor boundedExecutor) { 23 | ThreadPoolExecutor pool = boundedExecutor.getThreadPoolExecutor(); 24 | ThreadPoolStats instance = new ThreadPoolStats(); 25 | instance.poolSize = pool.getPoolSize(); 26 | instance.corePoolSize = pool.getCorePoolSize(); 27 | instance.maximumPoolSize = pool.getMaximumPoolSize(); 28 | instance.activeCount = pool.getActiveCount(); 29 | instance.taskCount = pool.getTaskCount(); 30 | instance.largestPoolSize = pool.getLargestPoolSize(); 31 | instance.completedTaskCount = pool.getCompletedTaskCount(); 32 | return instance; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/config/CacheConfig.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.config; 2 | 3 | import java.util.Arrays; 4 | 5 | import org.springframework.cache.CacheManager; 6 | import org.springframework.cache.annotation.CachingConfigurer; 7 | import org.springframework.cache.annotation.EnableCaching; 8 | import org.springframework.cache.concurrent.ConcurrentMapCache; 9 | import org.springframework.cache.support.SimpleCacheManager; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | 13 | @Configuration 14 | @EnableCaching 15 | public class CacheConfig implements CachingConfigurer { 16 | public static final String CACHE_ACCOUNT_REPORT_SUMMARY = "accountReportSummary"; 17 | 18 | public static final String CACHE_TRANSACTION_REPORT_SUMMARY = "transactionReportSummary"; 19 | 20 | @Bean 21 | @Override 22 | public CacheManager cacheManager() { 23 | SimpleCacheManager cacheManager = new SimpleCacheManager(); 24 | cacheManager.setCaches(Arrays.asList( 25 | new ConcurrentMapCache(CACHE_ACCOUNT_REPORT_SUMMARY), 26 | new ConcurrentMapCache(CACHE_TRANSACTION_REPORT_SUMMARY)) 27 | ); 28 | return cacheManager; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/config/ThymeleafConfig.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.config; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.context.annotation.Profile; 8 | import org.thymeleaf.templateresolver.FileTemplateResolver; 9 | import org.thymeleaf.templateresolver.ITemplateResolver; 10 | 11 | import io.roach.bank.ProfileNames; 12 | 13 | @Configuration 14 | @Profile(value = {ProfileNames.DEBUG}) 15 | public class ThymeleafConfig { 16 | @Autowired 17 | private ThymeleafProperties properties; 18 | 19 | @Bean 20 | public ITemplateResolver defaultTemplateResolver() { 21 | FileTemplateResolver resolver = new FileTemplateResolver(); 22 | resolver.setSuffix(properties.getSuffix()); 23 | resolver.setPrefix("bank-server/src/main/resources/templates/"); 24 | resolver.setTemplateMode(properties.getMode()); 25 | resolver.setCharacterEncoding(properties.getEncoding().name()); 26 | resolver.setCacheable(false); 27 | resolver.setOrder(0); 28 | return resolver; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /bank-client/README.md: -------------------------------------------------------------------------------- 1 | # Roach Bank Client 2 | 3 | Main bank client used for generating load and more, including: 4 | 5 | - Create accounts 6 | - Transfer funds between accounts 7 | - Query account balances 8 | - Report of accounts and transactions 9 | - Generate account import CSV files 10 | 11 | ## Usage 12 | 13 | Start the client with: 14 | 15 | chmod +x bank-client.jar 16 | ./bank-client.jar 17 | 18 | First connect to the ledger service endpoint: 19 | 20 | connect [url] 21 | 22 | Default URL is `http://localhost:8090/api` 23 | 24 | Type `help` for additional guidance. 25 | 26 | ## Workload commands 27 | 28 | Get help for a command: 29 | 30 | help balance 31 | 32 | Transfer funds between all accounts in the local region: 33 | 34 | transfer 35 | 36 | List regions and cities: 37 | 38 | regions 39 | 40 | Transfer funds between accounts in specified regions: 41 | 42 | transfer --regions <..> 43 | 44 | Query the balance of random accounts in the local region: 45 | 46 | balance --duration 5m30s --follower-reads 47 | 48 | ## Configuration 49 | 50 | All parameters in `application.yaml` can be overridden via CLI. See 51 | [Common Application Properties](http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html) 52 | for details. -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/web/config/RegionResourceAssembler.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.web.config; 2 | 3 | import io.roach.bank.api.LinkRelations; 4 | import io.roach.bank.api.Region; 5 | import org.springframework.hateoas.CollectionModel; 6 | import org.springframework.hateoas.EntityModel; 7 | import org.springframework.hateoas.server.SimpleRepresentationModelAssembler; 8 | import org.springframework.stereotype.Component; 9 | 10 | import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; 11 | import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; 12 | 13 | @Component 14 | public class RegionResourceAssembler implements SimpleRepresentationModelAssembler { 15 | @Override 16 | public void addLinks(EntityModel resource) { 17 | Region region = resource.getContent(); 18 | resource.add(linkTo(methodOn(RegionController.class) 19 | .getRegion(region.getName())) 20 | .withSelfRel() 21 | ); 22 | resource.add(linkTo(methodOn(RegionController.class) 23 | .listAllCities(region.getName())) 24 | .withRel(LinkRelations.CITY_LIST_REL)); 25 | } 26 | 27 | @Override 28 | public void addLinks(CollectionModel> resources) { 29 | } 30 | } 31 | 32 | 33 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/AccountPlan.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank; 2 | 3 | import jakarta.validation.constraints.NotNull; 4 | import org.springframework.validation.annotation.Validated; 5 | 6 | @Validated 7 | public class AccountPlan { 8 | private int accountsPerCity; 9 | 10 | @NotNull 11 | private String initialBalance; 12 | 13 | @NotNull 14 | private String currency; 15 | 16 | public int getAccountsPerCity() { 17 | return accountsPerCity; 18 | } 19 | 20 | public void setAccountsPerCity(int accountsPerCity) { 21 | this.accountsPerCity = accountsPerCity; 22 | } 23 | 24 | public String getInitialBalance() { 25 | return initialBalance; 26 | } 27 | 28 | public void setInitialBalance(String initialBalance) { 29 | this.initialBalance = initialBalance; 30 | } 31 | 32 | public String getCurrency() { 33 | return currency; 34 | } 35 | 36 | public void setCurrency(String currency) { 37 | this.currency = currency; 38 | } 39 | 40 | @Override 41 | public String toString() { 42 | return "AccountPlan{" + 43 | "accountsPerCity=" + accountsPerCity + 44 | ", initialBalance='" + initialBalance + '\'' + 45 | ", currency='" + currency + '\'' + 46 | '}'; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/repository/AccountRepository.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.repository; 2 | 3 | import io.roach.bank.api.support.Money; 4 | import io.roach.bank.domain.Account; 5 | import org.springframework.data.domain.Page; 6 | import org.springframework.data.domain.Pageable; 7 | import org.springframework.data.util.Pair; 8 | 9 | import java.math.BigDecimal; 10 | import java.util.Collection; 11 | import java.util.List; 12 | import java.util.Optional; 13 | import java.util.Set; 14 | import java.util.UUID; 15 | import java.util.function.Supplier; 16 | 17 | public interface AccountRepository { 18 | Account createAccount(Account account); 19 | 20 | List createAccounts(Supplier factory, int batchSize); 21 | 22 | Account getAccountReferenceById(UUID id); 23 | 24 | Optional getAccountById(UUID id); 25 | 26 | Money getBalance(UUID id); 27 | 28 | Money getBalanceSnapshot(UUID id); 29 | 30 | void closeAccount(UUID id); 31 | 32 | void openAccount(UUID id); 33 | 34 | void updateBalances(List> balanceUpdates, String city); 35 | 36 | void deleteAll(); 37 | 38 | List findTopByCity(Collection cities, int limit); 39 | 40 | List findByIDs(Set ids, String city, boolean forUpdate); 41 | 42 | Page findAll(Collection cities, Pageable page); 43 | } 44 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/repository/jpa/TransactionItemJpaRepository.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.repository.jpa; 2 | 3 | import java.util.UUID; 4 | 5 | import org.springframework.data.domain.Page; 6 | import org.springframework.data.domain.Pageable; 7 | import org.springframework.data.jpa.repository.JpaRepository; 8 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor; 9 | import org.springframework.data.jpa.repository.Query; 10 | import org.springframework.data.repository.query.Param; 11 | import org.springframework.transaction.annotation.Propagation; 12 | import org.springframework.transaction.annotation.Transactional; 13 | 14 | import io.roach.bank.domain.TransactionItem; 15 | 16 | @Transactional(propagation = Propagation.MANDATORY) 17 | public interface TransactionItemJpaRepository extends JpaRepository, 18 | JpaSpecificationExecutor { 19 | 20 | @Query(value 21 | = "select item from TransactionItem item " 22 | + "where item.transaction.id = :transactionId", 23 | countQuery 24 | = "select count(item.id.transactionId) from TransactionItem item " 25 | + "where item.transaction.id = :transactionId") 26 | Page findById( 27 | @Param("transactionId") UUID transactionId, 28 | Pageable page); 29 | } 30 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/web/push/AccountPayload.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.web.push; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.UUID; 5 | 6 | public class AccountPayload { 7 | private UUID id; 8 | 9 | private String href; 10 | 11 | private String name; 12 | 13 | private String city; 14 | 15 | private BigDecimal balance; 16 | 17 | private String currency; 18 | 19 | public UUID getId() { 20 | return id; 21 | } 22 | 23 | public void setId(UUID id) { 24 | this.id = id; 25 | } 26 | 27 | public String getCity() { 28 | return city; 29 | } 30 | 31 | public void setCity(String city) { 32 | this.city = city; 33 | } 34 | 35 | public String getName() { 36 | return name; 37 | } 38 | 39 | public void setName(String name) { 40 | this.name = name; 41 | } 42 | 43 | public String getCurrency() { 44 | return currency; 45 | } 46 | 47 | public void setCurrency(String currency) { 48 | this.currency = currency; 49 | } 50 | 51 | public BigDecimal getBalance() { 52 | return balance; 53 | } 54 | 55 | public void setBalance(BigDecimal balance) { 56 | this.balance = balance; 57 | } 58 | 59 | public String getHref() { 60 | return href; 61 | } 62 | 63 | public void setHref(String href) { 64 | this.href = href; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /.github/workflows/maven-settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | github 7 | 8 | 9 | 10 | github 11 | 12 | 13 | central 14 | https://repo1.maven.org/maven2 15 | 16 | 17 | github 18 | https://maven.pkg.github.com/cloudneutral/* 19 | 20 | true 21 | 22 | 23 | true 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | github 33 | ${env.USER_NAME} 34 | ${env.ACCESS_TOKEN} 35 | 36 | 37 | -------------------------------------------------------------------------------- /bank-server/src/test/java/io/roach/bank/AdhocTest.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.regex.Matcher; 7 | import java.util.regex.Pattern; 8 | 9 | public class AdhocTest { 10 | @Test 11 | public void testPatterns() { 12 | Pattern pattern = Pattern.compile("(aws|gcp|azure|az)-"); 13 | Matcher matcher = pattern.matcher("aws-us-west-1"); 14 | Assertions.assertTrue(matcher.find()); 15 | 16 | StringBuilder sb = new StringBuilder(); 17 | matcher.appendReplacement(sb, ""); 18 | matcher.appendTail(sb); 19 | 20 | System.out.println(sb); 21 | Assertions.assertEquals("us-west-1", sb.toString()); 22 | } 23 | 24 | @Test 25 | public void testPatterns2() { 26 | Pattern pattern = Pattern.compile("(aws|gcp|azure|az)-"); 27 | Matcher matcher = pattern.matcher("az-us-west-1"); 28 | Assertions.assertTrue(matcher.find()); 29 | 30 | StringBuilder sb = new StringBuilder(); 31 | matcher.appendReplacement(sb, ""); 32 | matcher.appendTail(sb); 33 | 34 | System.out.println(sb); 35 | Assertions.assertEquals("us-west-1", sb.toString()); 36 | } 37 | 38 | @Test 39 | public void testPatterns3() { 40 | Pattern pattern = Pattern.compile("(aws|gcp|azure|az)-"); 41 | Matcher matcher = pattern.matcher("us-west-1"); 42 | Assertions.assertFalse(matcher.find()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /bank-api/src/main/java/io/roach/bank/api/ConnectionPoolConfig.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.api; 2 | 3 | import org.springframework.hateoas.RepresentationModel; 4 | 5 | import com.fasterxml.jackson.annotation.JsonInclude; 6 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 7 | 8 | @JsonPropertyOrder({"links"}) 9 | @JsonInclude(JsonInclude.Include.NON_NULL) 10 | public class ConnectionPoolConfig extends RepresentationModel { 11 | public long connectionTimeout; 12 | 13 | public String poolName; 14 | 15 | public long idleTimeout; 16 | 17 | public long leakDetectionThreshold; 18 | 19 | public int maximumPoolSize; 20 | 21 | public long maxLifetime; 22 | 23 | public int minimumIdle; 24 | 25 | public long validationTimeout; 26 | 27 | public long getConnectionTimeout() { 28 | return connectionTimeout; 29 | } 30 | 31 | public String getPoolName() { 32 | return poolName; 33 | } 34 | 35 | public long getIdleTimeout() { 36 | return idleTimeout; 37 | } 38 | 39 | public long getLeakDetectionThreshold() { 40 | return leakDetectionThreshold; 41 | } 42 | 43 | public int getMaximumPoolSize() { 44 | return maximumPoolSize; 45 | } 46 | 47 | public long getMaxLifetime() { 48 | return maxLifetime; 49 | } 50 | 51 | public int getMinimumIdle() { 52 | return minimumIdle; 53 | } 54 | 55 | public long getValidationTimeout() { 56 | return validationTimeout; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/web/support/ResponseHeaderFilter.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.web.support; 2 | 3 | import java.io.IOException; 4 | 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.info.BuildProperties; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.web.filter.OncePerRequestFilter; 11 | 12 | import jakarta.servlet.FilterChain; 13 | import jakarta.servlet.ServletException; 14 | import jakarta.servlet.annotation.WebFilter; 15 | import jakarta.servlet.http.HttpServletRequest; 16 | import jakarta.servlet.http.HttpServletResponse; 17 | 18 | @Component 19 | @WebFilter("/api") 20 | public class ResponseHeaderFilter extends OncePerRequestFilter { 21 | protected final Logger logger = LoggerFactory.getLogger(getClass()); 22 | 23 | public static final String HEADER_NAME = "X-Application-Context"; 24 | 25 | public static final String HEADER_VERSION = "X-Application-Version"; 26 | 27 | @Autowired 28 | private BuildProperties buildProperties; 29 | 30 | @Override 31 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) 32 | throws ServletException, IOException { 33 | response.setHeader(HEADER_NAME, buildProperties.getName()); 34 | response.setHeader(HEADER_VERSION, buildProperties.getVersion()); 35 | filterChain.doFilter(request, response); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/config/NoRetryConfig.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.config; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.Profile; 6 | import org.springframework.core.env.Environment; 7 | import org.springframework.core.env.Profiles; 8 | import org.springframework.util.Assert; 9 | 10 | import io.roach.bank.ProfileNames; 11 | import jakarta.annotation.PostConstruct; 12 | 13 | /** 14 | * Configuration for normal Spring annotation-driven TX demarcation with 15 | * REQUIRES_NEW propagation at boundaries (REST controller methods). 16 | *

17 | * Does not perform any retrys on the server side but instead propagates 18 | * serialization errors to the client for retry. 19 | */ 20 | @Configuration 21 | @Profile(ProfileNames.RETRY_NONE) 22 | public class NoRetryConfig { 23 | @Autowired 24 | private Environment environment; 25 | 26 | @PostConstruct 27 | public void checkProfiles() { 28 | Assert.isTrue(!environment.acceptsProfiles(Profiles.of(ProfileNames.RETRY_SAVEPOINT)), 29 | "Cant have both RETRY_NONE and RETRY_SAVEPOINT"); 30 | Assert.isTrue(!environment.acceptsProfiles(Profiles.of(ProfileNames.RETRY_DRIVER)), 31 | "Cant have both RETRY_NONE and RETRY_DRIVER"); 32 | Assert.isTrue(!environment.acceptsProfiles(Profiles.of(ProfileNames.RETRY_CLIENT)), 33 | "Cant have both RETRY_NONE and RETRY_CLIENT"); 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /deploy/common/main.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | core_util.sh 4 | 5 | case "$OSTYPE" in 6 | darwin*) 7 | rootdir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 8 | selfname="$(basename "$(test -L "$0" && readlink "$0" || echo "$0")")" 9 | ;; 10 | *) 11 | rootdir="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" 12 | selfname="$(basename "$(readlink -f "${BASH_SOURCE[0]}")")" 13 | ;; 14 | esac 15 | 16 | fn_echo_header 17 | { 18 | echo -e "${lightblue}Cluster id:\t\t${default}$CLUSTER" 19 | echo -e "${lightblue}Node count:\t\t${default}$nodes" 20 | echo -e "${lightblue}CRDB nodes:\t\t${default}$crdbnodes" 21 | echo -e "${lightblue}CRDB version:\t\t${default}$releaseversion" 22 | echo -e "${lightblue}Client nodes:\t\t${default}${clients[*]}" 23 | echo -e "${lightblue}Cloud:\t\t${default}$cloud" 24 | echo -e "${lightblue}Machine types:\t\t${default}$machinetypes" 25 | echo -e "${lightblue}Zones:\t\t${default}$zones" 26 | } | column -s $'\t' -t 27 | 28 | if [ -z "${CLUSTER}" ]; then 29 | fn_echo_warning "No \$CLUSTER id variable set!" 30 | echo "Use: export CLUSTER='your-cluster-id'" 31 | exit 1 32 | fi 33 | 34 | if fn_prompt_yes_no "1/5: Create CRDB cluster?" Y; then 35 | 01_create_cluster.sh 36 | fi 37 | 38 | if fn_prompt_yes_no "2/5: Deploy Bank Servers?" Y; then 39 | 02_deploy_servers.sh 40 | fi 41 | 42 | if fn_prompt_yes_no "3/5: Deploy Bank Clients?" Y; then 43 | 03_deploy_clients.sh 44 | fi 45 | 46 | if fn_prompt_yes_no "4/5: Start Bank Servers?" Y; then 47 | 04_start_servers.sh 48 | fi 49 | 50 | fn_echo_info_nl "Done!" -------------------------------------------------------------------------------- /bank-client/src/main/java/io/roach/bank/client/RegionProvider.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.client; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.shell.CompletionContext; 8 | import org.springframework.shell.CompletionProposal; 9 | import org.springframework.shell.standard.ValueProvider; 10 | import org.springframework.util.StringUtils; 11 | 12 | import io.roach.bank.api.Region; 13 | import io.roach.bank.client.support.HypermediaClient; 14 | 15 | public class RegionProvider implements ValueProvider { 16 | @Autowired 17 | private HypermediaClient bankClient; 18 | 19 | @Override 20 | public List complete(CompletionContext completionContext) { 21 | List result = new ArrayList<>(); 22 | 23 | final String gateway = bankClient.getGatewayRegion(); 24 | 25 | for (Region k : bankClient.getRegions()) { 26 | if (k.getName().equals(gateway)) { 27 | CompletionProposal p = new CompletionProposal(k.getName()) 28 | .description("-> " 29 | + StringUtils.collectionToCommaDelimitedString(k.getCities())); 30 | result.add(0, p); 31 | } else { 32 | CompletionProposal p = new CompletionProposal(k.getName()) 33 | .description(StringUtils.collectionToCommaDelimitedString(k.getCities())); 34 | result.add(p); 35 | } 36 | } 37 | 38 | return result; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/util/TimeUtils.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.util; 2 | 3 | import java.lang.reflect.UndeclaredThrowableException; 4 | import java.time.Duration; 5 | import java.util.Locale; 6 | import java.util.concurrent.Callable; 7 | 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | public class TimeUtils { 12 | private static final Logger logger = LoggerFactory.getLogger(TimeUtils.class); 13 | 14 | public static String millisecondsToDisplayString(long timeMillis) { 15 | double seconds = (timeMillis / 1000.0) % 60; 16 | int minutes = (int) ((timeMillis / 60000) % 60); 17 | int hours = (int) ((timeMillis / 3600000)); 18 | 19 | StringBuilder sb = new StringBuilder(); 20 | if (hours > 0) { 21 | sb.append(String.format("%dh", hours)); 22 | } 23 | if (hours > 0 || minutes > 0) { 24 | sb.append(String.format("%dm", minutes)); 25 | } 26 | if (hours == 0 && seconds > 0) { 27 | sb.append(String.format(Locale.US, "%.1fs", seconds)); 28 | } 29 | return sb.toString(); 30 | } 31 | 32 | public static long executionTime(Callable task) { 33 | try { 34 | long start = System.nanoTime(); 35 | task.call(); 36 | long millis = Duration.ofNanos(System.nanoTime() - start).toMillis(); 37 | logger.debug("{} completed in {}", task, millisecondsToDisplayString(millis)); 38 | return millis; 39 | } catch (Exception e) { 40 | throw new UndeclaredThrowableException(e); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /bank-client/src/main/java/io/roach/bank/client/config/ConcurrencyConfig.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.client.config; 2 | 3 | import java.util.concurrent.Executors; 4 | import java.util.concurrent.ScheduledExecutorService; 5 | 6 | import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; 7 | import org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.scheduling.annotation.AsyncConfigurer; 11 | import org.springframework.scheduling.annotation.EnableAsync; 12 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 13 | 14 | @Configuration 15 | @EnableAsync 16 | public class ConcurrencyConfig implements AsyncConfigurer { 17 | @Override 18 | @Bean(name = "workloadExecutor") 19 | public ThreadPoolTaskExecutor getAsyncExecutor() { 20 | ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); 21 | executor.setMaxPoolSize(500); 22 | executor.setCorePoolSize(500); 23 | executor.setWaitForTasksToCompleteOnShutdown(true); 24 | executor.setThreadNamePrefix("worker"); 25 | return executor; 26 | } 27 | 28 | @Override 29 | public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { 30 | return new SimpleAsyncUncaughtExceptionHandler(); 31 | } 32 | 33 | @Bean(destroyMethod = "shutdown") 34 | public ScheduledExecutorService scheduledExecutor() { 35 | return Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/config/DriverSideRetryConfig.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.config; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.context.annotation.Profile; 8 | import org.springframework.core.env.Environment; 9 | import org.springframework.core.env.Profiles; 10 | import org.springframework.util.Assert; 11 | 12 | import io.roach.bank.ProfileNames; 13 | import jakarta.annotation.PostConstruct; 14 | 15 | @Configuration 16 | @Profile(ProfileNames.RETRY_DRIVER) 17 | public class DriverSideRetryConfig { 18 | protected final Logger logger = LoggerFactory.getLogger(getClass()); 19 | 20 | @Autowired 21 | private Environment environment; 22 | 23 | @PostConstruct 24 | public void checkProfiles() { 25 | Assert.isTrue(!environment.acceptsProfiles(Profiles.of(ProfileNames.RETRY_SAVEPOINT)), 26 | "Cant have both RETRY_DRIVER and RETRY_SAVEPOINT"); 27 | Assert.isTrue(!environment.acceptsProfiles(Profiles.of(ProfileNames.RETRY_CLIENT)), 28 | "Cant have both RETRY_DRIVER and RETRY_CLIENT"); 29 | Assert.isTrue(!environment.acceptsProfiles(Profiles.of(ProfileNames.RETRY_NONE)), 30 | "Cant have both RETRY_DRIVER and RETRY_NONE"); 31 | Assert.isTrue(!environment.acceptsProfiles(Profiles.of(ProfileNames.PSQL_LOCAL, ProfileNames.PSQL_DEV)), 32 | "Cant have driver-level retries for PSQL"); 33 | 34 | logger.info("Enabled JDBC-driver level retrys"); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/web/push/BalanceUpdateAspect.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.web.push; 2 | 3 | import io.roach.bank.AdvisorOrder; 4 | import io.roach.bank.domain.Account; 5 | import io.roach.bank.domain.Transaction; 6 | import org.aspectj.lang.annotation.AfterReturning; 7 | import org.aspectj.lang.annotation.Aspect; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.core.annotation.Order; 12 | import org.springframework.stereotype.Component; 13 | 14 | @Aspect 15 | @Component 16 | @Order(AdvisorOrder.OUTBOX_ADVISOR) 17 | public class BalanceUpdateAspect { 18 | protected final Logger logger = LoggerFactory.getLogger(getClass()); 19 | 20 | @Autowired 21 | private BalancePublisher balancePublisher; 22 | 23 | @AfterReturning(pointcut = "execution(* io.roach.bank.service.DefaultTransactionService.createTransaction(..))", 24 | returning = "transaction") 25 | public void doAfterTransaction(Transaction transaction) { 26 | transaction.getItems().forEach(transactionItem -> { 27 | Account account = transactionItem.getAccount(); 28 | 29 | AccountPayload payload = new AccountPayload(); 30 | payload.setId(account.getId()); 31 | payload.setName(account.getName()); 32 | payload.setBalance(account.getBalance().getAmount()); 33 | payload.setCurrency(account.getBalance().getCurrency().getCurrencyCode()); 34 | payload.setCity(account.getCity()); 35 | 36 | balancePublisher.publishAsync(payload); 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/web/config/ConfigurationController.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.web.config; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.core.env.Environment; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | import io.roach.bank.ProfileNames; 11 | import io.roach.bank.api.LinkRelations; 12 | import io.roach.bank.api.MessageModel; 13 | 14 | import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; 15 | import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; 16 | 17 | @RestController 18 | @RequestMapping(value = "/api/config") 19 | public class ConfigurationController { 20 | @Autowired 21 | private Environment environment; 22 | 23 | @GetMapping 24 | public ResponseEntity index() { 25 | MessageModel index = new MessageModel(); 26 | index.setMessage("Configuration resources"); 27 | 28 | index.add(linkTo(methodOn(RegionController.class) 29 | .index()) 30 | .withRel(LinkRelations.CONFIG_REGION_REL) 31 | .withTitle("Region configuration") 32 | ); 33 | 34 | if (!ProfileNames.acceptsPostgresSQL(environment)) { 35 | index.add(linkTo(methodOn(MultiRegionController.class) 36 | .index()) 37 | .withRel(LinkRelations.CONFIG_MULTI_REGION_REL) 38 | .withTitle("Multi-region configuration") 39 | ); 40 | } 41 | 42 | return ResponseEntity.ok(index); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /bank-client/src/main/java/io/roach/bank/client/support/Console.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.client.support; 2 | 3 | import java.util.Locale; 4 | 5 | import org.jline.terminal.Terminal; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.ansi.AnsiColor; 8 | import org.springframework.boot.ansi.AnsiOutput; 9 | import org.springframework.context.annotation.Lazy; 10 | import org.springframework.stereotype.Component; 11 | import org.springframework.util.Assert; 12 | 13 | @Component 14 | public class Console { 15 | private final Terminal terminal; 16 | 17 | @Autowired 18 | public Console(@Lazy Terminal terminal) { 19 | Assert.notNull(terminal, "terminal is null"); 20 | this.terminal = terminal; 21 | } 22 | 23 | public void success(String format, Object... args) { 24 | text(AnsiColor.BRIGHT_GREEN, format, args); 25 | } 26 | 27 | public void info(String format, Object... args) { 28 | text(AnsiColor.BRIGHT_CYAN, format, args); 29 | } 30 | 31 | public void warn(String format, Object... args) { 32 | text(AnsiColor.BRIGHT_YELLOW, format, args); 33 | } 34 | 35 | public void error(String format, Object... args) { 36 | text(AnsiColor.BRIGHT_RED, format, args); 37 | } 38 | 39 | public void text(AnsiColor color, String format, Object... args) { 40 | terminal.writer().println(ansiColor(color, String.format(Locale.US, format, args))); 41 | terminal.writer().flush(); 42 | } 43 | 44 | public void textf(AnsiColor color, String format, Object... args) { 45 | terminal.writer().printf(ansiColor(color, String.format(Locale.US, format, args))); 46 | terminal.writer().flush(); 47 | } 48 | 49 | private String ansiColor(AnsiColor color, String message) { 50 | return AnsiOutput.toString(color, message, AnsiColor.DEFAULT); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/web/transaction/TransactionResourceAssembler.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.web.transaction; 2 | 3 | import org.springframework.data.domain.PageRequest; 4 | import org.springframework.hateoas.server.core.DummyInvocationUtils; 5 | import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport; 6 | import org.springframework.stereotype.Component; 7 | 8 | import io.roach.bank.api.LinkRelations; 9 | import io.roach.bank.api.TransactionModel; 10 | import io.roach.bank.domain.Transaction; 11 | 12 | import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; 13 | import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; 14 | 15 | @Component 16 | public class TransactionResourceAssembler 17 | extends RepresentationModelAssemblerSupport { 18 | 19 | public TransactionResourceAssembler() { 20 | super(TransactionController.class, TransactionModel.class); 21 | } 22 | 23 | @Override 24 | public TransactionModel toModel(Transaction entity) { 25 | TransactionModel resource = new TransactionModel(); 26 | resource.add(linkTo(methodOn(TransactionController.class) 27 | .getTransaction(entity.getId())).withSelfRel()); 28 | resource.setTransactionType(entity.getTransactionType()); 29 | resource.setBookingDate(entity.getBookingDate()); 30 | resource.setTransactionDate(entity.getTransferDate()); 31 | resource.setCity(entity.getCity()); 32 | 33 | resource.add(linkTo(DummyInvocationUtils.methodOn(TransactionItemController.class) 34 | .getTransactionItems(entity.getId(), 35 | PageRequest.of(0, 5))) 36 | .withRel(LinkRelations.TRANSACTION_ITEMS_REL) 37 | .withTitle("Transaction items")); 38 | 39 | return resource; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /bank-client/src/test/java/io/roach/bank/client/util/DurationFormatTest.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.client.util; 2 | 3 | import java.text.SimpleDateFormat; 4 | import java.time.Duration; 5 | import java.time.LocalDateTime; 6 | import java.time.ZoneOffset; 7 | import java.time.format.DateTimeFormatter; 8 | import java.util.Date; 9 | 10 | import io.roach.bank.client.support.DurationFormat; 11 | import org.junit.jupiter.api.Assertions; 12 | import org.junit.jupiter.api.Test; 13 | 14 | 15 | public class DurationFormatTest { 16 | 17 | @Test 18 | public void parseDurationExpressions() { 19 | // 2020-09-03 07:03:22.165309+00:00 20 | System.out.println(LocalDateTime.now() 21 | .atOffset(ZoneOffset.UTC) 22 | .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSZ"))); 23 | 24 | System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSX").format(new Date())); 25 | 26 | Assertions.assertEquals(DurationFormat.parseDuration("30"), Duration.ofSeconds(30)); 27 | Assertions.assertEquals(DurationFormat.parseDuration("30s"), Duration.ofSeconds(30)); 28 | Assertions.assertEquals(DurationFormat.parseDuration("30m"), Duration.ofMinutes(30)); 29 | Assertions.assertEquals(DurationFormat.parseDuration("30h"), Duration.ofHours(30)); 30 | Assertions.assertEquals(DurationFormat.parseDuration("30d"), Duration.ofDays(30)); 31 | 32 | Assertions.assertEquals(DurationFormat.parseDuration("10m30s"), 33 | Duration.ofMinutes(10).plus(Duration.ofSeconds(30))); 34 | Assertions.assertEquals(DurationFormat.parseDuration("10h3m15s"), 35 | Duration.ofHours(10).plus(Duration.ofMinutes(3).plus(Duration.ofSeconds(15)))); 36 | Assertions.assertEquals(DurationFormat.parseDuration("10h 3m 15s"), 37 | Duration.ofHours(10).plus(Duration.ofMinutes(3).plus(Duration.ofSeconds(15)))); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /deploy/README.md: -------------------------------------------------------------------------------- 1 | # Cloud Deployment Tutorial 2 | 3 | Deployment tutorial for setting up a full-stack demo using either a single or multi-region cloud deployment. 4 | 5 | ## Prerequisites 6 | 7 | - [Roachprod](https://github.com/cockroachdb/cockroach/tree/master/pkg/cmd/roachprod) - a Cockroach Labs internal 8 | tool for ramping AWS/GCE/Azure VM clusters 9 | - You will need the AWS/GCE/AZ client SDK, and an account 10 | 11 | ## Create Cluster 12 | 13 | First create a CockroachDB cluster with a cloud provider of choice. There are available scripts for: 14 | 15 | - AWS 16 | - Azure 17 | - GCE 18 | 19 | ## Demo Instructions 20 | 21 | Once the cluster is setup and the bank is deployed, you can issue workload commands to create traffic. 22 | 23 | ### Starting the Client 24 | 25 | roachprod run $CLUSTER:10 26 | ./bank-client.jar 27 | 28 | In the client CLI, connect to localhost: 29 | 30 | connect 31 | 32 | Create accounts: 33 | 34 | accounts 35 | 36 | Transfer funds across local accounts: 37 | 38 | transfer 39 | 40 | Read account balances: 41 | 42 | balance 43 | 44 | ### Global Workload 45 | 46 | In a multi-region setup, it can be interesting to push traffic concurrently in each region: 47 | 48 | Connect to region 1: 49 | 50 | roachprod run cluster-name:10 51 | ./bank-client.jar connect 52 | transfer 53 | 54 | Connect to region 2: 55 | 56 | roachprod run cluster-name:11 57 | ./bank-client.jar connect 58 | transfer 59 | 60 | Connect to region 3: 61 | 62 | roachprod run cluster-name:12 63 | ./bank-client.jar connect 64 | transfer 65 | 66 | Type `help` for additional guidance. 67 | 68 | # Appendix: Deployment Diagrams 69 | 70 | Common high-level deployment view of single-region deployments. 71 | 72 | ![](../docs/deploy_singleregion.png) 73 | 74 | Common high-level deployment view of multi-region deployments. 75 | 76 | ![](../docs/deploy_multiregion.png) 77 | 78 | -------------------------------------------------------------------------------- /bank-api/src/main/java/io/roach/bank/api/AccountBatchForm.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.api; 2 | 3 | 4 | import org.springframework.hateoas.RepresentationModel; 5 | import org.springframework.hateoas.server.core.Relation; 6 | 7 | import com.fasterxml.jackson.annotation.JsonInclude; 8 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 9 | 10 | import jakarta.validation.constraints.NotNull; 11 | 12 | import static io.roach.bank.api.LinkRelations.ACCOUNT_BATCH_FORM_REL; 13 | import static io.roach.bank.api.LinkRelations.CURIE_PREFIX; 14 | 15 | @Relation(value = CURIE_PREFIX + ACCOUNT_BATCH_FORM_REL) 16 | @JsonPropertyOrder({"links"}) 17 | @JsonInclude(JsonInclude.Include.NON_NULL) 18 | public class AccountBatchForm extends RepresentationModel { 19 | @NotNull 20 | private String city; 21 | 22 | @NotNull 23 | private String prefix; 24 | 25 | @NotNull 26 | private String balance; 27 | 28 | @NotNull 29 | private String currency; 30 | 31 | @NotNull 32 | private Integer batchSize; 33 | 34 | public String getCity() { 35 | return city; 36 | } 37 | 38 | public void setCity(String city) { 39 | this.city = city; 40 | } 41 | 42 | public String getPrefix() { 43 | return prefix; 44 | } 45 | 46 | public void setPrefix(String prefix) { 47 | this.prefix = prefix; 48 | } 49 | 50 | public String getBalance() { 51 | return balance; 52 | } 53 | 54 | public void setBalance(String balance) { 55 | this.balance = balance; 56 | } 57 | 58 | public String getCurrency() { 59 | return currency; 60 | } 61 | 62 | public void setCurrency(String currency) { 63 | this.currency = currency; 64 | } 65 | 66 | public Integer getBatchSize() { 67 | return batchSize; 68 | } 69 | 70 | public void setBatchSize(Integer batchSize) { 71 | this.batchSize = batchSize; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /bank-client/src/main/java/io/roach/bank/client/Constants.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.client; 2 | 3 | import org.springframework.core.ParameterizedTypeReference; 4 | import org.springframework.hateoas.CollectionModel; 5 | import org.springframework.hateoas.PagedModel; 6 | 7 | import io.roach.bank.api.AccountModel; 8 | 9 | public abstract class Constants { 10 | public static final String ADMIN_COMMANDS = "1. Admin Commands"; 11 | 12 | public static final String POOL_COMMANDS = "2. Resource Pool"; 13 | 14 | public static final String CONFIG_COMMANDS = "3. Region Commands"; 15 | 16 | public static final String REPORTING_COMMANDS = "4. Reporting Commands"; 17 | 18 | public static final String WORKLOAD_COMMANDS = "5. Workload Commands"; 19 | 20 | public static final String LOGGING_COMMANDS = "6. Logging Commands"; 21 | 22 | public static final String ERROR_COMMANDS = "7. Error Commands"; 23 | 24 | public static final String DEFAULT_DURATION = "180m"; 25 | 26 | public static final String CONNECTED_CHECK = "connectedCheck"; 27 | 28 | public static final String DURATION_HELP = "Execution duration"; 29 | 30 | public static final String REGIONS_HELP = "Name of account regions." 31 | + "\n'gateway' refers to CRDB gateway nodee region." 32 | + "\n'all' includes all regions."; 33 | 34 | public static final String ACCOUNT_LIMIT_HELP = "Number of accounts per city (-1 server default)"; 35 | 36 | public static final String DEFAULT_ACCOUNT_LIMIT = "-1"; 37 | 38 | public static final String DEFAULT_REGION = "gateway"; 39 | 40 | public static final ParameterizedTypeReference> ACCOUNT_MODEL_PTR 41 | = new ParameterizedTypeReference<>() { 42 | }; 43 | 44 | public static final ParameterizedTypeReference> ACCOUNT_PAGE_MODEL_PTR 45 | = new ParameterizedTypeReference<>() { 46 | }; 47 | 48 | private Constants() { 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /bank-api/src/main/java/io/roach/bank/api/AccountSummary.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.api; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.Currency; 5 | 6 | public class AccountSummary { 7 | private String city; 8 | 9 | private Currency currency; 10 | 11 | private long numberOfAccounts; 12 | 13 | private BigDecimal totalBalance; 14 | 15 | private BigDecimal minBalance; 16 | 17 | private BigDecimal maxBalance; 18 | 19 | public String getCity() { 20 | return city; 21 | } 22 | 23 | public void setCity(String city) { 24 | this.city = city; 25 | } 26 | 27 | public Currency getCurrency() { 28 | return currency; 29 | } 30 | 31 | public void setCurrency(Currency currency) { 32 | this.currency = currency; 33 | } 34 | 35 | public long getNumberOfAccounts() { 36 | return numberOfAccounts; 37 | } 38 | 39 | public void setNumberOfAccounts(long numberOfAccounts) { 40 | this.numberOfAccounts = numberOfAccounts; 41 | } 42 | 43 | public BigDecimal getTotalBalance() { 44 | return totalBalance; 45 | } 46 | 47 | public void setTotalBalance(BigDecimal totalBalance) { 48 | this.totalBalance = totalBalance; 49 | } 50 | 51 | public BigDecimal getMinBalance() { 52 | return minBalance; 53 | } 54 | 55 | public void setMinBalance(BigDecimal minBalance) { 56 | this.minBalance = minBalance; 57 | } 58 | 59 | public BigDecimal getMaxBalance() { 60 | return maxBalance; 61 | } 62 | 63 | public void setMaxBalance(BigDecimal maxBalance) { 64 | this.maxBalance = maxBalance; 65 | } 66 | 67 | @Override 68 | public String toString() { 69 | return "AccountSummary{" + 70 | "region=" + city + 71 | ", numberOfAccounts=" + numberOfAccounts + 72 | ", totalBalance=" + totalBalance + 73 | ", minBalance=" + minBalance + 74 | ", maxBalance=" + maxBalance + 75 | '}'; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/web/transaction/TransactionItemResourceAssembler.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.web.transaction; 2 | 3 | import io.roach.bank.web.account.AccountController; 4 | import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport; 5 | import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder; 6 | import org.springframework.stereotype.Component; 7 | 8 | import io.roach.bank.api.LinkRelations; 9 | import io.roach.bank.api.TransactionItemModel; 10 | import io.roach.bank.domain.TransactionItem; 11 | 12 | import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; 13 | 14 | @Component 15 | public class TransactionItemResourceAssembler 16 | extends RepresentationModelAssemblerSupport { 17 | 18 | public TransactionItemResourceAssembler() { 19 | super(TransactionItemController.class, TransactionItemModel.class); 20 | } 21 | 22 | @Override 23 | public TransactionItemModel toModel(TransactionItem entity) { 24 | TransactionItemModel resource = new TransactionItemModel(); 25 | resource.setAmount(entity.getAmount()); 26 | resource.setNote(entity.getNote()); 27 | 28 | resource.add(linkTo(TransactionItemController.class) 29 | .slash(entity.getId().getTransactionId()) 30 | .slash(entity.getId().getAccountId()) 31 | .withSelfRel()); 32 | resource.add(WebMvcLinkBuilder 33 | .linkTo(WebMvcLinkBuilder.methodOn(AccountController.class) 34 | .getAccount(entity.getId().getAccountId())) 35 | .withRel(LinkRelations.ACCOUNT_REL) 36 | .withTitle("Booking account")); 37 | resource.add(WebMvcLinkBuilder 38 | .linkTo(WebMvcLinkBuilder.methodOn(TransactionController.class) 39 | .getTransaction(entity.getId().getTransactionId())) 40 | .withRel(LinkRelations.TRANSACTION_REL) 41 | .withTitle("Booking transaction")); 42 | 43 | return resource; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /bank-api/src/main/java/io/roach/bank/api/TransactionSummary.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.api; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.Currency; 5 | 6 | public class TransactionSummary { 7 | private String city; 8 | 9 | private Currency currency; 10 | 11 | private long numberOfTransactions; 12 | 13 | private long numberOfLegs; 14 | 15 | private BigDecimal totalTurnover; 16 | 17 | private BigDecimal totalCheckSum; 18 | 19 | public String getCity() { 20 | return city; 21 | } 22 | 23 | public void setCity(String city) { 24 | this.city = city; 25 | } 26 | 27 | public Currency getCurrency() { 28 | return currency; 29 | } 30 | 31 | public void setCurrency(Currency currency) { 32 | this.currency = currency; 33 | } 34 | 35 | public long getNumberOfTransactions() { 36 | return numberOfTransactions; 37 | } 38 | 39 | public void setNumberOfTransactions(long numberOfTransactions) { 40 | this.numberOfTransactions = numberOfTransactions; 41 | } 42 | 43 | public long getNumberOfLegs() { 44 | return numberOfLegs; 45 | } 46 | 47 | public void setNumberOfLegs(long numberOfLegs) { 48 | this.numberOfLegs = numberOfLegs; 49 | } 50 | 51 | public BigDecimal getTotalTurnover() { 52 | return totalTurnover; 53 | } 54 | 55 | public void setTotalTurnover(BigDecimal totalTurnover) { 56 | this.totalTurnover = totalTurnover; 57 | } 58 | 59 | public BigDecimal getTotalCheckSum() { 60 | return totalCheckSum; 61 | } 62 | 63 | public void setTotalCheckSum(BigDecimal totalCheckSum) { 64 | this.totalCheckSum = totalCheckSum; 65 | } 66 | 67 | @Override 68 | public String toString() { 69 | return "TransactionSummary{" + 70 | "city=" + city + 71 | ", numberOfTransactions=" + numberOfTransactions + 72 | ", numberOfLegs=" + numberOfLegs + 73 | ", totalTurnover=" + totalTurnover + 74 | ", totalCheckSum=" + totalCheckSum + 75 | '}'; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /bank-client/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | io.roach.bank 7 | bank-parent 8 | 2.1.1-SNAPSHOT 9 | ../pom.xml 10 | 11 | 12 | bank-client 13 | jar 14 | 15 | 16 | 17 | ${project.groupId} 18 | bank-api 19 | ${project.version} 20 | 21 | 22 | org.springframework.shell 23 | spring-shell-starter 24 | 25 | 26 | org.apache.httpcomponents.client5 27 | httpclient5 28 | 29 | 30 | 31 | org.junit.jupiter 32 | junit-jupiter-engine 33 | test 34 | 35 | 36 | 37 | 38 | bank-client 39 | 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-maven-plugin 44 | 45 | 46 | io.github.git-commit-id 47 | git-commit-id-maven-plugin 48 | 49 | 50 | org.apache.maven.plugins 51 | maven-jar-plugin 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/config/SavepointRetryConfig.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.config; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.context.annotation.Profile; 7 | import org.springframework.core.env.Environment; 8 | import org.springframework.core.env.Profiles; 9 | import org.springframework.data.cockroachdb.aspect.SavepointRetryAspect; 10 | import org.springframework.transaction.PlatformTransactionManager; 11 | import org.springframework.util.Assert; 12 | 13 | import io.roach.bank.ProfileNames; 14 | import jakarta.annotation.PostConstruct; 15 | 16 | /** 17 | * Transaction management configuration that puts a savepoint 18 | * retry advice before each transactional joinpoint. If a database 19 | * serialization error occurs that translates to a TransientException, 20 | * then the transaction is repeatingly rolled back to the savepoint. 21 | *

22 | * See https://www.cockroachlabs.com/docs/transactions.html#transaction-retries 23 | */ 24 | @Configuration 25 | @Profile(ProfileNames.RETRY_SAVEPOINT) 26 | public class SavepointRetryConfig { 27 | @Autowired 28 | private Environment environment; 29 | 30 | @PostConstruct 31 | public void checkProfiles() { 32 | Assert.isTrue(!environment.acceptsProfiles(Profiles.of(ProfileNames.JPA)), 33 | "Savepoints are not supported in JPA/Hibernate"); 34 | Assert.isTrue(!environment.acceptsProfiles(Profiles.of(ProfileNames.RETRY_NONE)), 35 | "Cant have both RETRY_SAVEPOINT and RETRY_NONE"); 36 | Assert.isTrue(!environment.acceptsProfiles(Profiles.of(ProfileNames.RETRY_DRIVER)), 37 | "Cant have both RETRY_SAVEPOINT and RETRY_DRIVER"); 38 | Assert.isTrue(!environment.acceptsProfiles(Profiles.of(ProfileNames.RETRY_CLIENT)), 39 | "Cant have both RETRY_SAVEPOINT and RETRY_CLIENT"); 40 | } 41 | 42 | @Bean 43 | public SavepointRetryAspect savepointTransactionAspect(PlatformTransactionManager transactionManager) { 44 | return new SavepointRetryAspect(transactionManager, "cockroach_restart"); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/web/ViewModel.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.web; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import io.roach.bank.api.Region; 7 | 8 | public class ViewModel { 9 | private int limit; 10 | 11 | private String gatewayRegion; 12 | 13 | private String primaryRegion; 14 | 15 | private String secondaryRegion; 16 | 17 | private String viewRegion; 18 | 19 | private boolean viewingGatewayRegion; 20 | 21 | private String randomFact; 22 | 23 | private final List regions = new ArrayList<>(); 24 | 25 | public int getLimit() { 26 | return limit; 27 | } 28 | 29 | public void setLimit(int limit) { 30 | this.limit = limit; 31 | } 32 | 33 | public String getGatewayRegion() { 34 | return gatewayRegion; 35 | } 36 | 37 | public void setGatewayRegion(String gatewayRegion) { 38 | this.gatewayRegion = gatewayRegion; 39 | } 40 | 41 | public String getPrimaryRegion() { 42 | return primaryRegion; 43 | } 44 | 45 | public void setPrimaryRegion(String primaryRegion) { 46 | this.primaryRegion = primaryRegion; 47 | } 48 | 49 | public String getSecondaryRegion() { 50 | return secondaryRegion; 51 | } 52 | 53 | public void setSecondaryRegion(String secondaryRegion) { 54 | this.secondaryRegion = secondaryRegion; 55 | } 56 | 57 | public String getViewRegion() { 58 | return viewRegion; 59 | } 60 | 61 | public void setViewRegion(String viewRegion) { 62 | this.viewRegion = viewRegion; 63 | } 64 | 65 | public boolean isViewingGatewayRegion() { 66 | return viewingGatewayRegion; 67 | } 68 | 69 | public void setViewingGatewayRegion(boolean viewingGatewayRegion) { 70 | this.viewingGatewayRegion = viewingGatewayRegion; 71 | } 72 | 73 | public String getRandomFact() { 74 | return randomFact; 75 | } 76 | 77 | public void setRandomFact(String randomFact) { 78 | this.randomFact = randomFact; 79 | } 80 | 81 | public List getRegions() { 82 | return regions; 83 | } 84 | 85 | public void addRegion(Region region) { 86 | regions.add(region); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/config/JdbcTransactionManagerConfig.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.config; 2 | 3 | import javax.sql.DataSource; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.context.annotation.Profile; 9 | import org.springframework.data.cockroachdb.aspect.TransactionAttributesAspect; 10 | import org.springframework.jdbc.core.JdbcTemplate; 11 | import org.springframework.jdbc.datasource.DataSourceTransactionManager; 12 | import org.springframework.transaction.PlatformTransactionManager; 13 | import org.springframework.transaction.annotation.EnableTransactionManagement; 14 | import org.springframework.transaction.annotation.TransactionManagementConfigurer; 15 | 16 | import io.roach.bank.AdvisorOrder; 17 | import io.roach.bank.ProfileNames; 18 | 19 | @Configuration 20 | @EnableTransactionManagement(order = AdvisorOrder.TRANSACTION_ADVISOR) 21 | @Profile(ProfileNames.JDBC) 22 | public class JdbcTransactionManagerConfig implements TransactionManagementConfigurer { 23 | @Autowired 24 | private DataSource dataSource; 25 | 26 | @Bean 27 | public PlatformTransactionManager transactionManager() { 28 | DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(); 29 | transactionManager.setDataSource(dataSource); 30 | transactionManager.setGlobalRollbackOnParticipationFailure(false); 31 | transactionManager.setEnforceReadOnly(true); 32 | transactionManager.setNestedTransactionAllowed(true); 33 | transactionManager.setRollbackOnCommitFailure(false); 34 | transactionManager.setDefaultTimeout(-1); 35 | return transactionManager; 36 | } 37 | 38 | @Override 39 | public PlatformTransactionManager annotationDrivenTransactionManager() { 40 | return new DataSourceTransactionManager(dataSource); 41 | } 42 | 43 | @Bean 44 | @Profile("!(" + ProfileNames.PSQL_LOCAL + "|" + ProfileNames.PSQL_DEV + ")") 45 | public TransactionAttributesAspect transactionAttributesAspect(JdbcTemplate jdbcTemplate) { 46 | return new TransactionAttributesAspect(jdbcTemplate); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /bank-api/src/main/java/io/roach/bank/api/Region.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.api; 2 | 3 | import java.util.Set; 4 | 5 | import org.springframework.hateoas.server.core.Relation; 6 | 7 | import static io.roach.bank.api.LinkRelations.CURIE_PREFIX; 8 | import static io.roach.bank.api.LinkRelations.REGION_LIST_REL; 9 | import static io.roach.bank.api.LinkRelations.REGION_REL; 10 | 11 | @Relation(value = CURIE_PREFIX + REGION_REL, 12 | collectionRelation = CURIE_PREFIX + REGION_LIST_REL) 13 | public class Region implements Comparable { 14 | private String name; 15 | 16 | private Set cities; 17 | 18 | private String databaseRegion; 19 | 20 | private boolean primary; 21 | 22 | private boolean secondary; 23 | 24 | public boolean isPrimary() { 25 | return primary; 26 | } 27 | 28 | public Region setPrimary(boolean primary) { 29 | this.primary = primary; 30 | return this; 31 | } 32 | 33 | public boolean isSecondary() { 34 | return secondary; 35 | } 36 | 37 | public void setSecondary(boolean secondary) { 38 | this.secondary = secondary; 39 | } 40 | 41 | public String getDatabaseRegion() { 42 | return databaseRegion; 43 | } 44 | 45 | public Region setDatabaseRegion(String databaseRegion) { 46 | this.databaseRegion = databaseRegion; 47 | return this; 48 | } 49 | 50 | public String getName() { 51 | return name; 52 | } 53 | 54 | public Region setName(String name) { 55 | this.name = name; 56 | return this; 57 | } 58 | 59 | public Set getCities() { 60 | return cities; 61 | } 62 | 63 | public Region setCities(Set cities) { 64 | this.cities = cities; 65 | return this; 66 | } 67 | 68 | @Override 69 | public int compareTo(Region o) { 70 | return name.compareTo(o.name); 71 | } 72 | 73 | @Override 74 | public String toString() { 75 | return "Region{" + 76 | "name='" + name + '\'' + 77 | ", cities=" + cities + 78 | ", databaseRegion='" + databaseRegion + '\'' + 79 | ", primary=" + primary + 80 | ", secondary=" + secondary + 81 | '}'; 82 | } 83 | } 84 | 85 | -------------------------------------------------------------------------------- /bank-api/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | io.roach.bank 7 | bank-parent 8 | 2.1.1-SNAPSHOT 9 | ../pom.xml 10 | 11 | 12 | bank-api 13 | jar 14 | 15 | 16 | Domain specific REST resource definitions with semantic descriptors, including 17 | resource attributes, forms and link relations. 18 | 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-hateoas 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-validation 28 | 29 | 30 | 31 | jakarta.persistence 32 | jakarta.persistence-api 33 | provided 34 | 35 | 36 | 37 | org.junit.jupiter 38 | junit-jupiter-engine 39 | test 40 | 41 | 42 | 43 | 44 | 45 | 46 | org.springframework.boot 47 | spring-boot-maven-plugin 48 | 49 | 50 | 51 | repackage 52 | 53 | 54 | 55 | 56 | true 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Maven+gitflow release script 3 | 4 | set -e 5 | 6 | case "$OSTYPE" in 7 | darwin*) 8 | default="\x1B[0m" 9 | cyan="\x1B[36m" 10 | creeol="\r\033[K" 11 | ;; 12 | *) 13 | default="\e[0m" 14 | cyan="\e[36m" 15 | creeol="\r\033[K" 16 | ;; 17 | esac 18 | 19 | fn_print_info(){ 20 | echo -en "${creeol}[${cyan} INFO ${default}] $@" 21 | echo -en "\n" 22 | } 23 | 24 | ########################## 25 | # Filter functions 26 | ########################## 27 | 28 | # Remove any version suffixes (such as '-SNAPSHOT') 29 | fn_filter_version(){ 30 | local v=$1 31 | local cleaned=$(echo ${v} | sed -e 's/[^0-9][^0-9]*$//') 32 | local last_num=$(echo ${cleaned} | sed -e 's/[0-9]*\.//g') 33 | local next_num=$(($last_num)) 34 | echo ${v} | sed -e "s/[0-9][0-9]*\([^0-9]*\)$/$next_num/" 35 | } 36 | 37 | # Advances the last number of the given version string by one 38 | fn_advance_version(){ 39 | local v=$1 40 | local cleaned=$(echo ${v} | sed -e 's/[^0-9][^0-9]*$//') 41 | local last_num=$(echo ${cleaned} | sed -e 's/[0-9]*\.//g') 42 | local next_num=$(($last_num+1)) 43 | echo ${v} | sed -e "s/[0-9][0-9]*\([^0-9]*\)$/$next_num/" 44 | } 45 | 46 | ########################## 47 | # Release metadata 48 | ########################## 49 | 50 | fn_print_info "Extracting pom.xml project version" 51 | 52 | # The current version 53 | pomVersion=$(echo 'VERSION=${project.version}' | mvn help:evaluate | grep '^VERSION=' | sed 's/^VERSION=//g') 54 | # The version to be released 55 | releaseVersion="$(fn_filter_version ${pomVersion})" 56 | # The next development version 57 | developmentVersion="$(fn_advance_version ${pomVersion})-SNAPSHOT" 58 | 59 | fn_print_info "Git branch: $(git rev-parse --abbrev-ref HEAD)" 60 | fn_print_info "POM version is ${pomVersion}" 61 | fn_print_info "Release version: ${releaseVersion}" 62 | fn_print_info "Next development version: ${developmentVersion}" 63 | 64 | echo -en "\n" 65 | 66 | while true; do 67 | read -p "Confirm releasing version '${releaseVersion}' of this project [y/N]" yn 68 | case ${yn} in 69 | [Yy]* ) break;; 70 | [Nn]* ) echo Exiting; exit 1;; 71 | * ) echo "Please answer yes or no.";; 72 | esac 73 | done 74 | 75 | mvn --batch-mode gitflow:release \ 76 | -DreleaseVersion=${releaseVersion} \ 77 | -DdevelopmentVersion=${developmentVersion} 78 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/web/support/ZoomExpression.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.web.support; 2 | 3 | import java.util.Collections; 4 | import java.util.Iterator; 5 | import java.util.LinkedList; 6 | import java.util.List; 7 | 8 | import org.springframework.web.bind.ServletRequestUtils; 9 | import org.springframework.web.context.request.RequestContextHolder; 10 | import org.springframework.web.context.request.ServletRequestAttributes; 11 | 12 | import jakarta.servlet.http.HttpServletRequest; 13 | 14 | /** 15 | * Simple zoom expression representing a projection rule for link relations. 16 | */ 17 | public class ZoomExpression implements Iterable { 18 | private final String expression; 19 | 20 | private final List relations = new LinkedList<>(); 21 | 22 | private ZoomExpression(String expression) { 23 | this.expression = expression; 24 | } 25 | 26 | public static ZoomExpression ofCurrentRequest() { 27 | HttpServletRequest currentRequest = 28 | ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()) 29 | .getRequest(); 30 | String zoom = ServletRequestUtils.getStringParameter(currentRequest, "zoom", ""); 31 | return ZoomExpression.of(zoom); 32 | } 33 | 34 | public static ZoomExpression of(String expression) { 35 | ZoomExpression ze = new ZoomExpression(expression); 36 | String parts[] = expression.split(("\\s*,\\s*")); 37 | for (String part : parts) { 38 | ze.add(part); 39 | } 40 | return ze; 41 | } 42 | 43 | public void add(String rel) { 44 | this.relations.add(rel); 45 | } 46 | 47 | @Override 48 | public Iterator iterator() { 49 | return Collections.unmodifiableList(relations).iterator(); 50 | } 51 | 52 | public String getExpression() { 53 | return expression; 54 | } 55 | 56 | public boolean containsRel(String name) { 57 | for (String relation : relations) { 58 | if (name.equals(relation)) { 59 | return true; 60 | } 61 | } 62 | return false; 63 | } 64 | 65 | public String rel(String name) { 66 | for (String relation : relations) { 67 | if (name.equals(relation)) { 68 | return relation; 69 | } 70 | } 71 | return null; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /bank-server/src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 0 11 | 256 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /bank-server/src/test/resources/db/etc.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO account (id,city,balance,currency,name,type,closed,allow_negative,updated_at) VALUES 2 | ('ea4bde66-de55-4e3f-bc00-1b2b8fa22bfe', 'stockholm', '100.00', 'SEK', 'test:1', 'A', false, 0, clock_timestamp()), 3 | ('3f50fdd1-97a2-407c-95da-e00bf0cae97b', 'stockholm', '100.00', 'SEK', 'test:2', 'A', false, 0, clock_timestamp()), 4 | ('ea4bde66-de55-4e3f-bc00-1b2b8fa22bfc', 'stockholm', '100.00', 'SEK', 'test:3', 'L', false, 1, clock_timestamp()), 5 | ('3f50fdd1-97a2-407c-95da-e00bf0cae97d', 'stockholm', '100.00', 'SEK', 'test:4', 'L', false, 1, clock_timestamp()) 6 | ; 7 | 8 | UPDATE account SET balance = account.balance + data_table.balance, updated_at=clock_timestamp() 9 | FROM (SELECT 10 | unnest(ARRAY['ea4bde66-de55-4e3f-bc00-1b2b8fa22bfe'::UUID,'3f50fdd1-97a2-407c-95da-e00bf0cae97b'::UUID]) as id, 11 | unnest(ARRAY[50,-50]) as balance) as data_table 12 | WHERE account.id=data_table.id AND account.closed=false 13 | AND (account.balance + data_table.balance) * abs(account.allow_negative-1) >= 0; 14 | 15 | SELECT unnest(ARRAY['ea4bde66-de55-4e3f-bc00-1b2b8fa22bfe','3f50fdd1-97a2-407c-95da-e00bf0cae97b']); 16 | SELECT unnest(ARRAY[50,-50]); 17 | 18 | select id,balance,allow_negative from account 19 | where id in('ea4bde66-de55-4e3f-bc00-1b2b8fa22bfe','3f50fdd1-97a2-407c-95da-e00bf0cae97b'); 20 | 21 | update account set allow_negative=0 where id='ea4bde66-de55-4e3f-bc00-1b2b8fa22bfe'; 22 | update account set allow_negative=0 where id='3f50fdd1-97a2-407c-95da-e00bf0cae97b'; 23 | 24 | update account set balance=100.00 where id='ea4bde66-de55-4e3f-bc00-1b2b8fa22bfe'; 25 | update account set balance=100.00 where id='3f50fdd1-97a2-407c-95da-e00bf0cae97b'; 26 | 27 | -- 28 | 29 | UPDATE account SET balance = account.balance + data_table.balance, updated_at=clock_timestamp() 30 | FROM (SELECT 31 | unnest(ARRAY['ea4bde66-de55-4e3f-bc00-1b2b8fa22bfc'::UUID,'3f50fdd1-97a2-407c-95da-e00bf0cae97d'::UUID]) as id, 32 | unnest(ARRAY[50,-50]) as balance) as data_table 33 | WHERE account.id=data_table.id AND account.closed=false 34 | AND (account.balance + data_table.balance) * abs(account.allow_negative-1) >= 0; 35 | 36 | select id,balance,allow_negative from account 37 | where id in ('ea4bde66-de55-4e3f-bc00-1b2b8fa22bfc','3f50fdd1-97a2-407c-95da-e00bf0cae97d'); 38 | select sum(balance) from account 39 | where id in ('ea4bde66-de55-4e3f-bc00-1b2b8fa22bfc','3f50fdd1-97a2-407c-95da-e00bf0cae97d'); -- 200 -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/ProfileNames.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank; 2 | 3 | import org.springframework.core.env.Environment; 4 | import org.springframework.core.env.Profiles; 5 | 6 | /** 7 | * Definition of Spring profile names for the application domain. 8 | */ 9 | public abstract class ProfileNames { 10 | /** 11 | * Propagate transaction retries to clients on serialization errors. 12 | */ 13 | public static final String RETRY_NONE = "retry-none"; 14 | 15 | /** 16 | * Handle serialization errors at JDBC drive-level 17 | */ 18 | public static final String RETRY_DRIVER = "retry-driver"; 19 | 20 | /** 21 | * Handle serialization errors at app/client level 22 | */ 23 | public static final String RETRY_CLIENT = "retry-client"; 24 | 25 | /** 26 | * Adopt server-side savepoint rollback retries on serialization errors. 27 | * JDBC only. 28 | */ 29 | public static final String RETRY_SAVEPOINT = "retry-savepoint"; 30 | 31 | /** 32 | * Use filesystem paths for Thymeleaf templates. 33 | */ 34 | public static final String DEBUG = "debug"; 35 | 36 | public static final String DEMO = "demo"; 37 | 38 | public static final String DEFAULT = "default"; 39 | 40 | /** 41 | * Enable transactional outbox pattern. 42 | */ 43 | public static final String OUTBOX = "outbox"; 44 | 45 | /** 46 | * Enable JPA repositories over JDBC. 47 | */ 48 | public static final String JPA = "jpa"; 49 | 50 | /** 51 | * Enable JDBC repositories. 52 | */ 53 | public static final String JDBC = "!jpa"; 54 | 55 | /** 56 | * crdb-jdbc driver local. 57 | */ 58 | public static final String CRDB_LOCAL = "crdb-local"; 59 | 60 | /** 61 | * crdb-jdbc driver dev. 62 | */ 63 | public static final String CRDB_DEV = "crdb-dev"; 64 | 65 | /** 66 | * pg-jdbc driver dev. 67 | */ 68 | public static final String PGJDBC_DEV = "pgjdbc-dev"; 69 | 70 | /** 71 | * PostgreSQL local 72 | */ 73 | public static final String PSQL_LOCAL = "psql-local"; 74 | 75 | /** 76 | * PostgreSQL dev 77 | */ 78 | public static final String PSQL_DEV = "psql-dev"; 79 | 80 | private ProfileNames() { 81 | } 82 | 83 | public static boolean acceptsPostgresSQL(Environment environment) { 84 | return environment.acceptsProfiles(Profiles.of(ProfileNames.PSQL_LOCAL, ProfileNames.PSQL_DEV)); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /bank-api/src/main/java/io/roach/bank/api/AccountForm.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.api; 2 | 3 | import jakarta.validation.constraints.NotBlank; 4 | import jakarta.validation.constraints.NotNull; 5 | import jakarta.validation.constraints.Size; 6 | 7 | import org.springframework.hateoas.RepresentationModel; 8 | import org.springframework.hateoas.server.core.Relation; 9 | 10 | import com.fasterxml.jackson.annotation.JsonInclude; 11 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 12 | 13 | import io.roach.bank.api.support.EnumPattern; 14 | 15 | import static io.roach.bank.api.LinkRelations.ACCOUNT_ONE_FORM_REL; 16 | import static io.roach.bank.api.LinkRelations.CURIE_PREFIX; 17 | 18 | @Relation(value = CURIE_PREFIX + ACCOUNT_ONE_FORM_REL) 19 | @JsonPropertyOrder({"links"}) 20 | @JsonInclude(JsonInclude.Include.NON_NULL) 21 | public class AccountForm extends RepresentationModel { 22 | @NotNull 23 | private String uuid = "auto"; 24 | 25 | @NotNull 26 | @Size(min = 2) 27 | private String city; 28 | 29 | @NotBlank 30 | private String name; 31 | 32 | @NotNull 33 | @Size(min = 2) 34 | private String currencyCode; 35 | 36 | private String description; 37 | 38 | @NotNull 39 | @EnumPattern(regexp = "EXPENSE|REVENUE|LIABILITY|ASSET", message = "invalid account type") 40 | private AccountType accountType; 41 | 42 | public AccountType getAccountType() { 43 | return accountType; 44 | } 45 | 46 | public void setAccountType(AccountType accountType) { 47 | this.accountType = accountType; 48 | } 49 | 50 | public String getDescription() { 51 | return description; 52 | } 53 | 54 | public void setDescription(String description) { 55 | this.description = description; 56 | } 57 | 58 | public String getUuid() { 59 | return uuid; 60 | } 61 | 62 | public void setUuid(String uuid) { 63 | this.uuid = uuid; 64 | } 65 | 66 | public String getCity() { 67 | return city; 68 | } 69 | 70 | public void setCity(String city) { 71 | this.city = city; 72 | } 73 | 74 | public String getName() { 75 | return name; 76 | } 77 | 78 | public void setName(String name) { 79 | this.name = name; 80 | } 81 | 82 | public String getCurrencyCode() { 83 | return currencyCode; 84 | } 85 | 86 | public void setCurrencyCode(String currencyCode) { 87 | this.currencyCode = currencyCode; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /bank-client/src/main/java/io/roach/bank/client/support/DurationFormat.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.client.support; 2 | 3 | import java.time.Duration; 4 | import java.util.Locale; 5 | import java.util.regex.Matcher; 6 | import java.util.regex.Pattern; 7 | 8 | public abstract class DurationFormat { 9 | private static final Pattern DURATION_PATTERN = Pattern.compile("([0-9]+)([smhdw])"); 10 | 11 | private DurationFormat() { 12 | } 13 | 14 | public static Duration parseDuration(String duration) { 15 | Matcher matcher = DURATION_PATTERN.matcher(duration.toLowerCase(Locale.ENGLISH)); 16 | Duration instant = Duration.ZERO; 17 | while (matcher.find()) { 18 | int ordinal = Integer.parseInt(matcher.group(1)); 19 | String token = matcher.group(2); 20 | switch (token) { 21 | case "s": 22 | instant = instant.plus(Duration.ofSeconds(ordinal)); 23 | break; 24 | case "m": 25 | instant = instant.plus(Duration.ofMinutes(ordinal)); 26 | break; 27 | case "h": 28 | instant = instant.plus(Duration.ofHours(ordinal)); 29 | break; 30 | case "d": 31 | instant = instant.plus(Duration.ofDays(ordinal)); 32 | break; 33 | case "w": 34 | instant = instant.plus(Duration.ofDays(ordinal * 7L)); 35 | break; 36 | default: 37 | throw new IllegalArgumentException("Invalid token " + token); 38 | } 39 | } 40 | if (instant.equals(Duration.ZERO)) { 41 | return Duration.ofSeconds(Integer.parseInt(duration)); 42 | } 43 | return instant; 44 | } 45 | 46 | public static String millisecondsToDisplayString(long timeMillis) { 47 | double seconds = (timeMillis / 1000.0) % 60; 48 | int minutes = (int) ((timeMillis / 60000) % 60); 49 | int hours = (int) ((timeMillis / 3600000)); 50 | 51 | StringBuilder sb = new StringBuilder(); 52 | if (hours > 0) { 53 | sb.append(String.format("%dh", hours)); 54 | } 55 | if (hours > 0 || minutes > 0) { 56 | sb.append(String.format("%dm", minutes)); 57 | } 58 | if (hours == 0 && seconds > 0) { 59 | sb.append(String.format(Locale.US, "%.1fs", seconds)); 60 | } 61 | return sb.toString(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/repository/jpa/JpaTransactionRepositoryAdapter.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.repository.jpa; 2 | 3 | import java.util.UUID; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.context.annotation.Profile; 7 | import org.springframework.data.domain.Page; 8 | import org.springframework.data.domain.Pageable; 9 | import org.springframework.stereotype.Repository; 10 | import org.springframework.transaction.annotation.Propagation; 11 | import org.springframework.transaction.annotation.Transactional; 12 | 13 | import io.roach.bank.ProfileNames; 14 | import io.roach.bank.domain.Transaction; 15 | import io.roach.bank.domain.TransactionItem; 16 | import io.roach.bank.repository.TransactionRepository; 17 | 18 | @Repository 19 | @Transactional(propagation = Propagation.MANDATORY) 20 | @Profile(ProfileNames.JPA) 21 | public class JpaTransactionRepositoryAdapter implements TransactionRepository { 22 | @Autowired 23 | private TransactionJpaRepository transactionRepository; 24 | 25 | @Autowired 26 | private TransactionItemJpaRepository itemRepository; 27 | 28 | @Override 29 | public Transaction createTransaction(Transaction transaction) { 30 | Transaction t = transactionRepository.save(transaction); 31 | transaction.getItems().forEach(transactionItem -> transactionItem.getId().setTransactionId(t.getId())); 32 | itemRepository.saveAll(transaction.getItems()); 33 | return t; 34 | } 35 | 36 | @Override 37 | public Transaction findTransactionById(UUID id) { 38 | return transactionRepository.findById(id).orElse(null); 39 | } 40 | 41 | @Override 42 | public Transaction findTransactionById(UUID id, String city) { 43 | return transactionRepository.findByIdAndCity(id, city).orElse(null); 44 | } 45 | 46 | @Override 47 | public Page findTransactions(Pageable pageable) { 48 | return transactionRepository.findAll(pageable); 49 | } 50 | 51 | @Override 52 | public TransactionItem findTransactionItemById(TransactionItem.Id id) { 53 | return itemRepository.getReferenceById(id); 54 | } 55 | 56 | @Override 57 | public Page findTransactionItems(UUID id, Pageable pageable) { 58 | return itemRepository.findById(id, pageable); 59 | } 60 | 61 | @Override 62 | public void deleteAll() { 63 | itemRepository.deleteAllInBatch(); 64 | transactionRepository.deleteAllInBatch(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /bank-api/src/main/java/io/roach/bank/api/support/CockroachFacts.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.api.support; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | 6 | public abstract class CockroachFacts { 7 | public static final List FACTS = Arrays.asList( 8 | "A cockroach can live for a week without its head. Due to their open circulatory system, and the fact that they breathe through little holes in each of their body segments, " 9 | + "they are not dependent on the mouth or head to breathe. The roach only dies because without a mouth, it can't drink water and dies of thirst.", 10 | "A cockroach can hold its breath for 40 minutes, and can even survive being submerged under water for half an hour. They hold their breath often to help regulate their loss of water.", 11 | "Cockroaches can run up to three miles in an hour, which means they can spread germs and bacteria throughout a home very quickly.", 12 | "Newborn German cockroaches become adults in as little as 36 days. In fact, the German cockroach is the most common of the cockroaches and has been implicated in outbreaks of illness and allergic reactions in many people.", 13 | "A one-day-old baby cockroach, which is about the size of a speck of dust, can run almost as fast as its parents.", 14 | "The American cockroach has shown a marked attraction to alcoholic beverages, especially beer. They are most likely attracted by the alcohol mixed with hops and sugar.", 15 | "The world's largest cockroach (which lives in South America) is six inches long with a one-foot wingspan.", 16 | "Cockroaches are believed to have originated more than 280 million years ago, in the Carboniferous era.", 17 | "There are more than 4,000 species of cockroaches worldwide, including the most common species, the German cockroach, in addition to other common species, the brownbanded cockroach and American cockroach.", 18 | "Because they are cold-blooded insects, cockroaches can live without food for one month, but will only survive one week without water.", 19 | "Cockroaches can eat anything.", 20 | "Some cockroaches can grow as long as 3 inches.", 21 | "Roaches can live up to a week without their head.", 22 | "Cockroaches can survive immense nuclear radiation." 23 | ); 24 | 25 | public static String nextFact() { 26 | return RandomData.selectRandom(FACTS); 27 | } 28 | 29 | public static String nextFact(int limit) { 30 | String fact = RandomData.selectRandom(FACTS); 31 | return fact.substring(0, Math.min(limit, fact.length())); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /bank-client/src/main/java/io/roach/bank/client/ClientApplication.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.client; 2 | 3 | import org.jline.utils.AttributedString; 4 | import org.jline.utils.AttributedStyle; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.WebApplicationType; 7 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 8 | import org.springframework.boot.builder.SpringApplicationBuilder; 9 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 10 | import org.springframework.context.annotation.ComponentScan; 11 | import org.springframework.context.annotation.Configuration; 12 | import org.springframework.context.event.EventListener; 13 | import org.springframework.shell.jline.PromptProvider; 14 | 15 | import io.roach.bank.client.event.ClearErrorsEvent; 16 | import io.roach.bank.client.event.ConnectionUpdatedEvent; 17 | import io.roach.bank.client.event.ExecutionErrorEvent; 18 | import io.roach.bank.client.support.Console; 19 | 20 | @Configuration 21 | @EnableAutoConfiguration 22 | @EnableConfigurationProperties 23 | @ComponentScan(basePackages = "io.roach.bank.client") 24 | public class ClientApplication implements PromptProvider { 25 | @Autowired 26 | private Console console; 27 | 28 | private transient String connection; 29 | 30 | private transient boolean errors; 31 | 32 | public static void main(String[] args) { 33 | new SpringApplicationBuilder(ClientApplication.class) 34 | .web(WebApplicationType.NONE) 35 | .headless(false) 36 | .logStartupInfo(true) 37 | .run(args); 38 | } 39 | 40 | @Override 41 | public AttributedString getPrompt() { 42 | if (connection != null) { 43 | return new AttributedString(connection 44 | + (errors ? " (ERROR)" : "") + ":$ ", 45 | AttributedStyle.DEFAULT.foreground(errors ? AttributedStyle.RED : AttributedStyle.GREEN)); 46 | } else { 47 | return new AttributedString("disconnected" 48 | + (errors ? " (ERROR)" : "") + ":$ ", 49 | AttributedStyle.DEFAULT.foreground(errors ? AttributedStyle.RED : AttributedStyle.YELLOW)); 50 | } 51 | } 52 | 53 | @EventListener 54 | public void handle(ConnectionUpdatedEvent event) { 55 | this.connection = event.getBaseUri().getHost(); 56 | } 57 | 58 | @EventListener 59 | public void handle(ExecutionErrorEvent event) { 60 | errors = true; 61 | } 62 | 63 | @EventListener 64 | public void handle(ClearErrorsEvent event) { 65 | console.success("Errors cleared"); 66 | errors = false; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/web/transaction/TransactionItemController.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.web.transaction; 2 | 3 | import java.util.UUID; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.data.cockroachdb.annotations.Retryable; 7 | import org.springframework.data.cockroachdb.annotations.TimeTravel; 8 | import org.springframework.data.cockroachdb.annotations.TimeTravelMode; 9 | import org.springframework.data.cockroachdb.annotations.TransactionBoundary; 10 | import org.springframework.data.domain.Page; 11 | import org.springframework.data.domain.Pageable; 12 | import org.springframework.data.web.PageableDefault; 13 | import org.springframework.data.web.PagedResourcesAssembler; 14 | import org.springframework.hateoas.PagedModel; 15 | import org.springframework.web.bind.annotation.GetMapping; 16 | import org.springframework.web.bind.annotation.PathVariable; 17 | import org.springframework.web.bind.annotation.RequestMapping; 18 | import org.springframework.web.bind.annotation.RestController; 19 | 20 | import io.roach.bank.api.TransactionItemModel; 21 | import io.roach.bank.domain.TransactionItem; 22 | import io.roach.bank.service.TransactionService; 23 | 24 | @RestController 25 | @RequestMapping(value = "/api/transaction/item") 26 | public class TransactionItemController { 27 | @Autowired 28 | private TransactionService bankService; 29 | 30 | @Autowired 31 | private TransactionItemResourceAssembler transactionItemResourceAssembler; 32 | 33 | @Autowired 34 | private PagedResourcesAssembler transactionItemPagedResourcesAssembler; 35 | 36 | @GetMapping(value = "/{transactionId}") 37 | @TransactionBoundary(timeTravel = @TimeTravel(mode = TimeTravelMode.FOLLOWER_READ), readOnly = true) 38 | @Retryable 39 | public PagedModel getTransactionItems( 40 | @PathVariable("transactionId") UUID transactionId, 41 | @PageableDefault(size = 5) Pageable page) { 42 | Page entities = bankService.findItemsByTransactionId(transactionId, page); 43 | return transactionItemPagedResourcesAssembler 44 | .toModel(entities, transactionItemResourceAssembler); 45 | } 46 | 47 | @GetMapping(value = "/{transactionId}/{accountId}") 48 | @TransactionBoundary(timeTravel = @TimeTravel(mode = TimeTravelMode.FOLLOWER_READ), readOnly = true) 49 | @Retryable 50 | public TransactionItemModel getTransactionLeg( 51 | @PathVariable("transactionId") UUID transactionId, 52 | @PathVariable("accountId") UUID accountId) { 53 | return transactionItemResourceAssembler.toModel(bankService.findItemById(transactionId, accountId)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /deploy/common/core_util.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Colorful console experience 3 | 4 | case "$OSTYPE" in 5 | darwin*) 6 | default="\x1B[0m" 7 | red="\x1B[31m" 8 | green="\x1B[32m" 9 | lightyellow="\x1B[93m" 10 | lightblue="\x1B[94m" 11 | cyan="\x1B[36m" 12 | lightcyan="\x1B[96m" 13 | creeol="\r\033[K" 14 | ;; 15 | *) 16 | default="\e[0m" 17 | red="\e[31m" 18 | green="\e[32m" 19 | lightyellow="\e[93m" 20 | lightblue="\e[94m" 21 | cyan="\e[36m" 22 | lightcyan="\e[96m" 23 | creeol="\r\033[K" 24 | ;; 25 | esac 26 | 27 | fn_sleep_time(){ 28 | sleep 0.5 29 | } 30 | 31 | fn_echo_info_nl(){ 32 | echo -en "${creeol}[${cyan} INFO ${default}] $*" 33 | fn_sleep_time 34 | echo -en "\n" 35 | } 36 | 37 | fn_echo_fail(){ 38 | echo -en "${creeol}[${red} FAIL ${default}] $*" 39 | fn_sleep_time 40 | } 41 | 42 | fn_echo_fail_nl(){ 43 | echo -en "${creeol}[${red} FAIL ${default}] $*" 44 | fn_sleep_time 45 | echo -en "\n" 46 | } 47 | 48 | fn_echo_dryrun_nl(){ 49 | echo -en "[DRYRUN] $*" 50 | echo -en "\n" 51 | } 52 | 53 | fn_echo_warning(){ 54 | echo -en "${lightyellow}Warning!${default} $*" 55 | fn_sleep_time 56 | } 57 | 58 | fn_echo_header(){ 59 | echo -e "" 60 | echo -e "${lightyellow}${title} ${default}" 61 | echo -e "==========================================${default}" 62 | } 63 | 64 | fn_prompt_yes_no(){ 65 | local prompt="$1" 66 | local initial="$2" 67 | 68 | if [ "${initial}" == "Y" ]; then 69 | prompt+=" [Y/n] " 70 | elif [ "${initial}" == "N" ]; then 71 | prompt+=" [y/N] " 72 | else 73 | prompt+=" [y/n] " 74 | fi 75 | 76 | while true; do 77 | read -e -p "${prompt}" -r yn 78 | case "${yn}" in 79 | [Yy]|[Yy][Ee][Ss]) return 0 ;; 80 | [Nn]|[Nn][Oo]) return 1 ;; 81 | *) echo -e "Please answer yes or no." ;; 82 | esac 83 | done 84 | } 85 | 86 | fn_failcheck(){ 87 | if [ "${dryrun}" == "on" ]; then 88 | fn_echo_dryrun_nl "$@" 89 | sleep 1 90 | else 91 | "$@" 92 | local status=$? 93 | if [ ${status} -ne 0 ]; then 94 | fn_echo_fail_nl "$@" >&2 95 | exit 1 96 | fi 97 | return ${status} 98 | fi 99 | } 100 | 101 | fn_open_url(){ 102 | case "$OSTYPE" in 103 | darwin*) 104 | open "$@" 105 | ;; 106 | linux*) 107 | if [ -n $BROWSER ]; then 108 | $BROWSER "$@" 109 | else 110 | fn_echo_fail_nl "Could not detect web browser to use." 111 | fi 112 | ;; 113 | *) 114 | fn_echo_fail_nl "Unknown OS: $OSTYPE" 115 | exit 1 116 | ;; 117 | esac 118 | } 119 | 120 | -------------------------------------------------------------------------------- /bank-api/src/main/java/io/roach/bank/api/TransactionModel.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.api; 2 | 3 | import java.time.LocalDate; 4 | 5 | import org.springframework.hateoas.CollectionModel; 6 | import org.springframework.hateoas.RepresentationModel; 7 | import org.springframework.hateoas.server.core.Relation; 8 | 9 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 10 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 11 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 12 | 13 | import io.roach.bank.api.support.LocalDateDeserializer; 14 | import io.roach.bank.api.support.LocalDateSerializer; 15 | 16 | import static io.roach.bank.api.LinkRelations.CURIE_PREFIX; 17 | 18 | /** 19 | * Describes a transaction leg/item resource representation. A transaction leg 20 | * represents a monetary, balanced, multi-legged transaction between two 21 | * or more accounts. 22 | */ 23 | @Relation(value = CURIE_PREFIX + LinkRelations.TRANSACTION_REL, 24 | collectionRelation = CURIE_PREFIX + LinkRelations.TRANSACTION_LIST_REL) 25 | @JsonPropertyOrder({"links", "transactionItems"}) 26 | public class TransactionModel extends RepresentationModel { 27 | private String city; 28 | 29 | private String transactionType; 30 | 31 | @JsonDeserialize(using = LocalDateDeserializer.class) 32 | @JsonSerialize(using = LocalDateSerializer.class) 33 | private LocalDate bookingDate; 34 | 35 | @JsonDeserialize(using = LocalDateDeserializer.class) 36 | @JsonSerialize(using = LocalDateSerializer.class) 37 | private LocalDate transactionDate; 38 | 39 | private CollectionModel transactionItems; 40 | 41 | public String getTransactionType() { 42 | return transactionType; 43 | } 44 | 45 | public void setTransactionType(String transactionType) { 46 | this.transactionType = transactionType; 47 | } 48 | 49 | public String getCity() { 50 | return city; 51 | } 52 | 53 | public TransactionModel setCity(String city) { 54 | this.city = city; 55 | return this; 56 | } 57 | 58 | public LocalDate getBookingDate() { 59 | return bookingDate; 60 | } 61 | 62 | public void setBookingDate(LocalDate bookingDate) { 63 | this.bookingDate = bookingDate; 64 | } 65 | 66 | public LocalDate getTransactionDate() { 67 | return transactionDate; 68 | } 69 | 70 | public void setTransactionDate(LocalDate transactionDate) { 71 | this.transactionDate = transactionDate; 72 | } 73 | 74 | public CollectionModel getTransactionItems() { 75 | return transactionItems; 76 | } 77 | 78 | public void setTransactionItems(CollectionModel transactionItems) { 79 | this.transactionItems = transactionItems; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /bank-client/src/main/java/io/roach/bank/client/Balance.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.client; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Map; 6 | 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.hateoas.Link; 9 | import org.springframework.shell.standard.ShellCommandGroup; 10 | import org.springframework.shell.standard.ShellComponent; 11 | import org.springframework.shell.standard.ShellMethod; 12 | import org.springframework.shell.standard.ShellMethodAvailability; 13 | import org.springframework.shell.standard.ShellOption; 14 | 15 | import io.roach.bank.api.AccountModel; 16 | import io.roach.bank.api.LinkRelations; 17 | import io.roach.bank.api.support.RandomData; 18 | import io.roach.bank.client.support.AsyncHelper; 19 | import io.roach.bank.client.support.DurationFormat; 20 | import io.roach.bank.client.support.HypermediaClient; 21 | 22 | import static io.roach.bank.api.LinkRelations.withCurie; 23 | 24 | @ShellComponent 25 | @ShellCommandGroup(Constants.WORKLOAD_COMMANDS) 26 | public class Balance extends AbstractCommand { 27 | @Autowired 28 | private HypermediaClient bankClient; 29 | 30 | @Autowired 31 | private AsyncHelper asyncHelper; 32 | 33 | @ShellMethod(value = "Read account balance", key = {"b", "balance"}) 34 | @ShellMethodAvailability(Constants.CONNECTED_CHECK) 35 | public void balance( 36 | @ShellOption(help = "use follower reads", defaultValue = "false") boolean followerReads, 37 | @ShellOption(help = Constants.ACCOUNT_LIMIT_HELP, defaultValue = Constants.DEFAULT_ACCOUNT_LIMIT) 38 | int limit, 39 | @ShellOption(help = Constants.REGIONS_HELP, 40 | defaultValue = Constants.DEFAULT_REGION, 41 | valueProvider = RegionProvider.class) String region, 42 | @ShellOption(help = Constants.DURATION_HELP, defaultValue = Constants.DEFAULT_DURATION) String duration 43 | ) { 44 | logger.info("Find max %d accounts per city in region [%s]".formatted(limit, region)); 45 | 46 | Map> accounts = bankClient.getTopAccounts(region, limit); 47 | 48 | accounts.forEach((city, accountModels) -> { 49 | final List links = new ArrayList<>(); 50 | 51 | accountModels.forEach(accountModel -> links.add(accountModel.getLink( 52 | followerReads 53 | ? withCurie(LinkRelations.ACCOUNT_BALANCE_SNAPSHOT_REL) 54 | : withCurie(LinkRelations.ACCOUNT_BALANCE_REL)) 55 | .get())); 56 | 57 | asyncHelper.runAsync(city + " (" + region + ")", 58 | () -> bankClient.get(RandomData.selectRandom(links)), 59 | DurationFormat.parseDuration(duration)); 60 | }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/web/account/AccountResourceAssembler.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.web.account; 2 | 3 | import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport; 4 | import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder; 5 | import org.springframework.stereotype.Component; 6 | 7 | import io.roach.bank.api.AccountModel; 8 | import io.roach.bank.api.AccountStatus; 9 | import io.roach.bank.api.LinkRelations; 10 | import io.roach.bank.domain.Account; 11 | 12 | import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; 13 | import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; 14 | 15 | @Component 16 | public class AccountResourceAssembler 17 | extends RepresentationModelAssemblerSupport { 18 | public AccountResourceAssembler() { 19 | super(AccountController.class, AccountModel.class); 20 | } 21 | 22 | @Override 23 | public AccountModel toModel(Account entity) { 24 | AccountModel resource = new AccountModel(); 25 | resource.setId(entity.getId()); 26 | resource.setCity(entity.getCity()); 27 | resource.setName(entity.getName()); 28 | resource.setBalance(entity.getBalance()); 29 | resource.setUpdatedAt(entity.getUpdatedAt()); 30 | resource.setStatus(entity.isClosed() ? AccountStatus.CLOSED : AccountStatus.OPEN); 31 | resource.setAllowNegativeBalance(entity.getAllowNegative() > 0); 32 | resource.setDescription(entity.getDescription()); 33 | resource.setAccountType(entity.getAccountType()); 34 | 35 | resource.add(linkTo(methodOn(AccountController.class) 36 | .getAccount(entity.getId())) 37 | .withSelfRel()); 38 | resource.add(linkTo(WebMvcLinkBuilder.methodOn(AccountController.class) 39 | .getAccountBalance(entity.getId())) 40 | .withRel(LinkRelations.ACCOUNT_BALANCE_REL) 41 | .withTitle("Account balance") 42 | ); 43 | resource.add(linkTo(WebMvcLinkBuilder.methodOn(AccountController.class) 44 | .getAccountBalanceSnapshot(entity.getId())) 45 | .withRel(LinkRelations.ACCOUNT_BALANCE_SNAPSHOT_REL) 46 | .withTitle("Account balance snapshot (follower read)") 47 | ); 48 | 49 | if (entity.isClosed()) { 50 | resource.add(linkTo(methodOn(AccountController.class) 51 | .openAccount(entity.getId()) 52 | ).withRel(LinkRelations.OPEN_REL) 53 | .withTitle("Open account")); 54 | } else { 55 | resource.add(linkTo(methodOn(AccountController.class) 56 | .closeAccount(entity.getId()) 57 | ).withRel(LinkRelations.CLOSE_REL) 58 | .withTitle("Close account")); 59 | } 60 | 61 | return resource; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /run-server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | case "$OSTYPE" in 6 | darwin*) 7 | default="\x1B[0m" 8 | cyan="\x1B[36m" 9 | lightblue="\x1B[94m" 10 | magenta="\x1B[35m" 11 | creeol="\r\033[K" 12 | ;; 13 | *) 14 | default="\e[0m" 15 | cyan="\e[36m" 16 | lightblue="\e[94m" 17 | magenta="\e[35m" 18 | creeol="\r\033[K" 19 | ;; 20 | esac 21 | 22 | fn_print_cyan(){ 23 | echo -en "${creeol}${cyan}$@${default}" 24 | echo -en "\n" 25 | } 26 | 27 | fn_print_blue(){ 28 | echo -en "${creeol}${lightblue}$@${default}" 29 | echo -en "\n" 30 | } 31 | 32 | ######################################## 33 | 34 | basedir=. 35 | jarfile=${basedir}/bank-server/target/bank-server.jar 36 | 37 | if [ ! -f "$jarfile" ]; then 38 | ./mvnw clean install 39 | fi 40 | 41 | ######################################## 42 | 43 | PS3='Please select datasource: ' 44 | options=("" "pgjdbc-dev" "crdb-dev" "crdb-local" "psql-dev" "psql-local") 45 | 46 | select option in "${options[@]}"; do 47 | case $option in 48 | "") 49 | break 50 | ;; 51 | *) 52 | db_option=$option 53 | fn_print_cyan "Selected profile: $option" 54 | break 55 | ;; 56 | esac 57 | done 58 | 59 | ######################################## 60 | 61 | PS3='Please select retry strategy: ' 62 | options=("" "retry-driver" "retry-savepoint" "retry-none") 63 | 64 | select option in "${options[@]}"; do 65 | case $option in 66 | "") 67 | break 68 | ;; 69 | *) 70 | retry_option=$option 71 | fn_print_cyan "Selected profile: $option" 72 | break 73 | ;; 74 | esac 75 | done 76 | 77 | ######################################## 78 | 79 | PS3='Please select optional profile(s) or 1) to start: ' 80 | options=("" "demo" "jpa" "outbox" "debug" "verbose" ) 81 | 82 | select option in "${options[@]}"; do 83 | case $option in 84 | "") 85 | break 86 | ;; 87 | *) 88 | if [ -n "${extra_option}" ]; then 89 | extra_option=$extra_option,$option 90 | else 91 | extra_option=$option 92 | fi 93 | fn_print_cyan "Selected profiles: $option" 94 | ;; 95 | esac 96 | done 97 | 98 | ######################################## 99 | 100 | function join_by { 101 | local d=${1-} f=${2-} 102 | if shift 2; then 103 | printf %s "$f" "${@/#/$d}" 104 | fi 105 | } 106 | 107 | profiles=$(join_by , $db_option $retry_option $extra_option) 108 | 109 | if [ -n "${profiles}" ]; then 110 | echo java -jar ${jarfile} --spring.profiles.active=$profiles "$@" 111 | sleep 3 112 | java -jar ${jarfile} --spring.profiles.active=$profiles "$@" 113 | else 114 | echo java -jar ${jarfile} "$@" 115 | sleep 3 116 | java -jar ${jarfile} "$@" 117 | fi 118 | 119 | -------------------------------------------------------------------------------- /bank-client/src/main/java/io/roach/bank/client/Report.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.client; 2 | 3 | import java.util.HashMap; 4 | import java.util.List; 5 | import java.util.Map; 6 | 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.shell.standard.ShellCommandGroup; 10 | import org.springframework.shell.standard.ShellComponent; 11 | import org.springframework.shell.standard.ShellMethod; 12 | import org.springframework.shell.standard.ShellMethodAvailability; 13 | import org.springframework.shell.standard.ShellOption; 14 | 15 | import io.roach.bank.api.LinkRelations; 16 | import io.roach.bank.client.support.HypermediaClient; 17 | 18 | import static io.roach.bank.api.LinkRelations.ACCOUNT_SUMMARY_REL; 19 | import static io.roach.bank.api.LinkRelations.REPORTING_REL; 20 | import static io.roach.bank.api.LinkRelations.TRANSACTION_SUMMARY_REL; 21 | 22 | @ShellComponent 23 | @ShellCommandGroup(Constants.REPORTING_COMMANDS) 24 | public class Report extends AbstractCommand { 25 | @Autowired 26 | private HypermediaClient bankClient; 27 | 28 | @ShellMethod(value = "Report account summary", key = {"report-accounts", "ra"}) 29 | @ShellMethodAvailability(Constants.CONNECTED_CHECK) 30 | public void reportAccounts(@ShellOption(help = Constants.REGIONS_HELP, 31 | defaultValue = Constants.DEFAULT_REGION, 32 | valueProvider = RegionProvider.class) String region 33 | ) { 34 | final Map parameters = new HashMap<>(); 35 | parameters.put("region", region); 36 | 37 | ResponseEntity accountSummary = bankClient.fromRoot() 38 | .follow(LinkRelations.withCurie(REPORTING_REL)) 39 | .follow(LinkRelations.withCurie(ACCOUNT_SUMMARY_REL)) 40 | .withTemplateParameters(parameters) 41 | .toEntity(List.class); 42 | 43 | accountSummary.getBody().forEach(item -> console.info("%s", item)); 44 | } 45 | 46 | @ShellMethod(value = "Report transaction summary", key = {"report-transactions", "rt"}) 47 | @ShellMethodAvailability(Constants.CONNECTED_CHECK) 48 | public void reportTransactions(@ShellOption(help = Constants.REGIONS_HELP, 49 | defaultValue = Constants.DEFAULT_REGION, 50 | valueProvider = RegionProvider.class) String region 51 | ) { 52 | final Map parameters = new HashMap<>(); 53 | parameters.put("region", region); 54 | 55 | ResponseEntity transactionSummary = bankClient.fromRoot() 56 | .follow(LinkRelations.withCurie(REPORTING_REL)) 57 | .follow(LinkRelations.withCurie(TRANSACTION_SUMMARY_REL)) 58 | .withTemplateParameters(parameters) 59 | .toEntity(List.class); 60 | 61 | transactionSummary.getBody().forEach(item -> console.info("%s", item)); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/config/ClientSideRetryConfig.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.config; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.context.annotation.Profile; 9 | import org.springframework.core.env.Environment; 10 | import org.springframework.core.env.Profiles; 11 | import org.springframework.data.cockroachdb.aspect.TransactionRetryAspect; 12 | import org.springframework.util.Assert; 13 | 14 | import io.micrometer.core.instrument.Counter; 15 | import io.micrometer.core.instrument.MeterRegistry; 16 | import io.micrometer.core.instrument.Timer; 17 | import io.roach.bank.ProfileNames; 18 | import jakarta.annotation.PostConstruct; 19 | 20 | /** 21 | * Transaction management with retries and exponential backoff handled at JDBC Driver level. 22 | */ 23 | @Configuration 24 | @Profile({ProfileNames.RETRY_CLIENT}) 25 | public class ClientSideRetryConfig { 26 | @Autowired 27 | private Environment environment; 28 | 29 | @Autowired 30 | private MeterRegistry meterRegistry; 31 | 32 | private Counter retryEvents; 33 | 34 | private Counter retryCalls; 35 | 36 | private Timer retryTime; 37 | 38 | @PostConstruct 39 | public void checkProfiles() { 40 | Assert.isTrue(!environment.acceptsProfiles(Profiles.of(ProfileNames.RETRY_NONE)), 41 | "Cant have both RETRY_CLIENT and RETRY_NONE"); 42 | Assert.isTrue(!environment.acceptsProfiles(Profiles.of(ProfileNames.RETRY_SAVEPOINT)), 43 | "Cant have both RETRY_CLIENT and RETRY_SAVEPOINT"); 44 | Assert.isTrue(!environment.acceptsProfiles(Profiles.of(ProfileNames.RETRY_DRIVER)), 45 | "Cant have both RETRY_CLIENT and RETRY_DRIVER"); 46 | 47 | this.retryEvents = Counter.builder("roach.bank.retries.event") 48 | .description("Number of transient error events") 49 | .register(meterRegistry); 50 | 51 | this.retryCalls = Counter.builder("roach.bank.retries.call") 52 | .description("Number of retry calls (closed loop cycles)") 53 | .register(meterRegistry); 54 | 55 | this.retryTime = Timer.builder("roach.bank.retries.time") 56 | .description("Time spent in retry wait loops") 57 | .register(meterRegistry); 58 | } 59 | 60 | @Bean 61 | public TransactionRetryAspect transactionRetryAspect() { 62 | TransactionRetryAspect retryAspect = new TransactionRetryAspect(); 63 | retryAspect.setRetryEventConsumer(retryEvent -> { 64 | this.retryEvents.increment(1); 65 | this.retryCalls.increment(retryEvent.getNumCalls()); 66 | this.retryTime.record(retryEvent.getElapsedTime().toMillis(), TimeUnit.MILLISECONDS); 67 | }); 68 | return retryAspect; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /bank-client/src/main/java/io/roach/bank/client/config/HypermediaConfig.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.client.config; 2 | 3 | import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; 4 | import org.apache.hc.client5.http.impl.classic.HttpClients; 5 | import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; 6 | import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; 7 | import org.apache.hc.core5.http.io.SocketConfig; 8 | import org.apache.hc.core5.pool.PoolConcurrencyPolicy; 9 | import org.apache.hc.core5.pool.PoolReusePolicy; 10 | import org.apache.hc.core5.util.Timeout; 11 | import org.springframework.beans.factory.annotation.Value; 12 | import org.springframework.boot.web.client.RestTemplateBuilder; 13 | import org.springframework.boot.web.client.RestTemplateCustomizer; 14 | import org.springframework.context.annotation.Bean; 15 | import org.springframework.context.annotation.Configuration; 16 | import org.springframework.hateoas.config.EnableHypermediaSupport; 17 | import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; 18 | import org.springframework.web.client.RestTemplate; 19 | 20 | import io.roach.bank.client.support.HypermediaClient; 21 | 22 | @Configuration 23 | @EnableHypermediaSupport(type = { 24 | EnableHypermediaSupport.HypermediaType.HAL_FORMS, EnableHypermediaSupport.HypermediaType.HAL 25 | }) 26 | public class HypermediaConfig implements RestTemplateCustomizer { 27 | @Value("${roachbank.http.maxTotal}") 28 | private int maxTotal; 29 | 30 | @Value("${roachbank.http.maxConnPerRoute}") 31 | private int maxConnPerRoute; 32 | 33 | @Bean 34 | public RestTemplate restTemplate(RestTemplateBuilder builder) { 35 | return builder.build(); 36 | } 37 | 38 | @Override 39 | public void customize(RestTemplate restTemplate) { 40 | if (maxConnPerRoute <= 0 || maxTotal <= 0) { 41 | maxConnPerRoute = Runtime.getRuntime().availableProcessors() * 8; 42 | maxTotal = maxConnPerRoute * 2; 43 | } 44 | 45 | PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create() 46 | .setDefaultSocketConfig(SocketConfig.custom() 47 | .setSoTimeout(Timeout.ofMinutes(1)) 48 | .build()) 49 | .setPoolConcurrencyPolicy(PoolConcurrencyPolicy.STRICT) 50 | .setConnPoolPolicy(PoolReusePolicy.LIFO) 51 | .setMaxConnTotal(maxTotal) 52 | .setMaxConnPerRoute(maxConnPerRoute) 53 | .build(); 54 | 55 | CloseableHttpClient client = HttpClients.custom() 56 | .setConnectionManager(connectionManager) 57 | .build(); 58 | 59 | restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory(client)); 60 | } 61 | 62 | @Bean 63 | public HypermediaClient restCommands(RestTemplate restTemplate) { 64 | return new HypermediaClient(restTemplate); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /bank-api/src/main/java/io/roach/bank/api/LinkRelations.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.api; 2 | 3 | /** 4 | * Defines domain specific web constants such as link relation names and resource names. 5 | */ 6 | public abstract class LinkRelations { 7 | // Transaction history rels 8 | public static final String TRANSACTION_REL = "transaction"; 9 | 10 | public static final String TRANSACTION_LIST_REL = "transaction-list"; 11 | 12 | public static final String TRANSACTION_ITEM_REL = "transaction-item"; 13 | 14 | public static final String TRANSACTION_ITEMS_REL = "transaction-item-list"; 15 | 16 | public static final String TRANSFER_FORM_REL = "transfer-form"; 17 | 18 | // Account rels 19 | public static final String ACCOUNT_REL = "account"; 20 | 21 | public static final String ACCOUNT_LIST_REL = "account-list"; 22 | 23 | public static final String ACCOUNT_TOP = "account-top"; 24 | 25 | public static final String ACCOUNT_ONE_FORM_REL = "account-form"; 26 | 27 | public static final String ACCOUNT_BATCH_FORM_REL = "account-batch"; 28 | 29 | public static final String ACCOUNT_BALANCE_REL = "account-balance"; 30 | 31 | public static final String ACCOUNT_BALANCE_SNAPSHOT_REL = "account-balance-snapshot"; 32 | 33 | // Reporting link relations 34 | 35 | public static final String REPORTING_REL = "reporting"; 36 | 37 | public static final String ACCOUNT_SUMMARY_REL = "account-summary"; 38 | 39 | public static final String TRANSACTION_SUMMARY_REL = "transaction-summary"; 40 | 41 | // Meta 42 | 43 | public static final String REGION_REL = "region"; 44 | 45 | public static final String REGION_LIST_REL = "region-list"; 46 | 47 | public static final String CITY_LIST_REL = "city-list"; 48 | 49 | public static final String GATEWAY_REGION_REL = "gateway-region"; 50 | 51 | 52 | // Generic context-scoped link relations 53 | 54 | public static final String OPEN_REL = "open"; 55 | 56 | public static final String CLOSE_REL = "close"; 57 | 58 | // Admin 59 | 60 | public static final String ADMIN_REL = "admin"; 61 | 62 | public static final String POOL_SIZE_REL = "pool-size"; 63 | 64 | public static final String POOL_CONFIG_REL = "pool-config"; 65 | 66 | public static final String TOGGLE_TRACE_LOG = "toggle-tracelog"; 67 | 68 | public static final String ACTUATOR_REL = "actuator"; 69 | 70 | public static final String CONFIG_INDEX_REL = "config"; 71 | 72 | public static final String CONFIG_REGION_REL = "config-region"; 73 | 74 | public static final String CONFIG_MULTI_REGION_REL = "config-multiregion"; 75 | 76 | 77 | // IANA standard link relations: 78 | // http://www.iana.org/assignments/link-relations/link-relations.xhtml 79 | 80 | public static final String CURIE_NAMESPACE = "roachbank"; 81 | 82 | public static final String CURIE_PREFIX = CURIE_NAMESPACE + ":"; 83 | 84 | private LinkRelations() { 85 | } 86 | 87 | public static String withCurie(String rel) { 88 | return CURIE_NAMESPACE + ":" + rel; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /deploy/common/01_create_cluster.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | fn_create_cluster() { 4 | if [ "${cloud}" = "aws" ]; then 5 | echo roachprod create $CLUSTER --clouds=aws --aws-machine-type-ssd=${machinetypes} --geo --local-ssd --nodes=${nodes} --aws-zones=${zones} 6 | fn_failcheck roachprod create $CLUSTER --clouds=aws --aws-machine-type-ssd=${machinetypes} --geo --local-ssd --nodes=${nodes} --aws-zones=${zones} --aws-profile crl-revenue --aws-config ~/rev.json 7 | elif [ "${cloud}" = "gce" ]; then 8 | echo roachprod create $CLUSTER --clouds=gce --gce-machine-type=${machinetypes} --geo --local-ssd --nodes=${nodes} --gce-zones=${zones} 9 | fn_failcheck roachprod create $CLUSTER --clouds=gce --gce-machine-type=${machinetypes} --geo --local-ssd --nodes=${nodes} --gce-zones=${zones} --aws-profile crl-revenue --aws-config ~/rev.json 10 | else 11 | echo roachprod create $CLUSTER --clouds=azure --azure-machine-type=${machinetypes} --geo --local-ssd --nodes=${nodes} --azure-locations=${zones} 12 | fn_failcheck roachprod create $CLUSTER --clouds=azure --azure-machine-type=${machinetypes} --geo --local-ssd --nodes=${nodes} --azure-locations=${zones} --aws-profile crl-revenue --aws-config ~/rev.json 13 | fi 14 | } 15 | 16 | fn_stage_cluster() { 17 | fn_echo_info_nl "Stage binaries $releaseversion" 18 | fn_failcheck roachprod stage $CLUSTER release $releaseversion 19 | } 20 | 21 | fn_start_cluster() { 22 | fn_echo_info_nl "Start CockroachDB nodes $crdbnodes" 23 | fn_failcheck roachprod start $CLUSTER:$crdbnodes 24 | fn_failcheck roachprod admin --open --ips $CLUSTER:1 25 | } 26 | 27 | fn_stage_lb() { 28 | i=0; 29 | for c in "${clients[@]}" 30 | do 31 | region=${regions[$i]} 32 | i=($i+1) 33 | 34 | fn_echo_info_nl "Stage client ${CLUSTER}:$c" 35 | 36 | fn_failcheck roachprod run ${CLUSTER}:$c 'sudo apt-get -qq update' 37 | fn_failcheck roachprod run ${CLUSTER}:$c 'sudo apt-get -qq install -y openjdk-17-jre-headless htop dstat haproxy' 38 | fn_failcheck roachprod run ${CLUSTER}:$c "./cockroach gen haproxy --insecure --host $(roachprod ip $CLUSTER:1 --external) --locality=region=$region" 39 | fn_failcheck roachprod run ${CLUSTER}:$c 'nohup haproxy -f haproxy.cfg > /dev/null 2>&1 &' 40 | done 41 | } 42 | 43 | fn_create_db() { 44 | fn_echo_info_nl "Creating database via $CLUSTER:1" 45 | 46 | fn_failcheck roachprod run $CLUSTER:1 < index() { 31 | MessageModel index = new MessageModel(); 32 | index.setMessage("Welcome to text-only Roach Bank. You are in a dark, cold lobby."); 33 | 34 | index.add(linkTo(methodOn(AccountController.class) 35 | .index()) 36 | .withRel(LinkRelations.ACCOUNT_REL) 37 | .withTitle("Account resource details") 38 | ); 39 | index.add(linkTo(methodOn(TransactionController.class) 40 | .index()) 41 | .withRel(LinkRelations.TRANSACTION_REL) 42 | .withTitle("Transaction resource details") 43 | ); 44 | index.add(Link.of(UriTemplate.of(linkTo(TransferController.class) 45 | .toUriComponentsBuilder().path( 46 | "/form{?limit,amount,regions}") // RFC-6570 template 47 | .build().toUriString()), 48 | LinkRelations.TRANSFER_FORM_REL) 49 | .withTitle("Form template for creating a transfer request") 50 | ); 51 | index.add(linkTo(methodOn(ReportController.class) 52 | .index()) 53 | .withRel(LinkRelations.REPORTING_REL) 54 | .withTitle("Reporting resource details") 55 | ); 56 | index.add(linkTo(methodOn(AdminController.class) 57 | .index()) 58 | .withRel(LinkRelations.ADMIN_REL) 59 | .withTitle("Admin resource details") 60 | ); 61 | index.add(linkTo(methodOn(ConfigurationController.class) 62 | .index()) 63 | .withRel(LinkRelations.CONFIG_INDEX_REL) 64 | .withTitle("Configuration resource details") 65 | ); 66 | 67 | return ResponseEntity.ok(index); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /bank-server/src/main/java/io/roach/bank/repository/jpa/AccountJpaRepository.java: -------------------------------------------------------------------------------- 1 | package io.roach.bank.repository.jpa; 2 | 3 | import java.util.Collection; 4 | import java.util.List; 5 | import java.util.Set; 6 | import java.util.UUID; 7 | import java.util.stream.Stream; 8 | 9 | import org.springframework.data.domain.Page; 10 | import org.springframework.data.domain.Pageable; 11 | import org.springframework.data.jpa.repository.JpaRepository; 12 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor; 13 | import org.springframework.data.jpa.repository.Lock; 14 | import org.springframework.data.jpa.repository.Query; 15 | import org.springframework.data.repository.query.Param; 16 | import org.springframework.transaction.annotation.Propagation; 17 | import org.springframework.transaction.annotation.Transactional; 18 | 19 | import io.roach.bank.api.support.Money; 20 | import io.roach.bank.domain.Account; 21 | import jakarta.persistence.LockModeType; 22 | import jakarta.persistence.Tuple; 23 | 24 | @Transactional(propagation = Propagation.MANDATORY) 25 | public interface AccountJpaRepository extends JpaRepository, 26 | JpaSpecificationExecutor { 27 | 28 | @Query(value = "select a.balance " 29 | + "from Account a " 30 | + "where a.id = ?1") 31 | Money findBalanceById(UUID id); 32 | 33 | @Query(value = "select " 34 | + "count (a.id), " 35 | + "count (distinct a.city), " 36 | + "sum (a.balance.amount), " 37 | + "min (a.balance.amount), " 38 | + "max (a.balance.amount), " 39 | + "a.balance.currency " 40 | + "from Account a " 41 | + "where a.city = ?1 " 42 | + "group by a.city,a.balance.currency") 43 | Stream accountSummary(String city); 44 | 45 | @Query(value = "select " 46 | + " count (distinct t.id), " 47 | + " count (t.id), " 48 | + " sum (abs(ti.amount.amount)), " 49 | + " sum (ti.amount.amount), " 50 | + " ti.amount.currency " 51 | + "from Transaction t join TransactionItem ti on t.id = ti.id.transactionId " 52 | + "where ti.city = ?1 " 53 | + "group by ti.city, ti.amount.currency") 54 | Stream transactionSummary(String city); 55 | 56 | @Query(value = "select a " 57 | + "from Account a " 58 | + "where a.id in (?1) and a.city = ?2") 59 | @Lock(LockModeType.PESSIMISTIC_READ) 60 | List findAllWithLock(Set ids, String city); 61 | 62 | @Query(value = "select a " 63 | + "from Account a " 64 | + "where a.id in (?1) and a.city = ?2") 65 | List findAll(Set ids, String city); 66 | 67 | @Query(value 68 | = "select a " 69 | + "from Account a " 70 | + "where a.city in (:cities)", 71 | countQuery 72 | = "select count(a.id) " 73 | + "from Account a " 74 | + "where a.city in (:cities)") 75 | Page findAll(@Param("cities") Collection cities, Pageable pageable); 76 | } 77 | -------------------------------------------------------------------------------- /bank-server/src/main/resources/static/js/color-modes.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Color mode toggler for Bootstrap's docs (https://getbootstrap.com/) 3 | * Copyright 2011-2023 The Bootstrap Authors 4 | * Licensed under the Creative Commons Attribution 3.0 Unported License. 5 | */ 6 | 7 | (() => { 8 | 'use strict' 9 | 10 | const getStoredTheme = () => localStorage.getItem('theme') 11 | const setStoredTheme = theme => localStorage.setItem('theme', theme) 12 | 13 | const getPreferredTheme = () => { 14 | const storedTheme = getStoredTheme() 15 | if (storedTheme) { 16 | return storedTheme 17 | } 18 | 19 | return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' 20 | } 21 | 22 | const setTheme = theme => { 23 | if (theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches) { 24 | document.documentElement.setAttribute('data-bs-theme', 'dark') 25 | } else { 26 | document.documentElement.setAttribute('data-bs-theme', theme) 27 | } 28 | } 29 | 30 | setTheme(getPreferredTheme()) 31 | 32 | const showActiveTheme = (theme, focus = false) => { 33 | const themeSwitcher = document.querySelector('#bd-theme') 34 | 35 | if (!themeSwitcher) { 36 | return 37 | } 38 | 39 | const themeSwitcherText = document.querySelector('#bd-theme-text') 40 | const activeThemeIcon = document.querySelector('.theme-icon-active use') 41 | const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`) 42 | const svgOfActiveBtn = btnToActive.querySelector('svg use').getAttribute('href') 43 | 44 | document.querySelectorAll('[data-bs-theme-value]').forEach(element => { 45 | element.classList.remove('active') 46 | element.setAttribute('aria-pressed', 'false') 47 | }) 48 | 49 | btnToActive.classList.add('active') 50 | btnToActive.setAttribute('aria-pressed', 'true') 51 | activeThemeIcon.setAttribute('href', svgOfActiveBtn) 52 | const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})` 53 | themeSwitcher.setAttribute('aria-label', themeSwitcherLabel) 54 | 55 | if (focus) { 56 | themeSwitcher.focus() 57 | } 58 | } 59 | 60 | window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { 61 | const storedTheme = getStoredTheme() 62 | if (storedTheme !== 'light' && storedTheme !== 'dark') { 63 | setTheme(getPreferredTheme()) 64 | } 65 | }) 66 | 67 | window.addEventListener('DOMContentLoaded', () => { 68 | showActiveTheme(getPreferredTheme()) 69 | 70 | document.querySelectorAll('[data-bs-theme-value]') 71 | .forEach(toggle => { 72 | toggle.addEventListener('click', () => { 73 | const theme = toggle.getAttribute('data-bs-theme-value') 74 | setStoredTheme(theme) 75 | setTheme(theme) 76 | showActiveTheme(theme, true) 77 | }) 78 | }) 79 | }) 80 | })() 81 | --------------------------------------------------------------------------------