├── .dockerignore ├── .editorconfig ├── .github ├── .codecov.yml └── workflows │ └── gradle.yml ├── .gitignore ├── .travis_deprecated.yml ├── Dockerfile.run-it-test-mongodb-replica-set ├── LICENSE ├── README.md ├── build.gradle ├── docker-compose-it-test.yml ├── gradle.properties ├── gradle ├── jacoco.gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml ├── scripts ├── codecov.sh ├── load_test.sh └── publish.sh ├── settings.gradle ├── src ├── main │ └── java │ │ └── com │ │ └── github │ │ └── silaev │ │ └── mongodb │ │ └── replicaset │ │ ├── MongoDbReplicaSet.java │ │ ├── converter │ │ ├── Converter.java │ │ ├── YmlConverter.java │ │ └── impl │ │ │ ├── MongoNodeToMongoSocketAddressConverter.java │ │ │ ├── StringToMongoRsStatusConverter.java │ │ │ ├── UserInputToApplicationPropertiesConverter.java │ │ │ ├── VersionConverter.java │ │ │ └── YmlConverterImpl.java │ │ ├── core │ │ └── Generated.java │ │ ├── exception │ │ ├── IncorrectUserInputException.java │ │ └── MongoNodeInitializationException.java │ │ ├── model │ │ ├── ApplicationProperties.java │ │ ├── MongoDbVersion.java │ │ ├── MongoNode.java │ │ ├── MongoNodeMutable.java │ │ ├── MongoReplicaSetProperties.java │ │ ├── MongoRsStatus.java │ │ ├── MongoRsStatusMutable.java │ │ ├── MongoSocketAddress.java │ │ ├── Pair.java │ │ ├── PropertyContainer.java │ │ ├── ReplicaSetMemberState.java │ │ └── UserInputProperties.java │ │ ├── service │ │ ├── ResourceService.java │ │ └── impl │ │ │ └── ResourceServiceImpl.java │ │ └── util │ │ └── StringUtils.java └── test │ ├── java │ └── com │ │ └── github │ │ └── silaev │ │ └── mongodb │ │ └── replicaset │ │ ├── MongoDbReplicaSetBuilderTest.java │ │ ├── MongoDbReplicaSetTest.java │ │ ├── converter │ │ └── impl │ │ │ ├── MongoNodeToMongoSocketAddressConverterTest.java │ │ │ ├── StringToMongoRsStatusConverterTest.java │ │ │ ├── UserInputToApplicationPropertiesConverterTest.java │ │ │ └── VersionConverterTest.java │ │ ├── core │ │ ├── EnabledIfSystemPropertyEnabledByDefault.java │ │ ├── EnabledIfSystemPropertyEnabledByDefaultCondition.java │ │ └── IntegrationTest.java │ │ ├── integration │ │ ├── PropertyEvaluationITTest.java │ │ ├── VersionSupportTest.java │ │ ├── api │ │ │ ├── BaseMongoDbReplicaSetApiITTest.java │ │ │ ├── MongoDbReplicaSetMultiNodeApiITTest.java │ │ │ ├── MongoDbReplicaSetSingleNodeApiITTest.java │ │ │ └── faulttolerance │ │ │ │ ├── MongoDbDelayedMembersITTest.java │ │ │ │ ├── MongoDbReplicaSetDistributionITTest.java │ │ │ │ ├── MongoDbReplicaSetFaultTolerancePSAApiITTest.java │ │ │ │ └── MongoDbReplicaSetFaultTolerancePSSApiITTest.java │ │ └── transaction │ │ │ ├── BaseMongoDbReplicaSetTransactionITTest.java │ │ │ ├── MongoDbReplicaSetTransactionMultiNodeITTest.java │ │ │ └── MongoDbReplicaSetTransactionSingleNodeITTest.java │ │ ├── model │ │ ├── DisconnectionType.java │ │ └── HardFailureAction.java │ │ └── util │ │ ├── CollectionUtils.java │ │ ├── ConnectionUtils.java │ │ └── SubscriberHelperUtils.java │ └── resources │ ├── enabled-false.yml │ ├── enabled-true.yml │ ├── logback-test.xml │ ├── mockito-extensions │ └── org.mockito.plugins.MockMaker │ └── shell-output │ ├── rs-status-double-primaries.txt │ ├── rs-status-framed.txt │ ├── rs-status-plain-wo-status.txt │ ├── rs-status-plain.txt │ ├── rs-status-wo-status.txt │ ├── rs-status-wo-version.txt │ ├── rs-status.txt │ └── timeout-exceeds.txt └── testcov.exclude /.dockerignore: -------------------------------------------------------------------------------- 1 | /uploads/ 2 | /out/ 3 | /.idea/ 4 | .git 5 | *.iml 6 | *.log 7 | /logs/ 8 | /scripts/ 9 | /tools/ 10 | Dockerfile.* 11 | docker-compose* -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | charset = utf-8 11 | indent_style = space 12 | indent_size = 4 13 | trim_trailing_whitespace = true 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.github/.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | notify: 3 | require_ci_to_pass: yes 4 | 5 | coverage: 6 | range: 80..100 7 | round: down 8 | precision: 2 9 | status: 10 | project: 11 | default: 12 | enabled: yes 13 | target: 80% 14 | threshold: 10% 15 | if_ci_failed: error 16 | patch: off 17 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: {} 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-18.04 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | cfg: 15 | - { mongodb-version: '3.6.14', use-host-docker-internal: false } 16 | - { mongodb-version: '4.0.10', use-host-docker-internal: false } 17 | - { mongodb-version: '4.2.8', use-host-docker-internal: false } 18 | - { mongodb-version: '4.4.4', use-host-docker-internal: false } 19 | - { mongodb-version: '4.4.4', use-host-docker-internal: true } 20 | - { mongodb-version: '5.0.5', use-host-docker-internal: false } 21 | - { mongodb-version: '5.0.5', use-host-docker-internal: true } 22 | 23 | steps: 24 | - uses: actions/checkout@v2 25 | - uses: actions/cache@v2 26 | with: 27 | path: | 28 | ~/.gradle/caches 29 | ~/.gradle/wrapper 30 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 31 | restore-keys: | 32 | ${{ runner.os }}-gradle- 33 | - name: Set up JDK 1.8 34 | uses: actions/setup-java@v1 35 | with: 36 | java-version: 1.8 37 | - name: Add dockerhost to OS host file 38 | if: ${{ !matrix.cfg.use-host-docker-internal }} 39 | run: sudo echo "127.0.0.1 dockerhost" | sudo tee -a /etc/hosts 40 | - name: Add host.docker.internal to OS host file 41 | if: ${{ matrix.cfg.use-host-docker-internal }} 42 | run: sudo echo "127.0.0.1 host.docker.internal" | sudo tee -a /etc/hosts 43 | - name: Grant execute permission for gradlew 44 | run: chmod +x gradlew 45 | - name: Build with Gradle agains host, mongodb-version ${{ matrix.cfg.mongodb-version }} 46 | run: | 47 | ./gradlew clean build --no-daemon \ 48 | -DmongoReplicaSetProperties.mongoDockerImageName=mongo:${{ matrix.cfg.mongodb-version }} \ 49 | -DmongoReplicaSetProperties.useHostDockerInternal=${{ matrix.cfg.use-host-docker-internal }} 50 | - name: Build with Gradle in docker-only mode ${{ matrix.cfg.mongodb-version }} 51 | env: 52 | MONGO_VERSION: ${{ matrix.cfg.mongodb-version }} 53 | USE_HOST_DOCKER_INTERNAL: ${{ matrix.cfg.use-host-docker-internal }} 54 | run: | 55 | docker-compose -f docker-compose-it-test.yml up -d; 56 | docker wait run-it-test-mongodb-replica-set; 57 | docker logs run-it-test-mongodb-replica-set; 58 | if [[ $(docker inspect run-it-test-mongodb-replica-set --format='{{.State.ExitCode}}') == '1' ]]; then 59 | echo $(docker inspect run-it-test-mongodb-replica-set --format='{{json .State}}'); 60 | exit 1; 61 | fi 62 | - name: Upload coverage to Codecov 63 | uses: codecov/codecov-action@v1 64 | with: 65 | verbose: false # optional (default = false) 66 | if: ${{ success() }} 67 | - name: Cleanup Gradle Cache 68 | # Remove some files from the Gradle cache, so they aren't cached by GitHub Actions. 69 | # Restoring these files from a GitHub Actions cache might cause problems for future builds. 70 | run: | 71 | rm -f ~/.gradle/caches/modules-2/modules-2.lock 72 | rm -f ~/.gradle/caches/modules-2/gc.properties 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff: 2 | .idea/**/workspace.xml 3 | .idea/**/tasks.xml 4 | .idea/dictionaries 5 | 6 | # Sensitive or high-churn files: 7 | .idea/**/dataSources/ 8 | .idea/**/dataSources.ids 9 | .idea/**/dataSources.xml 10 | .idea/**/dataSources.local.xml 11 | .idea/**/sqlDataSources.xml 12 | .idea/**/dynamic.xml 13 | .idea/**/uiDesigner.xml 14 | 15 | # Gradle: 16 | .idea/**/gradle.xml 17 | .idea/**/libraries 18 | 19 | # Package Files 20 | *.jar 21 | *.war 22 | *.ear 23 | *.zip 24 | *.tar.gz 25 | *.rar 26 | # except from Gradle wrapper jar file that is useful after 27 | # checking out from VCS 28 | !gradle-wrapper.jar 29 | 30 | #Build, gradle, and system files 31 | /build/ 32 | /.gradle/ 33 | /DiskStore/ 34 | /classes/ 35 | /target/ 36 | /uploads/ 37 | /out/ 38 | /.idea/ 39 | git 40 | #stor for certificate of all kind 41 | #/src/main/resources/cert-store/ 42 | 43 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 44 | hs_err_pid* 45 | 46 | *.iml 47 | *.log 48 | /logs/ 49 | .env -------------------------------------------------------------------------------- /.travis_deprecated.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | jdk: openjdk11 4 | 5 | services: docker 6 | 7 | dist: bionic 8 | 9 | addons: 10 | hosts: 11 | - dockerhost 12 | apt: 13 | packages: 14 | - docker-ce 15 | 16 | env: 17 | matrix: 18 | - MONGO_VERSION=3.6.14 DOCKER_COMPOSE_VERSION=1.23.2 FINALIZE=0 19 | - MONGO_VERSION=4.0.10 DOCKER_COMPOSE_VERSION=1.23.2 FINALIZE=0 20 | - MONGO_VERSION=4.2.8 DOCKER_COMPOSE_VERSION=1.23.2 FINALIZE=0 21 | - MONGO_VERSION=4.4.3 DOCKER_COMPOSE_VERSION=1.23.2 FINALIZE=1 22 | 23 | before_install: 24 | - chmod +x gradlew; 25 | - chmod +x scripts/codecov.sh; 26 | 27 | after_success: 28 | - ./scripts/codecov.sh 29 | 30 | script: 31 | - echo "$DOCKER_PASSWORD" | docker login -u "s256" --password-stdin; 32 | - echo "*** build and run sequential integration tests (MongoDB $MONGO_VERSION) against a host"; 33 | ./gradlew clean build --no-daemon -DmongoReplicaSetProperties.mongoDockerImageName=mongo:${MONGO_VERSION}; 34 | - echo "*** run integration tests (MongoDB $MONGO_VERSION) in docker-only mode"; 35 | docker-compose -f docker-compose-it-test.yml up -d; 36 | travis_wait 40 docker wait run-it-test-mongodb-replica-set; 37 | docker logs run-it-test-mongodb-replica-set; 38 | - if [[ $(docker inspect run-it-test-mongodb-replica-set --format='{{.State.ExitCode}}') == '1' ]]; then 39 | echo $(docker inspect run-it-test-mongodb-replica-set --format='{{json .State}}'); 40 | exit 1; 41 | fi 42 | 43 | before_cache: 44 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock 45 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ 46 | cache: 47 | directories: 48 | - $HOME/.gradle/caches/ 49 | - $HOME/.gradle/wrapper/ 50 | - $HOME/.m2/repository/ 51 | -------------------------------------------------------------------------------- /Dockerfile.run-it-test-mongodb-replica-set: -------------------------------------------------------------------------------- 1 | FROM bellsoft/liberica-openjdk-alpine:8 2 | COPY . . 3 | RUN chmod +x gradlew 4 | CMD ["sh", "-c", "./gradlew integrationTest --no-daemon \ 5 | -DmongoReplicaSetProperties.mongoDockerImageName=mongo:${MONGO_VERSION} \ 6 | -DmongoReplicaSetProperties.useHostDockerInternal=${USE_HOST_DOCKER_INTERNAL}"] 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019-2022 Konstantin Silaev 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Run MongoDB Atlas locally for testing 2 | ![build](https://github.com/silaev/mongodb-replica-set/workflows/build/badge.svg?branch=master) 3 | [![codecov](https://codecov.io/gh/silaev/mongodb-replica-set/branch/master/graph/badge.svg)](https://codecov.io/gh/silaev/mongodb-replica-set) 4 | 5 | #### Prerequisite 6 | - Java 8+ 7 | - Docker 8 | - Chart shows local and remote docker support for replicaSetNumber 9 | 10 | replicaSetNumber | local docker host | local docker host running tests from inside a container with mapping the Docker socket | remote docker daemon | availability of an arbiter node | 11 | :---: | :---: |:---: | :---: | :---: | 12 | 1 | + | + | + | - | 13 | from 2 to 7 (including) | only if adding either `host.docker.internal` (your Docker version should support it) or `dockerhost` to the OS host file. See Supported features for details | + | + | + | 14 | 15 | Tip: 16 | A single node replica set is the fastest among others. That is the default mode for MongoDbReplicaSet. 17 | However, to use only it, consider the [Testcontainers MongoDB module on GitHub](https://www.testcontainers.org/modules/databases/mongodb/) 18 | 19 | #### Getting it 20 | - Gradle: 21 | ```groovy 22 | dependencies { 23 | testCompile("com.github.silaev:mongodb-replica-set:${LATEST_RELEASE}") 24 | } 25 | ``` 26 | - Maven: 27 | ```xml 28 | 29 | 30 | com.github.silaev 31 | mongodb-replica-set 32 | ${LATEST_RELEASE} 33 | test 34 | 35 | 36 | ``` 37 | Replace ${LATEST_RELEASE} with [the Latest Version Number](https://search.maven.org/search?q=g:com.github.silaev%20AND%20a:mongodb-replica-set) 38 | If you cannot find a release on Maven, please, use Jitpack 39 | 40 | #### Run on Apple silicon 41 | Use digests for linux/arm64/v8 and Docker Desktop for Apple silicon supporting host.docker.internal (should be in the OS host file). 42 | Examples: 43 | 1. 44 | ``` 45 | MongoDbReplicaSet.builder() 46 | .mongoDockerImageName("mongo@sha256:8a823923d80e819e21ee6c179eabf42460b6b7d8ac3dd5f35b59419ae5413640") 47 | .useHostDockerInternal(true) 48 | .build()` 49 | ``` 50 | 2 51 | `./gradlew clean build -DmongoReplicaSetProperties.mongoDockerImageName=mongo@sha256:8a823923d80e819e21ee6c179eabf42460b6b7d8ac3dd5f35b59419ae5413640 -DmongoReplicaSetProperties.useHostDockerInternal=true` 52 | 53 | 54 | #### MongoDB versions that MongoDbReplicaSet is constantly tested against 55 | version | transaction support | 56 | ---------- | ---------- | 57 | 3.6.14 |-| 58 | 4.0.12 |+| 59 | 4.2.8 |+| 60 | 4.4.4 |+| 61 | 5.0.5 |+| 62 | 63 | #### Examples 64 |
65 | Click to see a single node example 66 | 67 | ```java 68 | class ITTest { 69 | @Test 70 | void testDefaultSingleNode() { 71 | try ( 72 | //create a single node mongoDbReplicaSet and auto-close it afterwards 73 | final MongoDbReplicaSet mongoDbReplicaSet = MongoDbReplicaSet.builder() 74 | .mongoDockerImageName("mongo:4.4.4") 75 | .build() 76 | ) { 77 | //start it 78 | mongoDbReplicaSet.start(); 79 | assertThat( 80 | mongoDbReplicaSet.nodeStates(mongoDbReplicaSet.getMongoRsStatus().getMembers()), 81 | hasItem(ReplicaSetMemberState.PRIMARY) 82 | ); 83 | assertNotNull(mongoDbReplicaSet.getReplicaSetUrl()); 84 | } 85 | } 86 | } 87 | ``` 88 |
89 | 90 |
91 | Click to see a fault tolerance example 92 | 93 | ```java 94 | class ITTest { 95 | @Test 96 | void testFaultTolerance() { 97 | try ( 98 | //create a PSA mongoDbReplicaSet and auto-close it afterwards 99 | final MongoDbReplicaSet mongoDbReplicaSet = MongoDbReplicaSet.builder() 100 | //with the latest mongo:4.4.4 docker image 101 | .mongoDockerImageName("mongo:4.4.4") 102 | //If true then use host.docker.internal of Docker, 103 | //otherwise take dockerhost of Qoomon docker-host. 104 | //Make sure that your OS host file includes one of them. 105 | //All new Docker versions support the first variant. 106 | .useHostDockerInternal(true) 107 | //with 2 working nodes 108 | .replicaSetNumber(2) 109 | //with an arbiter node 110 | .addArbiter(true) 111 | //create a proxy for each node to simulate network partitioning 112 | .addToxiproxy(true) 113 | .build() 114 | ) { 115 | //start it 116 | mongoDbReplicaSet.start(); 117 | assertNotNull(mongoDbReplicaSet.getReplicaSetUrl()); 118 | 119 | //get a primary node 120 | final MongoNode masterNode = mongoDbReplicaSet.getMasterMongoNode( 121 | mongoDbReplicaSet.getMongoRsStatus().getMembers() 122 | ); 123 | 124 | //cut off the primary node from network 125 | mongoDbReplicaSet.disconnectNodeFromNetwork(masterNode); 126 | //wait until a new primary is elected that is different from the masterNode 127 | mongoDbReplicaSet.waitForMasterReelection(masterNode); 128 | assertThat( 129 | mongoDbReplicaSet.nodeStates(mongoDbReplicaSet.getMongoRsStatus().getMembers()), 130 | hasItems( 131 | ReplicaSetMemberState.PRIMARY, 132 | ReplicaSetMemberState.ARBITER 133 | ) 134 | ); 135 | 136 | //bring back the disconnected masterNode 137 | mongoDbReplicaSet.connectNodeToNetwork(masterNode); 138 | //wait until all nodes are up and running 139 | mongoDbReplicaSet.waitForAllMongoNodesUp(); 140 | assertThat( 141 | mongoDbReplicaSet.nodeStates(mongoDbReplicaSet.getMongoRsStatus().getMembers()), 142 | hasItems( 143 | ReplicaSetMemberState.PRIMARY, 144 | ReplicaSetMemberState.ARBITER, 145 | ReplicaSetMemberState.SECONDARY 146 | ) 147 | ); 148 | } 149 | } 150 | } 151 | ``` 152 |
153 | 154 | - See more examples in the test sources [mongodb-replica-set on github](https://github.com/silaev/mongodb-replica-set/tree/master/src/test/java/com/github/silaev/mongodb/replicaset/integration) 155 | - See a full Spring Boot + Spring Data example [wms on github](https://github.com/silaev/wms/blob/master/src/test/java/com/silaev/wms/integration/ProductControllerITTest.java/) 156 | 157 | #### Motivation 158 | - Cross-platform solution that doesn't depend on fixed ports; 159 | - Testing MongoDB transactions to run against an environment close to a production one; 160 | - Testing production issues by recreating a real MongoDB replica set (currently without shards); 161 | - Education to newcomers to the MongoDB world (learning the behaviour of a distributed NoSQL database while 162 | dealing with network partitioning, analyze the election process and so on). 163 | 164 | #### General info 165 |
166 | Click to see how to create a 3 node replica set on fixed ports via Docker manually 167 | 168 | MongoDB starting from version 4 supports multi-document transactions only on a replica set. 169 | For example, to initialize a 3 node replica set on fixed ports via Docker, one has to do the following: 170 | 1. Add `127.0.0.1 mongo1 mongo2 mongo3` to the host file of an operation system; 171 | 2. Run in terminal: 172 | - `docker network create mongo-cluster` 173 | - `docker run --name mongo1 -d --net mongo-cluster -p 50001:50001 mongo:4.0.10 mongod --replSet docker-rs --port 50001` 174 | - `docker run --name mongo2 -d --net mongo-cluster -p 50002:50002 mongo:4.0.10 mongod --replSet docker-rs --port 50002` 175 | - `docker run --name mongo3 -d --net mongo-cluster -p 50003:50003 mongo:4.0.10 mongod --replSet docker-rs --port 50003` 176 | 3. Prepare the following unix end of lines script (optionally put it folder scripts or use rs.add on each node): 177 | ```js 178 | rs.initiate({ 179 | "_id": "docker-rs", 180 | "members": [ 181 | {"_id": 0, "host": "mongo1:50001"}, 182 | {"_id": 1, "host": "mongo2:50002"}, 183 | {"_id": 2, "host": "mongo3:50003"} 184 | ] 185 | }); 186 | ``` 187 | 4. Run in terminal: 188 | - `docker cp scripts/ mongo1:/scripts/` 189 | - `docker exec -it mongo1 /bin/sh -c "mongo --port 50001 < /scripts/init.js"` 190 | 191 | As we can see, there is a lot of operations to execute and we even didn't touch a non-fixed port approach. 192 | That's where the MongoDbReplicaSet might come in handy. 193 |
194 | 195 | #### Supported features 196 | Feature | Description | default value | how to set | 197 | ---------- | ----------- | ----------- | ----------- | 198 | replicaSetNumber | The number of voting nodes in a replica set including a master one | 1 | MongoDbReplicaSet.builder() | 199 | awaitNodeInitAttempts | The number of approximate seconds to wait for a master or an arbiter node(if addArbiter=true) | 29 starting from 0 | MongoDBReplicaSet.builder() | 200 | propertyFileName | yml file located on the classpath | none | MongoDbReplicaSet.builder() | 201 | mongoDockerImageName | a MongoDB docker file name | mongo:4.0.10 | finds first set:
1) MongoDbReplicaSet.builder()
2) the system property mongoReplicaSetProperties.mongoDockerImageName
3) propertyFile
4) default value | 202 | addArbiter | whether or not to add an arbiter node to a cluster | false | MongoDbReplicaSet.builder() | 203 | slaveDelayTimeout | whether or not to create one master and the others as delayed members | false | MongoDbReplicaSet.builder() | 204 | useHostDockerInternal | If true then use `host.docker.internal` of Docker, otherwise take `dockerhost` of Qoomon docker-host | false | finds first set:
1) MongoDbReplicaSet.builder()
2) the system property mongoReplicaSetProperties.useHostDockerInternal
3) default value| 205 | addToxiproxy | whether or not to create a proxy for each MongoDB node via Toxiproxy | false | MongoDbReplicaSet.builder() | 206 | enabled | whether or not MongoReplicaSet is enabled even if instantiated in a test | true | finds first set:
1) the system property mongoReplicaSetProperties.enabled
2) propertyFile
3) default value | 207 | commandLineOptions | command line options, example:`Arrays.asList("--oplogSize", "50")` | emptyList | MongoDbReplicaSet.builder() | 208 | 209 | a propertyFile.yml example: 210 | ```yaml 211 | mongoReplicaSetProperties: 212 | enabled: false 213 | mongoDockerImageName: mongo:4.1.13 214 | ``` 215 | 216 | #### License 217 | [Apache License, Version 2.0](https://github.com/silaev/mongodb-replica-set/blob/master/LICENSE/) 218 | 219 | #### Additional links 220 | * [mongo-replica-set-behind-firewall](https://serverfault.com/questions/815955/mongo-replica-set-behind-firewall) 221 | * [Support different networks](https://jira.mongodb.org/browse/SERVER-1889) 222 | 223 | #### Copyright 224 | Copyright (c) 2022 Konstantin Silaev 225 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | import io.franzbecker.gradle.lombok.task.DelombokTask 2 | 3 | buildscript { 4 | ext { 5 | junitVersion = '5.7.1' 6 | testcontainersVersion = '1.16.2' 7 | mongodbDriverVersion = '4.4.1' 8 | } 9 | } 10 | 11 | plugins { 12 | id 'java' 13 | id "io.franzbecker.gradle-lombok" version "4.0.0" 14 | id 'java-library' 15 | id 'idea' 16 | id "com.github.johnrengelman.shadow" version "5.2.0" 17 | id 'jacoco' 18 | id 'maven-publish' 19 | id "com.jfrog.bintray" version "1.8.5" 20 | } 21 | 22 | apply from: "$rootDir/gradle/jacoco.gradle" 23 | 24 | group = 'com.github.silaev' 25 | version = '0.4.3' 26 | 27 | repositories { 28 | mavenCentral() 29 | } 30 | 31 | lombok { 32 | version = "1.18.12" 33 | } 34 | 35 | configurations { 36 | propogated 37 | [implementation, testImplementation]*.extendsFrom propogated 38 | } 39 | 40 | dependencies { 41 | propogated(platform("org.testcontainers:testcontainers-bom:${testcontainersVersion}")) 42 | propogated('org.testcontainers:toxiproxy') 43 | 44 | implementation("org.yaml:snakeyaml:1.25") 45 | implementation("com.intellij:annotations:12.0") 46 | 47 | testImplementation(platform("org.junit:junit-bom:${junitVersion}")) 48 | testImplementation("org.junit.jupiter:junit-jupiter-params") 49 | testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") 50 | 51 | testImplementation('org.mockito:mockito-core:3.0.0') 52 | testImplementation('ch.qos.logback:logback-classic:1.2.3') 53 | testImplementation("org.mongodb:mongodb-driver-reactivestreams:${mongodbDriverVersion}") 54 | testImplementation("org.mongodb:mongodb-driver-sync:${mongodbDriverVersion}") 55 | testImplementation("org.mockito:mockito-junit-jupiter:3.3.0") 56 | testImplementation("org.assertj:assertj-core:3.18.1") 57 | } 58 | 59 | wrapper { 60 | gradleVersion = '6.4' 61 | distributionType = Wrapper.DistributionType.BIN 62 | } 63 | 64 | def junitParallelExecProperty = 'junit.jupiter.execution.parallel.enabled' 65 | 66 | tasks.withType(Test) { Test task -> 67 | task.useJUnitPlatform { JUnitPlatformOptions ops -> 68 | ops.excludeTags("integration-test") 69 | } 70 | 71 | task.failFast = true 72 | 73 | testLogging.showStandardStreams = true 74 | testLogging.exceptionFormat = 'full' 75 | 76 | reports.html.destination = file("${reporting.baseDir}/${name}") 77 | } 78 | 79 | task integrationTest(type: Test) { Test task -> 80 | task.useJUnitPlatform { JUnitPlatformOptions ops -> 81 | ops.includeTags("integration-test") 82 | } 83 | task.systemProperties( 84 | System.properties.findAll { 85 | it.key.toString().startsWith("mongoReplicaSetProperties") || 86 | it.key.toString().startsWith("junit") 87 | } 88 | ) 89 | if (!task.systemProperties.isEmpty()) { 90 | println("Detected test system properties: " + task.systemProperties) 91 | } 92 | 93 | task.failFast = true 94 | 95 | testLogging.showStandardStreams = true 96 | testLogging.exceptionFormat = 'full' 97 | 98 | if (System.properties.containsKey(junitParallelExecProperty) 99 | && (System.properties.get(junitParallelExecProperty))) { 100 | maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 101 | forkEvery = 1 102 | } 103 | 104 | task.minHeapSize('512m') 105 | task.maxHeapSize('3072m') 106 | 107 | check.dependsOn integrationTest 108 | integrationTest.mustRunAfter test 109 | finalizedBy jacocoTestReport 110 | } 111 | 112 | task delombok(type: DelombokTask) { 113 | ext.outputDir = file("$buildDir/delombok") 114 | outputs.dir(outputDir) 115 | sourceSets.main.java.srcDirs.each { 116 | inputs.dir(it) 117 | args(it, "-d", outputDir) 118 | } 119 | doFirst { 120 | outputDir.deleteDir() 121 | } 122 | } 123 | 124 | shadowJar { 125 | //https://github.com/johnrengelman/shadow/issues/463 126 | classifier = '' 127 | relocate 'org.yaml.snakeyaml', 'com.github.silaev.mongodb.replicaset.shaded' 128 | dependencies { 129 | include(dependency('org.yaml:snakeyaml')) 130 | } 131 | mergeServiceFiles() 132 | } 133 | 134 | task sourcesJar(type: Jar) { 135 | dependsOn delombok 136 | from delombok.outputDir 137 | archiveClassifier = 'sources' 138 | } 139 | 140 | task javadocJar(type: Jar) { 141 | from javadoc 142 | archiveClassifier = 'javadoc' 143 | } 144 | 145 | compileJava { 146 | sourceCompatibility = 1.8 147 | targetCompatibility = 1.8 148 | 149 | options.encoding = 'UTF-8' 150 | options.compilerArgs << "-Xlint:deprecation" 151 | } 152 | 153 | compileTestJava { 154 | sourceCompatibility = 1.8 155 | targetCompatibility = 1.8 156 | 157 | options.encoding = 'UTF-8' 158 | options.compilerArgs << "-Xlint:deprecation" 159 | } 160 | 161 | javadoc { 162 | dependsOn delombok 163 | source = delombok.outputDir 164 | options.locale = 'en_US' 165 | 166 | if (JavaVersion.current().isJava9Compatible()) { 167 | options.addBooleanOption('html5', true) 168 | } 169 | } 170 | 171 | def isPublishingProperty = 'isPublishing' 172 | 173 | sourceSets { 174 | def isPublishing = System.properties.containsKey(isPublishingProperty) && 175 | (System.properties.get(isPublishingProperty) == "true") 176 | main { 177 | java { 178 | srcDirs = isPublishing ? [delombok.outputDir] : ['src/main/java'] 179 | } 180 | } 181 | } 182 | 183 | publishing { 184 | publications { 185 | shadow(MavenPublication) { MavenPublication publication -> 186 | project.shadow.component(publication) 187 | 188 | artifact sourcesJar 189 | artifact javadocJar 190 | 191 | pom { 192 | name = 'MongoDB replica set' 193 | description = 'Java8 MongoDbReplicaSet to construct a full-featured MongoDB cluster for integration testing, reproducing production issues, learning distributed systems by the example of MongoDB' 194 | url = 'https://github.com/silaev/mongodb-replica-set' 195 | 196 | licenses { 197 | license { 198 | name = 'MIT' 199 | url = 'http://opensource.org/licenses/MIT' 200 | } 201 | } 202 | developers { 203 | developer { 204 | id = 'silaev' 205 | name = 'Konstantin Silaev' 206 | email = 'silaev256@gmail.com' 207 | } 208 | } 209 | scm { 210 | url = 'https://github.com/silaev/mongodb-replica-set/' 211 | connection = 'scm:git:git://github.com/silaev/mongodb-replica-set.git' 212 | developerConnection = 'scm:git:ssh://git@github.com/silaev/mongodb-replica-set.git' 213 | } 214 | } 215 | 216 | pom.withXml { 217 | def dependencyManagementDependencies = 218 | asNode().appendNode('dependencyManagement').appendNode('dependencies') 219 | 220 | project.configurations.propogated.allDependencies.each { 221 | if (Category.REGULAR_PLATFORM == getCategory(it)) { 222 | def dependencyNode = dependencyManagementDependencies.appendNode("dependency") 223 | dependencyNode.appendNode('groupId', it.group) 224 | dependencyNode.appendNode('artifactId', it.name) 225 | dependencyNode.appendNode('version', it.version) 226 | dependencyNode.appendNode("scope", "import") 227 | dependencyNode.appendNode("type", "pom") 228 | } else { 229 | def dependencyNode = asNode().get("dependencies")[0].appendNode("dependency") 230 | dependencyNode.appendNode('groupId', it.group) 231 | dependencyNode.appendNode('artifactId', it.name) 232 | if (!Objects.isNull(it.version)) { 233 | dependencyNode.appendNode('version', it.version) 234 | } 235 | dependencyNode.appendNode("scope", "compile") 236 | } 237 | } 238 | } 239 | 240 | } 241 | } 242 | repositories { 243 | maven { 244 | mavenLocal() 245 | } 246 | } 247 | } 248 | 249 | private static String getCategory(Dependency dependency) { 250 | dependency.properties.get("attributes")?.value?.value?.toString() 251 | } 252 | 253 | 254 | tasks.withType(PublishToMavenLocal) { 255 | onlyIf { 256 | System.properties.containsKey(isPublishingProperty) && 257 | System.properties.get(isPublishingProperty) == "true" 258 | } 259 | doLast { 260 | System.properties.setProperty('isPublishing', "false") 261 | } 262 | } 263 | 264 | tasks.withType(PublishToMavenRepository) { 265 | onlyIf { 266 | System.properties.containsKey(isPublishingProperty) && 267 | System.properties.get(isPublishingProperty) == "true" 268 | } 269 | doLast { 270 | System.properties.setProperty('isPublishing', "false") 271 | } 272 | } 273 | 274 | def bintrayUser = 'bintrayUser' 275 | def bintrayApiKey = 'bintrayApiKey' 276 | bintray { 277 | if (project.hasProperty(bintrayUser) && 278 | project.hasProperty(bintrayApiKey) 279 | ) { 280 | user = project.property(bintrayUser) 281 | key = project.property(bintrayApiKey) 282 | 283 | publications = ['shadow'] 284 | override = false 285 | 286 | pkg { 287 | repo = 'releases' 288 | name = project.name 289 | 290 | publish = true 291 | 292 | version { 293 | name = project.version 294 | vcsTag = project.version 295 | released = new Date() 296 | gpg { 297 | sign = true 298 | } 299 | } 300 | } 301 | } 302 | } 303 | 304 | compileJava.dependsOn(delombok) 305 | assemble.dependsOn shadowJar 306 | -------------------------------------------------------------------------------- /docker-compose-it-test.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | run-it: 4 | image: run-it-test-mongodb-replica-set:0.4.3 5 | container_name: "run-it-test-mongodb-replica-set" 6 | environment: 7 | - MONGO_VERSION=${MONGO_VERSION} 8 | - USE_HOST_DOCKER_INTERNAL=${USE_HOST_DOCKER_INTERNAL} 9 | build: 10 | context: . 11 | dockerfile: Dockerfile.run-it-test-mongodb-replica-set 12 | volumes: 13 | - /var/run/docker.sock:/var/run/docker.sock 14 | logging: 15 | driver: "json-file" 16 | options: 17 | max-size: "10m" 18 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | junit.jupiter.execution.parallel.enabled=false 2 | junit.jupiter.execution.parallel.mode.default=concurrent 3 | junit.jupiter.execution.parallel.config.strategy=dynamic 4 | #junit.jupiter.execution.parallel.config.fixed.parallelism=4 -------------------------------------------------------------------------------- /gradle/jacoco.gradle: -------------------------------------------------------------------------------- 1 | import groovy.transform.Memoized 2 | 3 | @Memoized 4 | private static List getSonarExclusions(projectDir) { 5 | return new File("${projectDir}/testcov.exclude").readLines() 6 | } 7 | 8 | jacoco { 9 | toolVersion = "0.8.5" 10 | } 11 | 12 | jacocoTestReport { 13 | executionData.from = fileTree(buildDir).include("/jacoco/*.exec") 14 | reports { 15 | xml.enabled true 16 | csv.enabled false 17 | html.enabled true 18 | xml.destination file("${buildDir}/reports/jacoco/report.xml") 19 | } 20 | 21 | afterEvaluate { 22 | classDirectories.from = files(classDirectories.files.collect { 23 | fileTree(dir: it, 24 | exclude: getSonarExclusions(projectDir)) 25 | }) 26 | } 27 | } 28 | 29 | jacocoTestCoverageVerification { 30 | executionData.from = fileTree(buildDir).include("/jacoco/*.exec") 31 | violationRules { 32 | rule { 33 | limit { 34 | def minTestCov = System.properties.get("minTestCov") 35 | minimum = (minTestCov == null) ? 0.80 : new BigDecimal(minTestCov.toString()) 36 | } 37 | } 38 | } 39 | 40 | afterEvaluate { 41 | classDirectories.from = files(classDirectories.files.collect { 42 | fileTree(dir: it, 43 | exclude: getSonarExclusions(projectDir)) 44 | }) 45 | } 46 | } 47 | 48 | check.dependsOn jacocoTestCoverageVerification 49 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silaev/mongodb-replica-set/48d826f8abc150f72632aa3683a81e7ecae80e06/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue May 12 07:49:05 MSK 2020 2 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.4-all.zip 3 | distributionBase=GRADLE_USER_HOME 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin, switch paths to Windows format before running java 129 | if $cygwin ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=$((i+1)) 158 | done 159 | case $i in 160 | (0) set -- ;; 161 | (1) set -- "$args0" ;; 162 | (2) set -- "$args0" "$args1" ;; 163 | (3) set -- "$args0" "$args1" "$args2" ;; 164 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=$(save "$@") 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 184 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 185 | cd "$(dirname "$0")" 186 | fi 187 | 188 | exec "$JAVACMD" "$@" 189 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem http://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk8 3 | install: 4 | - echo "Running an install command" 5 | - chmod +x gradlew 6 | - ./gradlew publishToMavenLocal -DisPublishing=true 7 | -------------------------------------------------------------------------------- /scripts/codecov.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$FINALIZE" == 1 ]; then 4 | 5 | if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then 6 | echo "*** send stats to codecov"; 7 | bash <(curl -s https://codecov.io/bash); 8 | fi 9 | 10 | fi 11 | -------------------------------------------------------------------------------- /scripts/load_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd .. 4 | 5 | dir="log" 6 | if [ -d "$dir" ]; then 7 | rm -rfv ${dir:?}/* 8 | else 9 | mkdir $dir 10 | fi 11 | 12 | q=0; while [[ q -lt $1 ]]; do ((q++)); echo "$q"; ./gradlew clean integrationTest -DmongoReplicaSetProperties.mongoDockerImageName=mongo:$2 --no-daemon --parallel > log/log$q.log; done 13 | -------------------------------------------------------------------------------- /scripts/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "*** upload to Bintray"; 4 | ./gradlew -PbintrayUser=$BINTRAY_USER -PbintrayApiKey=$BINTRAY_API_KEY -DisPublishing=true bintrayUpload; 5 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'mongodb-replica-set' 2 | -------------------------------------------------------------------------------- /src/main/java/com/github/silaev/mongodb/replicaset/converter/Converter.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.converter; 2 | 3 | public interface Converter { 4 | T convert(S source); 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/github/silaev/mongodb/replicaset/converter/YmlConverter.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.converter; 2 | 3 | import java.io.InputStream; 4 | 5 | /** 6 | * Converts a yml file to a proper dto. 7 | * 8 | * @author Konstantin Silaev 9 | */ 10 | public interface YmlConverter { 11 | T unmarshal(Class clazz, final InputStream io); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/github/silaev/mongodb/replicaset/converter/impl/MongoNodeToMongoSocketAddressConverter.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.converter.impl; 2 | 3 | import com.github.silaev.mongodb.replicaset.converter.Converter; 4 | import com.github.silaev.mongodb.replicaset.model.MongoNode; 5 | import com.github.silaev.mongodb.replicaset.model.MongoSocketAddress; 6 | 7 | /** 8 | * Converts MongoNode to MongoSocketAddress without a replication port 9 | * so that to search in hash maps. 10 | * 11 | * @author Konstantin Silaev on 2/24/2020 12 | */ 13 | public class MongoNodeToMongoSocketAddressConverter 14 | implements Converter { 15 | @Override 16 | public MongoSocketAddress convert(MongoNode mongoNode) { 17 | if (mongoNode == null) { 18 | return null; 19 | } 20 | return MongoSocketAddress.builder() 21 | .ip(mongoNode.getIp()) 22 | .mappedPort(mongoNode.getPort()) 23 | .build(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/github/silaev/mongodb/replicaset/converter/impl/StringToMongoRsStatusConverter.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.converter.impl; 2 | 3 | import com.github.silaev.mongodb.replicaset.MongoDbReplicaSet; 4 | import com.github.silaev.mongodb.replicaset.converter.Converter; 5 | import com.github.silaev.mongodb.replicaset.converter.YmlConverter; 6 | import com.github.silaev.mongodb.replicaset.model.MongoNode; 7 | import com.github.silaev.mongodb.replicaset.model.MongoNodeMutable; 8 | import com.github.silaev.mongodb.replicaset.model.MongoRsStatus; 9 | import com.github.silaev.mongodb.replicaset.model.MongoRsStatusMutable; 10 | import com.github.silaev.mongodb.replicaset.model.ReplicaSetMemberState; 11 | import com.github.silaev.mongodb.replicaset.util.StringUtils; 12 | import lombok.AllArgsConstructor; 13 | import lombok.SneakyThrows; 14 | import lombok.extern.slf4j.Slf4j; 15 | import lombok.val; 16 | 17 | import java.io.ByteArrayInputStream; 18 | import java.nio.charset.StandardCharsets; 19 | import java.util.Collections; 20 | import java.util.Comparator; 21 | import java.util.List; 22 | import java.util.Objects; 23 | import java.util.Optional; 24 | import java.util.regex.Pattern; 25 | import java.util.stream.Collectors; 26 | 27 | /** 28 | * Converts a string to an instance of MongoRsStatus. 29 | * 30 | * @author Konstantin Silaev 31 | */ 32 | @AllArgsConstructor 33 | @Slf4j 34 | public class StringToMongoRsStatusConverter implements Converter { 35 | 36 | private static final String MONGO_VERSION_MARKER = "MongoDB server version:"; 37 | private static final String OK = "\"ok\" : "; 38 | private static final Pattern OK_PATTERN = Pattern.compile("(?i).*" + OK); 39 | 40 | private final YmlConverter yamlConverter; 41 | private final VersionConverter versionConverter; 42 | 43 | public StringToMongoRsStatusConverter() { 44 | this.yamlConverter = new YmlConverterImpl(); 45 | this.versionConverter = new VersionConverter(); 46 | } 47 | 48 | @SneakyThrows 49 | @Override 50 | public MongoRsStatus convert(String source) { 51 | val io = new ByteArrayInputStream( 52 | extractJsonPayloadFromMongoDBShell(source).getBytes(StandardCharsets.UTF_8) 53 | ); 54 | MongoRsStatusMutable mongoRsStatusMutable; 55 | try { 56 | mongoRsStatusMutable = yamlConverter.unmarshal(MongoRsStatusMutable.class, io); 57 | } catch (Exception e) { 58 | log.error("Cannot convert to yaml format: \n{}", source); 59 | throw e; 60 | } 61 | if (Objects.isNull(mongoRsStatusMutable)) { 62 | return MongoRsStatus.of( 63 | 0, 64 | null, 65 | Collections.emptyList() 66 | ); 67 | } else { 68 | return MongoRsStatus.of( 69 | mongoRsStatusMutable.getStatus(), 70 | versionConverter.convert(mongoRsStatusMutable.getVersion()), 71 | getImmutableMembers(mongoRsStatusMutable) 72 | ); 73 | } 74 | } 75 | 76 | private List getImmutableMembers(final MongoRsStatusMutable mongoRsStatusMutable) { 77 | return Optional.ofNullable(mongoRsStatusMutable.getMembers()) 78 | .map(m -> mongoRsStatusMutable.getMembers().stream() 79 | .map(this::mongoNodeMapping) 80 | .sorted(Comparator.comparing(MongoNode::getPort)) 81 | .collect( 82 | Collectors.collectingAndThen( 83 | Collectors.toList(), 84 | Collections::unmodifiableList 85 | ) 86 | ) 87 | ).orElseGet(Collections::emptyList); 88 | } 89 | 90 | private MongoNode mongoNodeMapping(final MongoNodeMutable node) { 91 | val address = StringUtils.getArrayByDelimiter(node.getName()); 92 | return MongoNode.of( 93 | address[0], 94 | Integer.parseInt(address[1]), 95 | node.getHealth(), 96 | ReplicaSetMemberState.getByValue(node.getState()) 97 | ); 98 | } 99 | 100 | public String extractRawPayloadFromMongoDBShell(final String mongoDbReply) { 101 | return extractRawPayloadFromMongoDBShell(mongoDbReply, false); 102 | } 103 | 104 | private String extractJsonPayloadFromMongoDBShell(final String mongoDbReply) { 105 | return extractRawPayloadFromMongoDBShell(mongoDbReply, true); 106 | } 107 | 108 | private String extractRawPayloadFromMongoDBShell(final String mongoDbReply, final boolean formatJson) { 109 | String version = null; 110 | val lines = mongoDbReply.replace("\t", "").split("\n"); 111 | int idx = 0; 112 | val length = lines.length; 113 | while (idx < length) { 114 | String currentLine = lines[idx]; 115 | if (!currentLine.isEmpty() && currentLine.contains(MONGO_VERSION_MARKER)) { 116 | version = currentLine.substring(currentLine.indexOf(':') + 1).trim(); 117 | idx++; 118 | break; 119 | } 120 | idx++; 121 | } 122 | 123 | val sb = new StringBuilder(); 124 | for (int i = idx; i < length; i++) { 125 | final String line = lines[i]; 126 | if (!line.startsWith(MongoDbReplicaSet.WAITING_MSG)) { 127 | sb.append(line.replaceAll("\\s\\s", "")); 128 | } 129 | } 130 | if (formatJson) { 131 | val matcher = OK_PATTERN.matcher(sb); 132 | val endIndexOk = matcher.find() ? matcher.end() : 0; 133 | if (endIndexOk > 0) { 134 | val status = sb.substring(endIndexOk, endIndexOk + 1); 135 | sb.delete(endIndexOk - OK.length(), sb.length()); 136 | val strExtra = String.format("\"version\" : \"%s\",", version) + String.format("\"status\" : \"%s\"}", status); 137 | sb.append(strExtra); 138 | } 139 | } 140 | return sb.toString(); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/main/java/com/github/silaev/mongodb/replicaset/converter/impl/UserInputToApplicationPropertiesConverter.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.converter.impl; 2 | 3 | import com.github.silaev.mongodb.replicaset.MongoDbReplicaSet; 4 | import com.github.silaev.mongodb.replicaset.converter.Converter; 5 | import com.github.silaev.mongodb.replicaset.converter.YmlConverter; 6 | import com.github.silaev.mongodb.replicaset.exception.IncorrectUserInputException; 7 | import com.github.silaev.mongodb.replicaset.model.ApplicationProperties; 8 | import com.github.silaev.mongodb.replicaset.model.MongoReplicaSetProperties; 9 | import com.github.silaev.mongodb.replicaset.model.PropertyContainer; 10 | import com.github.silaev.mongodb.replicaset.model.UserInputProperties; 11 | import com.github.silaev.mongodb.replicaset.service.ResourceService; 12 | import com.github.silaev.mongodb.replicaset.service.impl.ResourceServiceImpl; 13 | import com.github.silaev.mongodb.replicaset.util.StringUtils; 14 | import lombok.AllArgsConstructor; 15 | import lombok.val; 16 | 17 | import java.util.Collections; 18 | import java.util.Objects; 19 | import java.util.Optional; 20 | 21 | /** 22 | * Converts MongoReplicaSetInputProperties to ApplicationProperties 23 | * by evaluating properties located in different sources 24 | * (a yml file, a system property or a default value). 25 | * 26 | * @author Konstantin Silaev 27 | */ 28 | @AllArgsConstructor 29 | public class UserInputToApplicationPropertiesConverter 30 | implements Converter { 31 | public static final int REPLICA_SET_NUMBER_DEFAULT = 1; 32 | public static final int AWAIT_NODE_INIT_ATTEMPTS = 29; 33 | public static final String MONGO_DOCKER_IMAGE_DEFAULT = "mongo:4.0.10"; 34 | public static final boolean USE_HOST_DOCKER_INTERNAL_DEFAULT = false; 35 | private static final Boolean ADD_ARBITER_DEFAULT = Boolean.FALSE; 36 | private static final boolean ENABLED_DEFAULT = true; 37 | private static final String YML_FORMAT = "yml"; 38 | private final YmlConverter ymlConverter; 39 | private final ResourceService resourceService; 40 | 41 | public UserInputToApplicationPropertiesConverter() { 42 | this.ymlConverter = new YmlConverterImpl(); 43 | this.resourceService = new ResourceServiceImpl(); 44 | } 45 | 46 | /** 47 | * Constructs a new MongoReplicaSetProperties from a provided yml file. 48 | * 49 | * @param propertyFileName a yml file 50 | * @return a instance of MongoReplicaSetProperties 51 | */ 52 | MongoReplicaSetProperties getFileProperties( 53 | final String propertyFileName 54 | ) { 55 | if ((Objects.isNull(propertyFileName)) || StringUtils.isBlank(propertyFileName)) { 56 | return new MongoReplicaSetProperties(); 57 | } 58 | 59 | if (!YML_FORMAT.equals( 60 | propertyFileName.substring(propertyFileName.lastIndexOf('.') + 1)) 61 | ) { 62 | throw new IllegalArgumentException( 63 | String.format("Incorrect file format: %s is not a %s file.", propertyFileName, YML_FORMAT) 64 | ); 65 | } 66 | 67 | return ymlConverter.unmarshal( 68 | PropertyContainer.class, 69 | resourceService.getResourceIO(propertyFileName) 70 | ).getMongoReplicaSetProperties(); 71 | } 72 | 73 | private String getMongoDockerImageName( 74 | final String mongoDockerImageNameInput, 75 | final String dockerImageNameFileProperty 76 | ) { 77 | 78 | return Optional.ofNullable(mongoDockerImageNameInput) 79 | .orElseGet( 80 | () -> Optional.ofNullable(System.getProperty("mongoReplicaSetProperties.mongoDockerImageName")) 81 | .orElseGet( 82 | () -> Optional.ofNullable(dockerImageNameFileProperty) 83 | .orElse(MONGO_DOCKER_IMAGE_DEFAULT) 84 | ) 85 | ); 86 | } 87 | 88 | private Boolean getUseHostDockerInternal(Boolean useHostDockerInternalInput) { 89 | return Optional.ofNullable(useHostDockerInternalInput) 90 | .orElseGet( 91 | () -> Optional.ofNullable(System.getProperty("mongoReplicaSetProperties.useHostDockerInternal")) 92 | .map(Boolean::valueOf) 93 | .orElse(USE_HOST_DOCKER_INTERNAL_DEFAULT) 94 | ); 95 | } 96 | 97 | private Boolean getEnabled(Boolean fileProperties) { 98 | return Optional.ofNullable(System.getProperty("mongoReplicaSetProperties.enabled")) 99 | .map(Boolean::valueOf) 100 | .orElseGet( 101 | () -> Optional.ofNullable(fileProperties) 102 | .orElse(ENABLED_DEFAULT) 103 | ); 104 | } 105 | 106 | public ApplicationProperties convert( 107 | final UserInputProperties inputProperties 108 | ) { 109 | validateInputProperties(inputProperties); 110 | 111 | val propertyFileName = inputProperties.getPropertyFileName(); 112 | val fileProperties = getFileProperties(propertyFileName); 113 | 114 | val replicaSetNumber = Optional.ofNullable(inputProperties.getReplicaSetNumber()) 115 | .orElse(UserInputToApplicationPropertiesConverter.REPLICA_SET_NUMBER_DEFAULT); 116 | val awaitNodeInitAttempts = Optional.ofNullable(inputProperties.getAwaitNodeInitAttempts()) 117 | .orElse(UserInputToApplicationPropertiesConverter.AWAIT_NODE_INIT_ATTEMPTS); 118 | val isEnabled = getEnabled(fileProperties.getEnabled()); 119 | val mongoDockerImageName = getMongoDockerImageName( 120 | inputProperties.getMongoDockerImageName(), 121 | fileProperties.getMongoDockerImageName() 122 | ); 123 | val addArbiter = Optional.ofNullable(inputProperties.getAddArbiter()) 124 | .orElse(UserInputToApplicationPropertiesConverter.ADD_ARBITER_DEFAULT); 125 | val addToxiproxy = Optional.ofNullable(inputProperties.getAddToxiproxy()).orElse(false); 126 | val slaveDelayTimeout = Optional.ofNullable(inputProperties.getSlaveDelayTimeout()).orElse(0); 127 | val slaveDelayNumber = Optional.ofNullable(inputProperties.getSlaveDelayNumber()).orElse(0); 128 | val useHostDockerInternal = getUseHostDockerInternal(inputProperties.getUseHostDockerInternal()); 129 | 130 | return ApplicationProperties.builder() 131 | .replicaSetNumber(replicaSetNumber) 132 | .addArbiter(addArbiter) 133 | .awaitNodeInitAttempts(awaitNodeInitAttempts) 134 | .mongoDockerImageName(mongoDockerImageName) 135 | .isEnabled(isEnabled) 136 | .addToxiproxy(addToxiproxy) 137 | .slaveDelayTimeout(slaveDelayTimeout) 138 | .slaveDelayNumber(slaveDelayNumber) 139 | .useHostDockerInternal(useHostDockerInternal) 140 | .commandLineOptions( 141 | Optional.ofNullable(inputProperties.getCommandLineOptions()).orElse(Collections.emptyList()) 142 | ) 143 | .build(); 144 | } 145 | 146 | private void validateInputProperties(final UserInputProperties inputProperties) { 147 | Optional.ofNullable(inputProperties.getReplicaSetNumber()) 148 | .ifPresent(n -> { 149 | if (n < 1 || n > MongoDbReplicaSet.MAX_VOTING_MEMBERS) { 150 | throw new IncorrectUserInputException( 151 | String.format( 152 | "Please, set replicaSetNumber more than 0 and less than %d", 153 | MongoDbReplicaSet.MAX_VOTING_MEMBERS 154 | ) 155 | ); 156 | } 157 | } 158 | ); 159 | 160 | if (Objects.nonNull(inputProperties.getAddArbiter()) && Objects.nonNull(inputProperties.getReplicaSetNumber()) && 161 | (inputProperties.getAddArbiter() && inputProperties.getReplicaSetNumber() == 1)) { 162 | throw new IncorrectUserInputException( 163 | "Adding an arbiter node is not supported for a single node replica set" 164 | ); 165 | } 166 | 167 | if (Objects.nonNull(inputProperties.getSlaveDelayTimeout()) && (Objects.nonNull(inputProperties.getReplicaSetNumber())) && 168 | inputProperties.getSlaveDelayTimeout() > 0 && inputProperties.getReplicaSetNumber() == 1) { 169 | throw new IncorrectUserInputException( 170 | "Cannot create a replica set with delayed members having only one member" 171 | ); 172 | } 173 | 174 | if (Objects.nonNull(inputProperties.getSlaveDelayNumber()) && (Objects.nonNull(inputProperties.getReplicaSetNumber())) && 175 | (inputProperties.getSlaveDelayNumber() > inputProperties.getReplicaSetNumber())) { 176 | throw new IncorrectUserInputException( 177 | "Cannot create a replica set with delayed members because slaveDelayNumber>replicaSetNumber" 178 | ); 179 | } 180 | 181 | if (Objects.nonNull(inputProperties.getSlaveDelayNumber()) && inputProperties.getSlaveDelayTimeout() == 0) { 182 | throw new IncorrectUserInputException( 183 | "Please, specify slaveDelayTimeout" 184 | ); 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/main/java/com/github/silaev/mongodb/replicaset/converter/impl/VersionConverter.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.converter.impl; 2 | 3 | import com.github.silaev.mongodb.replicaset.converter.Converter; 4 | import com.github.silaev.mongodb.replicaset.model.MongoDbVersion; 5 | import lombok.val; 6 | 7 | import java.util.Objects; 8 | 9 | /** 10 | * Converts a string to an instance of MongoDbVersion following Semantic Versioning. 11 | * 12 | * @author Konstantin Silaev 13 | */ 14 | public class VersionConverter implements Converter { 15 | @Override 16 | public MongoDbVersion convert(String source) { 17 | if (Objects.isNull(source)) { 18 | throw new IllegalArgumentException("Version is not supposed to be null"); 19 | } 20 | val strings = source.split("\\."); 21 | if (strings.length < 2) { 22 | throw new IllegalArgumentException( 23 | String.format( 24 | "Mongo DB version %s should have at least major and minor parts", 25 | source 26 | ) 27 | ); 28 | } 29 | return MongoDbVersion.of( 30 | Integer.parseInt(strings[0]), 31 | Integer.parseInt(strings[1]), 32 | strings.length == 3 ? Integer.parseInt(strings[2]) : 0 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/github/silaev/mongodb/replicaset/converter/impl/YmlConverterImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.converter.impl; 2 | 3 | import com.github.silaev.mongodb.replicaset.converter.YmlConverter; 4 | import lombok.val; 5 | import org.yaml.snakeyaml.Yaml; 6 | import org.yaml.snakeyaml.constructor.Constructor; 7 | import org.yaml.snakeyaml.representer.Representer; 8 | 9 | import java.io.InputStream; 10 | 11 | /** 12 | * Converts an input stream representing a yml file to a proper dto. 13 | * 14 | * @author Konstantin Silaev 15 | */ 16 | public class YmlConverterImpl implements YmlConverter { 17 | /** 18 | * Unmarshals an input stream into an instance of clazz 19 | * 20 | * @param clazz a target class 21 | * @param io an input stream representing a yml file 22 | * @param a type parameter for target class 23 | * @return T an instance of a target class 24 | */ 25 | public T unmarshal(Class clazz, final InputStream io) { 26 | val representer = new Representer(); 27 | representer.getPropertyUtils().setSkipMissingProperties(true); 28 | Yaml yaml = new Yaml( 29 | new Constructor(clazz), 30 | representer 31 | ); 32 | return yaml.load(io); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/github/silaev/mongodb/replicaset/core/Generated.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.core; 2 | 3 | 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | /** 10 | * Excludes marked methods from code coverage. 11 | *

12 | * Should be used only for experimental code. 13 | */ 14 | @Retention(RetentionPolicy.RUNTIME) 15 | @Target(ElementType.METHOD) 16 | public @interface Generated { 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/github/silaev/mongodb/replicaset/exception/IncorrectUserInputException.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.exception; 2 | 3 | /** 4 | * @author Konstantin Silaev 5 | */ 6 | public class IncorrectUserInputException extends RuntimeException { 7 | 8 | public IncorrectUserInputException(String errorMessage) { 9 | super(errorMessage); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/github/silaev/mongodb/replicaset/exception/MongoNodeInitializationException.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.exception; 2 | 3 | /** 4 | * @author Konstantin Silaev 5 | */ 6 | public class MongoNodeInitializationException extends RuntimeException { 7 | 8 | public MongoNodeInitializationException(String errorMessage) { 9 | super(errorMessage); 10 | } 11 | 12 | public MongoNodeInitializationException(String message, Throwable cause) { 13 | super(message, cause); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/github/silaev/mongodb/replicaset/model/ApplicationProperties.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.model; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * Immutable class property class to evaluate them from different sources. 10 | * 11 | * @author Konstantin Silaev 12 | */ 13 | @Getter 14 | @Builder 15 | public class ApplicationProperties { 16 | private final int replicaSetNumber; 17 | private final int awaitNodeInitAttempts; 18 | private final String mongoDockerImageName; 19 | private final boolean isEnabled; 20 | private final boolean addArbiter; 21 | private final boolean addToxiproxy; 22 | private final int slaveDelayTimeout; 23 | private final int slaveDelayNumber; 24 | private final boolean useHostDockerInternal; 25 | private final List commandLineOptions; 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/silaev/mongodb/replicaset/model/MongoDbVersion.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.model; 2 | 3 | import lombok.Value; 4 | 5 | /** 6 | * Immutable class to keep a Mongo Db version. 7 | * 8 | * @author Konstantin Silaev 9 | */ 10 | @Value(staticConstructor = "of") 11 | public class MongoDbVersion { 12 | int major; 13 | int minor; 14 | int patch; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/github/silaev/mongodb/replicaset/model/MongoNode.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.model; 2 | 3 | import lombok.Value; 4 | 5 | /** 6 | * Immutable class to load data via SnakeYml. 7 | * Describes a mongo node to use in public API. 8 | * 9 | * @author Konstantin Silaev 10 | */ 11 | @Value(staticConstructor = "of") 12 | public class MongoNode { 13 | String ip; 14 | Integer port; 15 | Double health; 16 | ReplicaSetMemberState state; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/github/silaev/mongodb/replicaset/model/MongoNodeMutable.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | /** 9 | * Mutable class to load data via an external library. 10 | * Describes a mongo node constructed by SnakeYml. 11 | * 12 | * @author Konstantin Silaev 13 | */ 14 | @Data 15 | @NoArgsConstructor 16 | @AllArgsConstructor 17 | @Builder 18 | public class MongoNodeMutable { 19 | private String name; 20 | private Double health; 21 | private Integer state; 22 | private String stateStr; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/github/silaev/mongodb/replicaset/model/MongoReplicaSetProperties.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.model; 2 | 3 | import lombok.Data; 4 | import lombok.NoArgsConstructor; 5 | 6 | /** 7 | * Mutable class to load data via an external library. 8 | * Describes a user defined properties constructed by SnakeYml. 9 | * 10 | * @author Konstantin Silaev 11 | */ 12 | @Data 13 | @NoArgsConstructor 14 | public class MongoReplicaSetProperties { 15 | private Boolean enabled; 16 | private String mongoDockerImageName; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/github/silaev/mongodb/replicaset/model/MongoRsStatus.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.model; 2 | 3 | import lombok.Value; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * Immutable class to load data via SnakeYml. 9 | * Describing a mongo cluster to use in public API. 10 | * 11 | * @author Konstantin Silaev 12 | */ 13 | @Value(staticConstructor = "of") 14 | public class MongoRsStatus { 15 | Integer status; 16 | MongoDbVersion version; 17 | List members; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/github/silaev/mongodb/replicaset/model/MongoRsStatusMutable.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.model; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * Mutable class to load data via an external library. 9 | * Describes a mongo cluster constructed by SnakeYml. 10 | * 11 | * @author Konstantin Silaev 12 | */ 13 | @Data 14 | public class MongoRsStatusMutable { 15 | private Integer status; 16 | private String version; 17 | private List members; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/github/silaev/mongodb/replicaset/model/MongoSocketAddress.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.model; 2 | 3 | import lombok.Builder; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.Getter; 6 | import lombok.ToString; 7 | 8 | /** 9 | * Immutable class representing a socket address for a mongo node. 10 | * 11 | * @author Konstantin Silaev 12 | */ 13 | @EqualsAndHashCode(of = {"ip", "mappedPort"}) 14 | @Builder 15 | @Getter 16 | @ToString 17 | public final class MongoSocketAddress { 18 | private final String ip; 19 | private final Integer replSetPort; 20 | private final Integer mappedPort; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/github/silaev/mongodb/replicaset/model/Pair.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.model; 2 | 3 | import lombok.Value; 4 | 5 | /** 6 | * An immutable class containing 2 values. 7 | * Used in order to avoid a verbose AbstractMap.SimpleImmutableEntry or 3rd party libraries. 8 | * 9 | * @author Konstantin Silaev on 1/29/2020 10 | */ 11 | @Value(staticConstructor = "of") 12 | public class Pair { 13 | L left; 14 | R right; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/github/silaev/mongodb/replicaset/model/PropertyContainer.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | /** 8 | * Represents a mutable container for properties coming from a yml file. 9 | * 10 | * @author Konstantin Silaev 11 | */ 12 | @Data 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | public class PropertyContainer { 16 | private MongoReplicaSetProperties mongoReplicaSetProperties; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/github/silaev/mongodb/replicaset/model/ReplicaSetMemberState.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.model; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | /** 7 | * Represents states that a MongoDb node has as per: 8 | * Replica Set Member States 9 | * 10 | * @author Konstantin Silaev on 10/3/2019 11 | */ 12 | @RequiredArgsConstructor 13 | public enum ReplicaSetMemberState { 14 | STARTUP(0), 15 | PRIMARY(1), 16 | SECONDARY(2), 17 | RECOVERING(3), 18 | STARTUP2(5), 19 | UNKNOWN(6), 20 | ARBITER(7), 21 | DOWN(8), 22 | ROLLBACK(9), 23 | NOT_RECOGNIZED(Integer.MAX_VALUE); 24 | 25 | @Getter 26 | private final int value; 27 | 28 | public static ReplicaSetMemberState getByValue(final int value) { 29 | for (ReplicaSetMemberState state : values()) { 30 | if (state.getValue() == value) { 31 | return state; 32 | } 33 | } 34 | return NOT_RECOGNIZED; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/github/silaev/mongodb/replicaset/model/UserInputProperties.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.model; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * Basic input properties coming from MongoReplicaSet's builder. 10 | * 11 | * @author Konstantin Silaev 12 | */ 13 | @Builder 14 | @Getter 15 | public final class UserInputProperties { 16 | private final Integer replicaSetNumber; 17 | private final Integer awaitNodeInitAttempts; 18 | private final String mongoDockerImageName; 19 | private final Boolean addArbiter; 20 | private final Boolean addToxiproxy; 21 | private final Integer slaveDelayTimeout; 22 | private final String propertyFileName; 23 | private final Integer slaveDelayNumber; 24 | private final Boolean useHostDockerInternal; 25 | private final List commandLineOptions; 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/silaev/mongodb/replicaset/service/ResourceService.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.service; 2 | 3 | import java.io.InputStream; 4 | 5 | public interface ResourceService { 6 | InputStream getResourceIO(final String fileName); 7 | 8 | String getString(final InputStream io); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/github/silaev/mongodb/replicaset/service/impl/ResourceServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.service.impl; 2 | 3 | import com.github.silaev.mongodb.replicaset.service.ResourceService; 4 | import lombok.SneakyThrows; 5 | 6 | import java.io.ByteArrayOutputStream; 7 | import java.io.InputStream; 8 | import java.nio.charset.StandardCharsets; 9 | 10 | /** 11 | * Gets resource as a stream making it possible to mock such a call. 12 | * Also has some helper methods for the same reason. 13 | * 14 | * @author Konstantin Silaev 15 | */ 16 | public class ResourceServiceImpl implements ResourceService { 17 | public InputStream getResourceIO(final String fileName) { 18 | return Thread.currentThread() 19 | .getContextClassLoader() 20 | .getResourceAsStream(fileName); 21 | } 22 | 23 | @SneakyThrows 24 | public String getString(final InputStream io) { 25 | final ByteArrayOutputStream result = new ByteArrayOutputStream(); 26 | final byte[] buffer = new byte[1024]; 27 | int length; 28 | while ((length = io.read(buffer)) != -1) { 29 | result.write(buffer, 0, length); 30 | } 31 | return result.toString(StandardCharsets.UTF_8.name()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/github/silaev/mongodb/replicaset/util/StringUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.util; 2 | 3 | import java.util.Optional; 4 | 5 | public class StringUtils { 6 | private StringUtils() { 7 | } 8 | 9 | public static boolean isBlank(String str) { 10 | int strLen; 11 | if (str != null && (strLen = str.length()) != 0) { 12 | for (int i = 0; i < strLen; ++i) { 13 | if (!Character.isWhitespace(str.charAt(i))) { 14 | return false; 15 | } 16 | } 17 | return true; 18 | } else { 19 | return true; 20 | } 21 | } 22 | 23 | public static String[] getArrayByDelimiter(final String s) { 24 | return Optional.ofNullable(s) 25 | .map(n -> n.split(":")) 26 | .orElseThrow( 27 | () -> new IllegalArgumentException("Parameter should not be null") 28 | ); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/com/github/silaev/mongodb/replicaset/MongoDbReplicaSetBuilderTest.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset; 2 | 3 | import com.github.silaev.mongodb.replicaset.converter.impl.UserInputToApplicationPropertiesConverter; 4 | import com.github.silaev.mongodb.replicaset.exception.IncorrectUserInputException; 5 | import lombok.val; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.function.Executable; 8 | import org.junit.jupiter.params.ParameterizedTest; 9 | import org.junit.jupiter.params.provider.ValueSource; 10 | 11 | import static org.assertj.core.api.Assertions.assertThat; 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | import static org.junit.jupiter.api.Assertions.assertThrows; 14 | 15 | /** 16 | * Constructs a MongoReplicaSet via a builder and verifies 17 | * the result coming from different sources 18 | * (a system property, a yml file, default value). 19 | */ 20 | class MongoDbReplicaSetBuilderTest { 21 | 22 | private static final String PREFIX = "mongoReplicaSetProperties."; 23 | private static final String DOCKER_IMAGE_NAME_PROPERTIES = PREFIX + "mongoDockerImageName"; 24 | private static final String ENABLED_PROPERTIES = PREFIX + "enabled"; 25 | private static final String USE_HOST_DOCKER_INTERNAL = PREFIX + "useHostDockerInternal"; 26 | 27 | @Test 28 | void shouldGetDefaultReplicaSetNumber() { 29 | //GIVEN 30 | val replicaNumberExpected = 31 | UserInputToApplicationPropertiesConverter.REPLICA_SET_NUMBER_DEFAULT; 32 | 33 | //WHEN 34 | val replicaSet = MongoDbReplicaSet.builder().build(); 35 | 36 | //THEN 37 | assertEquals( 38 | replicaNumberExpected, 39 | replicaSet.getReplicaSetNumber() 40 | ); 41 | } 42 | 43 | @ParameterizedTest(name = "{index}: replicaSetNumber: {0}") 44 | @ValueSource(ints = {0, MongoDbReplicaSet.MAX_VOTING_MEMBERS + 1}) 45 | void shouldThrowExceptionBecauseOfIncorrectReplicaSetNumber( 46 | final int replicaSetNumber 47 | ) { 48 | //GIVEN 49 | //replicaSetNumber 50 | 51 | //WHEN 52 | Executable executable = 53 | () -> MongoDbReplicaSet.builder().replicaSetNumber(replicaSetNumber).build(); 54 | 55 | //THEN 56 | assertThrows(IncorrectUserInputException.class, executable); 57 | } 58 | 59 | @Test 60 | void shouldGetDefaultAwaitNodeInitAttempts() { 61 | //GIVEN 62 | val awaitExpected = 63 | UserInputToApplicationPropertiesConverter.AWAIT_NODE_INIT_ATTEMPTS; 64 | 65 | //WHEN 66 | val replicaSet = MongoDbReplicaSet.builder().build(); 67 | 68 | //THEN 69 | assertEquals( 70 | awaitExpected, 71 | replicaSet.getAwaitNodeInitAttempts() 72 | ); 73 | } 74 | 75 | @Test 76 | void shouldGetDefaultMongoDockerImageName() { 77 | //GIVEN 78 | val mongoDockerImageExpected = 79 | UserInputToApplicationPropertiesConverter.MONGO_DOCKER_IMAGE_DEFAULT; 80 | 81 | //WHEN 82 | val replicaSet = MongoDbReplicaSet.builder().build(); 83 | 84 | //THEN 85 | assertEquals( 86 | mongoDockerImageExpected, 87 | replicaSet.getMongoDockerImageName() 88 | ); 89 | } 90 | 91 | @Test 92 | void shouldGetUseHostDockerInternalFromProperty() { 93 | //GIVEN 94 | try { 95 | System.setProperty(USE_HOST_DOCKER_INTERNAL, "true"); 96 | 97 | //WHEN 98 | val replicaSet = MongoDbReplicaSet.builder().build(); 99 | 100 | //THEN 101 | assertThat(replicaSet.getUseHostDockerInternal()).isTrue(); 102 | } finally { 103 | System.clearProperty(USE_HOST_DOCKER_INTERNAL); 104 | } 105 | } 106 | 107 | @Test 108 | void shouldGetUseHostDockerInternalFromInput() { 109 | //GIVEN 110 | try { 111 | System.setProperty(USE_HOST_DOCKER_INTERNAL, "false"); 112 | 113 | //WHEN 114 | final MongoDbReplicaSet replicaSet = MongoDbReplicaSet.builder().useHostDockerInternal(true).build(); 115 | 116 | //THEN 117 | assertThat(replicaSet.getUseHostDockerInternal()).isTrue(); 118 | } finally { 119 | System.clearProperty(USE_HOST_DOCKER_INTERNAL); 120 | } 121 | } 122 | 123 | @Test 124 | void shouldGetUseHostDockerInternalDefault() { 125 | //GIVEN 126 | try { 127 | System.clearProperty(USE_HOST_DOCKER_INTERNAL); 128 | 129 | //WHEN 130 | val replicaSet = MongoDbReplicaSet.builder().build(); 131 | 132 | //THEN 133 | assertThat(replicaSet.getUseHostDockerInternal()).isEqualTo( 134 | UserInputToApplicationPropertiesConverter.USE_HOST_DOCKER_INTERNAL_DEFAULT 135 | ); 136 | } finally { 137 | System.clearProperty(USE_HOST_DOCKER_INTERNAL); 138 | } 139 | } 140 | 141 | @Test 142 | void shouldGetMongoDockerImageNameFromSystemProperty() { 143 | //GIVEN 144 | try { 145 | val mongoDockerFile = "mongo:4.2.0"; 146 | System.setProperty(DOCKER_IMAGE_NAME_PROPERTIES, mongoDockerFile); 147 | 148 | //WHEN 149 | val replicaSet = MongoDbReplicaSet.builder().build(); 150 | 151 | //THEN 152 | assertEquals( 153 | mongoDockerFile, 154 | replicaSet.getMongoDockerImageName() 155 | ); 156 | } finally { 157 | System.clearProperty(DOCKER_IMAGE_NAME_PROPERTIES); 158 | } 159 | } 160 | 161 | @Test 162 | void shouldGetMongoDockerImageNameFromPropertyFile() { 163 | //GIVEN 164 | val propertyFileName = "enabled-false.yml"; 165 | val mongoDockerFile = "mongo:4.1.13"; 166 | 167 | //WHEN 168 | val replicaSet = MongoDbReplicaSet.builder() 169 | .propertyFileName(propertyFileName) 170 | .build(); 171 | 172 | //THEN 173 | assertEquals( 174 | mongoDockerFile, 175 | replicaSet.getMongoDockerImageName() 176 | ); 177 | } 178 | 179 | @Test 180 | void shouldGetDefaultIsEnabled() { 181 | //GIVEN 182 | val propertyFileName = "enabled-false.yml"; 183 | 184 | //WHEN 185 | val replicaSet = MongoDbReplicaSet.builder() 186 | .propertyFileName(propertyFileName) 187 | .build(); 188 | 189 | //THEN 190 | assertThat(replicaSet.isEnabled()).isFalse(); 191 | } 192 | 193 | @Test 194 | void shouldIsEnabledFromPropertyFile() { 195 | //GIVEN 196 | 197 | //WHEN 198 | val replicaSet = MongoDbReplicaSet.builder().build(); 199 | 200 | //THEN 201 | assertThat(replicaSet.isEnabled()).isTrue(); 202 | } 203 | 204 | @Test 205 | void shouldGetDefaultAddArbiter() { 206 | //GIVEN 207 | 208 | //WHEN 209 | val replicaSet = MongoDbReplicaSet.builder().build(); 210 | 211 | //THEN 212 | assertThat(replicaSet.getAddArbiter()).isFalse(); 213 | } 214 | 215 | @Test 216 | void shouldGetEnabledFromSystemProperty() { 217 | //GIVEN 218 | try { 219 | val enabled = Boolean.FALSE; 220 | System.setProperty(ENABLED_PROPERTIES, enabled.toString()); 221 | 222 | //WHEN 223 | val replicaSet = MongoDbReplicaSet.builder().build(); 224 | 225 | //THEN 226 | assertEquals(enabled, replicaSet.isEnabled()); 227 | 228 | } finally { 229 | System.clearProperty(ENABLED_PROPERTIES); 230 | } 231 | } 232 | 233 | @Test 234 | void shouldGetEnabledFromPropertyFile() { 235 | //GIVEN 236 | val enabled = Boolean.TRUE; 237 | val propertyFileName = "enabled-true.yml"; 238 | 239 | //WHEN 240 | val replicaSet = MongoDbReplicaSet.builder() 241 | .propertyFileName(propertyFileName) 242 | .build(); 243 | 244 | //THEN 245 | assertEquals(enabled, replicaSet.isEnabled()); 246 | } 247 | 248 | @Test 249 | void shouldGetDefaultEnabled() { 250 | //GIVEN 251 | val enabled = Boolean.TRUE; 252 | 253 | //WHEN 254 | val replicaSet = MongoDbReplicaSet.builder().build(); 255 | 256 | //THEN 257 | assertEquals(enabled, replicaSet.isEnabled()); 258 | } 259 | 260 | @Test 261 | void shouldThrowExceptionBecauseOfSlaveDelayOnSingleNode() { 262 | //GIVEN 263 | //replicaSetNumber 264 | 265 | //WHEN 266 | Executable executable = 267 | () -> MongoDbReplicaSet.builder().replicaSetNumber(1).slaveDelayTimeout(60).build(); 268 | 269 | //THEN 270 | assertThrows(IncorrectUserInputException.class, executable); 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /src/test/java/com/github/silaev/mongodb/replicaset/MongoDbReplicaSetTest.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset; 2 | 3 | import com.github.silaev.mongodb.replicaset.converter.impl.StringToMongoRsStatusConverter; 4 | import com.github.silaev.mongodb.replicaset.converter.impl.VersionConverter; 5 | import com.github.silaev.mongodb.replicaset.exception.IncorrectUserInputException; 6 | import com.github.silaev.mongodb.replicaset.exception.MongoNodeInitializationException; 7 | import com.github.silaev.mongodb.replicaset.model.MongoNode; 8 | import com.github.silaev.mongodb.replicaset.model.MongoRsStatus; 9 | import com.github.silaev.mongodb.replicaset.service.ResourceService; 10 | import com.github.silaev.mongodb.replicaset.service.impl.ResourceServiceImpl; 11 | import lombok.val; 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.junit.jupiter.api.Test; 14 | import org.junit.jupiter.api.extension.ExtendWith; 15 | import org.junit.jupiter.api.function.Executable; 16 | import org.junit.jupiter.params.ParameterizedTest; 17 | import org.junit.jupiter.params.provider.ValueSource; 18 | import org.mockito.Mock; 19 | import org.mockito.junit.jupiter.MockitoExtension; 20 | import org.testcontainers.containers.Container; 21 | import org.testcontainers.containers.GenericContainer; 22 | import org.testcontainers.containers.Network; 23 | 24 | import java.util.HashMap; 25 | import java.util.TreeMap; 26 | 27 | import static com.github.silaev.mongodb.replicaset.MongoDbReplicaSet.COMPARATOR_MAPPED_PORT; 28 | import static org.junit.jupiter.api.Assertions.assertThrows; 29 | import static org.mockito.Mockito.doReturn; 30 | import static org.mockito.Mockito.mock; 31 | import static org.mockito.Mockito.spy; 32 | import static org.mockito.Mockito.when; 33 | 34 | /** 35 | * @author Konstantin Silaev on 10/4/2019 36 | */ 37 | @ExtendWith(MockitoExtension.class) 38 | class MongoDbReplicaSetTest { 39 | private static final int CONTAINER_EXIT_CODE_ERROR = -1; 40 | private final ResourceService resourceService = new ResourceServiceImpl(); 41 | @Mock 42 | StringToMongoRsStatusConverter converter; 43 | private MongoDbReplicaSet replicaSet; 44 | 45 | @BeforeEach 46 | void setUp() { 47 | replicaSet = spy( 48 | new MongoDbReplicaSet( 49 | converter, 50 | new TreeMap<>(COMPARATOR_MAPPED_PORT), 51 | new HashMap<>(), 52 | new HashMap<>(), 53 | new HashMap<>(), 54 | mock(Network.class) 55 | )); 56 | } 57 | 58 | @Test 59 | void shouldNotGetReplicaSetUrl() { 60 | //GIVEN 61 | //replicaSet 62 | 63 | //WHEN 64 | Executable executable = replicaSet::getReplicaSetUrl; 65 | 66 | //THEN 67 | assertThrows(IllegalStateException.class, executable); 68 | } 69 | 70 | @Test 71 | void shouldNotGetMongoRsStatus() { 72 | //GIVEN 73 | //replicaSet 74 | 75 | //WHEN 76 | Executable executable = replicaSet::getMongoRsStatus; 77 | 78 | //THEN 79 | assertThrows(IllegalStateException.class, executable); 80 | } 81 | 82 | @Test 83 | void shouldNotCheckMongoNodeExitCodeAfterWaiting() { 84 | //GIVEN 85 | //replicaSet 86 | 87 | val container = mock(GenericContainer.class); 88 | val execResult = mock(Container.ExecResult.class); 89 | val nodeName = "nodeName"; 90 | val awaitNodeInitAttempts = 29; 91 | when(execResult.getExitCode()) 92 | .thenReturn(CONTAINER_EXIT_CODE_ERROR); 93 | val execResultStatusCommand = mock(Container.ExecResult.class); 94 | doReturn(execResultStatusCommand) 95 | .when(replicaSet) 96 | .execMongoDbCommandInContainer(container, MongoDbReplicaSet.STATUS_COMMAND); 97 | when(execResultStatusCommand.getStdout()).thenReturn("stdout"); 98 | 99 | //WHEN 100 | Executable executable = () -> replicaSet.checkMongoNodeExitCodeAfterWaiting( 101 | container, 102 | execResult, 103 | nodeName, 104 | awaitNodeInitAttempts 105 | ); 106 | 107 | //THEN 108 | assertThrows(MongoNodeInitializationException.class, executable); 109 | } 110 | 111 | @Test 112 | void shouldNotCheckMongoNodeExitCode() { 113 | //GIVEN 114 | //replicaSet 115 | 116 | val command = "command"; 117 | val execResult = mock(Container.ExecResult.class); 118 | when(execResult.getExitCode()) 119 | .thenReturn(CONTAINER_EXIT_CODE_ERROR); 120 | when(execResult.getStdout()).thenReturn("stdout"); 121 | 122 | //WHEN 123 | Executable executable = 124 | () -> replicaSet.checkMongoNodeExitCode(execResult, command); 125 | 126 | //THEN 127 | assertThrows(MongoNodeInitializationException.class, executable); 128 | } 129 | 130 | @ParameterizedTest(name = "{index}: version: {0}") 131 | @ValueSource(strings = {"3.6.13", "3.4.22", "2.6.22"}) 132 | void shouldNotTestVersion(final String inputVersion) { 133 | //GIVEN 134 | //inputVersion 135 | MongoRsStatus status = mock(MongoRsStatus.class); 136 | when(converter.convert(inputVersion)).thenReturn(status); 137 | when(status.getVersion()) 138 | .thenReturn(new VersionConverter().convert(inputVersion)); 139 | 140 | //WHEN 141 | Executable executable = () -> replicaSet.verifyVersion(inputVersion); 142 | 143 | //THEN 144 | assertThrows(IncorrectUserInputException.class, executable); 145 | } 146 | 147 | @Test 148 | void shouldTestFaultToleranceTestSupportAvailability() { 149 | //GIVEN 150 | doReturn(1).when(replicaSet).getReplicaSetNumber(); 151 | val mongoNode = mock(MongoNode.class); 152 | 153 | //WHEN 154 | Executable executableWaitForAllMongoNodesUp = 155 | () -> replicaSet.waitForAllMongoNodesUp(); 156 | Executable executableWaitForMasterReelection = 157 | () -> replicaSet.waitForMasterReelection(mongoNode); 158 | Executable executableStopNode = 159 | () -> replicaSet.stopNode(mongoNode); 160 | Executable executableKillNode = 161 | () -> replicaSet.killNode(mongoNode); 162 | Executable executableDisconnectNodeFromNetwork = 163 | () -> replicaSet.disconnectNodeFromNetwork(mongoNode); 164 | Executable executableConnectNodeToNetworkWithReconfiguration = 165 | () -> replicaSet.connectNodeToNetworkWithReconfiguration(mongoNode); 166 | Executable executableConnectNodeToNetwork = 167 | () -> replicaSet.connectNodeToNetwork(mongoNode); 168 | 169 | //THEN 170 | assertThrows(IllegalStateException.class, executableWaitForAllMongoNodesUp); 171 | assertThrows(IllegalStateException.class, executableStopNode); 172 | assertThrows(IllegalStateException.class, executableKillNode); 173 | assertThrows(IllegalStateException.class, executableWaitForMasterReelection); 174 | assertThrows(IllegalStateException.class, executableConnectNodeToNetwork); 175 | assertThrows(IllegalStateException.class, executableDisconnectNodeFromNetwork); 176 | assertThrows(IllegalStateException.class, executableConnectNodeToNetworkWithReconfiguration); 177 | } 178 | 179 | @Test 180 | void shouldNotCheckMongoNodeExitCodeAndStatus() { 181 | //GIVEN 182 | //replicaSet 183 | 184 | val command = "command"; 185 | val execResult = mock(Container.ExecResult.class); 186 | when(execResult.getExitCode()) 187 | .thenReturn(MongoDbReplicaSet.CONTAINER_EXIT_CODE_OK); 188 | val stdout = resourceService.getString( 189 | resourceService.getResourceIO("shell-output/timeout-exceeds.txt") 190 | ); 191 | when(execResult.getStdout()).thenReturn(stdout); 192 | when(converter.convert(stdout)).thenReturn(MongoRsStatus.of(0, null, null)); 193 | 194 | //WHEN 195 | Executable executable = 196 | () -> replicaSet.checkMongoNodeExitCodeAndStatus(execResult, command); 197 | 198 | //THEN 199 | assertThrows(MongoNodeInitializationException.class, executable); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/test/java/com/github/silaev/mongodb/replicaset/converter/impl/MongoNodeToMongoSocketAddressConverterTest.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.converter.impl; 2 | 3 | import com.github.silaev.mongodb.replicaset.model.MongoNode; 4 | import com.github.silaev.mongodb.replicaset.model.ReplicaSetMemberState; 5 | import lombok.val; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | import static org.junit.jupiter.api.Assertions.assertNotNull; 10 | import static org.junit.jupiter.api.Assertions.assertNull; 11 | 12 | /** 13 | * @author Konstantin Silaev on 2/25/2020 14 | */ 15 | class MongoNodeToMongoSocketAddressConverterTest { 16 | private final MongoNodeToMongoSocketAddressConverter converter = 17 | new MongoNodeToMongoSocketAddressConverter(); 18 | 19 | @Test 20 | void shouldConvert() { 21 | //GIVEN 22 | final String ip = "ip"; 23 | final int port = 27017; 24 | val mongoNode = MongoNode.of(ip, port, 1d, ReplicaSetMemberState.PRIMARY); 25 | 26 | //WHEN 27 | val socketAddress = converter.convert(mongoNode); 28 | 29 | //THEN 30 | assertNotNull(socketAddress); 31 | assertEquals(ip, socketAddress.getIp()); 32 | assertEquals(port, socketAddress.getMappedPort()); 33 | assertNull(socketAddress.getReplSetPort()); 34 | } 35 | 36 | @Test 37 | void shouldConvertNullToNull() { 38 | //GIVEN 39 | final MongoNode mongoNode = null; 40 | 41 | //WHEN 42 | val socketAddress = converter.convert(mongoNode); 43 | 44 | //THEN 45 | assertNull(socketAddress); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/test/java/com/github/silaev/mongodb/replicaset/converter/impl/StringToMongoRsStatusConverterTest.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.converter.impl; 2 | 3 | import com.github.silaev.mongodb.replicaset.service.ResourceService; 4 | import com.github.silaev.mongodb.replicaset.service.impl.ResourceServiceImpl; 5 | import lombok.val; 6 | import org.junit.jupiter.params.ParameterizedTest; 7 | import org.junit.jupiter.params.provider.CsvSource; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | 11 | class StringToMongoRsStatusConverterTest { 12 | private final StringToMongoRsStatusConverter converter = new StringToMongoRsStatusConverter( 13 | new YmlConverterImpl(), 14 | new VersionConverter() 15 | ); 16 | private final ResourceService resourceService = new ResourceServiceImpl(); 17 | 18 | @ParameterizedTest(name = "shouldConvert: {index}, fileName: {0}") 19 | @CsvSource(value = { 20 | "shell-output/rs-status.txt, 1, 5", 21 | "shell-output/rs-status-framed.txt, 1, 3", 22 | "shell-output/rs-status-plain.txt, 1, 0", 23 | "shell-output/timeout-exceeds.txt, 0, 0" 24 | }) 25 | void shouldConvert(final String fileName, final int status, final int membersNumber) { 26 | // GIVEN 27 | val rsStatus = resourceService.getString(resourceService.getResourceIO(fileName)); 28 | 29 | // THEN 30 | val mongoRsStatusActual = converter.convert(rsStatus); 31 | 32 | // WHEN 33 | assertThat(mongoRsStatusActual).isNotNull(); 34 | assertThat(mongoRsStatusActual.getStatus()).isEqualTo(status); 35 | val members = mongoRsStatusActual.getMembers(); 36 | assertThat(members.size()).isEqualTo(membersNumber); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/java/com/github/silaev/mongodb/replicaset/converter/impl/UserInputToApplicationPropertiesConverterTest.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.converter.impl; 2 | 3 | import com.github.silaev.mongodb.replicaset.converter.YmlConverter; 4 | import com.github.silaev.mongodb.replicaset.exception.IncorrectUserInputException; 5 | import com.github.silaev.mongodb.replicaset.model.MongoReplicaSetProperties; 6 | import com.github.silaev.mongodb.replicaset.model.PropertyContainer; 7 | import com.github.silaev.mongodb.replicaset.model.UserInputProperties; 8 | import com.github.silaev.mongodb.replicaset.service.ResourceService; 9 | import lombok.val; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.extension.ExtendWith; 12 | import org.junit.jupiter.api.function.Executable; 13 | import org.junit.jupiter.params.ParameterizedTest; 14 | import org.junit.jupiter.params.provider.MethodSource; 15 | import org.mockito.InjectMocks; 16 | import org.mockito.Mock; 17 | import org.mockito.junit.jupiter.MockitoExtension; 18 | 19 | import java.io.InputStream; 20 | import java.util.stream.Stream; 21 | 22 | import static org.junit.jupiter.api.Assertions.assertFalse; 23 | import static org.junit.jupiter.api.Assertions.assertNotNull; 24 | import static org.junit.jupiter.api.Assertions.assertNull; 25 | import static org.junit.jupiter.api.Assertions.assertThrows; 26 | import static org.mockito.Mockito.mock; 27 | import static org.mockito.Mockito.when; 28 | 29 | /** 30 | * @author Konstantin Silaev 31 | */ 32 | @ExtendWith(MockitoExtension.class) 33 | class UserInputToApplicationPropertiesConverterTest { 34 | @Mock 35 | private YmlConverter ymlConverter; 36 | 37 | @Mock 38 | private ResourceService resourceService; 39 | 40 | @InjectMocks 41 | private UserInputToApplicationPropertiesConverter converter; 42 | 43 | static Stream blankOrNullStrings() { 44 | return Stream.of("", " ", null); 45 | } 46 | 47 | @Test 48 | void shouldGetFileProperties() { 49 | //GIVEN 50 | val propertyFileName = "propertyFileName.yml"; 51 | val io = mock(InputStream.class); 52 | when(resourceService.getResourceIO(propertyFileName)) 53 | .thenReturn(io); 54 | val propertyContainer = mock(PropertyContainer.class); 55 | when(ymlConverter.unmarshal(PropertyContainer.class, io)) 56 | .thenReturn(propertyContainer); 57 | val mongoReplicaFileProperties = mock(MongoReplicaSetProperties.class); 58 | when(propertyContainer.getMongoReplicaSetProperties()) 59 | .thenReturn(mongoReplicaFileProperties); 60 | when(mongoReplicaFileProperties.getEnabled()).thenReturn(Boolean.FALSE); 61 | 62 | //WHEN 63 | val mongoReplicaFilePropertiesActual = 64 | converter.getFileProperties(propertyFileName); 65 | 66 | //THEN 67 | assertFalse(mongoReplicaFilePropertiesActual.getEnabled()); 68 | } 69 | 70 | @ParameterizedTest(name = "{index}: filePath: {0}") 71 | @MethodSource("blankOrNullStrings") 72 | void shouldGetDefaultsBecauseOfNullOrEmptyFile(String propertyFileName) { 73 | //GIVEN 74 | //propertyFileName 75 | 76 | //WHEN 77 | 78 | val mongoReplicaFilePropertiesActual = 79 | converter.getFileProperties(propertyFileName); 80 | 81 | //THEN 82 | assertNotNull(mongoReplicaFilePropertiesActual); 83 | assertNull(mongoReplicaFilePropertiesActual.getEnabled()); 84 | } 85 | 86 | @Test 87 | void shouldNotEvaluateEnabledBecauseOfFileFormat() { 88 | //GIVEN 89 | val propertyFileName = "propertyFileName"; 90 | 91 | //WHEN 92 | Executable executable = 93 | () -> converter.getFileProperties(propertyFileName); 94 | 95 | //THEN 96 | assertThrows(IllegalArgumentException.class, executable); 97 | } 98 | 99 | @Test 100 | void shouldNotConvertBecauseOfArbiterAndSingleNode() { 101 | //GIVEN 102 | val inputProperties = UserInputProperties.builder() 103 | .addArbiter(true) 104 | .replicaSetNumber(1) 105 | .build(); 106 | 107 | //WHEN 108 | Executable executable = () -> converter.convert(inputProperties); 109 | 110 | //THEN 111 | assertThrows(IncorrectUserInputException.class, executable); 112 | } 113 | 114 | @Test 115 | void shouldNotConvertBecauseSlaveDelayNumberIsMoreThanReplicaSetNumber() { 116 | //GIVEN 117 | val inputProperties = UserInputProperties.builder() 118 | .slaveDelayNumber(6) 119 | .replicaSetNumber(4) 120 | .slaveDelayTimeout(5000) 121 | .build(); 122 | 123 | //WHEN 124 | Executable executable = () -> converter.convert(inputProperties); 125 | 126 | //THEN 127 | assertThrows(IncorrectUserInputException.class, executable); 128 | } 129 | 130 | @Test 131 | void shouldNotConvertBecauseSlaveDelayTimeoutIsNotSet() { 132 | //GIVEN 133 | val inputProperties = UserInputProperties.builder() 134 | .slaveDelayNumber(6) 135 | .replicaSetNumber(4) 136 | .build(); 137 | 138 | //WHEN 139 | Executable executable = () -> converter.convert(inputProperties); 140 | 141 | //THEN 142 | assertThrows(IncorrectUserInputException.class, executable); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/test/java/com/github/silaev/mongodb/replicaset/converter/impl/VersionConverterTest.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.converter.impl; 2 | 3 | 4 | import lombok.val; 5 | import org.junit.jupiter.api.function.Executable; 6 | import org.junit.jupiter.params.ParameterizedTest; 7 | import org.junit.jupiter.params.provider.MethodSource; 8 | import org.junit.jupiter.params.provider.ValueSource; 9 | 10 | import java.util.stream.Stream; 11 | 12 | import static org.junit.Assert.assertEquals; 13 | import static org.junit.jupiter.api.Assertions.assertThrows; 14 | 15 | /** 16 | * @author Konstantin Silaev 17 | */ 18 | class VersionConverterTest { 19 | private final VersionConverter versionConverter = new VersionConverter(); 20 | 21 | private static Stream incorrectVersions() { 22 | return Stream.of("3", "", null); 23 | } 24 | 25 | @ParameterizedTest(name = "{index}: version: {0}") 26 | @ValueSource(strings = {"3.6.14", "3.6"}) 27 | void shouldConvert(String version) { 28 | //GIVEN 29 | String[] strings = version.split("\\."); 30 | val major = Integer.parseInt(strings[0]); 31 | val minor = Integer.parseInt(strings[1]); 32 | val patch = strings.length == 3 ? Integer.parseInt(strings[2]) : 0; 33 | 34 | //WHEN 35 | val mongoDbVersion = versionConverter.convert(version); 36 | 37 | //THEN 38 | assertEquals(major, mongoDbVersion.getMajor()); 39 | assertEquals(minor, mongoDbVersion.getMinor()); 40 | assertEquals(patch, mongoDbVersion.getPatch()); 41 | } 42 | 43 | @ParameterizedTest(name = "{index}: version: {0}") 44 | @MethodSource("incorrectVersions") 45 | void shouldNotConvert(String version) { 46 | //GIVEN 47 | //version 48 | 49 | //WHEN 50 | Executable executable = () -> versionConverter.convert(version); 51 | 52 | //THEN 53 | assertThrows(IllegalArgumentException.class, executable); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/com/github/silaev/mongodb/replicaset/core/EnabledIfSystemPropertyEnabledByDefault.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.core; 2 | 3 | import org.junit.jupiter.api.condition.EnabledIfSystemProperty; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | 6 | import java.lang.annotation.Documented; 7 | import java.lang.annotation.ElementType; 8 | import java.lang.annotation.Retention; 9 | import java.lang.annotation.RetentionPolicy; 10 | import java.lang.annotation.Target; 11 | 12 | 13 | /** 14 | * Taken and slightly modified from {@link EnabledIfSystemProperty} 15 | *

16 | * As opposed to {@code @EnabledIfSystemProperty} if the specified system property is undefined, 17 | * the annotated class or method will be enabled. 18 | * Similar might be achieved by DisableIfSystemProperty with negation of a regexp. 19 | */ 20 | @Target({ElementType.TYPE, ElementType.METHOD}) 21 | @Retention(RetentionPolicy.RUNTIME) 22 | @Documented 23 | @ExtendWith(EnabledIfSystemPropertyEnabledByDefaultCondition.class) 24 | public @interface EnabledIfSystemPropertyEnabledByDefault { 25 | 26 | /** 27 | * The name of the JVM system property to retrieve. 28 | * 29 | * @return the system property name; never blank 30 | * @see System#getProperty(String) 31 | */ 32 | String named(); 33 | 34 | /** 35 | * A regular expression that will be used to match against the retrieved 36 | * value of the {@link #named} JVM system property. 37 | * 38 | * @return the regular expression; never blank 39 | * @see String#matches(String) 40 | * @see java.util.regex.Pattern 41 | */ 42 | String matches(); 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/com/github/silaev/mongodb/replicaset/core/EnabledIfSystemPropertyEnabledByDefaultCondition.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.core; 2 | 3 | import org.junit.jupiter.api.condition.EnabledIfSystemProperty; 4 | import org.junit.jupiter.api.extension.ConditionEvaluationResult; 5 | import org.junit.jupiter.api.extension.ExecutionCondition; 6 | import org.junit.jupiter.api.extension.ExtensionContext; 7 | import org.junit.platform.commons.util.Preconditions; 8 | 9 | import java.util.Objects; 10 | import java.util.Optional; 11 | 12 | import static java.lang.String.format; 13 | import static org.junit.jupiter.api.extension.ConditionEvaluationResult.disabled; 14 | import static org.junit.jupiter.api.extension.ConditionEvaluationResult.enabled; 15 | import static org.junit.platform.commons.util.AnnotationUtils.findAnnotation; 16 | 17 | /** 18 | * Taken and slightly modified from {@link org.junit.jupiter.api.condition.EnabledIfSystemProperty} 19 | *

20 | * {@link ExecutionCondition} for {@link EnabledIfSystemPropertyExistsAndMatches @EnabledIfSystemPropertyExistsAndMatches}. 21 | * 22 | * @see EnabledIfSystemProperty 23 | */ 24 | class EnabledIfSystemPropertyEnabledByDefaultCondition implements ExecutionCondition { 25 | 26 | private static final ConditionEvaluationResult ENABLED_BY_DEFAULT = enabled( 27 | "@EnabledIfSystemProperty is not present"); 28 | 29 | @Override 30 | public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { 31 | Optional optional = findAnnotation(context.getElement(), 32 | EnabledIfSystemPropertyEnabledByDefault.class); 33 | 34 | if (!optional.isPresent()) { 35 | return ENABLED_BY_DEFAULT; 36 | } 37 | 38 | EnabledIfSystemPropertyEnabledByDefault annotation = optional.get(); 39 | String name = annotation.named().trim(); 40 | String regex = annotation.matches(); 41 | Preconditions.notBlank(name, () -> "The 'named' attribute must not be blank in " + annotation); 42 | Preconditions.notBlank(regex, () -> "The 'matches' attribute must not be blank in " + annotation); 43 | String actual = System.getProperty(name); 44 | 45 | // Nothing to match against? 46 | if (Objects.isNull(actual)) { 47 | return enabled(format("System property [%s] is enabled by default", name)); 48 | } 49 | if (actual.matches(regex)) { 50 | return enabled( 51 | format("System property [%s] with value [%s] matches regular expression [%s]", name, actual, regex)); 52 | } 53 | return disabled( 54 | format("System property [%s] with value [%s] does not match regular expression [%s]", name, actual, regex)); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/test/java/com/github/silaev/mongodb/replicaset/core/IntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.core; 2 | 3 | import org.junit.jupiter.api.Tag; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Target(ElementType.TYPE) 12 | @Tag("integration-test") 13 | public @interface IntegrationTest { 14 | } 15 | -------------------------------------------------------------------------------- /src/test/java/com/github/silaev/mongodb/replicaset/integration/PropertyEvaluationITTest.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.integration; 2 | 3 | import com.github.silaev.mongodb.replicaset.MongoDbReplicaSet; 4 | import com.github.silaev.mongodb.replicaset.core.IntegrationTest; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertFalse; 8 | 9 | @IntegrationTest 10 | class PropertyEvaluationITTest { 11 | private static final MongoDbReplicaSet MONGO_REPLICA_SET = MongoDbReplicaSet.builder() 12 | .replicaSetNumber(1) 13 | .awaitNodeInitAttempts(30) 14 | .propertyFileName("enabled-false.yml") 15 | .build(); 16 | 17 | @Test 18 | void shouldGetProperties() { 19 | // GIVEN 20 | //MONGO_REPLICA_SET 21 | 22 | // WHEN 23 | // MONGO_REPLICA_SET is initialized 24 | 25 | // THEN 26 | assertFalse(MONGO_REPLICA_SET.isEnabled()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/com/github/silaev/mongodb/replicaset/integration/VersionSupportTest.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.integration; 2 | 3 | import com.github.silaev.mongodb.replicaset.MongoDbReplicaSet; 4 | import com.github.silaev.mongodb.replicaset.core.IntegrationTest; 5 | import com.github.silaev.mongodb.replicaset.exception.IncorrectUserInputException; 6 | import lombok.val; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.api.function.Executable; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertThrows; 11 | 12 | /** 13 | * @author Konstantin Silaev on 10/4/2019 14 | */ 15 | @IntegrationTest 16 | class VersionSupportTest { 17 | @Test 18 | void shouldNotValidateVersion() { 19 | //GIVEN 20 | val replicaSet = MongoDbReplicaSet.builder() 21 | .mongoDockerImageName("mongo:3.4.22") 22 | .build(); 23 | 24 | //WHEN 25 | Executable executable = replicaSet::start; 26 | 27 | //THEN 28 | assertThrows(IncorrectUserInputException.class, executable); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/test/java/com/github/silaev/mongodb/replicaset/integration/api/BaseMongoDbReplicaSetApiITTest.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.integration.api; 2 | 3 | import com.github.silaev.mongodb.replicaset.MongoDbReplicaSet; 4 | import com.github.silaev.mongodb.replicaset.model.MongoNode; 5 | import com.github.silaev.mongodb.replicaset.model.ReplicaSetMemberState; 6 | import com.github.silaev.mongodb.replicaset.util.CollectionUtils; 7 | import com.github.silaev.mongodb.replicaset.util.ConnectionUtils; 8 | import com.github.silaev.mongodb.replicaset.util.StringUtils; 9 | import com.github.silaev.mongodb.replicaset.util.SubscriberHelperUtils; 10 | import com.mongodb.reactivestreams.client.MongoClients; 11 | import lombok.SneakyThrows; 12 | import lombok.extern.slf4j.Slf4j; 13 | import lombok.val; 14 | import org.bson.Document; 15 | import org.jetbrains.annotations.NotNull; 16 | 17 | import java.util.List; 18 | import java.util.concurrent.TimeUnit; 19 | import java.util.stream.Collectors; 20 | 21 | import static org.junit.jupiter.api.Assertions.assertEquals; 22 | import static org.junit.jupiter.api.Assertions.assertNotNull; 23 | import static org.junit.jupiter.api.Assertions.assertTrue; 24 | 25 | @Slf4j 26 | abstract class BaseMongoDbReplicaSetApiITTest { 27 | @SneakyThrows 28 | void shouldTestRsStatus( 29 | final MongoDbReplicaSet replicaSet, 30 | final int replicaSetNumber 31 | ) { 32 | // GIVEN 33 | //replicaSet 34 | val mongoRsUrl = replicaSet.getReplicaSetUrl(); 35 | val mongoRsStatus = replicaSet.getMongoRsStatus(); 36 | assertNotNull(mongoRsUrl); 37 | assertNotNull(mongoRsStatus); 38 | 39 | try ( 40 | val mongoReactiveClient = MongoClients.create( 41 | ConnectionUtils.getMongoClientSettingsWithTimeout(mongoRsUrl) 42 | ) 43 | ) { 44 | val db = mongoReactiveClient.getDatabase("admin"); 45 | 46 | // WHEN + THEN 47 | val subscriber = SubscriberHelperUtils.getSubscriber( 48 | db.runCommand(new Document("replSetGetStatus", 1)) 49 | ); 50 | val document = getDocument(subscriber.get(5, TimeUnit.SECONDS)); 51 | assertEquals(Double.valueOf("1"), document.get("ok", Double.class)); 52 | val mongoNodesActual = extractMongoNodes(document.getList("members", Document.class)); 53 | 54 | assertTrue( 55 | CollectionUtils.isEqualCollection( 56 | mongoRsStatus.getMembers(), 57 | mongoNodesActual 58 | ) 59 | ); 60 | assertEquals( 61 | replicaSetNumber + (replicaSet.getAddArbiter() ? 1 : 0), 62 | mongoNodesActual.size()); 63 | } 64 | } 65 | 66 | private List extractMongoNodes(final List members) { 67 | return members.stream() 68 | .map(this::getMongoNode) 69 | .collect(Collectors.toList()); 70 | } 71 | 72 | private MongoNode getMongoNode(final Document document) { 73 | val addresses = 74 | StringUtils.getArrayByDelimiter(document.getString("name")); 75 | return MongoNode.of( 76 | addresses[0], 77 | Integer.parseInt(addresses[1]), 78 | document.getDouble("health"), 79 | ReplicaSetMemberState.getByValue(document.getInteger("state")) 80 | ); 81 | } 82 | 83 | @NotNull 84 | private Document getDocument(final List documents) { 85 | return documents.get(0); 86 | } 87 | 88 | void shouldTestEnabled(final MongoDbReplicaSet replicaSet) { 89 | // GIVEN 90 | val mongoRsUrl = replicaSet.getReplicaSetUrl(); 91 | val mongoRsStatus = replicaSet.getMongoRsStatus(); 92 | 93 | // WHEN 94 | // replicaSet is initialized 95 | 96 | // THEN 97 | assertNotNull(mongoRsUrl); 98 | assertNotNull(mongoRsStatus); 99 | assertTrue(replicaSet.isEnabled()); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/test/java/com/github/silaev/mongodb/replicaset/integration/api/MongoDbReplicaSetMultiNodeApiITTest.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.integration.api; 2 | 3 | import com.github.silaev.mongodb.replicaset.MongoDbReplicaSet; 4 | import com.github.silaev.mongodb.replicaset.core.IntegrationTest; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.junit.jupiter.api.AfterAll; 7 | import org.junit.jupiter.api.BeforeAll; 8 | import org.junit.jupiter.api.Test; 9 | 10 | @IntegrationTest 11 | @Slf4j 12 | class MongoDbReplicaSetMultiNodeApiITTest extends 13 | BaseMongoDbReplicaSetApiITTest { 14 | private static final int REPLICA_SET_NUMBER = 2; 15 | 16 | private static final MongoDbReplicaSet MONGO_REPLICA_SET = MongoDbReplicaSet.builder() 17 | .replicaSetNumber(REPLICA_SET_NUMBER) 18 | .addArbiter(true) 19 | .build(); 20 | 21 | @BeforeAll 22 | static void setUpAll() { 23 | MONGO_REPLICA_SET.start(); 24 | } 25 | 26 | @AfterAll 27 | static void tearDownAll() { 28 | MONGO_REPLICA_SET.stop(); 29 | } 30 | 31 | @Test 32 | void shouldTestRsStatus() { 33 | super.shouldTestRsStatus(MONGO_REPLICA_SET, REPLICA_SET_NUMBER); 34 | } 35 | 36 | @Test 37 | void shouldTestEnabled() { 38 | super.shouldTestEnabled(MONGO_REPLICA_SET); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/com/github/silaev/mongodb/replicaset/integration/api/MongoDbReplicaSetSingleNodeApiITTest.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.integration.api; 2 | 3 | import com.github.silaev.mongodb.replicaset.MongoDbReplicaSet; 4 | import com.github.silaev.mongodb.replicaset.core.IntegrationTest; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.junit.jupiter.api.AfterAll; 7 | import org.junit.jupiter.api.BeforeAll; 8 | import org.junit.jupiter.api.Test; 9 | 10 | @IntegrationTest 11 | @Slf4j 12 | class MongoDbReplicaSetSingleNodeApiITTest extends 13 | BaseMongoDbReplicaSetApiITTest { 14 | private static final int REPLICA_SET_NUMBER = 1; 15 | 16 | private static final MongoDbReplicaSet MONGO_REPLICA_SET = MongoDbReplicaSet.builder() 17 | .replicaSetNumber(REPLICA_SET_NUMBER) 18 | .build(); 19 | 20 | @BeforeAll 21 | static void setUpAll() { 22 | MONGO_REPLICA_SET.start(); 23 | } 24 | 25 | @AfterAll 26 | static void tearDownAll() { 27 | MONGO_REPLICA_SET.stop(); 28 | } 29 | 30 | @Test 31 | void shouldTestRsStatus() { 32 | super.shouldTestRsStatus(MONGO_REPLICA_SET, REPLICA_SET_NUMBER); 33 | } 34 | 35 | @Test 36 | void shouldTestEnabled() { 37 | super.shouldTestEnabled(MONGO_REPLICA_SET); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/com/github/silaev/mongodb/replicaset/integration/api/faulttolerance/MongoDbDelayedMembersITTest.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.integration.api.faulttolerance; 2 | 3 | import com.github.silaev.mongodb.replicaset.MongoDbReplicaSet; 4 | import com.github.silaev.mongodb.replicaset.core.EnabledIfSystemPropertyEnabledByDefault; 5 | import com.github.silaev.mongodb.replicaset.core.IntegrationTest; 6 | import com.github.silaev.mongodb.replicaset.model.ReplicaSetMemberState; 7 | import lombok.extern.slf4j.Slf4j; 8 | import lombok.val; 9 | import org.junit.jupiter.api.Disabled; 10 | import org.junit.jupiter.api.Test; 11 | 12 | import java.util.Arrays; 13 | 14 | import static org.hamcrest.CoreMatchers.hasItems; 15 | import static org.hamcrest.MatcherAssert.assertThat; 16 | import static org.junit.jupiter.api.Assertions.assertNotNull; 17 | 18 | /** 19 | * Simulates a situation when a replica set with a delayed member 20 | * is reconfigured to a non-delayed one with or without losing primary first. 21 | * 22 | * @author Konstantin Silaev on 5/8/2020 23 | */ 24 | @IntegrationTest 25 | @Slf4j 26 | @EnabledIfSystemPropertyEnabledByDefault( 27 | named = "mongoReplicaSetProperties.mongoDockerImageName", 28 | matches = "^mongo:4.*" 29 | ) 30 | class MongoDbDelayedMembersITTest { 31 | @Test 32 | void shouldTestDelayedMembersBecomingSecondary() { 33 | try ( 34 | final MongoDbReplicaSet mongoReplicaSet = MongoDbReplicaSet.builder() 35 | .replicaSetNumber(4) 36 | .slaveDelayTimeout(50000) 37 | .slaveDelayNumber(1) 38 | .commandLineOptions(Arrays.asList("--oplogSize", "50")) 39 | .build() 40 | ) { 41 | mongoReplicaSet.start(); 42 | 43 | val mongoRsUrlPrimary = mongoReplicaSet.getReplicaSetUrl(); 44 | assertNotNull(mongoRsUrlPrimary); 45 | mongoReplicaSet.reconfigureReplSetToDefaults(); 46 | 47 | assertThat( 48 | mongoReplicaSet.nodeStates(mongoReplicaSet.getMongoRsStatus().getMembers()), 49 | hasItems( 50 | ReplicaSetMemberState.PRIMARY, 51 | ReplicaSetMemberState.SECONDARY, 52 | ReplicaSetMemberState.SECONDARY, 53 | ReplicaSetMemberState.SECONDARY 54 | ) 55 | ); 56 | } 57 | } 58 | 59 | /** 60 | * Disabled as unstable 61 | */ 62 | @Test 63 | @Disabled 64 | void shouldTestDelayedMemberMightBecomeSecondary() { 65 | try ( 66 | final MongoDbReplicaSet mongoReplicaSet = MongoDbReplicaSet.builder() 67 | .replicaSetNumber(4) 68 | .addToxiproxy(true) 69 | .slaveDelayTimeout(50000) 70 | .slaveDelayNumber(1) 71 | .build() 72 | ) { 73 | mongoReplicaSet.start(); 74 | 75 | val mongoRsUrlPrimary = mongoReplicaSet.getReplicaSetUrl(); 76 | assertNotNull(mongoRsUrlPrimary); 77 | val members = mongoReplicaSet.getMongoRsStatus().getMembers(); 78 | val masterNode = mongoReplicaSet.getMasterMongoNode(members); 79 | 80 | mongoReplicaSet.disconnectNodeFromNetwork(masterNode); 81 | mongoReplicaSet.waitForMongoNodesDown(1); 82 | mongoReplicaSet.waitForMasterReelection(masterNode); 83 | mongoReplicaSet.reconfigureReplSetToDefaults(); 84 | 85 | assertThat( 86 | mongoReplicaSet.nodeStates(mongoReplicaSet.getMongoRsStatus().getMembers()), 87 | hasItems( 88 | ReplicaSetMemberState.PRIMARY, 89 | ReplicaSetMemberState.SECONDARY, 90 | ReplicaSetMemberState.SECONDARY, 91 | ReplicaSetMemberState.DOWN 92 | ) 93 | ); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/test/java/com/github/silaev/mongodb/replicaset/integration/api/faulttolerance/MongoDbReplicaSetDistributionITTest.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.integration.api.faulttolerance; 2 | 3 | import com.github.silaev.mongodb.replicaset.MongoDbReplicaSet; 4 | import com.github.silaev.mongodb.replicaset.core.IntegrationTest; 5 | import com.github.silaev.mongodb.replicaset.model.DisconnectionType; 6 | import com.github.silaev.mongodb.replicaset.model.Pair; 7 | import com.github.silaev.mongodb.replicaset.model.ReplicaSetMemberState; 8 | import com.github.silaev.mongodb.replicaset.util.ConnectionUtils; 9 | import com.mongodb.MongoException; 10 | import com.mongodb.MongoTimeoutException; 11 | import com.mongodb.ReadPreference; 12 | import com.mongodb.WriteConcern; 13 | import com.mongodb.client.MongoClients; 14 | import com.mongodb.client.model.Filters; 15 | import lombok.val; 16 | import org.bson.Document; 17 | import org.junit.jupiter.api.function.Executable; 18 | import org.junit.jupiter.params.ParameterizedTest; 19 | import org.junit.jupiter.params.provider.ValueSource; 20 | 21 | import java.util.concurrent.TimeUnit; 22 | 23 | import static org.hamcrest.CoreMatchers.hasItems; 24 | import static org.hamcrest.MatcherAssert.assertThat; 25 | import static org.junit.jupiter.api.Assertions.assertEquals; 26 | import static org.junit.jupiter.api.Assertions.assertNotNull; 27 | import static org.junit.jupiter.api.Assertions.assertThrows; 28 | 29 | /** 30 | * Simulates two data centers scenario in which two members to Data Center 1 and one member to Data Center 2. 31 | * If one of the members of the replica set is an arbiter, distribute the arbiter to Data Center 1 with a data-bearing member. 32 | * 33 | * @author Konstantin Silaev on 1/28/2020 34 | */ 35 | @IntegrationTest 36 | class MongoDbReplicaSetDistributionITTest { 37 | /** 38 | * If Data Center 1 goes down, the replica set becomes read-only. 39 | * Uses hard node disconnection via whether removing a network of a container 40 | * or cutting a connection between a mongo node and a proxy container (Toxiproxy). 41 | */ 42 | @ParameterizedTest(name = "shouldTestReadOnlySecondaryAfterPrimaryAndArbiterHardOrSoftDisconnection: {index}: disconnectionType: {0}") 43 | @ValueSource(strings = {"HARD", "SOFT"}) 44 | void shouldTestReadOnlySecondaryAfterPrimaryAndArbiterHardOrSoftDisconnection( 45 | final DisconnectionType disconnectionType 46 | ) { 47 | //GIVEN 48 | val mongoDbReplicaSetBuilder = MongoDbReplicaSet.builder() 49 | .replicaSetNumber(2) 50 | .addArbiter(true); 51 | if (disconnectionType == DisconnectionType.SOFT) { 52 | mongoDbReplicaSetBuilder.addToxiproxy(true); 53 | } 54 | 55 | try ( 56 | final MongoDbReplicaSet mongoReplicaSet = mongoDbReplicaSetBuilder.build() 57 | ) { 58 | mongoReplicaSet.start(); 59 | val mongoRsUrlPrimary = mongoReplicaSet.getReplicaSetUrl(); 60 | val mongoRsUrlPrimaryPreferred = mongoReplicaSet.getReplicaSetUrl( 61 | ReadPreference.primaryPreferred().getName() 62 | ); 63 | val mongoRsUrlSecondary = mongoReplicaSet.getReplicaSetUrl( 64 | ReadPreference.secondary().getName() 65 | ); 66 | assertNotNull(mongoRsUrlPrimary); 67 | try ( 68 | val mongoSyncClient = com.mongodb.client.MongoClients.create( 69 | ConnectionUtils.getMongoClientSettingsWithTimeout(mongoRsUrlPrimary) 70 | ) 71 | ) { 72 | val docPair = Pair.of("abc", 5000); 73 | val doc = new Document(docPair.getLeft(), docPair.getRight()); 74 | val dbName = "test"; 75 | val collectionName = "foo"; 76 | val collection = mongoSyncClient.getDatabase(dbName).getCollection(collectionName); 77 | collection.withWriteConcern(WriteConcern.MAJORITY).insertOne(doc); 78 | val replicaSetMembers = mongoReplicaSet.getMongoRsStatus().getMembers(); 79 | val masterNode = mongoReplicaSet.getMasterMongoNode(replicaSetMembers); 80 | val arbiterNode = mongoReplicaSet.getArbiterMongoNode(replicaSetMembers); 81 | val filter = Filters.eq(docPair.getLeft(), docPair.getRight()); 82 | 83 | //WHEN 84 | mongoReplicaSet.disconnectNodeFromNetwork(arbiterNode); 85 | mongoReplicaSet.disconnectNodeFromNetwork(masterNode); 86 | 87 | mongoReplicaSet.waitForMongoNodesDown(2); 88 | 89 | final Executable executableReadOperation = () -> collection.find(filter).first(); 90 | final Executable executableWriteOperation = 91 | () -> collection.insertOne(new Document("xyz", 100)); 92 | 93 | //THEN 94 | //1. Primary is the default mode. All operations read from the current replica set primary. 95 | //Therefore, reads are not possible (MongoSocketReadException/MongoSocketReadTimeoutException/MongoTimeoutException). 96 | assertThrows(MongoException.class, executableReadOperation); 97 | 98 | //2. Secondary is not writeable. 99 | assertThrows(MongoTimeoutException.class, executableWriteOperation); 100 | 101 | //3. Test read preference "primary preferred". 102 | //In most situations, operations read from the primary but if it is unavailable, 103 | //operations read from secondary members. 104 | //3.1. withReadPreference on a collection. 105 | assertEquals(doc, collection 106 | .withReadPreference(ReadPreference.primaryPreferred()) 107 | .find(filter) 108 | .first() 109 | ); 110 | assertEquals(doc, collection 111 | .withReadPreference(ReadPreference.secondary()) 112 | .find(filter) 113 | .first() 114 | ); 115 | 116 | //3.2. withReadPreference on a url connection. 117 | assertEquals(doc, MongoClients.create(mongoRsUrlPrimaryPreferred) 118 | .getDatabase(dbName).getCollection(collectionName) 119 | .find(filter).first() 120 | ); 121 | assertEquals(doc, MongoClients.create(mongoRsUrlSecondary) 122 | .getDatabase(dbName).getCollection(collectionName) 123 | .find(filter).first() 124 | ); 125 | 126 | //BRING ALL DISCONNECTED NODES BACK 127 | switch (disconnectionType) { 128 | case HARD: 129 | mongoReplicaSet.connectNodeToNetworkWithReconfiguration(masterNode); 130 | mongoReplicaSet.connectNodeToNetworkWithoutRemoval(arbiterNode); 131 | break; 132 | case SOFT: 133 | mongoReplicaSet.connectNodeToNetwork(masterNode); 134 | mongoReplicaSet.connectNodeToNetwork(arbiterNode); 135 | break; 136 | default: 137 | throw new IllegalArgumentException(String.format("Cannot find disconnectionType: %s", disconnectionType)); 138 | } 139 | mongoReplicaSet.waitForAllMongoNodesUp(); 140 | mongoReplicaSet.waitForMaster(); 141 | assertThat( 142 | mongoReplicaSet.nodeStates(mongoReplicaSet.getMongoRsStatus().getMembers()), 143 | hasItems( 144 | ReplicaSetMemberState.PRIMARY, 145 | ReplicaSetMemberState.SECONDARY, 146 | ReplicaSetMemberState.ARBITER 147 | ) 148 | ); 149 | } 150 | } 151 | } 152 | 153 | /** 154 | * If Data Center 2 goes down, the replica set remains writeable as the members 155 | * in Data Center 1 can hold an election. 156 | *

157 | * Uses whether hard node disconnection via removing a network of a container 158 | * or soft one via cutting a connection between a mongo node and a proxy container (Toxiproxy). 159 | */ 160 | @ParameterizedTest(name = "shouldTestWriteablePrimaryAfterSecondaryHardOrSoftDisconnection: {index}: disconnectionType: {0}") 161 | @ValueSource(strings = {"HARD", "SOFT"}) 162 | void shouldTestWriteablePrimaryAfterSecondaryHardOrSoftDisconnection( 163 | final DisconnectionType disconnectionType 164 | ) { 165 | //GIVEN 166 | val mongoDbReplicaSetBuilder = MongoDbReplicaSet.builder() 167 | .replicaSetNumber(2) 168 | .addArbiter(true); 169 | if (disconnectionType == DisconnectionType.SOFT) { 170 | mongoDbReplicaSetBuilder.addToxiproxy(true); 171 | } 172 | try ( 173 | final MongoDbReplicaSet mongoReplicaSet = mongoDbReplicaSetBuilder.build() 174 | ) { 175 | mongoReplicaSet.start(); 176 | val mongoRsUrlPrimary = mongoReplicaSet.getReplicaSetUrl(); 177 | val mongoRsUrlSecondary = mongoReplicaSet.getReplicaSetUrl( 178 | ReadPreference.secondary().getName() 179 | ); 180 | assertNotNull(mongoRsUrlPrimary); 181 | 182 | try ( 183 | val mongoSyncClient = com.mongodb.client.MongoClients.create( 184 | ConnectionUtils.getMongoClientSettingsWithTimeout(mongoRsUrlPrimary) 185 | ) 186 | ) { 187 | val docPair = Pair.of("abc", 5000); 188 | val doc = new Document(docPair.getLeft(), docPair.getRight()); 189 | val dbName = "test"; 190 | val collectionName = "foo"; 191 | val collection = mongoSyncClient.getDatabase(dbName).getCollection(collectionName); 192 | collection.withWriteConcern(WriteConcern.MAJORITY).insertOne(doc); 193 | val replicaSetMembers = mongoReplicaSet.getMongoRsStatus().getMembers(); 194 | val secondaryNode = mongoReplicaSet.getSecondaryMongoNode(replicaSetMembers); 195 | val filterDoc = Filters.eq(docPair.getLeft(), docPair.getRight()); 196 | val newDocPair = Pair.of("xyz", 100); 197 | val newDoc = new Document(newDocPair.getLeft(), newDocPair.getRight()); 198 | val filterNewDoc = Filters.eq(newDocPair.getLeft(), newDocPair.getRight()); 199 | 200 | //WHEN 201 | mongoReplicaSet.disconnectNodeFromNetwork(secondaryNode); 202 | 203 | mongoReplicaSet.waitForMongoNodesDown(1); 204 | //mongoReplicaSet.waitForAllMongoNodesUpAllowingAnyDown(); 205 | 206 | collection.insertOne(newDoc); 207 | final Executable executableReadPreferenceOnCollection = () -> collection 208 | .withReadPreference(ReadPreference.secondary()) 209 | .find(filterDoc) 210 | .maxAwaitTime(5, TimeUnit.SECONDS) 211 | .maxTime(5, TimeUnit.SECONDS) 212 | .first(); 213 | final Executable executableReadPreferenceUrlConnection = () -> 214 | MongoClients.create( 215 | ConnectionUtils.getMongoClientSettingsWithTimeout(mongoRsUrlSecondary) 216 | ).getDatabase(dbName).getCollection(collectionName).find(filterDoc).first(); 217 | 218 | //THEN 219 | //1. read operations are supported. 220 | assertEquals(doc, collection.find(filterDoc).first()); 221 | //2. write operations are supported. 222 | assertEquals(newDoc, collection.find(filterNewDoc).first()); 223 | 224 | //3. secondary is unavailable. 225 | assertThrows(MongoException.class, executableReadPreferenceOnCollection); 226 | assertThrows(MongoTimeoutException.class, executableReadPreferenceUrlConnection); 227 | 228 | //BRING A DISCONNECTED NODE BACK 229 | mongoReplicaSet.connectNodeToNetwork(secondaryNode); 230 | mongoReplicaSet.waitForAllMongoNodesUp(); 231 | mongoReplicaSet.waitForMaster(); 232 | assertThat( 233 | mongoReplicaSet.nodeStates(mongoReplicaSet.getMongoRsStatus().getMembers()), 234 | hasItems( 235 | ReplicaSetMemberState.PRIMARY, 236 | ReplicaSetMemberState.SECONDARY, 237 | ReplicaSetMemberState.ARBITER 238 | ) 239 | ); 240 | } 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/test/java/com/github/silaev/mongodb/replicaset/integration/api/faulttolerance/MongoDbReplicaSetFaultTolerancePSAApiITTest.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.integration.api.faulttolerance; 2 | 3 | import com.github.silaev.mongodb.replicaset.MongoDbReplicaSet; 4 | import com.github.silaev.mongodb.replicaset.core.IntegrationTest; 5 | import com.github.silaev.mongodb.replicaset.model.HardFailureAction; 6 | import com.github.silaev.mongodb.replicaset.model.ReplicaSetMemberState; 7 | import lombok.extern.slf4j.Slf4j; 8 | import lombok.val; 9 | import org.junit.jupiter.api.AfterEach; 10 | import org.junit.jupiter.api.BeforeEach; 11 | import org.junit.jupiter.api.Test; 12 | import org.junit.jupiter.params.ParameterizedTest; 13 | import org.junit.jupiter.params.provider.ValueSource; 14 | 15 | import static org.hamcrest.CoreMatchers.hasItems; 16 | import static org.hamcrest.MatcherAssert.assertThat; 17 | import static org.junit.jupiter.api.Assertions.assertEquals; 18 | 19 | /** 20 | * A fault tolerance tests for Primary with a Secondary and an Arbiter (PSA) 21 | */ 22 | @IntegrationTest 23 | @Slf4j 24 | class MongoDbReplicaSetFaultTolerancePSAApiITTest { 25 | private static final int REPLICA_SET_NUMBER = 2; 26 | 27 | private final MongoDbReplicaSet mongoReplicaSet = MongoDbReplicaSet.builder() 28 | .replicaSetNumber(REPLICA_SET_NUMBER) 29 | .addArbiter(true) 30 | .build(); 31 | 32 | @BeforeEach 33 | void setUp() { 34 | mongoReplicaSet.start(); 35 | } 36 | 37 | @AfterEach 38 | void tearDown() { 39 | mongoReplicaSet.stop(); 40 | } 41 | 42 | @ParameterizedTest(name = "{index}: action: {0}") 43 | @ValueSource(strings = {"KILL", "STOP"}) 44 | void shouldTestFailoverBecauseOfContainerIsKilledOrStopped( 45 | final HardFailureAction action 46 | ) { 47 | //===STAGE 1: killing or stopping a master node. 48 | //GIVEN 49 | val mongoNodes = mongoReplicaSet.getMongoRsStatus().getMembers(); 50 | val nodeStatesBeforeFailure = mongoReplicaSet.nodeStates(mongoNodes); 51 | val currentMasterNode = mongoReplicaSet.getMasterMongoNode(mongoNodes); 52 | 53 | //WHEN: Kill or stop the master node. 54 | switch (action) { 55 | case KILL: 56 | mongoReplicaSet.killNode(currentMasterNode); 57 | break; 58 | case STOP: 59 | mongoReplicaSet.stopNode(currentMasterNode); 60 | break; 61 | default: 62 | throw new IllegalArgumentException(String.format("Cannot find action: %s", action)); 63 | } 64 | 65 | //THEN 66 | //Check the state of the members before the failure. 67 | assertThat( 68 | nodeStatesBeforeFailure, 69 | hasItems( 70 | ReplicaSetMemberState.PRIMARY, 71 | ReplicaSetMemberState.SECONDARY, 72 | ReplicaSetMemberState.ARBITER 73 | ) 74 | ); 75 | assertEquals(REPLICA_SET_NUMBER + 1, nodeStatesBeforeFailure.size()); 76 | 77 | //===STAGE 2: Surviving a failure. 78 | //WHEN: Wait for reelection. 79 | mongoReplicaSet.waitForMasterReelection(currentMasterNode); 80 | mongoReplicaSet.removeNodeFromReplSetConfigWithForce(currentMasterNode); 81 | val actualNodeStatesAfterElection = 82 | mongoReplicaSet.nodeStates(mongoReplicaSet.getMongoRsStatus().getMembers()); 83 | 84 | //THEN: Check the state of the members when election is over. 85 | assertThat( 86 | actualNodeStatesAfterElection, 87 | hasItems( 88 | ReplicaSetMemberState.PRIMARY, 89 | ReplicaSetMemberState.ARBITER 90 | ) 91 | ); 92 | assertEquals(REPLICA_SET_NUMBER, actualNodeStatesAfterElection.size()); 93 | } 94 | 95 | @Test 96 | void shouldTestFailoverBecauseOfContainerNetworkDisconnect() { 97 | //===STAGE 1: Disconnecting a master node from its network. 98 | //GIVEN 99 | val mongoNodes = mongoReplicaSet.getMongoRsStatus().getMembers(); 100 | val nodeStatesBeforeFailure = mongoReplicaSet.nodeStates(mongoNodes); 101 | val currentMasterNode = mongoReplicaSet.getMasterMongoNode(mongoNodes); 102 | 103 | //WHEN: Disconnect a master node from its network. 104 | mongoReplicaSet.disconnectNodeFromNetwork(currentMasterNode); 105 | 106 | //THEN 107 | //Check the state of members before the failure. 108 | assertThat( 109 | nodeStatesBeforeFailure, 110 | hasItems( 111 | ReplicaSetMemberState.PRIMARY, 112 | ReplicaSetMemberState.SECONDARY, 113 | ReplicaSetMemberState.ARBITER 114 | ) 115 | ); 116 | assertEquals(REPLICA_SET_NUMBER + 1, nodeStatesBeforeFailure.size()); 117 | 118 | //===STAGE 2: Surviving a failure. 119 | //WHEN: Wait for reelection. 120 | mongoReplicaSet.waitForMasterReelection(currentMasterNode); 121 | 122 | val actualNodeStatesAfterElection = 123 | mongoReplicaSet.nodeStates(mongoReplicaSet.getMongoRsStatus().getMembers()); 124 | 125 | //THEN: Check the state of members when election is over. 126 | assertThat( 127 | actualNodeStatesAfterElection, 128 | hasItems( 129 | ReplicaSetMemberState.PRIMARY, 130 | ReplicaSetMemberState.DOWN, 131 | ReplicaSetMemberState.ARBITER 132 | ) 133 | ); 134 | assertEquals(REPLICA_SET_NUMBER + 1, actualNodeStatesAfterElection.size()); 135 | 136 | //===STAGE 3: Connecting a disconnected node back. 137 | //WHEN: Connect back. 138 | mongoReplicaSet.connectNodeToNetworkWithForceRemoval(currentMasterNode); 139 | mongoReplicaSet.waitForAllMongoNodesUp(); 140 | val actualNodeStatesAfterConnectingBack = 141 | mongoReplicaSet.nodeStates(mongoReplicaSet.getMongoRsStatus().getMembers()); 142 | 143 | //THEN 144 | assertThat( 145 | actualNodeStatesAfterConnectingBack, 146 | hasItems( 147 | ReplicaSetMemberState.PRIMARY, 148 | ReplicaSetMemberState.ARBITER, 149 | ReplicaSetMemberState.SECONDARY 150 | ) 151 | ); 152 | assertEquals(REPLICA_SET_NUMBER + 1, actualNodeStatesAfterConnectingBack.size()); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/test/java/com/github/silaev/mongodb/replicaset/integration/api/faulttolerance/MongoDbReplicaSetFaultTolerancePSSApiITTest.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.integration.api.faulttolerance; 2 | 3 | import com.github.silaev.mongodb.replicaset.MongoDbReplicaSet; 4 | import com.github.silaev.mongodb.replicaset.core.IntegrationTest; 5 | import com.github.silaev.mongodb.replicaset.model.HardFailureAction; 6 | import com.github.silaev.mongodb.replicaset.model.ReplicaSetMemberState; 7 | import lombok.extern.slf4j.Slf4j; 8 | import lombok.val; 9 | import org.junit.jupiter.api.AfterEach; 10 | import org.junit.jupiter.api.BeforeEach; 11 | import org.junit.jupiter.api.Test; 12 | import org.junit.jupiter.params.ParameterizedTest; 13 | import org.junit.jupiter.params.provider.ValueSource; 14 | 15 | import static org.hamcrest.CoreMatchers.hasItems; 16 | import static org.hamcrest.MatcherAssert.assertThat; 17 | import static org.junit.jupiter.api.Assertions.assertEquals; 18 | 19 | /** 20 | * A fault tolerance tests for Primary with Two Secondary Members (P-S-S) 21 | */ 22 | @IntegrationTest 23 | @Slf4j 24 | class MongoDbReplicaSetFaultTolerancePSSApiITTest { 25 | private static final int REPLICA_SET_NUMBER = 3; 26 | 27 | private final MongoDbReplicaSet mongoReplicaSet = MongoDbReplicaSet.builder() 28 | .replicaSetNumber(REPLICA_SET_NUMBER) 29 | .build(); 30 | 31 | @BeforeEach 32 | void setUp() { 33 | mongoReplicaSet.start(); 34 | } 35 | 36 | @AfterEach 37 | void tearDown() { 38 | mongoReplicaSet.stop(); 39 | } 40 | 41 | @ParameterizedTest(name = "{index}: action: {0}") 42 | @ValueSource(strings = {"KILL", "STOP"}) 43 | void shouldTestFailoverBecauseOfContainerIsKilledOrStopped( 44 | final HardFailureAction action 45 | ) { 46 | //===STAGE 1: killing or stopping a master node. 47 | //GIVEN 48 | val mongoNodes = mongoReplicaSet.getMongoRsStatus().getMembers(); 49 | val nodeStatesBeforeFailover = mongoReplicaSet.nodeStates(mongoNodes); 50 | val currentMasterNode = mongoReplicaSet.getMasterMongoNode(mongoNodes); 51 | 52 | //WHEN: Kill or stop the master node. 53 | switch (action) { 54 | case KILL: 55 | mongoReplicaSet.killNode(currentMasterNode); 56 | break; 57 | case STOP: 58 | mongoReplicaSet.stopNode(currentMasterNode); 59 | break; 60 | default: 61 | throw new IllegalArgumentException(String.format("Cannot find action: %s", action)); 62 | } 63 | 64 | //THEN 65 | //1. Check the state of members before a failover. 66 | assertThat( 67 | nodeStatesBeforeFailover, 68 | hasItems( 69 | ReplicaSetMemberState.PRIMARY, 70 | ReplicaSetMemberState.SECONDARY, 71 | ReplicaSetMemberState.SECONDARY 72 | ) 73 | ); 74 | assertEquals(REPLICA_SET_NUMBER, nodeStatesBeforeFailover.size()); 75 | 76 | //===STAGE 2: Surviving a failure. 77 | //WHEN: Wait for reelection. 78 | mongoReplicaSet.waitForMasterReelection(currentMasterNode); 79 | mongoReplicaSet.removeNodeFromReplSetConfigWithForce(currentMasterNode); 80 | val actualNodeStatesAfterElection = 81 | mongoReplicaSet.nodeStates(mongoReplicaSet.getMongoRsStatus().getMembers()); 82 | 83 | //THEN: Check the state of members when election is over. 84 | assertThat( 85 | actualNodeStatesAfterElection, 86 | hasItems( 87 | ReplicaSetMemberState.PRIMARY, 88 | ReplicaSetMemberState.SECONDARY 89 | ) 90 | ); 91 | assertEquals(REPLICA_SET_NUMBER - 1, actualNodeStatesAfterElection.size()); 92 | } 93 | 94 | @Test 95 | void shouldTestFailoverBecauseOfContainerNetworkDisconnect() { 96 | //===STAGE 1: Disconnecting a master node from its network. 97 | //GIVEN 98 | val mongoNodes = mongoReplicaSet.getMongoRsStatus().getMembers(); 99 | val nodeStatesBeforeFailover = mongoReplicaSet.nodeStates(mongoNodes); 100 | val currentMasterNode = mongoReplicaSet.getMasterMongoNode(mongoNodes); 101 | 102 | //WHEN: Disconnect a master node from its network. 103 | mongoReplicaSet.disconnectNodeFromNetwork(currentMasterNode); 104 | 105 | //THEN 106 | //1. Check the state of members before the failure. 107 | assertThat( 108 | nodeStatesBeforeFailover, 109 | hasItems( 110 | ReplicaSetMemberState.PRIMARY, 111 | ReplicaSetMemberState.SECONDARY, 112 | ReplicaSetMemberState.SECONDARY 113 | ) 114 | ); 115 | assertEquals(REPLICA_SET_NUMBER, nodeStatesBeforeFailover.size()); 116 | 117 | //===STAGE 2: Surviving a failure. 118 | //WHEN: Wait for reelection. 119 | mongoReplicaSet.waitForMasterReelection(currentMasterNode); 120 | val actualNodeStatesAfterElection = 121 | mongoReplicaSet.nodeStates(mongoReplicaSet.getMongoRsStatus().getMembers()); 122 | 123 | //THEN: Check the state of members when election is over. 124 | assertThat( 125 | actualNodeStatesAfterElection, 126 | hasItems( 127 | ReplicaSetMemberState.PRIMARY, 128 | ReplicaSetMemberState.DOWN, 129 | ReplicaSetMemberState.SECONDARY 130 | ) 131 | ); 132 | assertEquals(REPLICA_SET_NUMBER, actualNodeStatesAfterElection.size()); 133 | 134 | //===STAGE 3: Connecting a disconnected node back. 135 | //WHEN: Connect back. 136 | mongoReplicaSet.connectNodeToNetwork(currentMasterNode); 137 | mongoReplicaSet.waitForAllMongoNodesUp(); 138 | val actualNodeStatesAfterConnectingBack = 139 | mongoReplicaSet.nodeStates(mongoReplicaSet.getMongoRsStatus().getMembers()); 140 | 141 | //THEN 142 | assertThat( 143 | actualNodeStatesAfterConnectingBack, 144 | hasItems( 145 | ReplicaSetMemberState.PRIMARY, 146 | ReplicaSetMemberState.SECONDARY, 147 | ReplicaSetMemberState.SECONDARY 148 | ) 149 | ); 150 | assertEquals(REPLICA_SET_NUMBER, actualNodeStatesAfterConnectingBack.size()); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/test/java/com/github/silaev/mongodb/replicaset/integration/transaction/BaseMongoDbReplicaSetTransactionITTest.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.integration.transaction; 2 | 3 | import com.github.silaev.mongodb.replicaset.MongoDbReplicaSet; 4 | import com.github.silaev.mongodb.replicaset.util.ConnectionUtils; 5 | import com.mongodb.ReadConcern; 6 | import com.mongodb.ReadPreference; 7 | import com.mongodb.TransactionOptions; 8 | import com.mongodb.WriteConcern; 9 | import com.mongodb.client.TransactionBody; 10 | import lombok.val; 11 | import org.bson.Document; 12 | 13 | import static org.junit.jupiter.api.Assertions.assertEquals; 14 | import static org.junit.jupiter.api.Assertions.assertNotNull; 15 | 16 | abstract class BaseMongoDbReplicaSetTransactionITTest { 17 | /** 18 | * Taken from https://docs.mongodb.com 19 | */ 20 | void shouldExecuteTransactions(final MongoDbReplicaSet replicaSet) { 21 | //GIVEN 22 | val mongoRsUrl = replicaSet.getReplicaSetUrl(); 23 | assertNotNull(mongoRsUrl); 24 | try ( 25 | val mongoSyncClient = com.mongodb.client.MongoClients.create( 26 | ConnectionUtils.getMongoClientSettingsWithTimeout(mongoRsUrl) 27 | ) 28 | ) { 29 | mongoSyncClient.getDatabase("mydb1").getCollection("foo") 30 | .withWriteConcern(WriteConcern.MAJORITY).insertOne(new Document("abc", 0)); 31 | mongoSyncClient.getDatabase("mydb2").getCollection("bar") 32 | .withWriteConcern(WriteConcern.MAJORITY).insertOne(new Document("xyz", 0)); 33 | 34 | try (val clientSession = mongoSyncClient.startSession()) { 35 | val txnOptions = TransactionOptions.builder() 36 | .readPreference(ReadPreference.primary()) 37 | .readConcern(ReadConcern.LOCAL) 38 | .writeConcern(WriteConcern.MAJORITY) 39 | .build(); 40 | 41 | val trxResult = "Inserted into collections in different databases"; 42 | 43 | //WHEN + THEN 44 | TransactionBody txnBody = () -> { 45 | val coll1 = mongoSyncClient.getDatabase("mydb1").getCollection("foo"); 46 | val coll2 = mongoSyncClient.getDatabase("mydb2").getCollection("bar"); 47 | 48 | coll1.insertOne(clientSession, new Document("abc", 1)); 49 | coll2.insertOne(clientSession, new Document("xyz", 999)); 50 | return trxResult; 51 | }; 52 | 53 | val trxResultActual = clientSession.withTransaction(txnBody, txnOptions); 54 | assertEquals(trxResult, trxResultActual); 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/com/github/silaev/mongodb/replicaset/integration/transaction/MongoDbReplicaSetTransactionMultiNodeITTest.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.integration.transaction; 2 | 3 | import com.github.silaev.mongodb.replicaset.MongoDbReplicaSet; 4 | import com.github.silaev.mongodb.replicaset.core.EnabledIfSystemPropertyEnabledByDefault; 5 | import com.github.silaev.mongodb.replicaset.core.IntegrationTest; 6 | import org.junit.jupiter.api.AfterAll; 7 | import org.junit.jupiter.api.BeforeAll; 8 | import org.junit.jupiter.api.Test; 9 | 10 | @IntegrationTest 11 | @EnabledIfSystemPropertyEnabledByDefault( 12 | named = "mongoReplicaSetProperties.mongoDockerImageName", 13 | matches = "^mongo:4.*|^mongo:5.*" 14 | ) 15 | class MongoDbReplicaSetTransactionMultiNodeITTest extends 16 | BaseMongoDbReplicaSetTransactionITTest { 17 | private static final int REPLICA_SET_NUMBER = 3; 18 | 19 | private static final MongoDbReplicaSet MONGO_REPLICA_SET = MongoDbReplicaSet.builder() 20 | .replicaSetNumber(REPLICA_SET_NUMBER) 21 | .build(); 22 | 23 | @BeforeAll 24 | static void setUpAll() { 25 | MONGO_REPLICA_SET.start(); 26 | } 27 | 28 | @AfterAll 29 | static void tearDownAll() { 30 | MONGO_REPLICA_SET.stop(); 31 | } 32 | 33 | @Test 34 | void shouldExecuteTransactions() { 35 | super.shouldExecuteTransactions(MONGO_REPLICA_SET); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/com/github/silaev/mongodb/replicaset/integration/transaction/MongoDbReplicaSetTransactionSingleNodeITTest.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.integration.transaction; 2 | 3 | import com.github.silaev.mongodb.replicaset.MongoDbReplicaSet; 4 | import com.github.silaev.mongodb.replicaset.core.EnabledIfSystemPropertyEnabledByDefault; 5 | import com.github.silaev.mongodb.replicaset.core.IntegrationTest; 6 | import org.junit.jupiter.api.AfterAll; 7 | import org.junit.jupiter.api.BeforeAll; 8 | import org.junit.jupiter.api.Test; 9 | 10 | @IntegrationTest 11 | @EnabledIfSystemPropertyEnabledByDefault( 12 | named = "mongoReplicaSetProperties.mongoDockerImageName", 13 | matches = "^mongo:4.*|^mongo:5.*" 14 | ) 15 | class MongoDbReplicaSetTransactionSingleNodeITTest extends 16 | BaseMongoDbReplicaSetTransactionITTest { 17 | private static final int REPLICA_SET_NUMBER = 1; 18 | 19 | private static final MongoDbReplicaSet MONGO_REPLICA_SET = MongoDbReplicaSet.builder() 20 | .replicaSetNumber(REPLICA_SET_NUMBER) 21 | .build(); 22 | 23 | @BeforeAll 24 | static void setUpAll() { 25 | MONGO_REPLICA_SET.start(); 26 | } 27 | 28 | @AfterAll 29 | static void tearDownAll() { 30 | MONGO_REPLICA_SET.stop(); 31 | } 32 | 33 | @Test 34 | void shouldExecuteTransactions() { 35 | super.shouldExecuteTransactions(MONGO_REPLICA_SET); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/com/github/silaev/mongodb/replicaset/model/DisconnectionType.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.model; 2 | 3 | /** 4 | * @author Konstantin Silaev on 3/10/2020 5 | */ 6 | public enum DisconnectionType { 7 | /** 8 | * Removing a network of a container. 9 | */ 10 | HARD, 11 | /** 12 | * Cutting a connection between a mongo node and a proxy container (Toxiproxy). 13 | */ 14 | SOFT 15 | } 16 | -------------------------------------------------------------------------------- /src/test/java/com/github/silaev/mongodb/replicaset/model/HardFailureAction.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.model; 2 | 3 | /** 4 | * @author Konstantin Silaev on 3/10/2020 5 | */ 6 | public enum HardFailureAction { 7 | KILL, STOP 8 | } 9 | -------------------------------------------------------------------------------- /src/test/java/com/github/silaev/mongodb/replicaset/util/CollectionUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.util; 2 | 3 | import com.mongodb.reactivestreams.client.MongoClient; 4 | import com.mongodb.reactivestreams.client.MongoCollection; 5 | import org.bson.Document; 6 | 7 | import java.util.Collection; 8 | import java.util.HashSet; 9 | import java.util.Objects; 10 | import java.util.Set; 11 | 12 | public class CollectionUtils { 13 | 14 | private CollectionUtils() { 15 | 16 | } 17 | 18 | public static boolean isEqualCollection(final Collection a, 19 | final Collection b) { 20 | Objects.requireNonNull(a); 21 | Objects.requireNonNull(b); 22 | Set set1 = new HashSet<>(a); 23 | Set set2 = new HashSet<>(b); 24 | 25 | return a.size() == b.size() && set1.equals(set2); 26 | } 27 | 28 | public static MongoCollection getCollection( 29 | final MongoClient mongoClient, 30 | final String dbName, final String collectionName 31 | ) { 32 | return mongoClient.getDatabase(dbName).getCollection(collectionName); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/com/github/silaev/mongodb/replicaset/util/ConnectionUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.util; 2 | 3 | import com.mongodb.ConnectionString; 4 | import com.mongodb.MongoClientSettings; 5 | import com.mongodb.WriteConcern; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | import java.util.concurrent.TimeUnit; 9 | 10 | /** 11 | * @author Konstantin Silaev on 3/19/2020 12 | */ 13 | public class ConnectionUtils { 14 | private ConnectionUtils() { 15 | } 16 | 17 | /** 18 | * Used for setting timeouts to fail-fast behaviour. 19 | * 20 | * @param mongoRsUrlPrimary a connection string 21 | * @return MongoClientSettings with timeouts set 22 | */ 23 | @NotNull 24 | public static MongoClientSettings getMongoClientSettingsWithTimeout( 25 | final String mongoRsUrlPrimary, 26 | final WriteConcern writeConcern, 27 | final int timeout 28 | ) { 29 | final ConnectionString connectionString = new ConnectionString(mongoRsUrlPrimary); 30 | return MongoClientSettings.builder() 31 | .writeConcern(writeConcern.withWTimeout(timeout, TimeUnit.SECONDS)) 32 | .applyToClusterSettings(c -> c.serverSelectionTimeout(timeout, TimeUnit.SECONDS)) 33 | .applyConnectionString(connectionString) 34 | .applyToSocketSettings( 35 | b -> b 36 | .readTimeout(timeout, TimeUnit.SECONDS) 37 | .connectTimeout(timeout, TimeUnit.SECONDS) 38 | ).build(); 39 | } 40 | 41 | @NotNull 42 | public static MongoClientSettings getMongoClientSettingsWithTimeout( 43 | final String mongoRsUrlPrimary 44 | ) { 45 | return getMongoClientSettingsWithTimeout(mongoRsUrlPrimary, WriteConcern.W1, 5); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/test/java/com/github/silaev/mongodb/replicaset/util/SubscriberHelperUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.silaev.mongodb.replicaset.util; 2 | 3 | import com.mongodb.MongoTimeoutException; 4 | import lombok.SneakyThrows; 5 | import lombok.extern.slf4j.Slf4j; 6 | import lombok.val; 7 | import org.bson.Document; 8 | import org.reactivestreams.Publisher; 9 | import org.reactivestreams.Subscriber; 10 | import org.reactivestreams.Subscription; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | import java.util.concurrent.CountDownLatch; 15 | import java.util.concurrent.TimeUnit; 16 | 17 | import static java.lang.String.format; 18 | 19 | /** 20 | * Subscriber helper utility class taken from: 21 | * 22 | * @see https://github.com/mongodb/mongo-java-driver-reactivestreams 23 | */ 24 | @Slf4j 25 | public final class SubscriberHelperUtils { 26 | private SubscriberHelperUtils() { 27 | } 28 | 29 | @SneakyThrows 30 | public static SubscriberHelperUtils.PrintDocumentSubscriber getSubscriber( 31 | final Publisher command 32 | ) { 33 | val subscriber = new SubscriberHelperUtils.PrintDocumentSubscriber(); 34 | command.subscribe(subscriber); 35 | subscriber.await(); 36 | return subscriber; 37 | } 38 | 39 | /** 40 | * A Subscriber that stores the publishers results and provides a latch so can block on completion. 41 | * 42 | * @param The publishers result type 43 | */ 44 | public static class ObservableSubscriber implements Subscriber { 45 | private final List received; 46 | private final List errors; 47 | private final CountDownLatch latch; 48 | private volatile Subscription subscription; 49 | private volatile boolean completed; 50 | 51 | ObservableSubscriber() { 52 | this.received = new ArrayList<>(); 53 | this.errors = new ArrayList<>(); 54 | this.latch = new CountDownLatch(1); 55 | } 56 | 57 | @Override 58 | public void onSubscribe(final Subscription s) { 59 | subscription = s; 60 | } 61 | 62 | @Override 63 | public void onNext(final T t) { 64 | received.add(t); 65 | } 66 | 67 | @Override 68 | public void onError(final Throwable t) { 69 | log.error(t.getMessage()); 70 | errors.add(t); 71 | onComplete(); 72 | } 73 | 74 | @Override 75 | public void onComplete() { 76 | completed = true; 77 | latch.countDown(); 78 | } 79 | 80 | public Subscription getSubscription() { 81 | return subscription; 82 | } 83 | 84 | public List getReceived() { 85 | return received; 86 | } 87 | 88 | public Throwable getError() { 89 | if (errors.size() > 0) { 90 | return errors.get(0); 91 | } 92 | return null; 93 | } 94 | 95 | public boolean isCompleted() { 96 | return completed; 97 | } 98 | 99 | public List get(final long timeout, final TimeUnit unit) throws Throwable { 100 | return await(timeout, unit).getReceived(); 101 | } 102 | 103 | public ObservableSubscriber await() throws Throwable { 104 | return await(Long.MAX_VALUE, TimeUnit.MILLISECONDS); 105 | } 106 | 107 | public ObservableSubscriber await(final long timeout, final TimeUnit unit) throws Throwable { 108 | subscription.request(Integer.MAX_VALUE); 109 | if (!latch.await(timeout, unit)) { 110 | throw new MongoTimeoutException("Publisher onComplete timed out"); 111 | } 112 | if (!errors.isEmpty()) { 113 | throw errors.get(0); 114 | } 115 | return this; 116 | } 117 | } 118 | 119 | /** 120 | * A Subscriber that immediately requests Integer.MAX_VALUE onSubscribe 121 | * 122 | * @param The publishers result type 123 | */ 124 | public static class OperationSubscriber extends ObservableSubscriber { 125 | 126 | @Override 127 | public void onSubscribe(final Subscription s) { 128 | super.onSubscribe(s); 129 | s.request(Integer.MAX_VALUE); 130 | } 131 | } 132 | 133 | /** 134 | * A Subscriber that prints a message including the received items on completion 135 | * 136 | * @param The publishers result type 137 | */ 138 | public static class PrintSubscriber extends OperationSubscriber { 139 | private final String message; 140 | 141 | /** 142 | * A Subscriber that outputs a message onComplete. 143 | * 144 | * @param message the message to output onComplete 145 | */ 146 | public PrintSubscriber(final String message) { 147 | this.message = message; 148 | } 149 | 150 | @Override 151 | public void onComplete() { 152 | log.debug(format(message, getReceived())); 153 | super.onComplete(); 154 | } 155 | } 156 | 157 | /** 158 | * A Subscriber that prints the json version of each document 159 | */ 160 | public static class PrintDocumentSubscriber extends OperationSubscriber { 161 | @Override 162 | public void onNext(final Document document) { 163 | super.onNext(document); 164 | log.debug(document.toJson()); 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/test/resources/enabled-false.yml: -------------------------------------------------------------------------------- 1 | mongoReplicaSetProperties: 2 | enabled: false 3 | mongoDockerImageName: mongo:4.1.13 4 | -------------------------------------------------------------------------------- /src/test/resources/enabled-true.yml: -------------------------------------------------------------------------------- 1 | mongoReplicaSetProperties: 2 | enabled: true 3 | -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | %d{HH:mm:ss.SSS} %-5level %logger - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | mock-maker-inline -------------------------------------------------------------------------------- /src/test/resources/shell-output/rs-status-double-primaries.txt: -------------------------------------------------------------------------------- 1 | MongoDB shell version v4.4.3 2 | 3 | connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb 4 | 5 | Implicit session: session { "id" : UUID("fcf16368-ad95-4a7c-b1d0-bcf3741fbcbb") } 6 | 7 | MongoDB server version: 4.4.3 8 | 9 | { 10 | "set" : "docker-rs", 11 | "date" : ISODate("2021-02-11T08:03:08.109Z"), 12 | "myState" : 1, 13 | "term" : NumberLong(2), 14 | "syncSourceHost" : "", 15 | "syncSourceId" : -1, 16 | "heartbeatIntervalMillis" : NumberLong(2000), 17 | "majorityVoteCount" : 2, 18 | "writeMajorityCount" : 2, 19 | "votingMembersCount" : 3, 20 | "writableVotingMembersCount" : 3, 21 | "optimes" : { 22 | "lastCommittedOpTime" : { 23 | "ts" : Timestamp(1613030575, 6), 24 | "t" : NumberLong(1) 25 | }, 26 | "lastCommittedWallTime" : ISODate("2021-02-11T08:02:55.711Z"), 27 | "readConcernMajorityOpTime" : { 28 | "ts" : Timestamp(1613030575, 6), 29 | "t" : NumberLong(1) 30 | }, 31 | "readConcernMajorityWallTime" : ISODate("2021-02-11T08:02:55.711Z"), 32 | "appliedOpTime" : { 33 | "ts" : Timestamp(1613030576, 1), 34 | "t" : NumberLong(1) 35 | }, 36 | "durableOpTime" : { 37 | "ts" : Timestamp(1613030576, 1), 38 | "t" : NumberLong(1) 39 | }, 40 | "lastAppliedWallTime" : ISODate("2021-02-11T08:02:56.759Z"), 41 | "lastDurableWallTime" : ISODate("2021-02-11T08:02:56.759Z") 42 | }, 43 | "lastStableRecoveryTimestamp" : Timestamp(1613030575, 6), 44 | "e 45 | lectionCandidateMetrics" : { 46 | "lastElectionReason" : "electionTimeout", 47 | "lastElectionDate" : ISODate("2021-02-11T08:03:07.531Z"), 48 | "electionTerm" : NumberLong(2), 49 | "lastCommittedOpTimeAtElection" : { 50 | "ts" : Timestamp(1613030575, 6), 51 | "t" : NumberLong(1) 52 | }, 53 | "lastSeenOpTimeAtElection" : { 54 | "ts" : Timestamp(1613030576, 1), 55 | "t" : NumberLong(1) 56 | }, 57 | "numVotesNeeded" : 2, 58 | "priorityAtElection" : 1, 59 | "electionTimeoutMillis" : NumberLong(10000), 60 | "priorPrimaryMemberId" : 0 61 | }, 62 | "electionParticipantMetrics" : { 63 | "votedForCandidate" : true, 64 | "electionTerm" : NumberLong(1), 65 | "lastVoteDate" : ISODate("2021-02-11T08:02:55.636Z"), 66 | "electionCandidateMemberId" : 0, 67 | "voteReason" : "", 68 | "lastAppliedOpTimeAtElection" : { 69 | "ts" : Timestamp(1613030564, 1), 70 | "t" : NumberLong(-1) 71 | }, 72 | "maxAppliedOpTimeInSet" : { 73 | "ts" : Timestamp(1613030564, 1), 74 | "t" : NumberLong(-1) 75 | }, 76 | "priorityAtElection" : 1 77 | }, 78 | "members" : [ 79 | { 80 | "_id" : 0, 81 | "name" : "dockerhost:49857", 82 | "health" : 1, 83 | "sta 84 | te" : 1, 85 | "stateStr" : "PRIMARY", 86 | "uptime" : 22, 87 | "optime" : { 88 | "ts" : Timestamp(1613030575, 6), 89 | "t" : NumberLong(1) 90 | }, 91 | "optimeDurable" : { 92 | "ts" : Timestamp(1613030575, 6), 93 | "t" : NumberLong(1) 94 | }, 95 | "optimeDate" : ISODate("2021-02-11T08:02:55Z"), 96 | "optimeDurableDate" : ISODate("2021-02-11T08:02:55Z"), 97 | "lastHeartbeat" : ISODate("2021-02-11T08:02:56.708Z"), 98 | "lastHeartbeatRecv" : ISODate("2021-02-11T08:03:07.676Z"), 99 | "pingMs" : NumberLong(2), 100 | "lastHeartbeatMessage" : "", 101 | "syncSourceHost" : "", 102 | "syncSourceId" : -1, 103 | "infoMessage" : "", 104 | "electionTime" : Timestamp(1613030575, 1), 105 | "electionDate" : ISODate("2021-02-11T08:02:55Z"), 106 | "configVersion" : 1, 107 | "configTerm" : 1 108 | }, 109 | { 110 | "_id" : 1, 111 | "name" : "dockerhost:49858", 112 | "health" : 1, 113 | "state" : 1, 114 | "stateStr" : "PRIMARY", 115 | "uptime" : 27, 116 | "optime" : { 117 | "ts" : Timestamp(1613030576, 1), 118 | "t" : NumberLong(1) 119 | }, 120 | "optimeDate" : ISODate("2021-02-11T08:02:56Z"), 121 | "syncSourceHost" : "" 122 | , 123 | "syncSourceId" : -1, 124 | "infoMessage" : "", 125 | "electionTime" : Timestamp(1613030587, 1), 126 | "electionDate" : ISODate("2021-02-11T08:03:07Z"), 127 | "configVersion" : 1, 128 | "configTerm" : 1, 129 | "self" : true, 130 | "lastHeartbeatMessage" : "" 131 | }, 132 | { 133 | "_id" : 2, 134 | "name" : "dockerhost:49859", 135 | "health" : 1, 136 | "state" : 2, 137 | "stateStr" : "SECONDARY", 138 | "uptime" : 22, 139 | "optime" : { 140 | "ts" : Timestamp(1613030576, 1), 141 | "t" : NumberLong(1) 142 | }, 143 | "optimeDurable" : { 144 | "ts" : Timestamp(1613030576, 1), 145 | "t" : NumberLong(1) 146 | }, 147 | "optimeDate" : ISODate("2021-02-11T08:02:56Z"), 148 | "optimeDurableDate" : ISODate("2021-02-11T08:02:56Z"), 149 | "lastHeartbeat" : ISODate("2021-02-11T08:03:07.542Z"), 150 | "lastHeartbeatRecv" : ISODate("2021-02-11T08:03:06.726Z"), 151 | "pingMs" : NumberLong(1), 152 | "lastHeartbeatMessage" : "", 153 | "syncSourceHost" : "dockerhost:49857", 154 | "syncSourceId" : 0, 155 | "infoMessage" : "", 156 | "configVersion" : 1, 157 | "configTerm" : 1 158 | } 159 | ], 160 | "ok" : 1, 161 | "$clusterTime" : { 162 | "clusterTim 163 | e" : Timestamp(1613030587, 1), 164 | "signature" : { 165 | "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="), 166 | "keyId" : NumberLong(0) 167 | } 168 | }, 169 | "operationTime" : Timestamp(1613030576, 1) 170 | } 171 | -------------------------------------------------------------------------------- /src/test/resources/shell-output/rs-status-framed.txt: -------------------------------------------------------------------------------- 1 | MongoDB shell version v4.4.3 2 | 3 | connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb 4 | 5 | Implicit session: session { "id" : UUID("54102cb3-a263-4627-859d-cac49d342eed") } 6 | 7 | MongoDB server version: 4.4.3 8 | 9 | { 10 | "set" : "docker-rs", 11 | "date" : ISODate("2021-01-10T10:35:26.229Z"), 12 | "myState" : 1, 13 | "term" : NumberLong(2), 14 | "syncSourceHost" : "", 15 | "syncSourceId" : -1, 16 | "heartbeatIntervalMillis" : NumberLong(2000), 17 | "majorityVoteCount" : 2, 18 | "writeMajorityCount" : 2, 19 | "votingMembersCount" : 3, 20 | "writableVotingMembersCount" : 3, 21 | "optimes" : { 22 | "lastCommittedOpTime" : { 23 | "ts" : Timestamp(1610274923, 1), 24 | "t" : NumberLong(2) 25 | }, 26 | "lastCommittedWallTime" : ISODate("2021-01-10T10:35:23.950Z"), 27 | "readConcernMajorityOpTime" : { 28 | "ts" : Timestamp(1610274923, 1), 29 | "t" : NumberLong(2) 30 | }, 31 | "readConcernMajorityWallTime" : ISODate("2021-01-10T10:35:23.950Z"), 32 | "appliedOpTime" : { 33 | "ts" : Timestamp(1610274923, 1), 34 | "t" : NumberLong(2) 35 | }, 36 | "durableOpTime" : { 37 | "ts" : Timestamp(1610274923, 1), 38 | "t" : NumberLong(2) 39 | }, 40 | "lastAppliedWallTime" : ISODate("2021-01-10T10:35:23.950Z"), 41 | "lastDurableWallTime" : ISODate("2021-01-10T10:35:23.950Z") 42 | }, 43 | "lastStableRecoveryTimestamp" : Timestamp(1610274908, 6), 44 | "e 45 | lectionCandidateMetrics" : { 46 | "lastElectionReason" : "electionTimeout", 47 | "lastElectionDate" : ISODate("2021-01-10T10:35:19.773Z"), 48 | "electionTerm" : NumberLong(2), 49 | "lastCommittedOpTimeAtElection" : { 50 | "ts" : Timestamp(1610274908, 6), 51 | "t" : NumberLong(1) 52 | }, 53 | "lastSeenOpTimeAtElection" : { 54 | "ts" : Timestamp(1610274908, 7), 55 | "t" : NumberLong(1) 56 | }, 57 | "numVotesNeeded" : 2, 58 | "priorityAtElection" : 1, 59 | "electionTimeoutMillis" : NumberLong(10000), 60 | "numCatchUpOps" : NumberLong(0), 61 | "newTermStartDate" : ISODate("2021-01-10T10:35:19.785Z"), 62 | "wMajorityWriteAvailabilityDate" : ISODate("2021-01-10T10:35:20.016Z") 63 | }, 64 | "electionParticipantMetrics" : { 65 | "votedForCandidate" : true, 66 | "electionTerm" : NumberLong(1), 67 | "lastVoteDate" : ISODate("2021-01-10T10:35:08.056Z"), 68 | "electionCandidateMemberId" : 0, 69 | "voteReason" : "", 70 | "lastAppliedOpTimeAtElection" : { 71 | "ts" : Timestamp(1610274897, 1), 72 | "t" : NumberLong(-1) 73 | }, 74 | "maxAppliedOpTimeInSet" : { 75 | "ts" : Timestamp(1610274897, 1), 76 | "t" : N 77 | umberLong(-1) 78 | }, 79 | "priorityAtElection" : 1 80 | }, 81 | "members" : [ 82 | { 83 | "_id" : 1, 84 | "name" : "dockerhost:56023", 85 | "health" : 1, 86 | "state" : 1, 87 | "stateStr" : "PRIMARY", 88 | "uptime" : 31, 89 | "optime" : { 90 | "ts" : Timestamp(1610274923, 1), 91 | "t" : NumberLong(2) 92 | }, 93 | "optimeDate" : ISODate("2021-01-10T10:35:23Z"), 94 | "syncSourceHost" : "", 95 | "syncSourceId" : -1, 96 | "infoMessage" : "", 97 | "electionTime" : Timestamp(1610274919, 1), 98 | "electionDate" : ISODate("2021-01-10T10:35:19Z"), 99 | "configVersion" : 3, 100 | "configTerm" : 2, 101 | "self" : true, 102 | "lastHeartbeatMessage" : "" 103 | }, 104 | { 105 | "_id" : 2, 106 | "name" : "dockerhost:56024", 107 | "health" : 1, 108 | "state" : 2, 109 | "stateStr" : "SECONDARY", 110 | "uptime" : 28, 111 | "optime" : { 112 | "ts" : Timestamp(1610274923, 1), 113 | "t" : NumberLong(2) 114 | }, 115 | "optimeDurable" : { 116 | "ts" : Timestamp(1610274923, 1), 117 | "t" : NumberLong(2) 118 | }, 119 | "optimeDate" : ISODate("2021-01-10T10:35:23Z"), 120 | "optimeDurableDate" : ISODate("2021-01-10T10:35:23Z"), 121 | "lastHeartb 122 | eat" : ISODate("2021-01-10T10:35:25.957Z"), 123 | "lastHeartbeatRecv" : ISODate("2021-01-10T10:35:25.966Z"), 124 | "pingMs" : NumberLong(1), 125 | "lastHeartbeatMessage" : "", 126 | "syncSourceHost" : "dockerhost:56023", 127 | "syncSourceId" : 1, 128 | "infoMessage" : "", 129 | "configVersion" : 3, 130 | "configTerm" : 2 131 | }, 132 | { 133 | "_id" : 3, 134 | "name" : "dockerhost:56026", 135 | "health" : 1, 136 | "state" : 2, 137 | "stateStr" : "SECONDARY", 138 | "uptime" : 2, 139 | "optime" : { 140 | "ts" : Timestamp(1610274923, 1), 141 | "t" : NumberLong(2) 142 | }, 143 | "optimeDurable" : { 144 | "ts" : Timestamp(1610274923, 1), 145 | "t" : NumberLong(2) 146 | }, 147 | "optimeDate" : ISODate("2021-01-10T10:35:23Z"), 148 | "optimeDurableDate" : ISODate("2021-01-10T10:35:23Z"), 149 | "lastHeartbeat" : ISODate("2021-01-10T10:35:25.957Z"), 150 | "lastHeartbeatRecv" : ISODate("2021-01-10T10:35:26.127Z"), 151 | "pingMs" : NumberLong(1), 152 | "lastHeartbeatMessage" : "", 153 | "syncSourceHost" : "", 154 | "syncSourceId" : -1, 155 | "infoMessage" : "", 156 | "configVersion" : 3, 157 | "configTerm" : 2 158 | } 159 | ], 160 | "o 161 | k" : 1, 162 | "$clusterTime" : { 163 | "clusterTime" : Timestamp(1610274923, 1), 164 | "signature" : { 165 | "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="), 166 | "keyId" : NumberLong(0) 167 | } 168 | }, 169 | "operationTime" : Timestamp(1610274923, 1) 170 | } 171 | -------------------------------------------------------------------------------- /src/test/resources/shell-output/rs-status-plain-wo-status.txt: -------------------------------------------------------------------------------- 1 | MongoDB shell version v3.4.22 2 | 3 | connecting to: mongodb://127.0.0.1:27017 4 | 5 | MongoDB server version: 3.4.22 6 | -------------------------------------------------------------------------------- /src/test/resources/shell-output/rs-status-plain.txt: -------------------------------------------------------------------------------- 1 | MongoDB shell version v3.4.22 2 | 3 | connecting to: mongodb://127.0.0.1:27017 4 | 5 | MongoDB server version: 3.4.22 6 | 7 | { "ok" : 1 } 8 | -------------------------------------------------------------------------------- /src/test/resources/shell-output/rs-status-wo-status.txt: -------------------------------------------------------------------------------- 1 | MongoDB shell version v4.2.0 2 | 3 | connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb 4 | 5 | Implicit session: session { "id" : UUID("2def7590-ba68-4db3-9415-508467e48d81") } 6 | 7 | MongoDB server version: 4.2.0 8 | 9 | { 10 | "set" : "docker-rs", 11 | "date" : ISODate("2019-08-20T10:55:31.809Z"), 12 | "myState" : 1, 13 | "term" : NumberLong(1), 14 | "syncingTo" : "", 15 | "syncSourceHost" : "", 16 | "syncSourceId" : -1, 17 | "heartbeatIntervalMillis" : NumberLong(2000), 18 | "optimes" : { 19 | "lastCommittedOpTime" : { 20 | "ts" : Timestamp(1566298529, 1), 21 | "t" : NumberLong(1) 22 | }, 23 | "lastCommittedWallTime" : ISODate("2019-08-20T10:55:29.393Z"), 24 | "readConcernMajorityOpTime" : { 25 | "ts" : Timestamp(1566298529, 1), 26 | "t" : NumberLong(1) 27 | }, 28 | "readConcernMajorityWallTime" : ISODate("2019-08-20T10:55:29.393Z"), 29 | "appliedOpTime" : { 30 | "ts" : Timestamp(1566298529, 1), 31 | "t" : NumberLong(1) 32 | }, 33 | "durableOpTime" : { 34 | "ts" : Timestamp(1566298529, 1), 35 | "t" : NumberLong(1) 36 | }, 37 | "lastAppliedWallTime" : ISODate("2019-08-20T10:55:29.393Z"), 38 | "lastDurableWallTime" : ISODate("2019-08-20T10:55:29.393Z") 39 | }, 40 | "lastStableRecoveryTimestamp" : Timestamp(1566298525, 2), 41 | "lastStableCheckpointTimestamp" : Timestamp(1566298525, 2), 42 | "members" : [ 43 | { 44 | "_id" : 0, 45 | "name" : "dockerhost:33570", 46 | "ip" : "192.168.208.2", 47 | "health" : 1, 48 | "state" : 2, 49 | "stateStr" : "SECONDARY", 50 | "uptime" : 17, 51 | "optime" : { 52 | "ts" : Timestamp(1566298529, 1), 53 | "t" : NumberLong(1) 54 | }, 55 | "optimeDurable" : { 56 | "ts" : Timestamp(1566298529, 1), 57 | "t" : NumberLong(1) 58 | }, 59 | "optimeDate" : ISODate("2019-08-20T10:55:29Z"), 60 | "optimeDurableDate" : ISODate("2019-08-20T10:55:29Z"), 61 | "lastHeartbeat" : ISODate("2019-08-20T10:55:31.405Z"), 62 | "lastHeartbeatRecv" : ISODate("2019-08-20T10:55:31.538Z"), 63 | "pingMs" : NumberLong(2), 64 | "lastHeartbeatMessage" : "", 65 | "syncingTo" : "", 66 | "syncSourceHost" : "", 67 | "syncSourceId" : -1, 68 | "infoMessage" : "", 69 | "configVersion" : 2 70 | }, 71 | { 72 | "_id" : 1, 73 | "name" : "dockerhost:33571", 74 | "ip" : "192.168.208.2", 75 | "health" : 1, 76 | "state" : 2, 77 | "stateStr" : "SECONDARY", 78 | "uptime" : 17, 79 | "optime" : { 80 | "ts" : Timestamp(1566298529, 1), 81 | "t" : NumberLong(1) 82 | }, 83 | "optimeDurable" : { 84 | "ts" : Timestamp(1566298529, 1), 85 | "t" : NumberLong(1) 86 | }, 87 | "optimeDate" : ISODate("2019-08-20T10:55:29Z"), 88 | "optimeDurableDate" : ISODate("2019-08-20T10:55:29Z"), 89 | "lastHeartbeat" : ISODate("2019-08-20T10:55:31.405Z"), 90 | "lastHeartbeatRecv" : ISODate("2019-08-20T10:55:31.528Z"), 91 | "pingMs" : NumberLong(5), 92 | "lastHeartbeatMessage" : "", 93 | "syncingTo" : "", 94 | "syncSourceHost" : "", 95 | "syncSourceId" : -1, 96 | "infoMessage" : "", 97 | "configVersion" : 2 98 | }, 99 | { 100 | "_id" : 2, 101 | "name" : "dockerhost:33572", 102 | "ip" : "192.168.208.2", 103 | "health" : 1, 104 | "state" : 2, 105 | "stateStr" : "SECONDARY", 106 | "uptime" : 17, 107 | "optime" : { 108 | "ts" : Timestamp(1566298529, 1), 109 | "t" : NumberLong(1) 110 | }, 111 | "optimeDurable" : { 112 | "ts" : Timestamp(1566298529, 1), 113 | "t" : NumberLong(1) 114 | }, 115 | "optimeDate" : ISODate("2019-08-20T10:55:29Z"), 116 | "optimeDurableDate" : ISODate("2019-08-20T10:55:29Z"), 117 | "lastHeartbeat" : ISODate("2019-08-20T10:55:31.453Z"), 118 | "lastHeartbeatRecv" : ISODate("2019-08-20T10:55:31.525Z"), 119 | "pingMs" : NumberLong(5), 120 | "lastHeartbeatMessage" : "", 121 | "syncingTo" : "", 122 | "syncSourceHost" : "", 123 | "syncSourceId" : -1, 124 | "infoMessage" : "", 125 | "configVersion" : 2 126 | }, 127 | { 128 | "_id" : 3, 129 | "name" : "dockerhost:33573", 130 | "ip" : "192.168.208.2", 131 | "health" : 1, 132 | "state" : 1, 133 | "stateStr" : "PRIMARY", 134 | "uptime" : 19, 135 | "optime" : { 136 | "ts" : Timestamp(1566298529, 1), 137 | "t" : NumberLong(1) 138 | }, 139 | "optimeDate" : ISODate("2019-08-20T10:55:29Z"), 140 | "syncingTo" : "", 141 | "syncSourceHost" : "", 142 | "syncSourceId" : -1, 143 | "infoMessage" : "could not find member to sync from", 144 | "electionTime" : Timestamp(1566298524, 1), 145 | "electionDate" : ISODate("2019-08-20T10:55:24Z"), 146 | "configVersion" : 2, 147 | "self" : true, 148 | "lastHeartbeatMessage" : "" 149 | }, 150 | { 151 | "_id" : 4, 152 | "name" : "dockerhost:33574", 153 | "ip" : "192.168.208.2", 154 | "health" : 1, 155 | "state" : 7, 156 | "stateStr" : "ARBITER", 157 | "uptime" : 2, 158 | "lastHeartbeat" : ISODate("2019-08-20T10:55:31.418Z"), 159 | "lastHeartbeatRecv" : ISODate("2019-08-20T10:55:31.593Z"), 160 | "pingMs" : NumberLong(10), 161 | "lastHeartbeatMessage" : "", 162 | "syncingTo" : "", 163 | "syncSourceHost" : "", 164 | "syncSourceId" : -1, 165 | "infoMessage" : "", 166 | "configVersion" : 2 167 | } 168 | ], 169 | "$clusterTime" : { 170 | "clusterTime" : Timestamp(1566298529, 1), 171 | "signature" : { 172 | "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="), 173 | "keyId" : NumberLong(0) 174 | } 175 | }, 176 | "operationTime" : Timestamp(1566298529, 1) 177 | } 178 | -------------------------------------------------------------------------------- /src/test/resources/shell-output/rs-status-wo-version.txt: -------------------------------------------------------------------------------- 1 | MongoDB shell version v4.2.0 2 | 3 | connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb 4 | 5 | Implicit session: session { "id" : UUID("2def7590-ba68-4db3-9415-508467e48d81") } 6 | 7 | { 8 | "set" : "docker-rs", 9 | "date" : ISODate("2019-08-20T10:55:31.809Z"), 10 | "myState" : 1, 11 | "term" : NumberLong(1), 12 | "syncingTo" : "", 13 | "syncSourceHost" : "", 14 | "syncSourceId" : -1, 15 | "heartbeatIntervalMillis" : NumberLong(2000), 16 | "optimes" : { 17 | "lastCommittedOpTime" : { 18 | "ts" : Timestamp(1566298529, 1), 19 | "t" : NumberLong(1) 20 | }, 21 | "lastCommittedWallTime" : ISODate("2019-08-20T10:55:29.393Z"), 22 | "readConcernMajorityOpTime" : { 23 | "ts" : Timestamp(1566298529, 1), 24 | "t" : NumberLong(1) 25 | }, 26 | "readConcernMajorityWallTime" : ISODate("2019-08-20T10:55:29.393Z"), 27 | "appliedOpTime" : { 28 | "ts" : Timestamp(1566298529, 1), 29 | "t" : NumberLong(1) 30 | }, 31 | "durableOpTime" : { 32 | "ts" : Timestamp(1566298529, 1), 33 | "t" : NumberLong(1) 34 | }, 35 | "lastAppliedWallTime" : ISODate("2019-08-20T10:55:29.393Z"), 36 | "lastDurableWallTime" : ISODate("2019-08-20T10:55:29.393Z") 37 | }, 38 | "lastStableRecoveryTimestamp" : Timestamp(1566298525, 2), 39 | "lastStableCheckpointTimestamp" : Timestamp(1566298525, 2), 40 | "members" : [ 41 | { 42 | "_id" : 0, 43 | "name" : "dockerhost:33570", 44 | "ip" : "192.168.208.2", 45 | "health" : 1, 46 | "state" : 2, 47 | "stateStr" : "SECONDARY", 48 | "uptime" : 17, 49 | "optime" : { 50 | "ts" : Timestamp(1566298529, 1), 51 | "t" : NumberLong(1) 52 | }, 53 | "optimeDurable" : { 54 | "ts" : Timestamp(1566298529, 1), 55 | "t" : NumberLong(1) 56 | }, 57 | "optimeDate" : ISODate("2019-08-20T10:55:29Z"), 58 | "optimeDurableDate" : ISODate("2019-08-20T10:55:29Z"), 59 | "lastHeartbeat" : ISODate("2019-08-20T10:55:31.405Z"), 60 | "lastHeartbeatRecv" : ISODate("2019-08-20T10:55:31.538Z"), 61 | "pingMs" : NumberLong(2), 62 | "lastHeartbeatMessage" : "", 63 | "syncingTo" : "", 64 | "syncSourceHost" : "", 65 | "syncSourceId" : -1, 66 | "infoMessage" : "", 67 | "configVersion" : 2 68 | }, 69 | { 70 | "_id" : 1, 71 | "name" : "dockerhost:33571", 72 | "ip" : "192.168.208.2", 73 | "health" : 1, 74 | "state" : 2, 75 | "stateStr" : "SECONDARY", 76 | "uptime" : 17, 77 | "optime" : { 78 | "ts" : Timestamp(1566298529, 1), 79 | "t" : NumberLong(1) 80 | }, 81 | "optimeDurable" : { 82 | "ts" : Timestamp(1566298529, 1), 83 | "t" : NumberLong(1) 84 | }, 85 | "optimeDate" : ISODate("2019-08-20T10:55:29Z"), 86 | "optimeDurableDate" : ISODate("2019-08-20T10:55:29Z"), 87 | "lastHeartbeat" : ISODate("2019-08-20T10:55:31.405Z"), 88 | "lastHeartbeatRecv" : ISODate("2019-08-20T10:55:31.528Z"), 89 | "pingMs" : NumberLong(5), 90 | "lastHeartbeatMessage" : "", 91 | "syncingTo" : "", 92 | "syncSourceHost" : "", 93 | "syncSourceId" : -1, 94 | "infoMessage" : "", 95 | "configVersion" : 2 96 | }, 97 | { 98 | "_id" : 2, 99 | "name" : "dockerhost:33572", 100 | "ip" : "192.168.208.2", 101 | "health" : 1, 102 | "state" : 2, 103 | "stateStr" : "SECONDARY", 104 | "uptime" : 17, 105 | "optime" : { 106 | "ts" : Timestamp(1566298529, 1), 107 | "t" : NumberLong(1) 108 | }, 109 | "optimeDurable" : { 110 | "ts" : Timestamp(1566298529, 1), 111 | "t" : NumberLong(1) 112 | }, 113 | "optimeDate" : ISODate("2019-08-20T10:55:29Z"), 114 | "optimeDurableDate" : ISODate("2019-08-20T10:55:29Z"), 115 | "lastHeartbeat" : ISODate("2019-08-20T10:55:31.453Z"), 116 | "lastHeartbeatRecv" : ISODate("2019-08-20T10:55:31.525Z"), 117 | "pingMs" : NumberLong(5), 118 | "lastHeartbeatMessage" : "", 119 | "syncingTo" : "", 120 | "syncSourceHost" : "", 121 | "syncSourceId" : -1, 122 | "infoMessage" : "", 123 | "configVersion" : 2 124 | }, 125 | { 126 | "_id" : 3, 127 | "name" : "dockerhost:33573", 128 | "ip" : "192.168.208.2", 129 | "health" : 1, 130 | "state" : 1, 131 | "stateStr" : "PRIMARY", 132 | "uptime" : 19, 133 | "optime" : { 134 | "ts" : Timestamp(1566298529, 1), 135 | "t" : NumberLong(1) 136 | }, 137 | "optimeDate" : ISODate("2019-08-20T10:55:29Z"), 138 | "syncingTo" : "", 139 | "syncSourceHost" : "", 140 | "syncSourceId" : -1, 141 | "infoMessage" : "could not find member to sync from", 142 | "electionTime" : Timestamp(1566298524, 1), 143 | "electionDate" : ISODate("2019-08-20T10:55:24Z"), 144 | "configVersion" : 2, 145 | "self" : true, 146 | "lastHeartbeatMessage" : "" 147 | }, 148 | { 149 | "_id" : 4, 150 | "name" : "dockerhost:33574", 151 | "ip" : "192.168.208.2", 152 | "health" : 1, 153 | "state" : 7, 154 | "stateStr" : "ARBITER", 155 | "uptime" : 2, 156 | "lastHeartbeat" : ISODate("2019-08-20T10:55:31.418Z"), 157 | "lastHeartbeatRecv" : ISODate("2019-08-20T10:55:31.593Z"), 158 | "pingMs" : NumberLong(10), 159 | "lastHeartbeatMessage" : "", 160 | "syncingTo" : "", 161 | "syncSourceHost" : "", 162 | "syncSourceId" : -1, 163 | "infoMessage" : "", 164 | "configVersion" : 2 165 | } 166 | ], 167 | "ok" : 1, 168 | "$clusterTime" : { 169 | "clusterTime" : Timestamp(1566298529, 1), 170 | "signature" : { 171 | "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="), 172 | "keyId" : NumberLong(0) 173 | } 174 | }, 175 | "operationTime" : Timestamp(1566298529, 1) 176 | } 177 | -------------------------------------------------------------------------------- /src/test/resources/shell-output/rs-status.txt: -------------------------------------------------------------------------------- 1 | MongoDB shell version v4.2.0 2 | 3 | connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb 4 | 5 | Implicit session: session { "id" : UUID("2def7590-ba68-4db3-9415-508467e48d81") } 6 | 7 | MongoDB server version: 4.2.0 8 | 9 | { 10 | "set" : "docker-rs", 11 | "date" : ISODate("2019-08-20T10:55:31.809Z"), 12 | "myState" : 1, 13 | "term" : NumberLong(1), 14 | "syncingTo" : "", 15 | "syncSourceHost" : "", 16 | "syncSourceId" : -1, 17 | "heartbeatIntervalMillis" : NumberLong(2000), 18 | "optimes" : { 19 | "lastCommittedOpTime" : { 20 | "ts" : Timestamp(1566298529, 1), 21 | "t" : NumberLong(1) 22 | }, 23 | "lastCommittedWallTime" : ISODate("2019-08-20T10:55:29.393Z"), 24 | "readConcernMajorityOpTime" : { 25 | "ts" : Timestamp(1566298529, 1), 26 | "t" : NumberLong(1) 27 | }, 28 | "readConcernMajorityWallTime" : ISODate("2019-08-20T10:55:29.393Z"), 29 | "appliedOpTime" : { 30 | "ts" : Timestamp(1566298529, 1), 31 | "t" : NumberLong(1) 32 | }, 33 | "durableOpTime" : { 34 | "ts" : Timestamp(1566298529, 1), 35 | "t" : NumberLong(1) 36 | }, 37 | "lastAppliedWallTime" : ISODate("2019-08-20T10:55:29.393Z"), 38 | "lastDurableWallTime" : ISODate("2019-08-20T10:55:29.393Z") 39 | }, 40 | "lastStableRecoveryTimestamp" : Timestamp(1566298525, 2), 41 | "lastStableCheckpointTimestamp" : Timestamp(1566298525, 2), 42 | "members" : [ 43 | { 44 | "_id" : 0, 45 | "name" : "dockerhost:33570", 46 | "ip" : "192.168.208.2", 47 | "health" : 1, 48 | "state" : 2, 49 | "stateStr" : "SECONDARY", 50 | "uptime" : 17, 51 | "optime" : { 52 | "ts" : Timestamp(1566298529, 1), 53 | "t" : NumberLong(1) 54 | }, 55 | "optimeDurable" : { 56 | "ts" : Timestamp(1566298529, 1), 57 | "t" : NumberLong(1) 58 | }, 59 | "optimeDate" : ISODate("2019-08-20T10:55:29Z"), 60 | "optimeDurableDate" : ISODate("2019-08-20T10:55:29Z"), 61 | "lastHeartbeat" : ISODate("2019-08-20T10:55:31.405Z"), 62 | "lastHeartbeatRecv" : ISODate("2019-08-20T10:55:31.538Z"), 63 | "pingMs" : NumberLong(2), 64 | "lastHeartbeatMessage" : "", 65 | "syncingTo" : "", 66 | "syncSourceHost" : "", 67 | "syncSourceId" : -1, 68 | "infoMessage" : "", 69 | "configVersion" : 2 70 | }, 71 | { 72 | "_id" : 1, 73 | "name" : "dockerhost:33571", 74 | "ip" : "192.168.208.2", 75 | "health" : 1, 76 | "state" : 2, 77 | "stateStr" : "SECONDARY", 78 | "uptime" : 17, 79 | "optime" : { 80 | "ts" : Timestamp(1566298529, 1), 81 | "t" : NumberLong(1) 82 | }, 83 | "optimeDurable" : { 84 | "ts" : Timestamp(1566298529, 1), 85 | "t" : NumberLong(1) 86 | }, 87 | "optimeDate" : ISODate("2019-08-20T10:55:29Z"), 88 | "optimeDurableDate" : ISODate("2019-08-20T10:55:29Z"), 89 | "lastHeartbeat" : ISODate("2019-08-20T10:55:31.405Z"), 90 | "lastHeartbeatRecv" : ISODate("2019-08-20T10:55:31.528Z"), 91 | "pingMs" : NumberLong(5), 92 | "lastHeartbeatMessage" : "", 93 | "syncingTo" : "", 94 | "syncSourceHost" : "", 95 | "syncSourceId" : -1, 96 | "infoMessage" : "", 97 | "configVersion" : 2 98 | }, 99 | { 100 | "_id" : 2, 101 | "name" : "dockerhost:33572", 102 | "ip" : "192.168.208.2", 103 | "health" : 1, 104 | "state" : 2, 105 | "stateStr" : "SECONDARY", 106 | "uptime" : 17, 107 | "optime" : { 108 | "ts" : Timestamp(1566298529, 1), 109 | "t" : NumberLong(1) 110 | }, 111 | "optimeDurable" : { 112 | "ts" : Timestamp(1566298529, 1), 113 | "t" : NumberLong(1) 114 | }, 115 | "optimeDate" : ISODate("2019-08-20T10:55:29Z"), 116 | "optimeDurableDate" : ISODate("2019-08-20T10:55:29Z"), 117 | "lastHeartbeat" : ISODate("2019-08-20T10:55:31.453Z"), 118 | "lastHeartbeatRecv" : ISODate("2019-08-20T10:55:31.525Z"), 119 | "pingMs" : NumberLong(5), 120 | "lastHeartbeatMessage" : "", 121 | "syncingTo" : "", 122 | "syncSourceHost" : "", 123 | "syncSourceId" : -1, 124 | "infoMessage" : "", 125 | "configVersion" : 2 126 | }, 127 | { 128 | "_id" : 3, 129 | "name" : "dockerhost:33573", 130 | "ip" : "192.168.208.2", 131 | "health" : 1, 132 | "state" : 1, 133 | "stateStr" : "PRIMARY", 134 | "uptime" : 19, 135 | "optime" : { 136 | "ts" : Timestamp(1566298529, 1), 137 | "t" : NumberLong(1) 138 | }, 139 | "optimeDate" : ISODate("2019-08-20T10:55:29Z"), 140 | "syncingTo" : "", 141 | "syncSourceHost" : "", 142 | "syncSourceId" : -1, 143 | "infoMessage" : "could not find member to sync from", 144 | "electionTime" : Timestamp(1566298524, 1), 145 | "electionDate" : ISODate("2019-08-20T10:55:24Z"), 146 | "configVersion" : 2, 147 | "self" : true, 148 | "lastHeartbeatMessage" : "" 149 | }, 150 | { 151 | "_id" : 4, 152 | "name" : "dockerhost:33574", 153 | "ip" : "192.168.208.2", 154 | "health" : 1, 155 | "state" : 7, 156 | "stateStr" : "ARBITER", 157 | "uptime" : 2, 158 | "lastHeartbeat" : ISODate("2019-08-20T10:55:31.418Z"), 159 | "lastHeartbeatRecv" : ISODate("2019-08-20T10:55:31.593Z"), 160 | "pingMs" : NumberLong(10), 161 | "lastHeartbeatMessage" : "", 162 | "syncingTo" : "", 163 | "syncSourceHost" : "", 164 | "syncSourceId" : -1, 165 | "infoMessage" : "", 166 | "configVersion" : 2 167 | } 168 | ], 169 | "ok" : 1, 170 | "$clusterTime" : { 171 | "clusterTime" : Timestamp(1566298529, 1), 172 | "signature" : { 173 | "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="), 174 | "keyId" : NumberLong(0) 175 | } 176 | }, 177 | "operationTime" : Timestamp(1566298529, 1) 178 | } -------------------------------------------------------------------------------- /src/test/resources/shell-output/timeout-exceeds.txt: -------------------------------------------------------------------------------- 1 | MongoDB shell version v4.4.0 2 | 3 | connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb 4 | 5 | Implicit session: session { "id" : UUID("dd574fc5-528c-437b-8960-62859bc5c247") } 6 | 7 | MongoDB server version: 4.4.0 8 | 9 | { 10 | "operationTime" : Timestamp(1597732974, 1), 11 | "ok" : 0, 12 | "errmsg" : "operation exceeded time limit", 13 | "code" : 50, 14 | "codeName" : "MaxTimeMSExpired", 15 | "$clusterTime" : { 16 | "clusterTime" : Timestamp(1597732974, 1), 17 | "signature" : { 18 | "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="), 19 | "keyId" : NumberLong(0) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /testcov.exclude: -------------------------------------------------------------------------------- 1 | com/github/silaev/mongodb/replicaset/model/** 2 | --------------------------------------------------------------------------------