├── .editorconfig ├── .github ├── ISSUE_TEMPLATE.md ├── dependabot.yml ├── maven-cd-settings.xml ├── maven-ci-settings.xml └── workflows │ ├── ci-4.x.yml │ ├── ci-5.x-stable.yml │ ├── ci-5.x.yml │ ├── ci-matrix-5.x.yml │ ├── ci.yml │ └── deploy.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── pom.xml ├── src ├── main │ ├── asciidoc │ │ └── index.adoc │ ├── generated │ │ └── io │ │ │ └── vertx │ │ │ └── redis │ │ │ └── client │ │ │ ├── PoolOptionsConverter.java │ │ │ ├── RedisClusterConnectOptionsConverter.java │ │ │ ├── RedisConnectOptionsConverter.java │ │ │ ├── RedisOptionsConverter.java │ │ │ ├── RedisReplicationConnectOptionsConverter.java │ │ │ ├── RedisSentinelConnectOptionsConverter.java │ │ │ └── RedisStandaloneConnectOptionsConverter.java │ └── java │ │ ├── examples │ │ ├── RedisConnectOptionsSupplier.java │ │ ├── RedisExamples.java │ │ └── package-info.java │ │ ├── io │ │ └── vertx │ │ │ └── redis │ │ │ ├── client │ │ │ ├── Command.java │ │ │ ├── CommandMap.java │ │ │ ├── ConstantSupplier.java │ │ │ ├── EventBusHandler.java │ │ │ ├── PoolOptions.java │ │ │ ├── ProtocolVersion.java │ │ │ ├── Redis.java │ │ │ ├── RedisAPI.java │ │ │ ├── RedisClientType.java │ │ │ ├── RedisCluster.java │ │ │ ├── RedisClusterConnectOptions.java │ │ │ ├── RedisClusterTransactions.java │ │ │ ├── RedisConnectOptions.java │ │ │ ├── RedisConnection.java │ │ │ ├── RedisOptions.java │ │ │ ├── RedisReplicas.java │ │ │ ├── RedisReplicationConnectOptions.java │ │ │ ├── RedisRole.java │ │ │ ├── RedisSentinelConnectOptions.java │ │ │ ├── RedisStandaloneConnectOptions.java │ │ │ ├── RedisTopology.java │ │ │ ├── Request.java │ │ │ ├── RequestGrouping.java │ │ │ ├── Response.java │ │ │ ├── ResponseType.java │ │ │ └── impl │ │ │ │ ├── ArrayQueue.java │ │ │ │ ├── ArrayStack.java │ │ │ │ ├── BaseRedisClient.java │ │ │ │ ├── CommandImpl.java │ │ │ │ ├── CommandReporter.java │ │ │ │ ├── KeyLocator.java │ │ │ │ ├── ParserHandler.java │ │ │ │ ├── PooledRedisConnection.java │ │ │ │ ├── RESPEncoder.java │ │ │ │ ├── RESPParser.java │ │ │ │ ├── ReadableBuffer.java │ │ │ │ ├── RedisAPIImpl.java │ │ │ │ ├── RedisClient.java │ │ │ │ ├── RedisClusterClient.java │ │ │ │ ├── RedisClusterConnection.java │ │ │ │ ├── RedisClusterImpl.java │ │ │ │ ├── RedisConnectException.java │ │ │ │ ├── RedisConnectionInternal.java │ │ │ │ ├── RedisConnectionManager.java │ │ │ │ ├── RedisReplicationClient.java │ │ │ │ ├── RedisReplicationConnection.java │ │ │ │ ├── RedisSentinelClient.java │ │ │ │ ├── RedisSentinelConnection.java │ │ │ │ ├── RedisStandaloneConnection.java │ │ │ │ ├── RedisURI.java │ │ │ │ ├── RequestImpl.java │ │ │ │ ├── Resolver.java │ │ │ │ ├── SentinelFailover.java │ │ │ │ ├── SharedSlots.java │ │ │ │ ├── Slots.java │ │ │ │ ├── ZModem.java │ │ │ │ ├── keys │ │ │ │ ├── BeginSearch.java │ │ │ │ ├── BeginSearchIndex.java │ │ │ │ ├── BeginSearchKeyword.java │ │ │ │ ├── FindKeys.java │ │ │ │ ├── FindKeysKeynum.java │ │ │ │ ├── FindKeysRange.java │ │ │ │ └── KeyConsumer.java │ │ │ │ └── types │ │ │ │ ├── AttributeType.java │ │ │ │ ├── BooleanType.java │ │ │ │ ├── BulkType.java │ │ │ │ ├── ErrorType.java │ │ │ │ ├── Multi.java │ │ │ │ ├── MultiType.java │ │ │ │ ├── NumberType.java │ │ │ │ ├── PushType.java │ │ │ │ └── SimpleStringType.java │ │ │ └── package-info.java │ │ └── module-info.java └── test │ ├── java │ └── io │ │ └── vertx │ │ └── tests │ │ └── redis │ │ ├── client │ │ ├── ClusterUtils.java │ │ ├── CommandGenerator.java │ │ ├── Pair.java │ │ ├── PreservesContext.java │ │ ├── RedisClient5SecureFailedTest.java │ │ ├── RedisClient5SecureTest.java │ │ ├── RedisClient5Test.java │ │ ├── RedisClient6SecureFailedTest.java │ │ ├── RedisClient6SecureTest.java │ │ ├── RedisClient6Test.java │ │ ├── RedisClient7Test.java │ │ ├── RedisClientLoadTest.java │ │ ├── RedisClientMetricsTest.java │ │ ├── RedisClientPikaSecureTest.java │ │ ├── RedisClientPikaTest.java │ │ ├── RedisClientREJSONTest.java │ │ ├── RedisClientTLSTest.java │ │ ├── RedisClientTest.java │ │ ├── RedisClientToBadServerTest.java │ │ ├── RedisClusterAskTest.java │ │ ├── RedisClusterMovedTest.java │ │ ├── RedisClusterMovedToNewNodeTest.java │ │ ├── RedisClusterTest.java │ │ ├── RedisClusterTransactionsTest.java │ │ ├── RedisConnectOptionsTest.java │ │ ├── RedisOptionsTest.java │ │ ├── RedisPoolMetricsTest.java │ │ ├── RedisPooledTest.java │ │ ├── RedisProtocolVersionTest.java │ │ ├── RedisPubSubClusterTest.java │ │ ├── RedisPubSubTest.java │ │ ├── RedisReconnectTest.java │ │ ├── RedisReplicationTest.java │ │ ├── RedisSentinelMasterFailoverTest.java │ │ ├── RedisSentinelTest.java │ │ ├── RedisTest.java │ │ ├── RedisThreadConsistencyTest.java │ │ ├── RedisTracingTest.java │ │ ├── SharedRedisConnectionTest.java │ │ └── TestUtils.java │ │ ├── containers │ │ ├── KeyPairAndCertificate.java │ │ ├── RedisCluster.java │ │ ├── RedisReplication.java │ │ ├── RedisSentinel.java │ │ └── RedisStandalone.java │ │ └── internal │ │ ├── ArrayQueueTest.java │ │ ├── BufferTest.java │ │ ├── CommandNormalizationTest.java │ │ ├── ConnectionRecyclingTest.java │ │ ├── RedisURITest.java │ │ ├── ReplyParserTest.java │ │ ├── RequestImplTest.java │ │ └── ZModemTest.java │ └── resources │ ├── logging.properties │ ├── pika.conf │ └── server-keystore.jks └── tools ├── commands.js ├── commands.json ├── package.json └── redis-api.hbs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | trim_trailing_whitespace = true 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | [**/examples/*.java] 12 | max_line_length = 84 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Version 2 | 3 | * vert.x core: 4 | * vert.x redis client: 5 | 6 | ### Context 7 | 8 | I encountered an exception which looks suspicious while ... 9 | 10 | ### Do you have a reproducer? 11 | 12 | * Link to github project/gist 13 | 14 | ### Steps to reproduce 15 | 16 | 1. ... 17 | 2. ... 18 | 3. ... 19 | 4. ... 20 | 21 | ### Extra 22 | 23 | * Anything that can be relevant 24 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "maven" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/maven-cd-settings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | false 20 | 21 | 22 | 23 | vertx-snapshots-repository 24 | ${env.VERTX_NEXUS_USERNAME} 25 | ${env.VERTX_NEXUS_PASSWORD} 26 | 27 | 28 | 29 | 30 | 31 | google-mirror 32 | 33 | true 34 | 35 | 36 | 37 | google-maven-central 38 | GCS Maven Central mirror EU 39 | https://maven-central.storage-download.googleapis.com/maven2/ 40 | 41 | true 42 | 43 | 44 | false 45 | 46 | 47 | 48 | 49 | 50 | google-maven-central 51 | GCS Maven Central mirror 52 | https://maven-central.storage-download.googleapis.com/maven2/ 53 | 54 | true 55 | 56 | 57 | false 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /.github/maven-ci-settings.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | false 20 | 21 | 22 | 23 | google-mirror 24 | 25 | true 26 | 27 | 28 | 29 | google-maven-central 30 | GCS Maven Central mirror EU 31 | https://maven-central.storage-download.googleapis.com/maven2/ 32 | 33 | true 34 | 35 | 36 | false 37 | 38 | 39 | 40 | 41 | 42 | google-maven-central 43 | GCS Maven Central mirror 44 | https://maven-central.storage-download.googleapis.com/maven2/ 45 | 46 | true 47 | 48 | 49 | false 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /.github/workflows/ci-4.x.yml: -------------------------------------------------------------------------------- 1 | name: vertx-redis-client (4.x) 2 | on: 3 | schedule: 4 | - cron: '0 4 * * *' 5 | jobs: 6 | CI: 7 | strategy: 8 | matrix: 9 | include: 10 | - os: ubuntu-latest 11 | jdk: 8 12 | - os: ubuntu-latest 13 | jdk: 17 14 | uses: ./.github/workflows/ci.yml 15 | with: 16 | branch: 4.x 17 | jdk: ${{ matrix.jdk }} 18 | os: ${{ matrix.os }} 19 | secrets: inherit 20 | Deploy: 21 | if: ${{ github.repository_owner == 'vert-x3' && (github.event_name == 'push' || github.event_name == 'schedule') }} 22 | needs: CI 23 | uses: ./.github/workflows/deploy.yml 24 | with: 25 | branch: 4.x 26 | jdk: 8 27 | secrets: inherit 28 | -------------------------------------------------------------------------------- /.github/workflows/ci-5.x-stable.yml: -------------------------------------------------------------------------------- 1 | name: vertx-redis-client (5.x-stable) 2 | on: 3 | push: 4 | branches: 5 | - '5.[0-9]+' 6 | pull_request: 7 | branches: 8 | - '5.[0-9]+' 9 | schedule: 10 | - cron: '0 6 * * *' 11 | jobs: 12 | CI-CD: 13 | uses: ./.github/workflows/ci-matrix-5.x.yml 14 | secrets: inherit 15 | with: 16 | branch: ${{ github.event_name == 'schedule' && vars.VERTX_5_STABLE_BRANCH || github.event.pull_request.head.sha || github.ref_name }} 17 | -------------------------------------------------------------------------------- /.github/workflows/ci-5.x.yml: -------------------------------------------------------------------------------- 1 | name: vertx-redis-client (5.x) 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | schedule: 10 | - cron: '0 5 * * *' 11 | jobs: 12 | CI-CD: 13 | uses: ./.github/workflows/ci-matrix-5.x.yml 14 | secrets: inherit 15 | with: 16 | branch: ${{ github.event.pull_request.head.sha || github.ref_name }} 17 | -------------------------------------------------------------------------------- /.github/workflows/ci-matrix-5.x.yml: -------------------------------------------------------------------------------- 1 | name: CI matrix (5.x) 2 | on: 3 | workflow_call: 4 | inputs: 5 | branch: 6 | required: true 7 | type: string 8 | jobs: 9 | CI: 10 | strategy: 11 | matrix: 12 | include: 13 | - os: ubuntu-latest 14 | jdk: 11 15 | - os: ubuntu-latest 16 | jdk: 21 17 | uses: ./.github/workflows/ci.yml 18 | with: 19 | branch: ${{ inputs.branch }} 20 | jdk: ${{ matrix.jdk }} 21 | os: ${{ matrix.os }} 22 | secrets: inherit 23 | Deploy: 24 | if: ${{ github.repository_owner == 'vert-x3' && (github.event_name == 'push' || github.event_name == 'schedule') }} 25 | needs: CI 26 | uses: ./.github/workflows/deploy.yml 27 | with: 28 | branch: ${{ inputs.branch }} 29 | jdk: 11 30 | secrets: inherit 31 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | workflow_call: 4 | inputs: 5 | branch: 6 | required: true 7 | type: string 8 | jdk: 9 | default: 8 10 | type: string 11 | os: 12 | default: ubuntu-latest 13 | type: string 14 | jobs: 15 | Test: 16 | name: Run tests 17 | runs-on: ${{ inputs.os }} 18 | steps: 19 | - uses: actions/cache@v3 20 | with: 21 | path: ~/.m2/repository 22 | key: maven-java-${{ inputs.jdk }} 23 | - name: Checkout 24 | uses: actions/checkout@v3 25 | with: 26 | ref: ${{ inputs.branch }} 27 | - name: Install JDK 28 | uses: actions/setup-java@v3 29 | with: 30 | java-version: ${{ inputs.jdk }} 31 | distribution: temurin 32 | - name: Run tests 33 | run: mvn -s .github/maven-ci-settings.xml -q clean verify -B 34 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | workflow_call: 4 | inputs: 5 | branch: 6 | required: true 7 | type: string 8 | jdk: 9 | default: 8 10 | type: string 11 | jobs: 12 | Deploy: 13 | name: Deploy to OSSRH 14 | runs-on: ubuntu-latest 15 | env: 16 | VERTX_NEXUS_USERNAME: ${{ secrets.VERTX_NEXUS_USERNAME }} 17 | VERTX_NEXUS_PASSWORD: ${{ secrets.VERTX_NEXUS_PASSWORD }} 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v2 21 | with: 22 | ref: ${{ inputs.branch }} 23 | - name: Install JDK 24 | uses: actions/setup-java@v2 25 | with: 26 | java-version: ${{ inputs.jdk }} 27 | distribution: temurin 28 | - name: Get project version 29 | run: echo "PROJECT_VERSION=$(mvn org.apache.maven.plugins:maven-help-plugin:evaluate -Dexpression=project.version -q -DforceStdout | grep -v '\[')" >> $GITHUB_ENV 30 | - name: Maven deploy 31 | if: ${{ endsWith(env.PROJECT_VERSION, '-SNAPSHOT') }} 32 | run: mvn deploy -s .github/maven-cd-settings.xml -DskipTests -B 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .gradle 3 | .idea 4 | .classpath 5 | .project 6 | .settings 7 | .yardoc 8 | .yardopts 9 | build 10 | target 11 | out 12 | *.iml 13 | *.ipr 14 | *.iws 15 | test-output 16 | Scratch.java 17 | ScratchTest.java 18 | test-results 19 | test-tmp 20 | *.class 21 | ScratchPad.java 22 | *.swp 23 | cscope.* 24 | node_modules 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vert.x Redis Client 2 | 3 | [![Build Status (5.x)](https://github.com/vert-x3/vertx-redis-client/actions/workflows/ci-5.x.yml/badge.svg)](https://github.com/vert-x3/vertx-redis-client/actions/workflows/ci-5.x.yml) 4 | [![Build Status (4.x)](https://github.com/vert-x3/vertx-redis-client/actions/workflows/ci-4.x.yml/badge.svg)](https://github.com/vert-x3/vertx-redis-client/actions/workflows/ci-4.x.yml) 5 | 6 | The Vert.x Redis client provides an asynchronous API to interact with a [Redis](http://redis.io) data-structure server. 7 | 8 | ## Documentation: 9 | 10 | The documentation can be read from the official vert.x [Documentation](http://vertx.io/docs/vertx-redis-client/java/). 11 | -------------------------------------------------------------------------------- /src/main/generated/io/vertx/redis/client/PoolOptionsConverter.java: -------------------------------------------------------------------------------- 1 | package io.vertx.redis.client; 2 | 3 | import io.vertx.core.json.JsonObject; 4 | import io.vertx.core.json.JsonArray; 5 | import java.time.Instant; 6 | import java.time.format.DateTimeFormatter; 7 | 8 | /** 9 | * Converter and mapper for {@link io.vertx.redis.client.PoolOptions}. 10 | * NOTE: This class has been automatically generated from the {@link io.vertx.redis.client.PoolOptions} original class using Vert.x codegen. 11 | */ 12 | public class PoolOptionsConverter { 13 | 14 | static void fromJson(Iterable> json, PoolOptions obj) { 15 | for (java.util.Map.Entry member : json) { 16 | switch (member.getKey()) { 17 | case "name": 18 | if (member.getValue() instanceof String) { 19 | obj.setName((String)member.getValue()); 20 | } 21 | break; 22 | case "cleanerInterval": 23 | if (member.getValue() instanceof Number) { 24 | obj.setCleanerInterval(((Number)member.getValue()).intValue()); 25 | } 26 | break; 27 | case "maxSize": 28 | if (member.getValue() instanceof Number) { 29 | obj.setMaxSize(((Number)member.getValue()).intValue()); 30 | } 31 | break; 32 | case "maxWaiting": 33 | if (member.getValue() instanceof Number) { 34 | obj.setMaxWaiting(((Number)member.getValue()).intValue()); 35 | } 36 | break; 37 | case "recycleTimeout": 38 | if (member.getValue() instanceof Number) { 39 | obj.setRecycleTimeout(((Number)member.getValue()).intValue()); 40 | } 41 | break; 42 | } 43 | } 44 | } 45 | 46 | static void toJson(PoolOptions obj, JsonObject json) { 47 | toJson(obj, json.getMap()); 48 | } 49 | 50 | static void toJson(PoolOptions obj, java.util.Map json) { 51 | if (obj.getName() != null) { 52 | json.put("name", obj.getName()); 53 | } 54 | json.put("cleanerInterval", obj.getCleanerInterval()); 55 | json.put("maxSize", obj.getMaxSize()); 56 | json.put("maxWaiting", obj.getMaxWaiting()); 57 | json.put("recycleTimeout", obj.getRecycleTimeout()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/generated/io/vertx/redis/client/RedisClusterConnectOptionsConverter.java: -------------------------------------------------------------------------------- 1 | package io.vertx.redis.client; 2 | 3 | import io.vertx.core.json.JsonObject; 4 | import io.vertx.core.json.JsonArray; 5 | import java.time.Instant; 6 | import java.time.format.DateTimeFormatter; 7 | 8 | /** 9 | * Converter and mapper for {@link io.vertx.redis.client.RedisClusterConnectOptions}. 10 | * NOTE: This class has been automatically generated from the {@link io.vertx.redis.client.RedisClusterConnectOptions} original class using Vert.x codegen. 11 | */ 12 | public class RedisClusterConnectOptionsConverter { 13 | 14 | static void fromJson(Iterable> json, RedisClusterConnectOptions obj) { 15 | for (java.util.Map.Entry member : json) { 16 | switch (member.getKey()) { 17 | case "useReplicas": 18 | if (member.getValue() instanceof String) { 19 | obj.setUseReplicas(io.vertx.redis.client.RedisReplicas.valueOf((String)member.getValue())); 20 | } 21 | break; 22 | case "hashSlotCacheTTL": 23 | if (member.getValue() instanceof Number) { 24 | obj.setHashSlotCacheTTL(((Number)member.getValue()).longValue()); 25 | } 26 | break; 27 | case "clusterTransactions": 28 | if (member.getValue() instanceof String) { 29 | obj.setClusterTransactions(io.vertx.redis.client.RedisClusterTransactions.valueOf((String)member.getValue())); 30 | } 31 | break; 32 | } 33 | } 34 | } 35 | 36 | static void toJson(RedisClusterConnectOptions obj, JsonObject json) { 37 | toJson(obj, json.getMap()); 38 | } 39 | 40 | static void toJson(RedisClusterConnectOptions obj, java.util.Map json) { 41 | if (obj.getUseReplicas() != null) { 42 | json.put("useReplicas", obj.getUseReplicas().name()); 43 | } 44 | json.put("hashSlotCacheTTL", obj.getHashSlotCacheTTL()); 45 | if (obj.getClusterTransactions() != null) { 46 | json.put("clusterTransactions", obj.getClusterTransactions().name()); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/generated/io/vertx/redis/client/RedisConnectOptionsConverter.java: -------------------------------------------------------------------------------- 1 | package io.vertx.redis.client; 2 | 3 | import io.vertx.core.json.JsonObject; 4 | import io.vertx.core.json.JsonArray; 5 | import java.time.Instant; 6 | import java.time.format.DateTimeFormatter; 7 | 8 | /** 9 | * Converter and mapper for {@link io.vertx.redis.client.RedisConnectOptions}. 10 | * NOTE: This class has been automatically generated from the {@link io.vertx.redis.client.RedisConnectOptions} original class using Vert.x codegen. 11 | */ 12 | public class RedisConnectOptionsConverter { 13 | 14 | static void fromJson(Iterable> json, RedisConnectOptions obj) { 15 | for (java.util.Map.Entry member : json) { 16 | switch (member.getKey()) { 17 | case "maxNestedArrays": 18 | if (member.getValue() instanceof Number) { 19 | obj.setMaxNestedArrays(((Number)member.getValue()).intValue()); 20 | } 21 | break; 22 | case "protocolNegotiation": 23 | if (member.getValue() instanceof Boolean) { 24 | obj.setProtocolNegotiation((Boolean)member.getValue()); 25 | } 26 | break; 27 | case "preferredProtocolVersion": 28 | if (member.getValue() instanceof String) { 29 | obj.setPreferredProtocolVersion(io.vertx.redis.client.ProtocolVersion.valueOf((String)member.getValue())); 30 | } 31 | break; 32 | case "user": 33 | if (member.getValue() instanceof String) { 34 | obj.setUser((String)member.getValue()); 35 | } 36 | break; 37 | case "password": 38 | if (member.getValue() instanceof String) { 39 | obj.setPassword((String)member.getValue()); 40 | } 41 | break; 42 | case "endpoints": 43 | if (member.getValue() instanceof JsonArray) { 44 | java.util.ArrayList list = new java.util.ArrayList<>(); 45 | ((Iterable)member.getValue()).forEach( item -> { 46 | if (item instanceof String) 47 | list.add((String)item); 48 | }); 49 | obj.setEndpoints(list); 50 | } 51 | break; 52 | case "maxWaitingHandlers": 53 | if (member.getValue() instanceof Number) { 54 | obj.setMaxWaitingHandlers(((Number)member.getValue()).intValue()); 55 | } 56 | break; 57 | } 58 | } 59 | } 60 | 61 | static void toJson(RedisConnectOptions obj, JsonObject json) { 62 | toJson(obj, json.getMap()); 63 | } 64 | 65 | static void toJson(RedisConnectOptions obj, java.util.Map json) { 66 | json.put("maxNestedArrays", obj.getMaxNestedArrays()); 67 | json.put("protocolNegotiation", obj.isProtocolNegotiation()); 68 | if (obj.getPreferredProtocolVersion() != null) { 69 | json.put("preferredProtocolVersion", obj.getPreferredProtocolVersion().name()); 70 | } 71 | if (obj.getUser() != null) { 72 | json.put("user", obj.getUser()); 73 | } 74 | if (obj.getPassword() != null) { 75 | json.put("password", obj.getPassword()); 76 | } 77 | if (obj.getEndpoints() != null) { 78 | JsonArray array = new JsonArray(); 79 | obj.getEndpoints().forEach(item -> array.add(item)); 80 | json.put("endpoints", array); 81 | } 82 | json.put("maxWaitingHandlers", obj.getMaxWaitingHandlers()); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/generated/io/vertx/redis/client/RedisReplicationConnectOptionsConverter.java: -------------------------------------------------------------------------------- 1 | package io.vertx.redis.client; 2 | 3 | import io.vertx.core.json.JsonObject; 4 | import io.vertx.core.json.JsonArray; 5 | import java.time.Instant; 6 | import java.time.format.DateTimeFormatter; 7 | 8 | /** 9 | * Converter and mapper for {@link io.vertx.redis.client.RedisReplicationConnectOptions}. 10 | * NOTE: This class has been automatically generated from the {@link io.vertx.redis.client.RedisReplicationConnectOptions} original class using Vert.x codegen. 11 | */ 12 | public class RedisReplicationConnectOptionsConverter { 13 | 14 | static void fromJson(Iterable> json, RedisReplicationConnectOptions obj) { 15 | for (java.util.Map.Entry member : json) { 16 | switch (member.getKey()) { 17 | case "topology": 18 | if (member.getValue() instanceof String) { 19 | obj.setTopology(io.vertx.redis.client.RedisTopology.valueOf((String)member.getValue())); 20 | } 21 | break; 22 | case "useReplicas": 23 | if (member.getValue() instanceof String) { 24 | obj.setUseReplicas(io.vertx.redis.client.RedisReplicas.valueOf((String)member.getValue())); 25 | } 26 | break; 27 | } 28 | } 29 | } 30 | 31 | static void toJson(RedisReplicationConnectOptions obj, JsonObject json) { 32 | toJson(obj, json.getMap()); 33 | } 34 | 35 | static void toJson(RedisReplicationConnectOptions obj, java.util.Map json) { 36 | if (obj.getTopology() != null) { 37 | json.put("topology", obj.getTopology().name()); 38 | } 39 | if (obj.getUseReplicas() != null) { 40 | json.put("useReplicas", obj.getUseReplicas().name()); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/generated/io/vertx/redis/client/RedisSentinelConnectOptionsConverter.java: -------------------------------------------------------------------------------- 1 | package io.vertx.redis.client; 2 | 3 | import io.vertx.core.json.JsonObject; 4 | import io.vertx.core.json.JsonArray; 5 | import java.time.Instant; 6 | import java.time.format.DateTimeFormatter; 7 | 8 | /** 9 | * Converter and mapper for {@link io.vertx.redis.client.RedisSentinelConnectOptions}. 10 | * NOTE: This class has been automatically generated from the {@link io.vertx.redis.client.RedisSentinelConnectOptions} original class using Vert.x codegen. 11 | */ 12 | public class RedisSentinelConnectOptionsConverter { 13 | 14 | static void fromJson(Iterable> json, RedisSentinelConnectOptions obj) { 15 | for (java.util.Map.Entry member : json) { 16 | switch (member.getKey()) { 17 | case "role": 18 | if (member.getValue() instanceof String) { 19 | obj.setRole(io.vertx.redis.client.RedisRole.valueOf((String)member.getValue())); 20 | } 21 | break; 22 | case "masterName": 23 | if (member.getValue() instanceof String) { 24 | obj.setMasterName((String)member.getValue()); 25 | } 26 | break; 27 | case "autoFailover": 28 | if (member.getValue() instanceof Boolean) { 29 | obj.setAutoFailover((Boolean)member.getValue()); 30 | } 31 | break; 32 | } 33 | } 34 | } 35 | 36 | static void toJson(RedisSentinelConnectOptions obj, JsonObject json) { 37 | toJson(obj, json.getMap()); 38 | } 39 | 40 | static void toJson(RedisSentinelConnectOptions obj, java.util.Map json) { 41 | if (obj.getRole() != null) { 42 | json.put("role", obj.getRole().name()); 43 | } 44 | if (obj.getMasterName() != null) { 45 | json.put("masterName", obj.getMasterName()); 46 | } 47 | json.put("autoFailover", obj.isAutoFailover()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/generated/io/vertx/redis/client/RedisStandaloneConnectOptionsConverter.java: -------------------------------------------------------------------------------- 1 | package io.vertx.redis.client; 2 | 3 | import io.vertx.core.json.JsonObject; 4 | import io.vertx.core.json.JsonArray; 5 | import java.time.Instant; 6 | import java.time.format.DateTimeFormatter; 7 | 8 | /** 9 | * Converter and mapper for {@link io.vertx.redis.client.RedisStandaloneConnectOptions}. 10 | * NOTE: This class has been automatically generated from the {@link io.vertx.redis.client.RedisStandaloneConnectOptions} original class using Vert.x codegen. 11 | */ 12 | public class RedisStandaloneConnectOptionsConverter { 13 | 14 | static void fromJson(Iterable> json, RedisStandaloneConnectOptions obj) { 15 | for (java.util.Map.Entry member : json) { 16 | switch (member.getKey()) { 17 | } 18 | } 19 | } 20 | 21 | static void toJson(RedisStandaloneConnectOptions obj, JsonObject json) { 22 | toJson(obj, json.getMap()); 23 | } 24 | 25 | static void toJson(RedisStandaloneConnectOptions obj, java.util.Map json) { 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/examples/RedisConnectOptionsSupplier.java: -------------------------------------------------------------------------------- 1 | package examples; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.core.Promise; 5 | import io.vertx.core.Vertx; 6 | import io.vertx.redis.client.RedisConnectOptions; 7 | import io.vertx.redis.client.RedisOptions; 8 | 9 | import java.util.concurrent.atomic.AtomicReference; 10 | import java.util.function.Function; 11 | import java.util.function.Supplier; 12 | 13 | public class RedisConnectOptionsSupplier implements Supplier> { 14 | private final Vertx vertx; 15 | private final RedisOptions options; 16 | private final Function creator; 17 | private final Supplier userName; 18 | private final Supplier password; 19 | private final AtomicReference> future; 20 | 21 | public RedisConnectOptionsSupplier(Vertx vertx, RedisOptions options, Function creator, 22 | Supplier userName, Supplier password) { 23 | this.vertx = vertx; 24 | this.options = options; 25 | this.creator = creator; 26 | this.userName = userName; 27 | this.password = password; 28 | this.future = new AtomicReference<>(); 29 | } 30 | 31 | @Override 32 | public Future get() { 33 | while (true) { 34 | Future currentFuture = this.future.get(); 35 | if (currentFuture != null) { 36 | return currentFuture; 37 | } 38 | 39 | Promise promise = Promise.promise(); 40 | Future future = promise.future(); 41 | if (this.future.compareAndSet(null, future)) { 42 | OPTS result = creator.apply(options); 43 | result.setUser(userName.get()); 44 | result.setPassword(password.get()); // TODO run on worker thread 45 | promise.complete(result); 46 | 47 | // clean up in 10 minutes, to force refresh 48 | vertx.setTimer(10 * 60 * 1000, ignored -> { 49 | this.future.set(null); 50 | }); 51 | 52 | return future; 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/examples/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Paulo Lopes 3 | */ 4 | @Source 5 | package examples; 6 | 7 | import io.vertx.docgen.Source; -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/ConstantSupplier.java: -------------------------------------------------------------------------------- 1 | package io.vertx.redis.client; 2 | 3 | import java.util.function.Supplier; 4 | 5 | class ConstantSupplier implements Supplier { 6 | private final T value; 7 | 8 | ConstantSupplier(T value) { 9 | this.value = value; 10 | } 11 | 12 | @Override 13 | public T get() { 14 | return value; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/ProtocolVersion.java: -------------------------------------------------------------------------------- 1 | package io.vertx.redis.client; 2 | 3 | import io.vertx.codegen.annotations.VertxGen; 4 | 5 | /** 6 | * Redis protocol version to be used. 7 | */ 8 | @VertxGen 9 | public enum ProtocolVersion { 10 | /** 11 | * RESP2 12 | */ 13 | RESP2("2"), 14 | /** 15 | * RESP3 16 | */ 17 | RESP3("3"), 18 | ; 19 | 20 | private final String value; 21 | 22 | ProtocolVersion(String value) { 23 | this.value = value; 24 | } 25 | 26 | public String getValue() { 27 | return value; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/RedisClientType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Red Hat, Inc. 3 | *

4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | *

8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | *

11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | *

14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | package io.vertx.redis.client; 17 | 18 | import io.vertx.codegen.annotations.VertxGen; 19 | 20 | /** 21 | * Define what kind of behavior is expected from the client. 22 | */ 23 | @VertxGen 24 | public enum RedisClientType { 25 | 26 | /** 27 | * The client should work in single server mode (the default). 28 | */ 29 | STANDALONE, 30 | 31 | /** 32 | * The client should work in sentinel mode. When this mode is active, 33 | * use the {@link RedisRole} to define which role to get the client 34 | * connection to. 35 | */ 36 | SENTINEL, 37 | 38 | /** 39 | * The client should work in cluster mode. When this mode is active, 40 | * use the {@link RedisReplicas} to define when replica nodes can be 41 | * used for read only queries, and use {@link RedisClusterTransactions} 42 | * to define how transactions should be handled. 43 | */ 44 | CLUSTER, 45 | 46 | /** 47 | * The client should work in replication mode. When this mode is active, 48 | * use the {@link RedisReplicas} to define when replica nodes can be 49 | * used for read only queries. 50 | */ 51 | REPLICATION 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/RedisCluster.java: -------------------------------------------------------------------------------- 1 | package io.vertx.redis.client; 2 | 3 | import io.vertx.codegen.annotations.VertxGen; 4 | import io.vertx.core.Future; 5 | import io.vertx.redis.client.impl.RedisClusterImpl; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * Utilities for Redis cluster. Calling {@code create()} with an instance of {@link Redis} 11 | * or {@link RedisConnection} that are not Redis cluster client/connection leads to 12 | * an exception. 13 | * 14 | * @see #onAllNodes(Request) 15 | * @see #onAllMasterNodes(Request) 16 | * @see #groupByNodes(List) 17 | */ 18 | @VertxGen 19 | public interface RedisCluster { 20 | static RedisCluster create(Redis client) { 21 | return new RedisClusterImpl(client); 22 | } 23 | 24 | static RedisCluster create(RedisConnection connection) { 25 | return new RedisClusterImpl(connection); 26 | } 27 | 28 | /** 29 | * Runs the {@code request} against all nodes in the cluster. 30 | * Returns a future that completes with a list of responses, one from each 31 | * node, or failure when one of the operations fails. Note that in case 32 | * of a failure, there are no guarantees that the request was or wasn't 33 | * executed successfully on other Redis cluster nodes. No result order 34 | * is guaranteed either. 35 | * 36 | * @param request the request, must not be {@code null} 37 | * @return the future result, never {@code null} 38 | */ 39 | Future> onAllNodes(Request request); 40 | 41 | /** 42 | * Runs the {@code request} against all master nodes in the cluster. 43 | * Returns a future that completes with a list of responses, one from each 44 | * master node, or failure when one of the operations fails. Note that in case 45 | * of a failure, there are no guarantees that the request was or wasn't 46 | * executed successfully on other Redis cluster master nodes. No result order 47 | * is guaranteed either. 48 | * 49 | * @param request the request, must not be {@code null} 50 | * @return the future result, never {@code null} 51 | */ 52 | Future> onAllMasterNodes(Request request); 53 | 54 | /** 55 | * Groups the {@code requests} into a {@link RequestGrouping}, which contains: 56 | *

    57 | *
  • keyed requests: requests that include a key and it is therefore possible 58 | * to determine to which master node they should be sent; all requests in each inner list 59 | * in the {@code keyed} collection are guaranteed to be sent to the same master node;
  • 60 | *
  • unkeyed requests: requests that do not include a key and it is therefore 61 | * not possible to determine to which master node they should be sent.
  • 62 | *
63 | * If any of the {@code requests} includes multiple keys that belong to different master nodes, 64 | * the resulting future will fail. 65 | *

66 | * If the cluster client was created with {@link RedisReplicas#SHARE} or {@link RedisReplicas#ALWAYS} 67 | * and the commands are executed individually (using {@link RedisConnection#send(Request) send()}, 68 | * not {@link RedisConnection#batch(List) batch()}), it is possible that the commands will be spread 69 | * across different replicas of the same master node. 70 | *

71 | * Note that this method is only reliable in case the Redis cluster is in a stable 72 | * state. In case of resharding, failover or in general any change of cluster topology, 73 | * there are no guarantees on the validity of the result. 74 | * 75 | * @param requests the requests, must not be {@code null} 76 | * @return the requests grouped by the cluster node assignment 77 | */ 78 | Future groupByNodes(List requests); 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/RedisClusterTransactions.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Red Hat, Inc. 3 | *

4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | *

8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | *

11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | *

14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | package io.vertx.redis.client; 17 | 18 | import io.vertx.codegen.annotations.VertxGen; 19 | 20 | /** 21 | * Define how Redis transactions are handled in a clustered Redis client. 22 | *

23 | * This is only meaningful in case of a {@linkplain RedisClientType#CLUSTER cluster} Redis client. 24 | */ 25 | @VertxGen 26 | public enum RedisClusterTransactions { 27 | /** 28 | * Redis transactions are not supported in the cluster. Transaction 29 | * commands lead to an error. 30 | */ 31 | DISABLED, 32 | 33 | /** 34 | * Redis transactions are supported in the cluster, but only when they 35 | * target a single node. The {@code MULTI} command is queued and is only 36 | * issued when the next command is executed. This next command binds 37 | * the connection to the corresponding node of the Redis cluster (so it should 38 | * have keys, otherwise the target node is random). All subsequent commands 39 | * are targeted to that node. If some of the subsequent commands have a key 40 | * that belongs to another node, the command fails on the server side. 41 | *

42 | * If {@code WATCH} is used before {@code MULTI}, its key(s) determine to which 43 | * node the connection is bound and the subsequent {@code MULTI} is not queued. 44 | * If {@code WATCH} keys belong to multiple nodes, the command fails on the client side. 45 | */ 46 | SINGLE_NODE, 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/RedisConnection.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Red Hat, Inc. 3 | *

4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | *

8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | *

11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | *

14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | package io.vertx.redis.client; 17 | 18 | import io.vertx.codegen.annotations.Fluent; 19 | import io.vertx.codegen.annotations.Nullable; 20 | import io.vertx.codegen.annotations.VertxGen; 21 | import io.vertx.core.Future; 22 | import io.vertx.core.Handler; 23 | import io.vertx.core.streams.ReadStream; 24 | 25 | import java.util.List; 26 | 27 | /** 28 | * A simple Redis client. 29 | */ 30 | @VertxGen 31 | public interface RedisConnection extends ReadStream { 32 | 33 | /** 34 | * {@inheritDoc} 35 | */ 36 | @Fluent 37 | @Override 38 | RedisConnection exceptionHandler(Handler handler); 39 | 40 | /** 41 | * {@inheritDoc} 42 | */ 43 | @Fluent 44 | @Override 45 | RedisConnection handler(@Nullable Handler handler); 46 | 47 | /** 48 | * {@inheritDoc} 49 | */ 50 | @Fluent 51 | @Override 52 | RedisConnection pause(); 53 | 54 | /** 55 | * {@inheritDoc} 56 | */ 57 | @Fluent 58 | @Override 59 | RedisConnection resume(); 60 | 61 | /** 62 | * {@inheritDoc} 63 | */ 64 | @Fluent 65 | @Override 66 | RedisConnection fetch(long amount); 67 | 68 | /** 69 | * {@inheritDoc} 70 | */ 71 | @Fluent 72 | @Override 73 | RedisConnection endHandler(@Nullable Handler endHandler); 74 | 75 | /** 76 | * Send the given command to the redis server or cluster. 77 | * 78 | * @param command the command to send 79 | * @return a future with the result of the operation 80 | */ 81 | Future<@Nullable Response> send(Request command); 82 | 83 | /** 84 | * Sends a list of commands in a single IO operation, this prevents any inter twinning to happen from other 85 | * client users. 86 | * 87 | * @param commands list of command to send 88 | * @return a future with the result of the operation 89 | */ 90 | Future> batch(List commands); 91 | 92 | /** 93 | * Closes the connection or returns to the pool. 94 | */ 95 | Future close(); 96 | 97 | /** 98 | * Flag to notify if the pending message queue (commands in transit) is full. 99 | *

100 | * When the pending message queue is full and a new send command is issued it will result in a exception to be thrown. 101 | * Checking this flag before sending can allow the application to wait before sending the next message. 102 | * 103 | * @return true is queue is full. 104 | */ 105 | boolean pendingQueueFull(); 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/RedisReplicas.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Red Hat, Inc. 3 | *

4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | *

8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | *

11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | *

14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | package io.vertx.redis.client; 17 | 18 | import io.vertx.codegen.annotations.VertxGen; 19 | 20 | /** 21 | * When should Redis replica nodes be used for queries. 22 | *

23 | * This is only meaningful in case of a {@linkplain RedisClientType#REPLICATION replication} 24 | * and {@linkplain RedisClientType#CLUSTER cluster} Redis client. 25 | *

26 | */ 27 | @VertxGen 28 | public enum RedisReplicas { 29 | 30 | /** 31 | * Never use REPLICA, queries are always run on a MASTER node. 32 | */ 33 | NEVER, 34 | 35 | /** 36 | * Queries can be randomly run on both MASTER and REPLICA nodes. 37 | */ 38 | SHARE, 39 | 40 | /** 41 | * Queries are always run on REPLICA nodes (never on MASTER node). 42 | */ 43 | ALWAYS 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/RedisRole.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Red Hat, Inc. 3 | *

4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | *

8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | *

11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | *

14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | package io.vertx.redis.client; 17 | 18 | import io.vertx.codegen.annotations.VertxGen; 19 | 20 | /** 21 | * The client role; that is, to which kind of node should the connection be established. 22 | *

23 | * This is only meaningful in case of a {@linkplain RedisClientType#SENTINEL sentinel} Redis client. 24 | *

25 | */ 26 | @VertxGen 27 | public enum RedisRole { 28 | /** 29 | * Use a MASTER node connection. 30 | */ 31 | MASTER, 32 | 33 | /** 34 | * Use a REPLICA node connection. 35 | */ 36 | REPLICA, 37 | 38 | /** 39 | * Use a SENTINEL node connection. 40 | */ 41 | SENTINEL 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/RedisStandaloneConnectOptions.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Red Hat, Inc. 3 | *

4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | *

8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | *

11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | *

14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | package io.vertx.redis.client; 17 | 18 | import io.vertx.codegen.annotations.DataObject; 19 | import io.vertx.codegen.json.annotations.JsonGen; 20 | import io.vertx.core.json.JsonObject; 21 | 22 | import java.util.List; 23 | 24 | @DataObject 25 | @JsonGen(publicConverter = false) 26 | public class RedisStandaloneConnectOptions extends RedisConnectOptions { 27 | 28 | public RedisStandaloneConnectOptions() { 29 | super(); 30 | } 31 | 32 | public RedisStandaloneConnectOptions(RedisOptions options) { 33 | super(options); 34 | } 35 | 36 | public RedisStandaloneConnectOptions(RedisStandaloneConnectOptions other) { 37 | super(other); 38 | } 39 | 40 | public RedisStandaloneConnectOptions(JsonObject json) { 41 | super(json); 42 | RedisStandaloneConnectOptionsConverter.fromJson(json, this); 43 | } 44 | 45 | @Override 46 | public RedisStandaloneConnectOptions setMaxNestedArrays(int maxNestedArrays) { 47 | return (RedisStandaloneConnectOptions) super.setMaxNestedArrays(maxNestedArrays); 48 | } 49 | 50 | @Override 51 | public RedisStandaloneConnectOptions setProtocolNegotiation(boolean protocolNegotiation) { 52 | return (RedisStandaloneConnectOptions) super.setProtocolNegotiation(protocolNegotiation); 53 | } 54 | 55 | @Override 56 | public RedisStandaloneConnectOptions setPreferredProtocolVersion(ProtocolVersion preferredProtocolVersion) { 57 | return (RedisStandaloneConnectOptions) super.setPreferredProtocolVersion(preferredProtocolVersion); 58 | } 59 | 60 | @Override 61 | public RedisStandaloneConnectOptions setUser(String user) { 62 | return (RedisStandaloneConnectOptions) super.setUser(user); 63 | } 64 | 65 | @Override 66 | public RedisStandaloneConnectOptions setPassword(String password) { 67 | return (RedisStandaloneConnectOptions) super.setPassword(password); 68 | } 69 | 70 | @Override 71 | public RedisStandaloneConnectOptions setEndpoints(List endpoints) { 72 | return (RedisStandaloneConnectOptions) super.setEndpoints(endpoints); 73 | } 74 | 75 | @Override 76 | public RedisStandaloneConnectOptions addConnectionString(String connectionString) { 77 | return (RedisStandaloneConnectOptions) super.addConnectionString(connectionString); 78 | } 79 | 80 | @Override 81 | public RedisStandaloneConnectOptions setConnectionString(String connectionString) { 82 | return (RedisStandaloneConnectOptions) super.setConnectionString(connectionString); 83 | } 84 | 85 | @Override 86 | public RedisStandaloneConnectOptions setMaxWaitingHandlers(int maxWaitingHandlers) { 87 | return (RedisStandaloneConnectOptions) super.setMaxWaitingHandlers(maxWaitingHandlers); 88 | } 89 | 90 | /** 91 | * Converts this object to JSON notation. 92 | * 93 | * @return JSON 94 | */ 95 | public JsonObject toJson() { 96 | final JsonObject json = super.toJson(); 97 | RedisStandaloneConnectOptionsConverter.toJson(this, json); 98 | return json; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/RedisTopology.java: -------------------------------------------------------------------------------- 1 | package io.vertx.redis.client; 2 | 3 | import io.vertx.codegen.annotations.VertxGen; 4 | 5 | /** 6 | * How the Redis multi-node topology should be obtained. 7 | *

8 | * This is only meaningful in case of a {@linkplain RedisClientType#REPLICATION replication} 9 | * Redis client. In case of a {@linkplain RedisClientType#CLUSTER cluster} and 10 | * {@linkplain RedisClientType#SENTINEL sentinel} Redis client, topology is currently 11 | * always discovered automatically and the topology mode is ignored. 12 | *

13 | */ 14 | @VertxGen 15 | public enum RedisTopology { 16 | /** 17 | * Redis topology is discovered automatically from the first node that is capable of providing 18 | * the necessary information. 19 | */ 20 | DISCOVER, 21 | 22 | /** 23 | * Redis topology is configured statically. The user must ensure that the configuration is correct. 24 | *

25 | * In case of a {@linkplain RedisClientType#REPLICATION replication} Redis client, the first 26 | * node in the list is considered a master and the remaining nodes in the list 27 | * are considered replicas. 28 | *

29 | *

30 | * In case of a {@linkplain RedisClientType#CLUSTER cluster} and {@linkplain RedisClientType#SENTINEL sentinel} 31 | * Redis client, static topology configuration is currently not meaningful and is ignored. 32 | *

33 | */ 34 | STATIC, 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/RequestGrouping.java: -------------------------------------------------------------------------------- 1 | package io.vertx.redis.client; 2 | 3 | import io.vertx.codegen.annotations.DataObject; 4 | import io.vertx.codegen.annotations.GenIgnore; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Collection; 8 | import java.util.List; 9 | import java.util.Objects; 10 | 11 | /** 12 | * A result of {@link RedisCluster#groupByNodes(List)}. 13 | * 14 | * @see #getKeyed() 15 | * @see #getUnkeyed() 16 | */ 17 | @DataObject 18 | public class RequestGrouping { 19 | 20 | private final Collection> keyed; 21 | private final List unkeyed; 22 | 23 | public RequestGrouping() { 24 | this.keyed = new ArrayList<>(); 25 | this.unkeyed = new ArrayList<>(); 26 | } 27 | 28 | public RequestGrouping(Collection> keyed, List unkeyed) { 29 | this.keyed = Objects.requireNonNull(keyed); 30 | this.unkeyed = Objects.requireNonNull(unkeyed); 31 | } 32 | 33 | /** 34 | * Returns a collection of request groups such that all requests in each group are 35 | * guaranteed to be sent to the same master node. 36 | *

37 | * Does not include any request that doesn't specify a key; use {@link #getUnkeyed()} 38 | * to get those. 39 | */ 40 | @GenIgnore 41 | public Collection> getKeyed() { 42 | return keyed; 43 | } 44 | 45 | /** 46 | * Returns a collection of requests that do not specify a key and would therefore 47 | * be executed on random node. 48 | */ 49 | @GenIgnore 50 | public List getUnkeyed() { 51 | return unkeyed; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/ResponseType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Red Hat, Inc. 3 | *

4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | *

8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | *

11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | *

14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | package io.vertx.redis.client; 17 | 18 | import io.vertx.codegen.annotations.VertxGen; 19 | 20 | /** 21 | * Define the response types that the client can receive from REDIS. 22 | */ 23 | @VertxGen 24 | public enum ResponseType { 25 | /** 26 | * C String simple String. 27 | */ 28 | SIMPLE, 29 | 30 | /** 31 | * C String simple String representing an error. 32 | */ 33 | ERROR, 34 | 35 | /** 36 | * boolean value. 37 | */ 38 | BOOLEAN, 39 | 40 | /** 41 | * numeric value. 42 | */ 43 | NUMBER, 44 | 45 | /** 46 | * byte array value. 47 | */ 48 | BULK, 49 | 50 | /** 51 | * Push message 52 | */ 53 | PUSH, 54 | 55 | /** 56 | * Attribute message 57 | */ 58 | ATTRIBUTE, 59 | 60 | /** 61 | * List of multiple bulk responses (List, Set, Map). 62 | */ 63 | MULTI 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/impl/ArrayQueue.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Red Hat, Inc. 3 | *

4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | *

8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | *

11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | *

14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | package io.vertx.redis.client.impl; 17 | 18 | import io.vertx.codegen.annotations.Nullable; 19 | 20 | import java.util.Objects; 21 | 22 | public final class ArrayQueue { 23 | 24 | private int 25 | cur, // current number of elements 26 | front, // front index 27 | back; // back index 28 | 29 | private final Object[] queue; 30 | 31 | /** 32 | * Creates a new empty queue. 33 | */ 34 | public ArrayQueue(int capacity) { 35 | queue = new Object[capacity]; 36 | back = -1; 37 | front = 0; 38 | } 39 | 40 | /** 41 | * Tests if the queue is logically empty. 42 | * 43 | * @return true if the queue is empty and false otherwise 44 | */ 45 | boolean isEmpty() { 46 | return cur == 0; 47 | } 48 | 49 | /** 50 | * Puts a value into the back of the queue. It works with wraparound. 51 | * If the queue is full, it doubles its size. 52 | * 53 | * @param value the item to insert. 54 | * @throws IndexOutOfBoundsException if the queue is full. 55 | * @throws NullPointerException if the value is {@code null} 56 | */ 57 | public void offer(T value) { 58 | Objects.requireNonNull(value, "'value' cannot be null"); 59 | if (isFull()) { 60 | throw new IndexOutOfBoundsException(); 61 | } 62 | back++; 63 | if (back == Integer.MAX_VALUE) { 64 | // ensure the indexes stay positive 65 | back %= queue.length; 66 | } 67 | queue[back % queue.length] = value; 68 | cur++; 69 | } 70 | 71 | /** 72 | * Returns the first element in the queue. 73 | * 74 | * @return element at front of the queue, null if empty. 75 | */ 76 | @SuppressWarnings("unchecked") 77 | @Nullable T peek() { 78 | if (isEmpty()) { 79 | return null; 80 | } else { 81 | return (T) queue[front % queue.length]; 82 | } 83 | } 84 | 85 | /** 86 | * Returns and removes the front element of the queue. It works with wraparound. 87 | * 88 | * @return element at front of the queue or {@code null} if the queue is empty. 89 | */ 90 | public @Nullable T poll() { 91 | if (isEmpty()) { 92 | return null; 93 | } 94 | T e = peek(); 95 | queue[front % queue.length] = null; // for garbage collection 96 | front++; 97 | if (front == Integer.MAX_VALUE) { 98 | // ensure the indexes stay positive 99 | front %= queue.length; 100 | } 101 | cur--; 102 | return e; 103 | } 104 | 105 | public int freeSlots() { 106 | return queue.length - cur; 107 | } 108 | 109 | /** 110 | * Tests if the queue is logically full 111 | */ 112 | boolean isFull() { 113 | return cur == queue.length; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/impl/ArrayStack.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Red Hat, Inc. 3 | *

4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | *

8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | *

11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | *

14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | package io.vertx.redis.client.impl; 17 | 18 | import io.vertx.codegen.annotations.Nullable; 19 | 20 | final class ArrayStack { 21 | 22 | private final Object[] stack; 23 | private int pointer; 24 | 25 | ArrayStack(int size) { 26 | stack = new Object[size]; 27 | pointer = -1; 28 | } 29 | 30 | void push(T item) { 31 | stack[++pointer] = item; 32 | } 33 | 34 | @SuppressWarnings("unchecked") 35 | T pop() { 36 | final Object o = stack[pointer]; 37 | stack[pointer--] = null; 38 | return (T) o; 39 | } 40 | 41 | boolean empty() { 42 | return pointer == -1; 43 | } 44 | 45 | @SuppressWarnings("unchecked") 46 | @Nullable T peek() { 47 | if (pointer == -1) { 48 | return null; 49 | } 50 | return (T) stack[pointer]; 51 | } 52 | 53 | int size() { 54 | return pointer + 1; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/impl/BaseRedisClient.java: -------------------------------------------------------------------------------- 1 | package io.vertx.redis.client.impl; 2 | 3 | import io.vertx.codegen.annotations.Nullable; 4 | import io.vertx.core.Future; 5 | import io.vertx.core.Vertx; 6 | import io.vertx.core.internal.VertxInternal; 7 | import io.vertx.core.internal.logging.Logger; 8 | import io.vertx.core.internal.logging.LoggerFactory; 9 | import io.vertx.core.net.NetClientOptions; 10 | import io.vertx.core.tracing.TracingPolicy; 11 | import io.vertx.redis.client.PoolOptions; 12 | import io.vertx.redis.client.Redis; 13 | import io.vertx.redis.client.RedisConnectOptions; 14 | import io.vertx.redis.client.Request; 15 | import io.vertx.redis.client.Response; 16 | 17 | import java.util.Collections; 18 | import java.util.List; 19 | import java.util.function.Supplier; 20 | 21 | public abstract class BaseRedisClient implements Redis { 22 | 23 | private static final Logger LOG = LoggerFactory.getLogger(BaseRedisClient.class); 24 | 25 | protected final VertxInternal vertx; 26 | protected final Supplier> connectOptions; 27 | protected final RedisConnectionManager connectionManager; 28 | 29 | public BaseRedisClient(Vertx vertx, NetClientOptions tcpOptions, PoolOptions poolOptions, Supplier> connectOptions, TracingPolicy tracingPolicy) { 30 | this.vertx = (VertxInternal) vertx; 31 | this.connectOptions = connectOptions; 32 | this.connectionManager = new RedisConnectionManager(this.vertx, tcpOptions, poolOptions, (Supplier) connectOptions, tracingPolicy); 33 | this.connectionManager.start(); 34 | } 35 | 36 | public RedisConnectionManager connectionManager() { 37 | return connectionManager; 38 | } 39 | 40 | @Override 41 | public Future close() { 42 | return this.connectionManager.close(); 43 | } 44 | 45 | @Override 46 | public Future<@Nullable Response> send(Request request) { 47 | final CommandImpl cmd = (CommandImpl) request.command(); 48 | if (cmd.isPubSub()) { 49 | // mixing pubSub cannot be used on a one-shot operation 50 | return vertx.getOrCreateContext().failedFuture("PubSub command in connection-less mode not allowed"); 51 | } else if (cmd.isTransactional()) { 52 | return vertx.getOrCreateContext().failedFuture("Transactional command in connection-less mode not allowed"); 53 | } 54 | 55 | return connect() 56 | .compose(conn -> 57 | conn.send(request) 58 | // regardless of the result, return the connection to the pool 59 | .eventually(() -> 60 | conn.close() 61 | .onFailure(LOG::warn))); 62 | } 63 | 64 | @Override 65 | public Future> batch(List requests) { 66 | if (requests.isEmpty()) { 67 | LOG.debug("Empty batch"); 68 | return vertx.getOrCreateContext().succeededFuture(Collections.emptyList()); 69 | } else { 70 | for (Request req : requests) { 71 | final CommandImpl cmd = (CommandImpl) req.command(); 72 | if (cmd.isPubSub()) { 73 | // mixing pubSub cannot be used on a one-shot operation 74 | return Future.failedFuture("PubSub command in connection-less batch not allowed"); 75 | } 76 | // someone might expect that for symmetry with `send()`, we'll also check the commands here 77 | // and fail if any of them is transactional, but that would be wrong -- a batch is always 78 | // executed on a single connection and can therefore contain the whole transaction 79 | } 80 | 81 | return connect() 82 | .compose(conn -> 83 | conn.batch(requests) 84 | // regardless of the result, return the connection to the pool 85 | .eventually(() -> 86 | conn.close().onFailure(LOG::warn))); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/impl/CommandReporter.java: -------------------------------------------------------------------------------- 1 | package io.vertx.redis.client.impl; 2 | 3 | import io.vertx.core.AsyncResult; 4 | import io.vertx.core.Context; 5 | import io.vertx.core.internal.VertxInternal; 6 | import io.vertx.core.spi.metrics.ClientMetrics; 7 | import io.vertx.core.spi.tracing.SpanKind; 8 | import io.vertx.core.spi.tracing.TagExtractor; 9 | import io.vertx.core.spi.tracing.VertxTracer; 10 | import io.vertx.core.tracing.TracingPolicy; 11 | 12 | import java.util.function.Function; 13 | 14 | class CommandReporter { 15 | enum Tags { 16 | // Generic 17 | NETWORK_PEER_ADDRESS("network.peer.address", reporter-> reporter.networkPeerAddress), 18 | NETWORK_PEER_PORT("network.peer.port", reporter -> reporter.networkPeerPort), 19 | PEER_ADDRESS("peer.address", reporter -> reporter.address), 20 | SERVER_ADDRESS("server.address", reporter -> reporter.serverAddress), 21 | SERVER_PORT("server.port", reporter -> reporter.serverPort), 22 | SPAN_KIND("span.kind", reporter -> "client"), 23 | 24 | // DB 25 | // See https://opentelemetry.io/docs/specs/semconv/database/redis/ 26 | 27 | DB_USER("db.user", reporter -> reporter.user), 28 | DB_NAMESPACE("db.namespace", reporter -> reporter.database), 29 | DB_OPERATION_NAME("db.operation.name", reporter -> reporter.command), 30 | DB_SYSTEM("db.system", reporter -> "redis"); 31 | 32 | final String name; 33 | final Function valueFunction; 34 | 35 | Tags(String name, Function valueFunction) { 36 | this.name = name; 37 | this.valueFunction = valueFunction; 38 | } 39 | } 40 | 41 | private static final TagExtractor REQUEST_TAG_EXTRACTOR = new TagExtractor<>() { 42 | private final Tags[] TAGS = Tags.values(); 43 | 44 | @Override 45 | public int len(CommandReporter obj) { 46 | return TAGS.length; 47 | } 48 | 49 | @Override 50 | public String name(CommandReporter obj, int index) { 51 | return TAGS[index].name; 52 | } 53 | 54 | @Override 55 | public String value(CommandReporter obj, int index) { 56 | return TAGS[index].valueFunction.apply(obj); 57 | } 58 | }; 59 | 60 | private final VertxTracer tracer; 61 | private final ClientMetrics metrics; 62 | private final Context context; 63 | private final TracingPolicy tracingPolicy; 64 | private final String command; 65 | private final String address; 66 | private final String user; 67 | private final String database; 68 | private final String networkPeerAddress; 69 | private final String networkPeerPort; 70 | private final String serverAddress; 71 | private final String serverPort; 72 | 73 | private Object trace; 74 | private Object metric; 75 | 76 | CommandReporter(RedisConnectionInternal conn, String command) { 77 | VertxInternal vertx = conn.vertx(); 78 | RedisURI uri = conn.uri(); 79 | this.tracer = vertx.tracer(); 80 | this.metrics = conn.metrics(); 81 | this.context = vertx.getContext(); 82 | this.tracingPolicy = conn.tracingPolicy(); 83 | this.command = command; 84 | this.address = uri.socketAddress().toString(); 85 | this.networkPeerAddress = conn.remoteAddress().hostAddress(); 86 | this.networkPeerPort = String.valueOf(conn.remoteAddress().port()); 87 | this.serverAddress = uri.socketAddress().host(); 88 | this.serverPort = String.valueOf(uri.socketAddress().port()); 89 | this.user = uri.user(); 90 | // the connection doesn't track the current database, so we have to report "unknown" when tainted 91 | this.database = conn.isTainted() ? null : (uri.select() == null ? "0" : String.valueOf(uri.select())); 92 | } 93 | 94 | public void before() { 95 | if (tracer != null) { 96 | trace = tracer.sendRequest(context, SpanKind.RPC, tracingPolicy, this, "Command", (k, v) -> {}, REQUEST_TAG_EXTRACTOR); 97 | } 98 | if (metrics != null) { 99 | metric = metrics.requestBegin(command, command); 100 | metrics.requestEnd(metric); 101 | } 102 | } 103 | 104 | public void after(AsyncResult ar) { 105 | if (tracer != null) { 106 | tracer.receiveResponse(context, ar.succeeded() ? ar.result() : null, trace, ar.failed() ? ar.cause() : null, TagExtractor.empty()); 107 | } 108 | if (metrics != null) { 109 | if (ar.succeeded()) { 110 | metrics.responseBegin(metric, null); 111 | metrics.responseEnd(metric); 112 | } else { 113 | metrics.requestReset(metric); 114 | } 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/impl/KeyLocator.java: -------------------------------------------------------------------------------- 1 | package io.vertx.redis.client.impl; 2 | 3 | import io.vertx.redis.client.impl.keys.BeginSearch; 4 | import io.vertx.redis.client.impl.keys.FindKeys; 5 | 6 | public final class KeyLocator { 7 | 8 | public final Boolean ro; 9 | public final BeginSearch begin; 10 | public final FindKeys find; 11 | 12 | public KeyLocator(Boolean ro, BeginSearch begin, FindKeys find) { 13 | this.ro = ro; 14 | this.begin = begin; 15 | this.find = find; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/impl/ParserHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Red Hat, Inc. 3 | *

4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | *

8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | *

11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | *

14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | package io.vertx.redis.client.impl; 17 | 18 | import io.vertx.redis.client.Response; 19 | 20 | public interface ParserHandler { 21 | 22 | void handle(Response response); 23 | 24 | void fail(Throwable t); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/impl/PooledRedisConnection.java: -------------------------------------------------------------------------------- 1 | package io.vertx.redis.client.impl; 2 | 3 | import io.vertx.codegen.annotations.Nullable; 4 | import io.vertx.core.Future; 5 | import io.vertx.core.Handler; 6 | import io.vertx.core.internal.pool.Lease; 7 | import io.vertx.core.spi.metrics.PoolMetrics; 8 | import io.vertx.redis.client.RedisConnection; 9 | import io.vertx.redis.client.Request; 10 | import io.vertx.redis.client.Response; 11 | 12 | import java.util.List; 13 | import java.util.concurrent.atomic.AtomicBoolean; 14 | 15 | /** 16 | * A pooled Redis connection 17 | */ 18 | public class PooledRedisConnection implements RedisConnection { 19 | 20 | private final Lease lease; 21 | private final RedisConnectionInternal connection; 22 | private final PoolMetrics metrics; 23 | private final Object metric; 24 | private final AtomicBoolean ended = new AtomicBoolean(); 25 | 26 | public PooledRedisConnection(Lease lease, PoolMetrics poolMetrics, Object metric) { 27 | this.lease = lease; 28 | this.connection = lease.get(); 29 | this.metrics = poolMetrics; 30 | this.metric = metric; 31 | } 32 | 33 | public RedisConnectionInternal actual() { 34 | return connection; 35 | } 36 | 37 | @Override 38 | public RedisConnection exceptionHandler(Handler handler) { 39 | connection.exceptionHandler(handler); 40 | return this; 41 | } 42 | 43 | @Override 44 | public RedisConnection handler(Handler handler) { 45 | connection.handler(handler); 46 | return this; 47 | } 48 | 49 | @Override 50 | public RedisConnection pause() { 51 | connection.pause(); 52 | return this; 53 | } 54 | 55 | @Override 56 | public RedisConnection resume() { 57 | connection.resume(); 58 | return this; 59 | } 60 | 61 | @Override 62 | public RedisConnection fetch(long amount) { 63 | connection.fetch(amount); 64 | return this; 65 | } 66 | 67 | @Override 68 | public RedisConnection endHandler(@Nullable Handler endHandler) { 69 | connection.endHandler(endHandler); 70 | return this; 71 | } 72 | 73 | @Override 74 | public Future<@Nullable Response> send(Request command) { 75 | CommandReporter reporter = new CommandReporter(connection, command.command().toString()); 76 | reporter.before(); 77 | return connection.send(command) 78 | .andThen(reporter::after); 79 | } 80 | 81 | @Override 82 | public Future> batch(List commands) { 83 | CommandReporter reporter = new CommandReporter(connection, "batch"); 84 | reporter.before(); 85 | return connection.batch(commands) 86 | .andThen(reporter::after); 87 | } 88 | 89 | @Override 90 | public Future close() { 91 | if (connection.reset()) { 92 | lease.recycle(); 93 | } 94 | 95 | if (ended.compareAndSet(false, true)) { 96 | if (metrics != null) { 97 | metrics.end(metric); 98 | } 99 | } 100 | 101 | return Future.succeededFuture(); 102 | } 103 | 104 | @Override 105 | public boolean pendingQueueFull() { 106 | return connection.pendingQueueFull(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/impl/RESPEncoder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Red Hat, Inc. 3 | *

4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | *

8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | *

11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | *

14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | package io.vertx.redis.client.impl; 17 | 18 | public final class RESPEncoder { 19 | 20 | // precache -1 21 | private static final byte[] NEG_ONE = convert(-1); 22 | 23 | // Cache 256 number conversions. That should cover a huge 24 | // percentage of numbers passed over the wire. 25 | private static final int NUM_MAP_LENGTH = 256; 26 | private static final byte[][] NUM_MAP = new byte[NUM_MAP_LENGTH][]; 27 | 28 | static { 29 | for (int i = 0; i < NUM_MAP_LENGTH; i++) { 30 | NUM_MAP[i] = convert(i); 31 | } 32 | } 33 | 34 | /** 35 | * Convert the given long value to a byte[] 36 | */ 37 | private static byte[] convert(long value) { 38 | boolean negative = value < 0; 39 | // Checked javadoc: If the argument is equal to 10^n for integer n, then the result is n. 40 | // Also, if negative, leave another slot for the sign. 41 | long abs = Math.abs(value); 42 | int index = (value == 0 ? 0 : (int) Math.log10(abs)) + (negative ? 2 : 1); 43 | byte[] bytes = new byte[index]; 44 | // Put the sign in the slot we saved 45 | if (negative) bytes[0] = '-'; 46 | long next = abs; 47 | while ((next /= 10) > 0) { 48 | bytes[--index] = (byte) ('0' + (abs % 10)); 49 | abs = next; 50 | } 51 | bytes[--index] = (byte) ('0' + abs); 52 | return bytes; 53 | } 54 | 55 | // Optimized for the direct to ASCII bytes case 56 | // About 5x faster than using Long.toString.bytes 57 | public static byte[] numToBytes(long value) { 58 | if (value >= 0 && value < NUM_MAP_LENGTH) { 59 | int index = (int) value; 60 | return NUM_MAP[index]; 61 | } else if (value == -1) { 62 | return NEG_ONE; 63 | } 64 | return convert(value); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/impl/RedisAPIImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Red Hat, Inc. 3 | *

4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | *

8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | *

11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | *

14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | package io.vertx.redis.client.impl; 17 | 18 | import io.vertx.core.Future; 19 | import io.vertx.core.internal.logging.Logger; 20 | import io.vertx.core.internal.logging.LoggerFactory; 21 | import io.vertx.redis.client.Command; 22 | import io.vertx.redis.client.Redis; 23 | import io.vertx.redis.client.RedisAPI; 24 | import io.vertx.redis.client.RedisConnection; 25 | import io.vertx.redis.client.Request; 26 | import io.vertx.redis.client.Response; 27 | 28 | public class RedisAPIImpl implements RedisAPI { 29 | 30 | private static final Logger LOG = LoggerFactory.getLogger(RedisAPIImpl.class); 31 | 32 | private final Redis redis; 33 | private final RedisConnection connection; 34 | 35 | public RedisAPIImpl(RedisConnection connection) { 36 | this.connection = connection; 37 | this.redis = null; 38 | } 39 | 40 | public RedisAPIImpl(Redis redis) { 41 | this.connection = null; 42 | this.redis = redis; 43 | } 44 | 45 | @Override 46 | public Future send(Command cmd, String... args) { 47 | final Request req = Request.cmd(cmd); 48 | 49 | if (args != null) { 50 | for (String o : args) { 51 | if (o == null) { 52 | throw new IllegalArgumentException("Null argument not allowed"); 53 | } else { 54 | req.arg(o); 55 | } 56 | } 57 | } 58 | 59 | if (redis != null) { 60 | // operating in pooled mode 61 | return redis.send(req); 62 | } else if (connection != null) { 63 | // operating on connection mode 64 | return connection.send(req); 65 | } 66 | 67 | return Future.failedFuture(new IllegalStateException("Invalid state: no pool or connection available")); 68 | } 69 | 70 | @Override 71 | public void close() { 72 | if (redis != null) { 73 | // operating in pooled mode 74 | redis.close(); 75 | } else if (connection != null) { 76 | // operating on connection mode 77 | connection.close().onFailure(LOG::warn); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/impl/RedisClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Red Hat, Inc. 3 | *

4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | *

8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | *

11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | *

14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | package io.vertx.redis.client.impl; 17 | 18 | import io.vertx.core.Future; 19 | import io.vertx.core.Promise; 20 | import io.vertx.core.Vertx; 21 | import io.vertx.core.net.NetClientOptions; 22 | import io.vertx.core.tracing.TracingPolicy; 23 | import io.vertx.redis.client.PoolOptions; 24 | import io.vertx.redis.client.Redis; 25 | import io.vertx.redis.client.RedisConnection; 26 | import io.vertx.redis.client.RedisStandaloneConnectOptions; 27 | 28 | import java.util.function.Supplier; 29 | 30 | public class RedisClient extends BaseRedisClient implements Redis { 31 | 32 | public RedisClient(Vertx vertx, NetClientOptions tcpOptions, PoolOptions poolOptions, Supplier> connectOptions, TracingPolicy tracingPolicy) { 33 | super(vertx, tcpOptions, poolOptions, connectOptions, tracingPolicy); 34 | } 35 | 36 | @Override 37 | public Future connect() { 38 | // so that the caller is called back on its original context 39 | Promise promise = vertx.promise(); 40 | connectOptions.get() 41 | .compose(opts -> connectionManager.getConnection(opts.getEndpoint(), null)) 42 | .onComplete(promise); 43 | return promise.future(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/impl/RedisConnectException.java: -------------------------------------------------------------------------------- 1 | package io.vertx.redis.client.impl; 2 | 3 | import java.net.ConnectException; 4 | 5 | /** 6 | * Used to signal connection failure in certain Redis clients that don't propagate underlying 7 | * exceptions directly, but use a custom logic. Intentionally a subclass of {@code ConnectException}, 8 | * so that users of Redis clients can handle connection failures uniformly. 9 | */ 10 | class RedisConnectException extends ConnectException { 11 | RedisConnectException(String msg) { 12 | super(msg); 13 | } 14 | 15 | @Override 16 | public Throwable fillInStackTrace() { 17 | return this; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/impl/RedisConnectionInternal.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Red Hat, Inc. 3 | *

4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | *

8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | *

11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | *

14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | package io.vertx.redis.client.impl; 17 | 18 | import io.vertx.core.internal.VertxInternal; 19 | import io.vertx.core.net.SocketAddress; 20 | import io.vertx.core.spi.metrics.ClientMetrics; 21 | import io.vertx.core.tracing.TracingPolicy; 22 | import io.vertx.redis.client.RedisConnection; 23 | 24 | public interface RedisConnectionInternal extends RedisConnection { 25 | 26 | boolean isValid(); 27 | 28 | void forceClose(); 29 | 30 | /** 31 | * Returns {@code true} is this connection can be reset. This means that the connection didn't enter PubSub mode. 32 | */ 33 | boolean reset(); 34 | 35 | /** 36 | * Returns whether this connection is "tainted". A connection is called tainted if it changes the default state, 37 | * for example, when a connection enters pub sub mode, or specific features are activated such as changing a database 38 | * or different authentication is used. 39 | */ 40 | boolean isTainted(); 41 | 42 | VertxInternal vertx(); 43 | 44 | /** 45 | * Returns the {@linkplain RedisURI URI} of the Redis server to which this connection is connected. 46 | */ 47 | RedisURI uri(); 48 | 49 | /** 50 | * Returns the {@linkplain ClientMetrics client metrics} for this connection. 51 | */ 52 | ClientMetrics metrics(); 53 | 54 | /** 55 | * Returns the {@linkplain TracingPolicy tracing policy} configured for this connection. 56 | */ 57 | TracingPolicy tracingPolicy(); 58 | 59 | /** 60 | * Returns the {@linkplain SocketAddress remote address} of the Redis server to which this connection is connected. 61 | */ 62 | SocketAddress remoteAddress(); 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/impl/RedisSentinelConnection.java: -------------------------------------------------------------------------------- 1 | package io.vertx.redis.client.impl; 2 | 3 | import io.vertx.codegen.annotations.Nullable; 4 | import io.vertx.core.Future; 5 | import io.vertx.core.Handler; 6 | import io.vertx.redis.client.RedisConnection; 7 | import io.vertx.redis.client.Request; 8 | import io.vertx.redis.client.Response; 9 | 10 | import java.util.List; 11 | import java.util.concurrent.atomic.AtomicReference; 12 | 13 | public class RedisSentinelConnection implements RedisConnection { 14 | 15 | private final AtomicReference connection; 16 | private final SentinelFailover failover; 17 | 18 | public RedisSentinelConnection(PooledRedisConnection connection, SentinelFailover failover) { 19 | this.connection = new AtomicReference<>(connection); 20 | this.failover = failover; 21 | failover.addConnection(this); 22 | } 23 | 24 | void reconnect(PooledRedisConnection newConnection) { 25 | connection.set(newConnection); 26 | } 27 | 28 | @Override 29 | public RedisConnection exceptionHandler(Handler handler) { 30 | connection.get().exceptionHandler(handler); 31 | return this; 32 | } 33 | 34 | @Override 35 | public RedisConnection handler(Handler handler) { 36 | connection.get().handler(handler); 37 | return this; 38 | } 39 | 40 | @Override 41 | public RedisConnection pause() { 42 | connection.get().pause(); 43 | return this; 44 | } 45 | 46 | @Override 47 | public RedisConnection resume() { 48 | connection.get().resume(); 49 | return this; 50 | } 51 | 52 | @Override 53 | public RedisConnection fetch(long amount) { 54 | connection.get().fetch(amount); 55 | return this; 56 | } 57 | 58 | @Override 59 | public RedisConnection endHandler(@Nullable Handler endHandler) { 60 | connection.get().endHandler(endHandler); 61 | return this; 62 | } 63 | 64 | @Override 65 | public Future<@Nullable Response> send(Request command) { 66 | return connection.get().send(command); 67 | } 68 | 69 | @Override 70 | public Future> batch(List commands) { 71 | return connection.get().batch(commands); 72 | } 73 | 74 | Future closeDelegate() { 75 | return connection.get().close(); 76 | } 77 | 78 | @Override 79 | public Future close() { 80 | failover.removeConnection(this); 81 | return closeDelegate(); 82 | } 83 | 84 | @Override 85 | public boolean pendingQueueFull() { 86 | return connection.get().pendingQueueFull(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/impl/Resolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Red Hat, Inc. 3 | *

4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | *

8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | *

11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | *

14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | package io.vertx.redis.client.impl; 17 | 18 | import io.vertx.core.Completable; 19 | import io.vertx.redis.client.RedisSentinelConnectOptions; 20 | 21 | @FunctionalInterface 22 | interface Resolver { 23 | 24 | void resolve(String endpoint, RedisSentinelConnectOptions parameter, Completable callback); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/impl/SentinelFailover.java: -------------------------------------------------------------------------------- 1 | package io.vertx.redis.client.impl; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.core.internal.logging.Logger; 5 | import io.vertx.core.internal.logging.LoggerFactory; 6 | import io.vertx.redis.client.Command; 7 | import io.vertx.redis.client.RedisRole; 8 | import io.vertx.redis.client.Request; 9 | import io.vertx.redis.client.ResponseType; 10 | 11 | import java.util.Set; 12 | import java.util.concurrent.ConcurrentHashMap; 13 | import java.util.concurrent.atomic.AtomicReference; 14 | import java.util.function.Function; 15 | 16 | class SentinelFailover { 17 | private static final Logger LOG = LoggerFactory.getLogger(SentinelFailover.class); 18 | 19 | private static final int RETRIES = 3; 20 | 21 | private final String masterSetName; 22 | private final Function> connectionFactory; 23 | 24 | private final AtomicReference sentinelConnection = new AtomicReference<>(); 25 | private final Set masterConnections = ConcurrentHashMap.newKeySet(); 26 | 27 | SentinelFailover(String masterSetName, Function> connectionFactory) { 28 | this.masterSetName = masterSetName; 29 | this.connectionFactory = connectionFactory; 30 | } 31 | 32 | void start() { 33 | start(RETRIES); 34 | } 35 | 36 | private void start(int retries) { 37 | connectionFactory.apply(RedisRole.SENTINEL) 38 | .onFailure(t -> { 39 | if (retries == 0) { 40 | LOG.error("Failed to obtain a connection to Redis sentinel, automatic failover will not work: " + t); 41 | } else { 42 | start(retries - 1); 43 | } 44 | }) 45 | .onSuccess(sentinel -> { 46 | PooledRedisConnection old = sentinelConnection.getAndSet(sentinel); 47 | if (old != null) { 48 | old.close() 49 | .onFailure(err -> LOG.warn("Failed to close connection: " + err)); 50 | } 51 | 52 | sentinel.handler(msg -> { 53 | if (msg.type() == ResponseType.PUSH 54 | && "message".equalsIgnoreCase(msg.get(0).toString()) 55 | && msg.get(2).toString().startsWith(masterSetName + " ")) { 56 | reconnectAll(); 57 | } 58 | }); 59 | sentinel.exceptionHandler(t -> { 60 | sentinel.close(); 61 | start(RETRIES); 62 | }); 63 | sentinel.send(Request.cmd(Command.SUBSCRIBE).arg("+switch-master")) 64 | .onFailure(t -> { 65 | sentinel.close(); 66 | if (retries == 0) { 67 | LOG.error("Failed to subscribe +switch-master on Redis sentinel connection, reconnection to new master on failover will not work: " + t); 68 | } else { 69 | start(retries - 1); 70 | } 71 | }); 72 | }); 73 | } 74 | 75 | private void reconnectAll() { 76 | for (RedisSentinelConnection connection : masterConnections) { 77 | connection.closeDelegate() 78 | .recover(ignored -> Future.succeededFuture()) 79 | .compose(ignored -> connectionFactory.apply(RedisRole.MASTER)) 80 | .onSuccess(connection::reconnect) 81 | .onFailure(t -> { 82 | LOG.error("Failed to reconnect to master after failover: " + t); 83 | }); 84 | } 85 | } 86 | 87 | void addConnection(RedisSentinelConnection sentinelConn) { 88 | masterConnections.add(sentinelConn); 89 | } 90 | 91 | void removeConnection(RedisSentinelConnection sentinelConn) { 92 | masterConnections.remove(sentinelConn); 93 | } 94 | 95 | void close() { 96 | PooledRedisConnection sentinelConnection = this.sentinelConnection.get(); 97 | if (sentinelConnection != null) { 98 | sentinelConnection.close() 99 | .onFailure(err -> LOG.warn("Failed to close connection: " + err)); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/impl/SharedSlots.java: -------------------------------------------------------------------------------- 1 | package io.vertx.redis.client.impl; 2 | 3 | import io.vertx.core.Completable; 4 | import io.vertx.core.Future; 5 | import io.vertx.core.Promise; 6 | import io.vertx.core.Vertx; 7 | import io.vertx.core.internal.logging.Logger; 8 | import io.vertx.core.internal.logging.LoggerFactory; 9 | import io.vertx.redis.client.Command; 10 | import io.vertx.redis.client.RedisClusterConnectOptions; 11 | import io.vertx.redis.client.RedisConnection; 12 | import io.vertx.redis.client.RedisReplicas; 13 | import io.vertx.redis.client.Request; 14 | 15 | import java.util.List; 16 | import java.util.Set; 17 | import java.util.concurrent.ConcurrentHashMap; 18 | import java.util.concurrent.atomic.AtomicReference; 19 | import java.util.function.Supplier; 20 | 21 | /** 22 | * Exactly one instance of this class exists for each instance of {@link RedisClusterClient} 23 | * and is also shared between all {@link RedisClusterConnection}s obtained from that client. 24 | */ 25 | class SharedSlots { 26 | private static final Logger LOG = LoggerFactory.getLogger(SharedSlots.class); 27 | 28 | private final Vertx vertx; 29 | private final Supplier> connectOptions; 30 | private final RedisConnectionManager connectionManager; 31 | 32 | private final AtomicReference> slots = new AtomicReference<>(); 33 | 34 | SharedSlots(Vertx vertx, Supplier> connectOptions, RedisConnectionManager connectionManager) { 35 | this.vertx = vertx; 36 | this.connectOptions = connectOptions; 37 | this.connectionManager = connectionManager; 38 | } 39 | 40 | Future get() { 41 | while (true) { 42 | Future slots = this.slots.get(); 43 | if (slots != null) { 44 | return slots; 45 | } 46 | 47 | Promise promise = Promise.promise(); 48 | Future future = promise.future(); 49 | if (this.slots.compareAndSet(null, future)) { 50 | LOG.debug("Obtaining hash slot assignment"); 51 | // attempt to load the slots from the first good endpoint 52 | connectOptions.get() 53 | .onSuccess(opts -> getSlots(opts, 0, ConcurrentHashMap.newKeySet(), promise)) 54 | .onFailure(promise::fail); 55 | return future; 56 | } 57 | } 58 | } 59 | 60 | private void getSlots(RedisClusterConnectOptions connectOptions, int index, Set failures, Completable onGotSlots) { 61 | List endpoints = connectOptions.getEndpoints(); 62 | if (index >= endpoints.size()) { 63 | // stop condition 64 | StringBuilder message = new StringBuilder("Cannot connect to any of the provided endpoints"); 65 | for (Throwable failure : failures) { 66 | message.append("\n- ").append(failure); 67 | } 68 | onGotSlots.fail(new RedisConnectException(message.toString())); 69 | scheduleInvalidation(connectOptions); 70 | return; 71 | } 72 | 73 | connectionManager.getConnection(endpoints.get(index), RedisReplicas.NEVER != connectOptions.getUseReplicas() ? Request.cmd(Command.READONLY) : null) 74 | .onFailure(err -> { 75 | // try with the next endpoint 76 | failures.add(err); 77 | getSlots(connectOptions, index + 1, failures, onGotSlots); 78 | }) 79 | .onSuccess(conn -> { 80 | getSlots(endpoints.get(index), conn).onComplete(result -> { 81 | // the connection is not needed anymore, regardless of success or failure 82 | // (on success, we just finish, on failure, we'll try another endpoint) 83 | conn.close().onFailure(LOG::warn); 84 | 85 | if (result.failed()) { 86 | // the slots command failed, try with next endpoint 87 | failures.add(result.cause()); 88 | getSlots(connectOptions, index + 1, failures, onGotSlots); 89 | } else { 90 | Slots slots = result.result(); 91 | onGotSlots.succeed(slots); 92 | scheduleInvalidation(connectOptions); 93 | } 94 | }); 95 | }); 96 | } 97 | 98 | private Future getSlots(String endpoint, RedisConnection conn) { 99 | return conn 100 | .send(Request.cmd(Command.CLUSTER).arg("SLOTS")) 101 | .compose(reply -> { 102 | if (reply == null || reply.size() == 0) { 103 | // no slots available we can't really proceed 104 | return Future.failedFuture("CLUSTER SLOTS No slots available in the cluster."); 105 | } 106 | 107 | Slots result; 108 | try { 109 | result = new Slots(endpoint, reply); 110 | } catch (Exception e) { 111 | return Future.failedFuture("CLUSTER SLOTS response invalid: " + e); 112 | } 113 | return Future.succeededFuture(result); 114 | }); 115 | } 116 | 117 | void invalidate() { 118 | slots.set(null); 119 | } 120 | 121 | private void scheduleInvalidation(RedisClusterConnectOptions connectOptions) { 122 | vertx.setTimer(connectOptions.getHashSlotCacheTTL(), ignored -> invalidate()); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/impl/Slots.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Red Hat, Inc. 3 | *

4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | *

8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | *

11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | *

14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | package io.vertx.redis.client.impl; 17 | 18 | import io.vertx.codegen.annotations.Nullable; 19 | import io.vertx.redis.client.Response; 20 | 21 | import java.util.ArrayList; 22 | import java.util.HashSet; 23 | import java.util.List; 24 | import java.util.Random; 25 | import java.util.Set; 26 | 27 | // see https://redis.io/commands/cluster-slots/ 28 | class Slots { 29 | 30 | // we need some randomness, it doesn't need to be cryptographically secure 31 | private static final Random RANDOM = new Random(); 32 | 33 | static class Slot { 34 | private final int start; 35 | private final int end; 36 | private final String[] endpoints; 37 | 38 | Slot(int start, int end, int size) { 39 | this.start = start; 40 | this.end = end; 41 | endpoints = new String[size]; 42 | } 43 | } 44 | 45 | private final long ts = System.currentTimeMillis(); 46 | private final int size; 47 | private final Slot[] slots; 48 | private final String[] endpoints; 49 | private final String[] masterEndpoints; 50 | 51 | Slots(String connectionString, Response reply) { 52 | size = reply.size(); 53 | slots = new Slot[size]; 54 | 55 | final RedisURI uri = new RedisURI(connectionString); 56 | 57 | Set uniqueEndpoints = new HashSet<>(); 58 | final List masterEndpoints = new ArrayList<>(); 59 | 60 | for (int i = 0; i < reply.size(); i++) { 61 | // multibulk 62 | Response s = reply.get(i); 63 | // single bulk 64 | slots[i] = new Slot( 65 | // start 66 | s.get(0).toInteger(), 67 | // end 68 | s.get(1).toInteger(), 69 | // size 70 | s.size() - 2); 71 | 72 | // array of all clients, clients[2] = master, others are replicas 73 | for (int index = 2; index < s.size(); index++) { 74 | final Response c = s.get(index); 75 | final Response hostField = c.get(0); 76 | final Response portField = c.get(1); 77 | final String host; 78 | if (hostField == null || "".equals(hostField.toString())) { 79 | // since Redis 7.0, `null` or `""` means the "current" host should be used 80 | host = uri.socketAddress().host(); 81 | } else { 82 | host = hostField.toString().contains(":") ? "[" + hostField + "]" : hostField.toString(); 83 | } 84 | 85 | final String endpoint = uri.protocol() + "://" + uri.userinfo() + host + ":" + portField.toInteger(); 86 | slots[i].endpoints[index - 2] = endpoint; 87 | uniqueEndpoints.add(endpoint); 88 | if (index == 2) { 89 | masterEndpoints.add(endpoint); 90 | } 91 | } 92 | } 93 | 94 | endpoints = new String[uniqueEndpoints.size()]; 95 | int i = 0; 96 | for (String endpoint : uniqueEndpoints) { 97 | endpoints[i++] = endpoint; 98 | } 99 | this.masterEndpoints = masterEndpoints.toArray(new String[0]); 100 | } 101 | 102 | boolean contains(String endpoint) { 103 | for (String entry : endpoints) { 104 | if (endpoint.equals(entry)) { 105 | return true; 106 | } 107 | } 108 | return false; 109 | } 110 | 111 | int size() { 112 | return size; 113 | } 114 | 115 | String[] endpointsForSlot(int index) { 116 | return slots[index].endpoints; 117 | } 118 | 119 | @Nullable String[] endpointsForKey(int key) { 120 | for (Slot s : slots) { 121 | if (key >= s.start && key <= s.end) { 122 | return s.endpoints; 123 | } 124 | } 125 | return null; 126 | } 127 | 128 | String randomEndPoint(boolean onlyMasterEndpoints) { 129 | if (onlyMasterEndpoints) { 130 | return masterEndpoints[RANDOM.nextInt(masterEndpoints.length)]; 131 | } 132 | return endpoints[RANDOM.nextInt(endpoints.length)]; 133 | } 134 | 135 | String[] endpoints() { 136 | return endpoints; 137 | } 138 | 139 | String[] masterEndpoints() { 140 | return masterEndpoints; 141 | } 142 | 143 | long timestamp() { 144 | return ts; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/impl/keys/BeginSearch.java: -------------------------------------------------------------------------------- 1 | package io.vertx.redis.client.impl.keys; 2 | 3 | import java.util.List; 4 | 5 | @FunctionalInterface 6 | public interface BeginSearch { 7 | int begin(List args, int arity); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/impl/keys/BeginSearchIndex.java: -------------------------------------------------------------------------------- 1 | package io.vertx.redis.client.impl.keys; 2 | 3 | import java.util.List; 4 | 5 | public class BeginSearchIndex implements BeginSearch { 6 | 7 | private final int index; 8 | 9 | public BeginSearchIndex(int index) { 10 | this.index = index; 11 | } 12 | 13 | @Override 14 | public int begin(List args, int arity) { 15 | final int result = index - 1; 16 | if (result >= args.size()) { 17 | return -1; 18 | } 19 | return result; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/impl/keys/BeginSearchKeyword.java: -------------------------------------------------------------------------------- 1 | package io.vertx.redis.client.impl.keys; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | import java.util.List; 5 | 6 | public class BeginSearchKeyword implements BeginSearch { 7 | 8 | private static final byte UPPER_CASE_OFFSET = 'A' - 'a'; 9 | 10 | private final byte[] keyword; 11 | private final int startsFrom; 12 | 13 | public BeginSearchKeyword(String keyword, int startsFrom) { 14 | this.keyword = keyword.getBytes(StandardCharsets.UTF_8); 15 | this.startsFrom = startsFrom; 16 | toUpperCase(this.keyword); 17 | } 18 | 19 | @Override 20 | public int begin(List args, int arity) { 21 | int start = startsFrom >= 0 ? 22 | startsFrom : 23 | Math.abs(arity) + startsFrom; 24 | 25 | for (int i = start; i < args.size(); i++) { 26 | if (match(args.get(i))) { 27 | return i + 1; 28 | } 29 | } 30 | // not found 31 | return -1; 32 | } 33 | 34 | /** 35 | * @param b ASCII character 36 | * @return uppercase version of arg if it was lowercase, otherwise returns arg 37 | */ 38 | private static byte toUpperCase(final byte b) { 39 | if (b < 'a' || b > 'z') { 40 | return b; 41 | } 42 | return (byte) (b + UPPER_CASE_OFFSET); 43 | } 44 | 45 | /** 46 | * Converts in place all lower case letters to upper case in the byte array provided. 47 | */ 48 | private static void toUpperCase(final byte[] bytes) { 49 | final int length = bytes.length; 50 | for (int i = 0; i < length; ++i) { 51 | if (bytes[i] >= 'a' && bytes[i] <= 'z') { 52 | bytes[i] = (byte) (bytes[i] + UPPER_CASE_OFFSET); 53 | } 54 | } 55 | } 56 | 57 | private boolean match(byte[] a) { 58 | if (a != null) { 59 | int length = a.length; 60 | if (keyword.length != length) { 61 | return false; 62 | } else { 63 | for (int i = 0; i < keyword.length; i++) { 64 | if (keyword[i] != toUpperCase(a[i])) { 65 | return false; 66 | } 67 | } 68 | return true; 69 | } 70 | } else { 71 | return false; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/impl/keys/FindKeys.java: -------------------------------------------------------------------------------- 1 | package io.vertx.redis.client.impl.keys; 2 | 3 | import java.util.List; 4 | 5 | @FunctionalInterface 6 | public interface FindKeys { 7 | 8 | int forEach(List args, int arity, int offset, KeyConsumer collector); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/impl/keys/FindKeysKeynum.java: -------------------------------------------------------------------------------- 1 | package io.vertx.redis.client.impl.keys; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | import java.util.List; 5 | 6 | public class FindKeysKeynum implements FindKeys { 7 | 8 | final int keyNumIdx; 9 | final int firstKey; 10 | final int keyStep; 11 | 12 | public FindKeysKeynum(int keyNumIdx, int firstKey, int keyStep) { 13 | this.keyNumIdx = keyNumIdx; 14 | this.firstKey = firstKey; 15 | this.keyStep = keyStep; 16 | } 17 | 18 | @Override 19 | public int forEach(List args, int arity, int offset, KeyConsumer collector) { 20 | int keys = Integer.parseInt(new String(args.get(offset + keyNumIdx), StandardCharsets.US_ASCII)); 21 | int i, stop; 22 | for (i = offset + firstKey, stop = 0; ++stop <= keys; i += keyStep) { 23 | collector.accept(offset, i, keyStep); 24 | } 25 | return i; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/impl/keys/FindKeysRange.java: -------------------------------------------------------------------------------- 1 | package io.vertx.redis.client.impl.keys; 2 | 3 | import java.util.List; 4 | 5 | public class FindKeysRange implements FindKeys { 6 | 7 | private final int lastKey; 8 | private final int keyStep; 9 | private final int limit; 10 | 11 | public FindKeysRange(int lastKey, int keyStep, int limit) { 12 | this.lastKey = lastKey; 13 | this.keyStep = keyStep; 14 | this.limit = limit; 15 | } 16 | 17 | @Override 18 | public int forEach(List args, int arity, int offset, KeyConsumer collector) { 19 | int stop; 20 | 21 | if (lastKey >= 0) { 22 | stop = offset + lastKey; 23 | } else { 24 | stop = args.size() + lastKey; 25 | if (lastKey == -1) { 26 | // if lastkey is has the value of -1, we use the limit to stop the search by a factor. 27 | // 0 and 1 mean no limit. 28 | // 2 means half of the remaining arguments, 3 means a third, and so on. 29 | if (limit > 1) { 30 | stop = offset + ((stop - offset) / limit); 31 | } 32 | } 33 | } 34 | 35 | int i; 36 | for (i = offset; i <= stop; i += keyStep) { 37 | collector.accept(offset, i, keyStep); 38 | } 39 | 40 | return i; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/impl/keys/KeyConsumer.java: -------------------------------------------------------------------------------- 1 | package io.vertx.redis.client.impl.keys; 2 | 3 | @FunctionalInterface 4 | public interface KeyConsumer { 5 | void accept(int begin, int keyIdx, int keyStep); 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/impl/types/AttributeType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Red Hat, Inc. 3 | *

4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | *

8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | *

11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | *

14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | package io.vertx.redis.client.impl.types; 17 | 18 | import io.vertx.redis.client.Response; 19 | import io.vertx.redis.client.ResponseType; 20 | 21 | import java.util.HashMap; 22 | import java.util.Iterator; 23 | import java.util.Map; 24 | import java.util.Set; 25 | 26 | /** 27 | * A Redis MULTI response can represent a List/Set/Map type. 28 | */ 29 | public final class AttributeType implements Multi { 30 | 31 | public static AttributeType create(long length) { 32 | return new AttributeType(new Response[(int) length * 2]); 33 | } 34 | 35 | private final Map map; 36 | private final Response[] replies; 37 | // mutable temporary state 38 | private int count; 39 | private String key; 40 | 41 | private AttributeType(Response[] replies) { 42 | this.replies = replies; 43 | this.count = 0; 44 | this.map = new HashMap<>(); 45 | } 46 | 47 | @Override 48 | public ResponseType type() { 49 | return ResponseType.ATTRIBUTE; 50 | } 51 | 52 | public void add(Response reply) { 53 | if (count % 2 == 0) { 54 | key = reply.toString(); 55 | } else { 56 | if (key != null) { 57 | map.put(key, reply); 58 | } 59 | } 60 | this.replies[this.count++] = reply; 61 | } 62 | 63 | public boolean complete() { 64 | return count == replies.length; 65 | } 66 | 67 | @Override 68 | public Response get(int index) { 69 | return replies[index]; 70 | } 71 | 72 | @Override 73 | public Response get(String key) { 74 | return map.get(key); 75 | } 76 | 77 | @Override 78 | public boolean containsKey(String key) { 79 | return map.containsKey(key); 80 | } 81 | 82 | @Override 83 | public Set getKeys() { 84 | return map.keySet(); 85 | } 86 | 87 | @Override 88 | public int size() { 89 | return replies.length; 90 | } 91 | 92 | @Override 93 | public boolean isArray() { 94 | return true; 95 | } 96 | 97 | @Override 98 | public boolean isMap() { 99 | return true; 100 | } 101 | 102 | @Override 103 | public String toString() { 104 | final StringBuilder sb = new StringBuilder(); 105 | 106 | sb.append('{'); 107 | boolean more = false; 108 | for (Map.Entry kv : map.entrySet()) { 109 | if (more) { 110 | sb.append(", "); 111 | } 112 | 113 | String key = kv.getKey(); 114 | 115 | if (key == null) { 116 | sb.append("null"); 117 | } else { 118 | sb.append(key); 119 | } 120 | 121 | sb.append(':'); 122 | 123 | Response value = kv.getValue(); 124 | 125 | if (value == null) { 126 | sb.append("null"); 127 | } else { 128 | sb.append(value); 129 | } 130 | 131 | more = true; 132 | } 133 | sb.append('}'); 134 | 135 | return sb.toString(); 136 | } 137 | 138 | @Override 139 | public Iterator iterator() { 140 | return new Iterator() { 141 | private int idx = 0; 142 | 143 | @Override 144 | public boolean hasNext() { 145 | return idx < replies.length; 146 | } 147 | 148 | @Override 149 | public Response next() { 150 | final Response value = replies[idx]; 151 | 152 | idx += 2; 153 | 154 | return value; 155 | } 156 | }; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/impl/types/BooleanType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Red Hat, Inc. 3 | *

4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | *

8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | *

11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | *

14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | package io.vertx.redis.client.impl.types; 17 | 18 | import io.vertx.redis.client.Response; 19 | import io.vertx.redis.client.ResponseType; 20 | 21 | public final class BooleanType implements Response { 22 | 23 | public static final BooleanType TRUE = new BooleanType(true); 24 | public static final BooleanType FALSE = new BooleanType(false); 25 | 26 | public static BooleanType create(Boolean value) { 27 | return new BooleanType(value); 28 | } 29 | 30 | private final Boolean value; 31 | 32 | private BooleanType(Boolean value) { 33 | this.value = value; 34 | } 35 | 36 | @Override 37 | public ResponseType type() { 38 | return ResponseType.BOOLEAN; 39 | } 40 | 41 | @Override 42 | public Boolean toBoolean() { 43 | return value; 44 | } 45 | 46 | @Override 47 | public String toString() { 48 | return value.toString(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/impl/types/BulkType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Red Hat, Inc. 3 | *

4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | *

8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | *

11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | *

14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | package io.vertx.redis.client.impl.types; 17 | 18 | import io.vertx.core.buffer.Buffer; 19 | import io.vertx.redis.client.Response; 20 | import io.vertx.redis.client.ResponseType; 21 | 22 | import java.nio.charset.Charset; 23 | import java.nio.charset.StandardCharsets; 24 | 25 | public final class BulkType implements Response { 26 | 27 | public static final BulkType EMPTY = new BulkType(Buffer.buffer(""), false); 28 | 29 | public static BulkType create(Buffer message, boolean verbatim) { 30 | return new BulkType(message, verbatim); 31 | } 32 | 33 | private final Buffer message; 34 | private final String format; 35 | 36 | private BulkType(Buffer message, boolean verbatim) { 37 | if (verbatim) { 38 | format = message.getString(0, 3); 39 | assert message.getByte(3) == ':'; 40 | this.message = message.slice(4, message.length()); 41 | } else { 42 | this.format = null; 43 | this.message = message; 44 | } 45 | } 46 | 47 | @Override 48 | public String format() { 49 | return format; 50 | } 51 | 52 | @Override 53 | public ResponseType type() { 54 | return ResponseType.BULK; 55 | } 56 | 57 | @Override 58 | public String toString() { 59 | return toString(StandardCharsets.UTF_8); 60 | } 61 | 62 | @Override 63 | public String toString(Charset encoding) { 64 | return new String(message.getBytes(), encoding); 65 | } 66 | 67 | @Override 68 | public Buffer toBuffer() { 69 | return message; 70 | } 71 | 72 | @Override 73 | public byte[] toBytes() { 74 | return message.getBytes(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/impl/types/ErrorType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Red Hat, Inc. 3 | *

4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | *

8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | *

11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | *

14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | package io.vertx.redis.client.impl.types; 17 | 18 | import io.vertx.codegen.annotations.Nullable; 19 | import io.vertx.redis.client.Response; 20 | import io.vertx.redis.client.ResponseType; 21 | 22 | public final class ErrorType extends Throwable implements Response { 23 | 24 | public static ErrorType create(String message) { 25 | return new ErrorType(message, null); 26 | } 27 | 28 | public static ErrorType create(String message, Throwable cause) { 29 | return new ErrorType(message, cause); 30 | } 31 | 32 | private ErrorType(String message, Throwable cause) { 33 | super(message, cause, false, false); 34 | } 35 | 36 | @Override 37 | public ResponseType type() { 38 | return ResponseType.ERROR; 39 | } 40 | 41 | public boolean is(String kind) { 42 | String message = getMessage(); 43 | if (message == null) { 44 | return kind == null; 45 | } else { 46 | if (kind.equals("ERR")) { 47 | return message.startsWith(kind); 48 | } else { 49 | // this is to address databases like PIKA which drift from the official 50 | // protocol by always prefixing errors with ERR 51 | return message.startsWith(kind) || message.startsWith("ERR " + kind); 52 | } 53 | } 54 | } 55 | 56 | @Override 57 | public String toString() { 58 | return getMessage(); 59 | } 60 | 61 | public @Nullable String slice(char sep, int index) { 62 | String message = getMessage(); 63 | if (message == null) { 64 | return null; 65 | } 66 | 67 | int start = 0; 68 | int count = 0; 69 | for (int i = 0; i < message.length(); i++) { 70 | if (message.charAt(i) == sep) { 71 | if (++count > index) { 72 | return message.substring(start, i); 73 | } else { 74 | start = i + 1; 75 | } 76 | } 77 | } 78 | // EOL 79 | if (count == index) { 80 | return message.substring(start); 81 | } 82 | 83 | return null; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/impl/types/Multi.java: -------------------------------------------------------------------------------- 1 | package io.vertx.redis.client.impl.types; 2 | 3 | import io.vertx.redis.client.Response; 4 | 5 | public interface Multi extends Response { 6 | 7 | /** 8 | * Adds a reply to the current response as it gets parsed from the wire. 9 | * 10 | * @param reply the reply to add 11 | */ 12 | void add(Response reply); 13 | 14 | 15 | /** 16 | * Signals that we received all required replies. 17 | */ 18 | boolean complete(); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/impl/types/NumberType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Red Hat, Inc. 3 | *

4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | *

8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | *

11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | *

14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | package io.vertx.redis.client.impl.types; 17 | 18 | import io.vertx.redis.client.Response; 19 | import io.vertx.redis.client.ResponseType; 20 | 21 | import java.math.BigInteger; 22 | 23 | public final class NumberType implements Response { 24 | 25 | public static NumberType create(Number value) { 26 | return new NumberType(value); 27 | } 28 | 29 | private final Number value; 30 | 31 | private NumberType(Number value) { 32 | this.value = value; 33 | } 34 | 35 | @Override 36 | public Number toNumber() { 37 | return value; 38 | } 39 | 40 | @Override 41 | public ResponseType type() { 42 | return ResponseType.NUMBER; 43 | } 44 | 45 | @Override 46 | public BigInteger toBigInteger() { 47 | if (value instanceof BigInteger) { 48 | return (BigInteger) value; 49 | } else { 50 | return BigInteger.valueOf(value.longValue()); 51 | } 52 | } 53 | 54 | @Override 55 | public Double toDouble() { 56 | return value.doubleValue(); 57 | } 58 | 59 | @Override 60 | public Float toFloat() { 61 | return value.floatValue(); 62 | } 63 | 64 | @Override 65 | public Long toLong() { 66 | return value.longValue(); 67 | } 68 | 69 | @Override 70 | public Integer toInteger() { 71 | return value.intValue(); 72 | } 73 | 74 | @Override 75 | public Short toShort() { 76 | return value.shortValue(); 77 | } 78 | 79 | @Override 80 | public Byte toByte() { 81 | return value.byteValue(); 82 | } 83 | 84 | @Override 85 | public String toString() { 86 | return value.toString(); 87 | } 88 | 89 | @Override 90 | public Boolean toBoolean() { 91 | return value.intValue() == 1; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/impl/types/PushType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Red Hat, Inc. 3 | *

4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | *

8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | *

11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | *

14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | package io.vertx.redis.client.impl.types; 17 | 18 | import io.vertx.redis.client.Response; 19 | import io.vertx.redis.client.ResponseType; 20 | 21 | import java.util.Iterator; 22 | 23 | public final class PushType implements Multi { 24 | 25 | public static PushType create(long length) { 26 | return new PushType(new Response[(int) length]); 27 | } 28 | 29 | private final Response[] replies; 30 | // mutable temporary state 31 | private int count; 32 | 33 | private PushType(Response[] replies) { 34 | this.replies = replies; 35 | this.count = 0; 36 | } 37 | 38 | @Override 39 | public ResponseType type() { 40 | return ResponseType.PUSH; 41 | } 42 | 43 | public void add(Response reply) { 44 | this.replies[this.count++] = reply; 45 | } 46 | 47 | public boolean complete() { 48 | return count == replies.length; 49 | } 50 | 51 | @Override 52 | public Response get(int index) { 53 | return replies[index]; 54 | } 55 | 56 | @Override 57 | public int size() { 58 | return replies.length; 59 | } 60 | 61 | @Override 62 | public boolean isArray() { 63 | return true; 64 | } 65 | 66 | @Override 67 | public boolean isMap() { 68 | return false; 69 | } 70 | 71 | @Override 72 | public String toString() { 73 | final StringBuilder sb = new StringBuilder(); 74 | 75 | sb.append('['); 76 | boolean more = false; 77 | for (Response r : replies) { 78 | if (more) { 79 | sb.append(", "); 80 | } 81 | 82 | if (r == null) { 83 | sb.append("null"); 84 | } else { 85 | sb.append(r); 86 | } 87 | more = true; 88 | } 89 | sb.append(']'); 90 | 91 | return sb.toString(); 92 | } 93 | 94 | @Override 95 | public Iterator iterator() { 96 | return new Iterator() { 97 | private int idx = 0; 98 | 99 | @Override 100 | public boolean hasNext() { 101 | return idx < replies.length; 102 | } 103 | 104 | @Override 105 | public Response next() { 106 | return replies[idx++]; 107 | } 108 | }; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/client/impl/types/SimpleStringType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Red Hat, Inc. 3 | *

4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | *

8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | *

11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | *

14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | package io.vertx.redis.client.impl.types; 17 | 18 | import io.vertx.redis.client.Response; 19 | import io.vertx.redis.client.ResponseType; 20 | 21 | public final class SimpleStringType implements Response { 22 | 23 | public static final SimpleStringType OK = new SimpleStringType("OK"); 24 | 25 | public static SimpleStringType create(String message) { 26 | return new SimpleStringType(message); 27 | } 28 | 29 | private final String message; 30 | 31 | private SimpleStringType(String message) { 32 | this.message = message; 33 | } 34 | 35 | @Override 36 | public ResponseType type() { 37 | return ResponseType.SIMPLE; 38 | } 39 | 40 | @Override 41 | public String toString() { 42 | return message; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/io/vertx/redis/package-info.java: -------------------------------------------------------------------------------- 1 | @ModuleGen(name = "vertx-redis", groupPackage = "io.vertx") 2 | package io.vertx.redis; 3 | 4 | import io.vertx.codegen.annotations.ModuleGen; 5 | -------------------------------------------------------------------------------- /src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | 2 | module io.vertx.redis.client { 3 | requires io.vertx.core; 4 | requires io.vertx.core.logging; 5 | requires static io.vertx.docgen; 6 | requires static io.vertx.codegen.api; 7 | requires static io.vertx.codegen.json; 8 | exports io.vertx.redis.client; 9 | } 10 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/client/ClusterUtils.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.client; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.core.Vertx; 5 | import io.vertx.redis.client.Command; 6 | import io.vertx.redis.client.Redis; 7 | import io.vertx.redis.client.RedisConnection; 8 | import io.vertx.redis.client.Request; 9 | import io.vertx.redis.client.Response; 10 | 11 | import java.util.function.BiPredicate; 12 | 13 | class ClusterUtils { 14 | private final Vertx vertx; 15 | private final Redis client; 16 | 17 | ClusterUtils(Vertx vertx, Redis client) { 18 | this.vertx = vertx; 19 | this.client = client; 20 | } 21 | 22 | Future connectToMasterThatServesSlot(int slot) { 23 | return connectToMasterAndReturnId((min, max) -> slot >= min && slot <= max); 24 | } 25 | 26 | Future connectToMasterThatDoesntServeSlot(int slot) { 27 | return connectToMasterAndReturnId((min, max) -> slot < min || slot > max); 28 | } 29 | 30 | private Future connectToMasterAndReturnId(BiPredicate predicate) { 31 | return client.send(Request.cmd(Command.CLUSTER).arg("SLOTS")) 32 | .compose(slots -> { 33 | for (int i = 0; i < slots.size(); i++) { 34 | Response slot = slots.get(i); 35 | if (predicate.test(slot.get(0).toInteger(), slot.get(1).toInteger())) { 36 | Response node = slot.get(2); 37 | String endpoint = "redis://" + node.get(0) + ":" + node.get(1); 38 | String id = node.get(2).toString(); 39 | Redis redis = Redis.createClient(vertx, endpoint); 40 | return redis.connect() 41 | .map(conn -> new Result(redis, conn, id)); 42 | } 43 | } 44 | return Future.failedFuture("Couldn't find matching slot"); 45 | }); 46 | } 47 | 48 | static class Result { 49 | final Redis redis; 50 | final RedisConnection conn; 51 | final String id; 52 | 53 | Result(Redis redis, RedisConnection conn, String id) { 54 | this.redis = redis; 55 | this.conn = conn; 56 | this.id = id; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/client/Pair.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.client; 2 | 3 | final class Pair { 4 | final A a; 5 | final B b; 6 | 7 | Pair(A a, B b) { 8 | this.a = a; 9 | this.b = b; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/client/PreservesContext.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.client; 2 | 3 | import io.vertx.core.Context; 4 | import io.vertx.core.Vertx; 5 | import io.vertx.core.internal.ContextInternal; 6 | import io.vertx.ext.unit.Async; 7 | import io.vertx.ext.unit.TestContext; 8 | import io.vertx.redis.client.Command; 9 | import io.vertx.redis.client.Redis; 10 | import io.vertx.redis.client.RedisConnection; 11 | import io.vertx.redis.client.Request; 12 | 13 | import java.util.Collections; 14 | 15 | class PreservesContext { 16 | static void sendWithoutConnect(Redis client, TestContext test) { 17 | Async async = test.async(); 18 | 19 | Context context = ContextInternal.current().duplicate(); 20 | context.runOnContext(ignored -> { 21 | client.send(Request.cmd(Command.PING)).onComplete(result -> { 22 | test.assertTrue(result.succeeded()); 23 | test.assertTrue(context == Vertx.currentContext()); 24 | async.complete(); 25 | }); 26 | }); 27 | } 28 | 29 | static void batchWithoutConnect(Redis client, TestContext test) { 30 | Async async = test.async(); 31 | 32 | Context context = ContextInternal.current().duplicate(); 33 | context.runOnContext(ignored -> { 34 | client.batch(Collections.singletonList(Request.cmd(Command.PING))).onComplete(result -> { 35 | test.assertTrue(result.succeeded()); 36 | test.assertTrue(context == Vertx.currentContext()); 37 | async.complete(); 38 | }); 39 | }); 40 | } 41 | 42 | static void connect(Redis client, TestContext test) { 43 | Async async = test.async(); 44 | 45 | Context context = ContextInternal.current().duplicate(); 46 | context.runOnContext(ignored -> { 47 | client.connect().onComplete(connectResult -> { 48 | test.assertTrue(connectResult.succeeded()); 49 | test.assertTrue(context == Vertx.currentContext()); 50 | async.complete(); 51 | }); 52 | }); 53 | } 54 | 55 | static void connectThenSend(Redis client, TestContext test) { 56 | Async async = test.async(); 57 | 58 | Context context = ContextInternal.current().duplicate(); 59 | context.runOnContext(ignored -> { 60 | client.connect().onComplete(connectResult -> { 61 | test.assertTrue(connectResult.succeeded()); 62 | 63 | RedisConnection connection = connectResult.result(); 64 | connection.send(Request.cmd(Command.PING)).onComplete(result -> { 65 | test.assertTrue(result.succeeded()); 66 | test.assertTrue(context == Vertx.currentContext()); 67 | async.complete(); 68 | }); 69 | }); 70 | }); 71 | } 72 | 73 | static void connectThenBatch(Redis client, TestContext test) { 74 | Async async = test.async(); 75 | 76 | Context context = ContextInternal.current().duplicate(); 77 | context.runOnContext(ignored -> { 78 | client.connect().onComplete(connectResult -> { 79 | test.assertTrue(connectResult.succeeded()); 80 | 81 | RedisConnection connection = connectResult.result(); 82 | connection.batch(Collections.singletonList(Request.cmd(Command.PING))).onComplete(result -> { 83 | test.assertTrue(result.succeeded()); 84 | test.assertTrue(context == Vertx.currentContext()); 85 | async.complete(); 86 | }); 87 | }); 88 | }); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/client/RedisClient5SecureFailedTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.client; 2 | 3 | import io.vertx.ext.unit.Async; 4 | import io.vertx.ext.unit.TestContext; 5 | import io.vertx.ext.unit.junit.RunTestOnContext; 6 | import io.vertx.ext.unit.junit.VertxUnitRunner; 7 | import io.vertx.redis.client.Redis; 8 | import io.vertx.redis.client.RedisOptions; 9 | import io.vertx.tests.redis.containers.RedisStandalone; 10 | import org.junit.After; 11 | import org.junit.Before; 12 | import org.junit.ClassRule; 13 | import org.junit.Rule; 14 | import org.junit.Test; 15 | import org.junit.runner.RunWith; 16 | 17 | import static io.vertx.redis.client.Command.GET; 18 | import static io.vertx.redis.client.Request.cmd; 19 | import static io.vertx.tests.redis.client.TestUtils.randomKey; 20 | 21 | @RunWith(VertxUnitRunner.class) 22 | public class RedisClient5SecureFailedTest { 23 | 24 | @ClassRule 25 | public static final RedisStandalone redis = RedisStandalone.builder().setVersion("5.0").setPassword("foobar").build(); 26 | 27 | @Rule 28 | public final RunTestOnContext rule = new RunTestOnContext(); 29 | 30 | private Redis client; 31 | 32 | @Before 33 | public void before() { 34 | client = Redis.createClient( 35 | rule.vertx(), 36 | new RedisOptions().setConnectionString("redis://:foobared@" + redis.getHost() + ":" + redis.getPort())); 37 | } 38 | 39 | @After 40 | public void after() { 41 | client.close(); 42 | } 43 | 44 | @Test 45 | public void testBasicInterop(TestContext should) { 46 | final Async test = should.async(); 47 | final String nonexisting = randomKey(); 48 | 49 | client.send(cmd(GET).arg(nonexisting)).onComplete(reply -> { 50 | should.assertTrue(reply.failed()); 51 | should.assertEquals("ERR invalid password", reply.cause().getMessage()); 52 | should.assertNull(reply.result()); 53 | test.complete(); 54 | }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/client/RedisClient5SecureTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.client; 2 | 3 | import io.vertx.ext.unit.Async; 4 | import io.vertx.ext.unit.TestContext; 5 | import io.vertx.ext.unit.junit.RunTestOnContext; 6 | import io.vertx.ext.unit.junit.VertxUnitRunner; 7 | import io.vertx.redis.client.Redis; 8 | import io.vertx.redis.client.RedisOptions; 9 | import io.vertx.tests.redis.containers.RedisStandalone; 10 | import org.junit.After; 11 | import org.junit.Before; 12 | import org.junit.ClassRule; 13 | import org.junit.Rule; 14 | import org.junit.Test; 15 | import org.junit.runner.RunWith; 16 | 17 | import static io.vertx.redis.client.Command.GET; 18 | import static io.vertx.redis.client.Request.cmd; 19 | import static io.vertx.tests.redis.client.TestUtils.randomKey; 20 | 21 | @RunWith(VertxUnitRunner.class) 22 | public class RedisClient5SecureTest { 23 | 24 | @ClassRule 25 | public static final RedisStandalone redis = RedisStandalone.builder().setVersion("5.0").setPassword("foobar").build(); 26 | 27 | @Rule 28 | public final RunTestOnContext rule = new RunTestOnContext(); 29 | 30 | private Redis client; 31 | 32 | @Before 33 | public void before() { 34 | client = Redis.createClient( 35 | rule.vertx(), 36 | new RedisOptions().setConnectionString("redis://:foobar@" + redis.getHost() + ":" + redis.getPort())); 37 | } 38 | 39 | @After 40 | public void after() { 41 | client.close(); 42 | } 43 | 44 | @Test 45 | public void testBasicInterop(TestContext should) { 46 | final Async test = should.async(); 47 | final String nonexisting = randomKey(); 48 | 49 | client.send(cmd(GET).arg(nonexisting)).onComplete(reply -> { 50 | should.assertTrue(reply.succeeded()); 51 | should.assertNull(reply.result()); 52 | test.complete(); 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/client/RedisClient5Test.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.client; 2 | 3 | import io.vertx.core.json.JsonObject; 4 | import io.vertx.ext.unit.Async; 5 | import io.vertx.ext.unit.TestContext; 6 | import io.vertx.ext.unit.junit.RunTestOnContext; 7 | import io.vertx.ext.unit.junit.VertxUnitRunner; 8 | import io.vertx.redis.client.Redis; 9 | import io.vertx.redis.client.RedisOptions; 10 | import io.vertx.tests.redis.containers.RedisStandalone; 11 | import org.junit.After; 12 | import org.junit.Before; 13 | import org.junit.ClassRule; 14 | import org.junit.Rule; 15 | import org.junit.Test; 16 | import org.junit.runner.RunWith; 17 | 18 | import static io.vertx.redis.client.Command.GET; 19 | import static io.vertx.redis.client.Command.HGETALL; 20 | import static io.vertx.redis.client.Command.HSET; 21 | import static io.vertx.redis.client.Command.SET; 22 | import static io.vertx.redis.client.Request.cmd; 23 | import static io.vertx.tests.redis.client.TestUtils.randomKey; 24 | 25 | @RunWith(VertxUnitRunner.class) 26 | public class RedisClient5Test { 27 | 28 | @ClassRule 29 | public static final RedisStandalone redis = RedisStandalone.builder().setVersion("5.0").build(); 30 | 31 | @Rule 32 | public final RunTestOnContext rule = new RunTestOnContext(); 33 | 34 | private Redis client; 35 | 36 | @Before 37 | public void before(TestContext should) { 38 | final Async before = should.async(); 39 | 40 | client = Redis.createClient( 41 | rule.vertx(), 42 | new RedisOptions().setConnectionString(redis.getRedisUri())); 43 | 44 | client.connect().onComplete(onConnect -> { 45 | should.assertTrue(onConnect.succeeded()); 46 | before.complete(); 47 | }); 48 | } 49 | 50 | @After 51 | public void after() { 52 | client.close(); 53 | } 54 | 55 | @Test(timeout = 10_000L) 56 | public void testBasicInterop(TestContext should) { 57 | final Async test = should.async(); 58 | final String nonexisting = randomKey(); 59 | final String mykey = randomKey(); 60 | 61 | client.send(cmd(GET).arg(nonexisting)).onComplete(reply0 -> { 62 | should.assertTrue(reply0.succeeded()); 63 | should.assertNull(reply0.result()); 64 | 65 | client.send(cmd(SET).arg(mykey).arg("Hello")).onComplete(reply1 -> { 66 | should.assertTrue(reply1.succeeded()); 67 | client.send(cmd(GET).arg(mykey)).onComplete(reply2 -> { 68 | should.assertTrue(reply2.succeeded()); 69 | should.assertEquals("Hello", reply2.result().toString()); 70 | test.complete(); 71 | }); 72 | }); 73 | }); 74 | } 75 | 76 | @Test(timeout = 10_000L) 77 | public void testJson(TestContext should) { 78 | final Async test = should.async(); 79 | final String mykey = randomKey(); 80 | 81 | JsonObject json = new JsonObject() 82 | .put("key", "value"); 83 | 84 | client.send(cmd(HSET).arg(mykey).arg(json)) 85 | .compose(ignored -> client.send(cmd(HGETALL).arg(mykey))) 86 | .onComplete(should.asyncAssertSuccess(value -> { 87 | should.assertEquals("value", value.get("key").toString()); 88 | test.complete(); 89 | })); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/client/RedisClient6SecureFailedTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.client; 2 | 3 | import io.vertx.ext.unit.Async; 4 | import io.vertx.ext.unit.TestContext; 5 | import io.vertx.ext.unit.junit.RunTestOnContext; 6 | import io.vertx.ext.unit.junit.VertxUnitRunner; 7 | import io.vertx.redis.client.Redis; 8 | import io.vertx.redis.client.RedisOptions; 9 | import io.vertx.redis.client.impl.types.ErrorType; 10 | import io.vertx.tests.redis.containers.RedisStandalone; 11 | import org.junit.After; 12 | import org.junit.Before; 13 | import org.junit.ClassRule; 14 | import org.junit.Rule; 15 | import org.junit.Test; 16 | import org.junit.runner.RunWith; 17 | 18 | import static io.vertx.redis.client.Command.GET; 19 | import static io.vertx.redis.client.Request.cmd; 20 | import static io.vertx.tests.redis.client.TestUtils.randomKey; 21 | 22 | @RunWith(VertxUnitRunner.class) 23 | public class RedisClient6SecureFailedTest { 24 | 25 | @ClassRule 26 | public static final RedisStandalone redis = RedisStandalone.builder().setVersion("6.2").setPassword("foobar").build(); 27 | 28 | @Rule 29 | public final RunTestOnContext rule = new RunTestOnContext(); 30 | 31 | private Redis client; 32 | 33 | @Before 34 | public void before() { 35 | client = Redis.createClient( 36 | rule.vertx(), 37 | new RedisOptions().setConnectionString("redis://:foobared@" + redis.getHost() + ":" + redis.getPort())); 38 | } 39 | 40 | @After 41 | public void after() { 42 | client.close(); 43 | } 44 | 45 | @Test 46 | public void testBasicInterop(TestContext should) { 47 | final Async test = should.async(); 48 | final String nonexisting = randomKey(); 49 | 50 | client.send(cmd(GET).arg(nonexisting)).onComplete(reply -> { 51 | should.assertTrue(reply.failed()); 52 | should.assertTrue(((ErrorType) reply.cause()).is("WRONGPASS")); 53 | test.complete(); 54 | }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/client/RedisClient6SecureTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.client; 2 | 3 | import io.vertx.ext.unit.Async; 4 | import io.vertx.ext.unit.TestContext; 5 | import io.vertx.ext.unit.junit.RunTestOnContext; 6 | import io.vertx.ext.unit.junit.VertxUnitRunner; 7 | import io.vertx.redis.client.Redis; 8 | import io.vertx.redis.client.RedisOptions; 9 | import io.vertx.tests.redis.containers.RedisStandalone; 10 | import org.junit.After; 11 | import org.junit.Before; 12 | import org.junit.ClassRule; 13 | import org.junit.Rule; 14 | import org.junit.Test; 15 | import org.junit.runner.RunWith; 16 | 17 | import static io.vertx.redis.client.Command.GET; 18 | import static io.vertx.redis.client.Request.cmd; 19 | import static io.vertx.tests.redis.client.TestUtils.randomKey; 20 | 21 | @RunWith(VertxUnitRunner.class) 22 | public class RedisClient6SecureTest { 23 | 24 | @ClassRule 25 | public static final RedisStandalone redis = RedisStandalone.builder().setVersion("6.2").setPassword("foobar").build(); 26 | 27 | @Rule 28 | public final RunTestOnContext rule = new RunTestOnContext(); 29 | 30 | private Redis client; 31 | 32 | @Before 33 | public void before() { 34 | client = Redis.createClient( 35 | rule.vertx(), 36 | new RedisOptions().setConnectionString("redis://:foobar@" + redis.getHost() + ":" + redis.getPort())); 37 | } 38 | 39 | @After 40 | public void after() { 41 | client.close(); 42 | } 43 | 44 | @Test 45 | public void testBasicInterop(TestContext should) { 46 | final Async test = should.async(); 47 | final String nonexisting = randomKey(); 48 | 49 | client.send(cmd(GET).arg(nonexisting)).onComplete(reply -> { 50 | should.assertTrue(reply.succeeded()); 51 | should.assertNull(reply.result()); 52 | test.complete(); 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/client/RedisClientMetricsTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.client; 2 | 3 | import io.vertx.core.Vertx; 4 | import io.vertx.core.net.SocketAddress; 5 | import io.vertx.core.spi.metrics.ClientMetrics; 6 | import io.vertx.core.spi.metrics.VertxMetrics; 7 | import io.vertx.ext.unit.Async; 8 | import io.vertx.ext.unit.TestContext; 9 | import io.vertx.ext.unit.junit.VertxUnitRunner; 10 | import io.vertx.redis.client.Command; 11 | import io.vertx.redis.client.Redis; 12 | import io.vertx.redis.client.RedisOptions; 13 | import io.vertx.redis.client.Request; 14 | import io.vertx.redis.client.impl.CommandImpl; 15 | import io.vertx.tests.redis.containers.RedisStandalone; 16 | import org.junit.After; 17 | import org.junit.Before; 18 | import org.junit.ClassRule; 19 | import org.junit.Test; 20 | import org.junit.runner.RunWith; 21 | 22 | import java.util.ArrayList; 23 | import java.util.Arrays; 24 | import java.util.Collections; 25 | import java.util.List; 26 | 27 | @RunWith(VertxUnitRunner.class) 28 | public class RedisClientMetricsTest { 29 | @ClassRule 30 | public static final RedisStandalone redis = new RedisStandalone(); 31 | 32 | Vertx vertx; 33 | ClientMetrics metrics; 34 | Redis client; 35 | String reportedNamespace; 36 | 37 | @Before 38 | public void setup() { 39 | vertx = Vertx.builder() 40 | .withMetrics(ignored -> new VertxMetrics() { 41 | @Override 42 | public ClientMetrics createClientMetrics(SocketAddress remoteAddress, String type, String namespace) { 43 | reportedNamespace = namespace; 44 | return metrics; 45 | } 46 | }) 47 | .build(); 48 | client = Redis.createClient(vertx, new RedisOptions().setConnectionString(redis.getRedisUri()).setMetricsName("the-namespace")); 49 | } 50 | 51 | @After 52 | public void teardown(TestContext test) { 53 | vertx.close().onComplete(test.asyncAssertSuccess()); 54 | } 55 | 56 | @Test 57 | public void success(TestContext test) { 58 | testClientMetrics(test, Request.cmd(Command.PING), true); 59 | } 60 | 61 | @Test 62 | public void failure(TestContext test) { 63 | testClientMetrics(test, Request.cmd(new CommandImpl("NONEXISTING COMMAND", 0, true, false, false)), false); 64 | } 65 | 66 | private void testClientMetrics(TestContext test, Request request, boolean success) { 67 | Async async = test.async(); 68 | 69 | Object metric = new Object(); 70 | List actions = Collections.synchronizedList(new ArrayList<>()); 71 | 72 | metrics = new ClientMetrics() { 73 | @Override 74 | public Object requestBegin(String uri, Object request) { 75 | actions.add("requestBegin"); 76 | return metric; 77 | } 78 | 79 | @Override 80 | public void requestEnd(Object requestMetric, long bytesWritten) { 81 | test.assertTrue(requestMetric == metric); 82 | actions.add("requestEnd"); 83 | } 84 | 85 | @Override 86 | public void responseBegin(Object requestMetric, Object response) { 87 | test.assertTrue(requestMetric == metric); 88 | actions.add("responseBegin"); 89 | } 90 | 91 | @Override 92 | public void responseEnd(Object requestMetric, long bytesRead) { 93 | test.assertTrue(requestMetric == metric); 94 | actions.add("responseEnd"); 95 | } 96 | 97 | @Override 98 | public void requestReset(Object requestMetric) { 99 | test.assertTrue(requestMetric == metric); 100 | actions.add("fail"); 101 | } 102 | }; 103 | 104 | vertx.runOnContext(ignored -> { 105 | client.send(request).onComplete(result -> { 106 | if (success) { 107 | test.assertTrue(result.succeeded()); 108 | test.assertEquals(Arrays.asList("requestBegin", "requestEnd", "responseBegin", "responseEnd"), actions); 109 | } else { 110 | test.assertTrue(result.failed()); 111 | test.assertEquals(Arrays.asList("requestBegin", "requestEnd", "fail"), actions); 112 | } 113 | test.assertEquals("the-namespace", reportedNamespace); 114 | async.complete(); 115 | }); 116 | }); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/client/RedisClientPikaSecureTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.client; 2 | 3 | import io.vertx.core.json.JsonObject; 4 | import io.vertx.ext.unit.Async; 5 | import io.vertx.ext.unit.TestContext; 6 | import io.vertx.ext.unit.junit.RunTestOnContext; 7 | import io.vertx.ext.unit.junit.VertxUnitRunner; 8 | import io.vertx.redis.client.Redis; 9 | import io.vertx.redis.client.RedisOptions; 10 | import org.junit.After; 11 | import org.junit.Before; 12 | import org.junit.ClassRule; 13 | import org.junit.Rule; 14 | import org.junit.Test; 15 | import org.junit.runner.RunWith; 16 | import org.testcontainers.containers.BindMode; 17 | import org.testcontainers.containers.GenericContainer; 18 | 19 | import static io.vertx.redis.client.Command.GET; 20 | import static io.vertx.redis.client.Command.HGETALL; 21 | import static io.vertx.redis.client.Command.HSET; 22 | import static io.vertx.redis.client.Command.SET; 23 | import static io.vertx.redis.client.Request.cmd; 24 | import static io.vertx.tests.redis.client.TestUtils.randomKey; 25 | 26 | @RunWith(VertxUnitRunner.class) 27 | public class RedisClientPikaSecureTest { 28 | 29 | @Rule 30 | public final RunTestOnContext rule = new RunTestOnContext(); 31 | 32 | @ClassRule 33 | public static final GenericContainer redis = new GenericContainer<>("pikadb/pika:latest") 34 | .withClasspathResourceMapping("pika.conf", "/pika/conf/pika.conf", BindMode.READ_ONLY) 35 | .withExposedPorts(9221); 36 | 37 | private Redis client; 38 | 39 | @Before 40 | public void before(TestContext should) { 41 | final Async before = should.async(); 42 | 43 | client = Redis.createClient( 44 | rule.vertx(), 45 | new RedisOptions().setConnectionString("redis://:userpass@" + redis.getHost() + ":" + redis.getFirstMappedPort())); 46 | 47 | client.connect().onComplete(onConnect -> { 48 | should.assertTrue(onConnect.succeeded()); 49 | before.complete(); 50 | }); 51 | } 52 | 53 | @After 54 | public void after() { 55 | client.close(); 56 | } 57 | 58 | @Test(timeout = 10_000L) 59 | public void testBasicInterop(TestContext should) { 60 | final Async test = should.async(); 61 | final String nonexisting = randomKey(); 62 | final String mykey = randomKey(); 63 | 64 | client.send(cmd(GET).arg(nonexisting)).onComplete(reply0 -> { 65 | should.assertTrue(reply0.succeeded()); 66 | should.assertNull(reply0.result()); 67 | 68 | client.send(cmd(SET).arg(mykey).arg("Hello")).onComplete(reply1 -> { 69 | should.assertTrue(reply1.succeeded()); 70 | client.send(cmd(GET).arg(mykey)).onComplete(reply2 -> { 71 | should.assertTrue(reply2.succeeded()); 72 | should.assertEquals("Hello", reply2.result().toString()); 73 | test.complete(); 74 | }); 75 | }); 76 | }); 77 | } 78 | 79 | @Test(timeout = 10_000L) 80 | public void testJson(TestContext should) { 81 | final Async test = should.async(); 82 | final String mykey = randomKey(); 83 | 84 | JsonObject json = new JsonObject() 85 | .put("key", "value"); 86 | 87 | client.send(cmd(HSET).arg(mykey).arg(json)) 88 | .compose(ignored -> client.send(cmd(HGETALL).arg(mykey))) 89 | .onComplete(should.asyncAssertSuccess(value -> { 90 | should.assertEquals("value", value.get("key").toString()); 91 | test.complete(); 92 | })); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/client/RedisClientPikaTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.client; 2 | 3 | import io.vertx.core.json.JsonObject; 4 | import io.vertx.ext.unit.Async; 5 | import io.vertx.ext.unit.TestContext; 6 | import io.vertx.ext.unit.junit.RunTestOnContext; 7 | import io.vertx.ext.unit.junit.VertxUnitRunner; 8 | import io.vertx.redis.client.Redis; 9 | import io.vertx.redis.client.RedisOptions; 10 | import org.junit.After; 11 | import org.junit.Before; 12 | import org.junit.ClassRule; 13 | import org.junit.Rule; 14 | import org.junit.Test; 15 | import org.junit.runner.RunWith; 16 | import org.testcontainers.containers.GenericContainer; 17 | 18 | import static io.vertx.redis.client.Command.GET; 19 | import static io.vertx.redis.client.Command.HGETALL; 20 | import static io.vertx.redis.client.Command.HSET; 21 | import static io.vertx.redis.client.Command.SET; 22 | import static io.vertx.redis.client.Request.cmd; 23 | import static io.vertx.tests.redis.client.TestUtils.randomKey; 24 | 25 | @RunWith(VertxUnitRunner.class) 26 | public class RedisClientPikaTest { 27 | 28 | @Rule 29 | public final RunTestOnContext rule = new RunTestOnContext(); 30 | 31 | @ClassRule 32 | public static final GenericContainer redis = new GenericContainer<>("pikadb/pika:latest") 33 | .withExposedPorts(9221); 34 | 35 | private Redis client; 36 | 37 | @Before 38 | public void before(TestContext should) { 39 | final Async before = should.async(); 40 | 41 | client = Redis.createClient( 42 | rule.vertx(), 43 | new RedisOptions().setConnectionString("redis://" + redis.getHost() + ":" + redis.getFirstMappedPort())); 44 | 45 | client.connect().onComplete(onConnect -> { 46 | should.assertTrue(onConnect.succeeded()); 47 | before.complete(); 48 | }); 49 | } 50 | 51 | @After 52 | public void after() { 53 | client.close(); 54 | } 55 | 56 | @Test(timeout = 10_000L) 57 | public void testBasicInterop(TestContext should) { 58 | final Async test = should.async(); 59 | final String nonexisting = randomKey(); 60 | final String mykey = randomKey(); 61 | 62 | client.send(cmd(GET).arg(nonexisting)).onComplete(reply0 -> { 63 | should.assertTrue(reply0.succeeded()); 64 | should.assertNull(reply0.result()); 65 | 66 | client.send(cmd(SET).arg(mykey).arg("Hello")).onComplete(reply1 -> { 67 | should.assertTrue(reply1.succeeded()); 68 | client.send(cmd(GET).arg(mykey)).onComplete(reply2 -> { 69 | should.assertTrue(reply2.succeeded()); 70 | should.assertEquals("Hello", reply2.result().toString()); 71 | test.complete(); 72 | }); 73 | }); 74 | }); 75 | } 76 | 77 | @Test(timeout = 10_000L) 78 | public void testJson(TestContext should) { 79 | final Async test = should.async(); 80 | final String mykey = randomKey(); 81 | 82 | JsonObject json = new JsonObject() 83 | .put("key", "value"); 84 | 85 | client.send(cmd(HSET).arg(mykey).arg(json)) 86 | .compose(ignored -> client.send(cmd(HGETALL).arg(mykey))) 87 | .onComplete(should.asyncAssertSuccess(value -> { 88 | should.assertEquals("value", value.get("key").toString()); 89 | test.complete(); 90 | })); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/client/RedisClientREJSONTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Red Hat, Inc. 3 | *

4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | *

8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | *

11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | *

14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | package io.vertx.tests.redis.client; 17 | 18 | import io.vertx.core.Future; 19 | import io.vertx.ext.unit.Async; 20 | import io.vertx.ext.unit.TestContext; 21 | import io.vertx.ext.unit.junit.RunTestOnContext; 22 | import io.vertx.ext.unit.junit.VertxUnitRunner; 23 | import io.vertx.redis.client.Command; 24 | import io.vertx.redis.client.Redis; 25 | import org.junit.ClassRule; 26 | import org.junit.Rule; 27 | import org.junit.Test; 28 | import org.junit.runner.RunWith; 29 | import org.testcontainers.containers.GenericContainer; 30 | 31 | import static io.vertx.redis.client.Request.cmd; 32 | 33 | /** 34 | * This test relies on a REJSON capable Redis server. 35 | */ 36 | @RunWith(VertxUnitRunner.class) 37 | public class RedisClientREJSONTest { 38 | 39 | @Rule 40 | public final RunTestOnContext rule = new RunTestOnContext(); 41 | 42 | @ClassRule 43 | public static final GenericContainer container = new GenericContainer<>("redislabs/rejson:2.4.7") 44 | .withExposedPorts(6379); 45 | 46 | @Test(timeout = 30_000L) 47 | public void testJsonSetAndGet(TestContext should) { 48 | final Async test = should.async(); 49 | 50 | final Redis client = Redis.createClient( 51 | rule.vertx(), 52 | "redis://" + container.getHost() + ":" + container.getFirstMappedPort()); 53 | 54 | client 55 | .send(cmd(Command.create("JSON.SET")).arg("foo").arg(".").arg("\"bar\"")) 56 | .compose(response -> { 57 | // OK 58 | return client.send(cmd(Command.create("JSON.GET")).arg("foo")); 59 | }) 60 | .compose(response -> { 61 | should.assertEquals("\"bar\"", response.toString()); 62 | return client.send(cmd(Command.create("JSON.TYPE")).arg("foo").arg(".")); 63 | }) 64 | .compose(response -> { 65 | should.assertEquals("string", response.toString()); 66 | return Future.succeededFuture(); 67 | }) 68 | .onFailure(should::fail) 69 | .onSuccess(v -> { 70 | test.complete(); 71 | }); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/client/RedisClientTLSTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.client; 2 | 3 | import io.vertx.core.buffer.Buffer; 4 | import io.vertx.core.net.JksOptions; 5 | import io.vertx.core.net.NetClientOptions; 6 | import io.vertx.ext.unit.Async; 7 | import io.vertx.ext.unit.TestContext; 8 | import io.vertx.ext.unit.junit.RunTestOnContext; 9 | import io.vertx.ext.unit.junit.VertxUnitRunner; 10 | import io.vertx.redis.client.Command; 11 | import io.vertx.redis.client.Redis; 12 | import io.vertx.redis.client.RedisOptions; 13 | import io.vertx.redis.client.Request; 14 | import io.vertx.tests.redis.containers.KeyPairAndCertificate; 15 | import io.vertx.tests.redis.containers.RedisStandalone; 16 | import org.junit.ClassRule; 17 | import org.junit.Rule; 18 | import org.junit.Test; 19 | import org.junit.runner.RunWith; 20 | 21 | @RunWith(VertxUnitRunner.class) 22 | public class RedisClientTLSTest { 23 | 24 | private static final KeyPairAndCertificate serverKey = KeyPairAndCertificate.generateSelfSigned("redis"); 25 | 26 | private static final KeyPairAndCertificate clientKey = KeyPairAndCertificate.generateSelfSigned("test"); 27 | 28 | private static NetClientOptions createNetClientOptions() { 29 | return new NetClientOptions() 30 | .setKeyCertOptions(new JksOptions().setValue(Buffer.buffer(clientKey.privateKeyAsJKS())).setPassword("")) 31 | .setTrustOptions(new JksOptions().setValue(Buffer.buffer(serverKey.certificateAsJKS()))) 32 | .setTcpKeepAlive(true) 33 | .setTcpNoDelay(true) 34 | // we cannot know the hostname, and hostname verification is not important for this test 35 | .setHostnameVerificationAlgorithm(""); 36 | } 37 | 38 | @ClassRule 39 | public static final RedisStandalone redis = RedisStandalone.builder() 40 | .enableTls(serverKey) 41 | .enableMutualTls(clientKey) 42 | .build(); 43 | 44 | @Rule 45 | public final RunTestOnContext rule = new RunTestOnContext(); 46 | 47 | @Test 48 | public void testConnection(TestContext should) { 49 | final Async test = should.async(); 50 | 51 | Redis client = Redis.createClient( 52 | rule.vertx(), 53 | new RedisOptions() 54 | .setNetClientOptions(createNetClientOptions()) 55 | .setConnectionString("rediss://" + redis.getHost() + ":" + redis.getPort())); 56 | 57 | client.connect() 58 | .onComplete(should.asyncAssertSuccess(conn -> { 59 | conn.send(Request.cmd(Command.PING)) 60 | .onComplete(should.asyncAssertSuccess(ignored -> { 61 | conn.close(); 62 | test.complete(); 63 | })); 64 | })); 65 | } 66 | 67 | @Test 68 | public void testInvalidConnection(TestContext should) { 69 | final Async test = should.async(); 70 | 71 | Redis client = Redis.createClient( 72 | rule.vertx(), 73 | new RedisOptions() 74 | .setNetClientOptions(createNetClientOptions()) 75 | // in this test, Redis requires SSL and doesn't accept plain text connections; 76 | // the connection string has wrong scheme 77 | .setConnectionString("redis://" + redis.getHost() + ":" + redis.getPort())); 78 | 79 | client.connect() 80 | .onComplete(should.asyncAssertFailure(ignored -> { 81 | test.complete(); 82 | })); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/client/RedisClientToBadServerTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.client; 2 | 3 | import io.vertx.core.net.NetSocket; 4 | import io.vertx.ext.unit.Async; 5 | import io.vertx.ext.unit.TestContext; 6 | import io.vertx.ext.unit.junit.RunTestOnContext; 7 | import io.vertx.ext.unit.junit.VertxUnitRunner; 8 | import io.vertx.redis.client.Redis; 9 | import io.vertx.redis.client.RedisOptions; 10 | import org.junit.Rule; 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | 14 | @RunWith(VertxUnitRunner.class) 15 | public class RedisClientToBadServerTest { 16 | 17 | @Rule 18 | public final RunTestOnContext rule = new RunTestOnContext(); 19 | 20 | @Test 21 | public void helloTest(TestContext should) { 22 | final Async before = should.async(); 23 | 24 | rule.vertx() 25 | .createNetServer() 26 | .connectHandler(NetSocket::close) 27 | .listen(9876); 28 | 29 | Redis client = Redis.createClient( 30 | rule.vertx(), 31 | new RedisOptions().setConnectionString("redis://localhost:9876")); 32 | 33 | client.connect().onComplete(onConnect -> { 34 | should.assertFalse(onConnect.succeeded()); 35 | before.complete(); 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/client/RedisClusterAskTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.client; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.ext.unit.Async; 5 | import io.vertx.ext.unit.TestContext; 6 | import io.vertx.ext.unit.junit.RunTestOnContext; 7 | import io.vertx.ext.unit.junit.VertxUnitRunner; 8 | import io.vertx.redis.client.Command; 9 | import io.vertx.redis.client.Redis; 10 | import io.vertx.redis.client.RedisClientType; 11 | import io.vertx.redis.client.RedisConnection; 12 | import io.vertx.redis.client.RedisOptions; 13 | import io.vertx.redis.client.RedisReplicas; 14 | import io.vertx.redis.client.Request; 15 | import io.vertx.tests.redis.containers.RedisCluster; 16 | import org.junit.After; 17 | import org.junit.Before; 18 | import org.junit.ClassRule; 19 | import org.junit.Rule; 20 | import org.junit.Test; 21 | import org.junit.runner.RunWith; 22 | 23 | @RunWith(VertxUnitRunner.class) 24 | public class RedisClusterAskTest { 25 | @ClassRule 26 | public static final RedisCluster redis = new RedisCluster(); 27 | 28 | @Rule 29 | public final RunTestOnContext rule = new RunTestOnContext(); 30 | 31 | private final RedisOptions options = new RedisOptions() 32 | .setType(RedisClientType.CLUSTER) 33 | .setUseReplicas(RedisReplicas.NEVER) 34 | .addConnectionString(redis.getRedisNode0Uri()) 35 | .addConnectionString(redis.getRedisNode1Uri()) 36 | .addConnectionString(redis.getRedisNode2Uri()) 37 | .addConnectionString(redis.getRedisNode3Uri()) 38 | .addConnectionString(redis.getRedisNode4Uri()) 39 | .addConnectionString(redis.getRedisNode5Uri()); 40 | 41 | private Redis client; 42 | private ClusterUtils cluster; 43 | 44 | @Before 45 | public void createClient() { 46 | client = Redis.createClient(rule.vertx(), options); 47 | cluster = new ClusterUtils(rule.vertx(), client); 48 | } 49 | 50 | @After 51 | public void cleanRedis() { 52 | client.close(); 53 | } 54 | 55 | @Test 56 | public void test(TestContext test) { 57 | Async async = test.async(); 58 | 59 | // slot number: 16287 60 | // keys hashing to the slot: x, exs 61 | int slot = 16287; 62 | String key1 = "x"; 63 | String key2 = "exs"; 64 | 65 | client.connect().compose(clusterConn -> { 66 | return cluster.connectToMasterThatServesSlot(slot).compose(masterResult -> { 67 | Redis master = masterResult.redis; 68 | RedisConnection masterConn = masterResult.conn; 69 | String masterId = masterResult.id; 70 | return cluster.connectToMasterThatDoesntServeSlot(slot).compose(otherMasterResult -> { 71 | Redis otherMaster = otherMasterResult.redis; 72 | RedisConnection otherMasterConn = otherMasterResult.conn; 73 | String otherMasterId = otherMasterResult.id; 74 | return clusterConn.send(Request.cmd(Command.SET).arg(key1).arg("fubar")) 75 | .compose(ignored -> { 76 | return otherMasterConn.send(Request.cmd(Command.CLUSTER).arg("SETSLOT").arg(slot).arg("IMPORTING").arg(masterId)); 77 | }) 78 | .compose(ignored -> { 79 | return masterConn.send(Request.cmd(Command.CLUSTER).arg("SETSLOT").arg(slot).arg("MIGRATING").arg(otherMasterId)); 80 | }) 81 | .compose(ignored -> { 82 | return clusterConn.send(Request.cmd(Command.GET).arg(key1)); 83 | }) 84 | .compose(result -> { 85 | test.assertEquals("fubar", result.toString()); 86 | return clusterConn.send(Request.cmd(Command.GET).arg(key2)); // ASK 87 | }) 88 | .compose(result -> { 89 | test.assertEquals(null, result); 90 | return clusterConn.send(Request.cmd(Command.SET).arg(key2).arg("quux")); // ASK 91 | }) 92 | .compose(ignored -> { 93 | return clusterConn.send(Request.cmd(Command.GET).arg(key2)); // ASK 94 | }) 95 | .compose(result -> { 96 | test.assertEquals("quux", result.toString()); 97 | master.close(); 98 | otherMaster.close(); 99 | return Future.succeededFuture(); 100 | }); 101 | }); 102 | }); 103 | }).onComplete(test.asyncAssertSuccess(ignored -> { 104 | async.complete(); 105 | })); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/client/RedisClusterMovedTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.client; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.core.net.SocketAddress; 5 | import io.vertx.ext.unit.Async; 6 | import io.vertx.ext.unit.TestContext; 7 | import io.vertx.ext.unit.junit.RunTestOnContext; 8 | import io.vertx.ext.unit.junit.VertxUnitRunner; 9 | import io.vertx.redis.client.Command; 10 | import io.vertx.redis.client.Redis; 11 | import io.vertx.redis.client.RedisClientType; 12 | import io.vertx.redis.client.RedisConnection; 13 | import io.vertx.redis.client.RedisOptions; 14 | import io.vertx.redis.client.RedisReplicas; 15 | import io.vertx.redis.client.Request; 16 | import io.vertx.redis.client.impl.PooledRedisConnection; 17 | import io.vertx.tests.redis.containers.RedisCluster; 18 | import org.junit.After; 19 | import org.junit.Before; 20 | import org.junit.ClassRule; 21 | import org.junit.Rule; 22 | import org.junit.Test; 23 | import org.junit.runner.RunWith; 24 | 25 | @RunWith(VertxUnitRunner.class) 26 | public class RedisClusterMovedTest { 27 | @ClassRule 28 | public static final RedisCluster redis = new RedisCluster(); 29 | 30 | @Rule 31 | public final RunTestOnContext rule = new RunTestOnContext(); 32 | 33 | private final RedisOptions options = new RedisOptions() 34 | .setType(RedisClientType.CLUSTER) 35 | .setUseReplicas(RedisReplicas.NEVER) 36 | .addConnectionString(redis.getRedisNode0Uri()) 37 | .addConnectionString(redis.getRedisNode1Uri()) 38 | .addConnectionString(redis.getRedisNode2Uri()) 39 | .addConnectionString(redis.getRedisNode3Uri()) 40 | .addConnectionString(redis.getRedisNode4Uri()) 41 | .addConnectionString(redis.getRedisNode5Uri()); 42 | 43 | private Redis client; 44 | private ClusterUtils cluster; 45 | 46 | @Before 47 | public void createClient() { 48 | client = Redis.createClient(rule.vertx(), options); 49 | cluster = new ClusterUtils(rule.vertx(), client); 50 | } 51 | 52 | @After 53 | public void cleanRedis() { 54 | client.close(); 55 | } 56 | 57 | @Test 58 | public void test(TestContext test) { 59 | Async async = test.async(); 60 | 61 | // slot number: 16287 62 | // keys hashing to the slot: x, exs 63 | int slot = 16287; 64 | String key1 = "x"; 65 | String key2 = "exs"; 66 | 67 | client.connect().compose(clusterConn -> { 68 | return cluster.connectToMasterThatServesSlot(slot).compose(masterResult -> { 69 | Redis master = masterResult.redis; 70 | RedisConnection masterConn = masterResult.conn; 71 | String masterId = masterResult.id; 72 | return cluster.connectToMasterThatDoesntServeSlot(slot).compose(otherMasterResult -> { 73 | Redis otherMaster = otherMasterResult.redis; 74 | RedisConnection otherMasterConn = otherMasterResult.conn; 75 | String otherMasterId = otherMasterResult.id; 76 | return clusterConn.send(Request.cmd(Command.SET).arg(key1).arg("fubar")) 77 | .compose(ignored -> { 78 | return clusterConn.send(Request.cmd(Command.SET).arg(key2).arg("quux")); 79 | }) 80 | .compose(ignored -> { 81 | return otherMasterConn.send(Request.cmd(Command.CLUSTER).arg("SETSLOT").arg(slot).arg("IMPORTING").arg(masterId)); 82 | }) 83 | .compose(ignored -> { 84 | return masterConn.send(Request.cmd(Command.CLUSTER).arg("SETSLOT").arg(slot).arg("MIGRATING").arg(otherMasterId)); 85 | }) 86 | .compose(ignored -> { 87 | SocketAddress otherMasterAddr = ((PooledRedisConnection) otherMasterConn).actual().uri().socketAddress(); 88 | return masterConn.send(Request.cmd(Command.MIGRATE).arg(otherMasterAddr.host()).arg(otherMasterAddr.port()) 89 | .arg("").arg(0).arg(5000).arg("KEYS").arg(key1).arg(key2)); 90 | }) 91 | .compose(ignored -> { 92 | return masterConn.send(Request.cmd(Command.CLUSTER).arg("SETSLOT").arg(slot).arg("NODE").arg(otherMasterId)); 93 | }) 94 | .compose(ignored -> { 95 | return otherMasterConn.send(Request.cmd(Command.CLUSTER).arg("SETSLOT").arg(slot).arg("NODE").arg(otherMasterId)); 96 | }) 97 | .compose(ignored -> { 98 | return clusterConn.send(Request.cmd(Command.GET).arg(key1)); // MOVED 99 | }) 100 | .compose(result -> { 101 | test.assertEquals("fubar", result.toString()); 102 | return clusterConn.send(Request.cmd(Command.GET).arg(key2)); // not MOVED, slots were reread 103 | }) 104 | .compose(result -> { 105 | test.assertEquals("quux", result.toString()); 106 | master.close(); 107 | otherMaster.close(); 108 | return Future.succeededFuture(); 109 | }); 110 | }); 111 | }); 112 | }).onComplete(test.asyncAssertSuccess(ignored -> { 113 | async.complete(); 114 | })); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/client/RedisOptionsTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.client; 2 | 3 | import io.vertx.core.tracing.TracingPolicy; 4 | import io.vertx.redis.client.RedisClientType; 5 | import io.vertx.redis.client.RedisOptions; 6 | import io.vertx.redis.client.RedisRole; 7 | import org.junit.Test; 8 | 9 | import java.util.ArrayList; 10 | import java.util.Collections; 11 | import java.util.List; 12 | 13 | import static org.junit.Assert.assertEquals; 14 | 15 | public class RedisOptionsTest { 16 | 17 | @Test 18 | public void testRedisOptions() { 19 | RedisOptions options = new RedisOptions(); 20 | 21 | assertEquals(RedisClientType.STANDALONE, options.getType()); // default value 22 | assertEquals(6, options.getMaxPoolSize()); // default value 23 | assertEquals("redis://localhost:6379", options.getEndpoint()); // default value 24 | assertEquals(Collections.singletonList("redis://localhost:6379"), options.getEndpoints()); // default value 25 | } 26 | 27 | @Test 28 | public void testOptionsCopy() { 29 | List endpoints = new ArrayList<>(3); 30 | endpoints.add("redis://localhost:123"); 31 | endpoints.add("redis://localhost:124"); 32 | endpoints.add("redis://localhost:125"); 33 | 34 | // Set values for which there is no default to ensure they are copied correctly 35 | RedisOptions original = new RedisOptions() 36 | .setEndpoints(endpoints) 37 | .setMasterName("someOtherMaster") 38 | .setRole(RedisRole.SENTINEL) 39 | .setPassword("myPassword") 40 | .setTracingPolicy(TracingPolicy.ALWAYS); 41 | 42 | RedisOptions copy = new RedisOptions(original); 43 | 44 | assertEquals(RedisClientType.STANDALONE, copy.getType()); // default value 45 | assertEquals(6, copy.getMaxPoolSize()); // default value 46 | assertEquals(endpoints, copy.getEndpoints()); 47 | assertEquals("someOtherMaster", copy.getMasterName()); 48 | assertEquals(RedisRole.SENTINEL, copy.getRole()); 49 | assertEquals("myPassword", copy.getPassword()); 50 | assertEquals(TracingPolicy.ALWAYS, copy.getTracingPolicy()); 51 | } 52 | 53 | @Test 54 | public void testFromJsonInstance() { 55 | List endpoints = new ArrayList<>(3); 56 | endpoints.add("redis://localhost:123"); 57 | endpoints.add("redis://localhost:124"); 58 | endpoints.add("redis://localhost:125"); 59 | 60 | RedisOptions original = new RedisOptions() 61 | .setEndpoints(endpoints) 62 | .setMasterName("someOtherMaster") 63 | .setRole(RedisRole.SENTINEL) 64 | .setPassword("myPassword") 65 | .setTracingPolicy(TracingPolicy.ALWAYS); 66 | 67 | RedisOptions copy = new RedisOptions(original.toJson()); 68 | 69 | assertEquals(RedisClientType.STANDALONE, copy.getType()); // default value 70 | assertEquals(6, copy.getMaxPoolSize()); // default value 71 | assertEquals(endpoints, copy.getEndpoints()); 72 | assertEquals("someOtherMaster", copy.getMasterName()); 73 | assertEquals(RedisRole.SENTINEL, copy.getRole()); 74 | assertEquals("myPassword", copy.getPassword()); 75 | assertEquals(TracingPolicy.ALWAYS, copy.getTracingPolicy()); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/client/RedisProtocolVersionTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.client; 2 | 3 | import io.vertx.ext.unit.Async; 4 | import io.vertx.ext.unit.TestContext; 5 | import io.vertx.ext.unit.junit.RunTestOnContext; 6 | import io.vertx.ext.unit.junit.VertxUnitRunner; 7 | import io.vertx.redis.client.Command; 8 | import io.vertx.redis.client.ProtocolVersion; 9 | import io.vertx.redis.client.Redis; 10 | import io.vertx.redis.client.RedisOptions; 11 | import io.vertx.redis.client.Request; 12 | import io.vertx.tests.redis.containers.RedisStandalone; 13 | import org.junit.ClassRule; 14 | import org.junit.Rule; 15 | import org.junit.Test; 16 | import org.junit.runner.RunWith; 17 | 18 | import java.util.Arrays; 19 | import java.util.HashSet; 20 | 21 | @RunWith(VertxUnitRunner.class) 22 | public class RedisProtocolVersionTest { 23 | @ClassRule 24 | public static final RedisStandalone redis = new RedisStandalone(); 25 | 26 | @Rule 27 | public final RunTestOnContext rule = new RunTestOnContext(); 28 | 29 | @Test 30 | public void resp2(TestContext test) { 31 | RedisOptions options = new RedisOptions() 32 | .setConnectionString(redis.getRedisUri()) 33 | .setPreferredProtocolVersion(ProtocolVersion.RESP2); 34 | 35 | Redis client = Redis.createClient(rule.vertx(), options); 36 | 37 | Async async = test.async(); 38 | client 39 | .send(Request.cmd(Command.DEL).arg("myhash")) 40 | .flatMap(ignored -> { 41 | return client.send(Request.cmd(Command.HSET).arg("myhash").arg("field1").arg(1).arg("field2").arg(2)); 42 | }) 43 | .flatMap(response -> { 44 | test.assertEquals(2, response.toInteger()); 45 | return client.send(Request.cmd(Command.HGETALL).arg("myhash")); 46 | }) 47 | .onSuccess(response -> { 48 | test.assertTrue(response.isArray()); 49 | 50 | test.assertTrue(response.containsKey("field1")); 51 | test.assertEquals(1, response.get("field1").toInteger()); 52 | 53 | test.assertTrue(response.containsKey("field2")); 54 | test.assertEquals(2, response.get("field2").toInteger()); 55 | 56 | test.assertEquals(new HashSet<>(Arrays.asList("field1", "field2")), response.getKeys()); 57 | 58 | test.assertEquals("field1", response.get(0).toString()); 59 | 60 | client.close(); 61 | async.complete(); 62 | }).onFailure(test::fail); 63 | } 64 | 65 | @Test 66 | public void resp3(TestContext test) { 67 | RedisOptions options = new RedisOptions() 68 | .setConnectionString(redis.getRedisUri()) 69 | .setPreferredProtocolVersion(ProtocolVersion.RESP3); 70 | 71 | Redis client = Redis.createClient(rule.vertx(), options); 72 | 73 | Async async = test.async(); 74 | client 75 | .send(Request.cmd(Command.DEL).arg("myhash")) 76 | .flatMap(ignored -> { 77 | return client.send(Request.cmd(Command.HSET).arg("myhash").arg("field1").arg(3).arg("field2").arg(4)); 78 | }) 79 | .flatMap(response -> { 80 | test.assertEquals(2, response.toInteger()); 81 | return client.send(Request.cmd(Command.HGETALL).arg("myhash")); 82 | }) 83 | .onSuccess(response -> { 84 | test.assertTrue(response.isMap()); 85 | 86 | test.assertTrue(response.containsKey("field1")); 87 | test.assertEquals(3, response.get("field1").toInteger()); 88 | 89 | test.assertTrue(response.containsKey("field2")); 90 | test.assertEquals(4, response.get("field2").toInteger()); 91 | 92 | test.assertEquals(new HashSet<>(Arrays.asList("field1", "field2")), response.getKeys()); 93 | 94 | try { 95 | response.get(0); 96 | test.fail("Map-typed Multi should fail on get(int)"); 97 | } catch (Exception expected) { 98 | } 99 | 100 | client.close(); 101 | async.complete(); 102 | }).onFailure(test::fail); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/client/RedisPubSubClusterTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.client; 2 | 3 | import io.vertx.ext.unit.Async; 4 | import io.vertx.ext.unit.TestContext; 5 | import io.vertx.ext.unit.junit.RunTestOnContext; 6 | import io.vertx.ext.unit.junit.VertxUnitRunner; 7 | import io.vertx.redis.client.Command; 8 | import io.vertx.redis.client.EventBusHandler; 9 | import io.vertx.redis.client.Redis; 10 | import io.vertx.redis.client.RedisConnection; 11 | import io.vertx.redis.client.RedisOptions; 12 | import io.vertx.redis.client.Request; 13 | import io.vertx.tests.redis.containers.RedisCluster; 14 | import org.junit.After; 15 | import org.junit.Before; 16 | import org.junit.ClassRule; 17 | import org.junit.Rule; 18 | import org.junit.Test; 19 | import org.junit.runner.RunWith; 20 | 21 | @RunWith(VertxUnitRunner.class) 22 | public class RedisPubSubClusterTest { 23 | 24 | @ClassRule 25 | public static final RedisCluster redis = new RedisCluster(); 26 | 27 | @Rule 28 | public final RunTestOnContext rule = new RunTestOnContext(); 29 | 30 | private Redis redisPublish; 31 | private Redis redisSubscribe; 32 | 33 | private RedisConnection pubConn; 34 | private RedisConnection subConn; 35 | 36 | @Before 37 | public void before(TestContext should) { 38 | Async test = should.async(); 39 | RedisOptions options = new RedisOptions().setConnectionString(redis.getRedisNode0Uri()); 40 | 41 | redisPublish = Redis.createClient(rule.vertx(), options); 42 | redisPublish.connect().onComplete(should.asyncAssertSuccess(connectPub -> { 43 | pubConn = connectPub; 44 | 45 | redisSubscribe = Redis.createClient(rule.vertx(), options); 46 | redisSubscribe.connect().onComplete(should.asyncAssertSuccess(connectSub -> { 47 | subConn = connectSub; 48 | 49 | test.complete(); 50 | })); 51 | })); 52 | } 53 | 54 | @After 55 | public void after() { 56 | redisPublish.close(); 57 | redisSubscribe.close(); 58 | } 59 | 60 | @Test 61 | public void testSubscribeMultipleTimes(TestContext should) { 62 | final int N = 10; 63 | final String channel = "chan1"; 64 | 65 | final Async test = should.async(N); 66 | for (int i = 0; i < N; i++) { 67 | rule.vertx().eventBus().consumer(channel, msg -> { 68 | test.countDown(); 69 | }); 70 | } 71 | 72 | rule.vertx().eventBus().consumer("io.vertx.redis." + channel, msg -> { 73 | rule.vertx().eventBus().publish(channel, msg.body()); 74 | }); 75 | 76 | subConn.handler(EventBusHandler.create(rule.vertx())); 77 | subUnsub(channel, N, should.async(N)); 78 | 79 | rule.vertx().setTimer(1000, id -> { 80 | pubConn.send(Request.cmd(Command.PUBLISH).arg(channel).arg("hello")) 81 | .onComplete(should.asyncAssertSuccess()); 82 | }); 83 | } 84 | 85 | private void subUnsub(String channel, int attempts, Async testSub) { 86 | subConn.send(Request.cmd(Command.UNSUBSCRIBE).arg(channel)).onComplete(unreply -> { 87 | subConn.send(Request.cmd(Command.SUBSCRIBE).arg(channel)).onComplete(reply -> { 88 | testSub.countDown(); 89 | if (attempts > 1) { 90 | subUnsub(channel, attempts - 1, testSub); 91 | } 92 | }); 93 | }); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/client/RedisReconnectTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.client; 2 | 3 | import io.vertx.core.Context; 4 | import io.vertx.ext.unit.Async; 5 | import io.vertx.ext.unit.TestContext; 6 | import io.vertx.ext.unit.junit.RunTestOnContext; 7 | import io.vertx.ext.unit.junit.VertxUnitRunner; 8 | import io.vertx.redis.client.Command; 9 | import io.vertx.redis.client.Redis; 10 | import io.vertx.redis.client.RedisConnection; 11 | import io.vertx.redis.client.RedisOptions; 12 | import io.vertx.redis.client.Request; 13 | import io.vertx.tests.redis.containers.RedisStandalone; 14 | import org.junit.After; 15 | import org.junit.Before; 16 | import org.junit.ClassRule; 17 | import org.junit.Rule; 18 | import org.junit.Test; 19 | import org.junit.runner.RunWith; 20 | 21 | @RunWith(VertxUnitRunner.class) 22 | public class RedisReconnectTest { 23 | 24 | private static final int RETRIES = 10; 25 | 26 | @ClassRule 27 | public static final RedisStandalone redis = new RedisStandalone(); 28 | 29 | @Rule 30 | public final RunTestOnContext rule = new RunTestOnContext(); 31 | 32 | private Redis client; 33 | 34 | // this connection will mutate during tests with re-connect 35 | private RedisConnection connection; 36 | 37 | @Before 38 | public void before(TestContext should) { 39 | final Async before = should.async(); 40 | 41 | Context context = rule.vertx().getOrCreateContext(); 42 | client = Redis.createClient(rule.vertx(), new RedisOptions().setConnectionString(redis.getRedisUri())); 43 | client.connect().onComplete(onConnect -> { 44 | should.assertTrue(onConnect.succeeded()); 45 | should.assertEquals(context, rule.vertx().getOrCreateContext()); 46 | connection = onConnect.result(); 47 | before.complete(); 48 | }); 49 | } 50 | 51 | @After 52 | public void after() { 53 | client.close(); 54 | } 55 | 56 | @Test 57 | public void testConnection(TestContext should) { 58 | final Async test = should.async(); 59 | 60 | connection 61 | .exceptionHandler(should::fail) 62 | .endHandler(end -> { 63 | // the connection was closed, will reconnect 64 | reconnect(0); 65 | }) 66 | .send(Request.cmd(Command.CLIENT).arg("LIST")) 67 | .onFailure(should::fail) 68 | .onSuccess(list -> { 69 | String res = list.toString(); 70 | // this is a hack 71 | String id = res.substring(3, res.indexOf(' ')); 72 | 73 | // kill the connection 74 | final RedisConnection orig = connection; 75 | 76 | connection 77 | .send(Request.cmd(Command.CLIENT).arg("KILL").arg("SKIPME").arg("no").arg("ID").arg(id)) 78 | .onFailure(should::fail) 79 | .onSuccess(kill -> { 80 | should.assertEquals(1, kill.toInteger()); 81 | // wait until the connection is updated 82 | rule.vertx() 83 | .setPeriodic(500, t -> { 84 | if (orig != connection) { 85 | // when the 2 references change 86 | // it means the connection has been replaced 87 | test.complete(); 88 | } 89 | }); 90 | }); 91 | 92 | }); 93 | } 94 | 95 | private void reconnect(int retry) { 96 | if (retry < RETRIES) { 97 | // retry with backoff 98 | long backoff = (long) (Math.pow(2, Math.min(retry, RETRIES)) * RETRIES); 99 | 100 | rule 101 | .vertx() 102 | .setTimer(backoff, timer -> client.connect().onComplete(onReconnect -> { 103 | if (onReconnect.failed()) { 104 | reconnect(retry + 1); 105 | } else { 106 | connection = onReconnect.result(); 107 | } 108 | })); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/client/RedisSentinelMasterFailoverTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.client; 2 | 3 | import io.vertx.ext.unit.Async; 4 | import io.vertx.ext.unit.TestContext; 5 | import io.vertx.ext.unit.junit.RunTestOnContext; 6 | import io.vertx.ext.unit.junit.VertxUnitRunner; 7 | import io.vertx.redis.client.Command; 8 | import io.vertx.redis.client.Redis; 9 | import io.vertx.redis.client.RedisClientType; 10 | import io.vertx.redis.client.RedisOptions; 11 | import io.vertx.redis.client.RedisRole; 12 | import io.vertx.redis.client.Request; 13 | import io.vertx.tests.redis.containers.RedisSentinel; 14 | import org.junit.ClassRule; 15 | import org.junit.Rule; 16 | import org.junit.Test; 17 | import org.junit.runner.RunWith; 18 | 19 | import static io.vertx.tests.redis.client.TestUtils.retryUntilSuccess; 20 | 21 | @RunWith(VertxUnitRunner.class) 22 | public class RedisSentinelMasterFailoverTest { 23 | @ClassRule 24 | public static final RedisSentinel redis = new RedisSentinel(); 25 | 26 | @Rule 27 | public final RunTestOnContext rule = new RunTestOnContext(); 28 | 29 | @Test 30 | public void test(TestContext test) { 31 | Async async = test.async(); 32 | 33 | Redis.createClient( 34 | rule.vertx(), 35 | new RedisOptions() 36 | .setType(RedisClientType.SENTINEL) 37 | .addConnectionString(redis.getRedisSentinel0Uri()) 38 | .addConnectionString(redis.getRedisSentinel1Uri()) 39 | .addConnectionString(redis.getRedisSentinel2Uri()) 40 | .setRole(RedisRole.MASTER) 41 | .setAutoFailover(true)) 42 | .connect() 43 | .onComplete(test.asyncAssertSuccess(conn -> { 44 | conn.send(Request.cmd(Command.SET).arg("key").arg("value")) 45 | .compose(ignored -> conn.send(Request.cmd(Command.SHUTDOWN))) 46 | .onComplete(test.asyncAssertFailure(ignored -> { // connection closed 47 | retryUntilSuccess(rule.vertx(), () -> conn.send(Request.cmd(Command.GET).arg("key")), 50) 48 | .onComplete(test.asyncAssertSuccess(response -> { 49 | test.assertEquals("value", response.toString()); 50 | async.complete(); 51 | })); 52 | })); 53 | })); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/client/RedisThreadConsistencyTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.client; 2 | 3 | import io.vertx.core.Future; 4 | import io.vertx.core.VerticleBase; 5 | import io.vertx.core.http.HttpMethod; 6 | import io.vertx.ext.unit.Async; 7 | import io.vertx.ext.unit.TestContext; 8 | import io.vertx.ext.unit.junit.RunTestOnContext; 9 | import io.vertx.ext.unit.junit.VertxUnitRunner; 10 | import io.vertx.redis.client.Redis; 11 | import io.vertx.redis.client.RedisAPI; 12 | import io.vertx.redis.client.RedisOptions; 13 | import io.vertx.tests.redis.containers.RedisStandalone; 14 | import org.junit.ClassRule; 15 | import org.junit.Rule; 16 | import org.junit.Test; 17 | import org.junit.runner.RunWith; 18 | 19 | import java.util.Collections; 20 | 21 | @RunWith(VertxUnitRunner.class) 22 | public class RedisThreadConsistencyTest { 23 | 24 | @ClassRule 25 | public static final RedisStandalone redis = new RedisStandalone(); 26 | 27 | @Rule 28 | public final RunTestOnContext rule = new RunTestOnContext(); 29 | 30 | @Test 31 | public void redisThreadConsistencyTest(TestContext should) { 32 | final Async test = should.async(); 33 | 34 | rule.vertx() 35 | .deployVerticle(new Verticle( 36 | RedisAPI.api( 37 | Redis.createClient( 38 | rule.vertx(), 39 | new RedisOptions().setConnectionString(redis.getRedisUri()))))) 40 | .onFailure(should::fail) 41 | .onSuccess(id -> { 42 | rule.vertx() 43 | .createHttpClient() 44 | .request(HttpMethod.GET, 8080, "localhost", "/") 45 | .onFailure(should::fail) 46 | .onSuccess(req -> { 47 | req.send() 48 | .onFailure(should::fail) 49 | .onSuccess(res -> { 50 | should.assertEquals(res.getHeader("initialThread"), res.getHeader("threadAfterRedisExecution")); 51 | test.complete(); 52 | }); 53 | }); 54 | }); 55 | } 56 | 57 | static class Verticle extends VerticleBase { 58 | 59 | RedisAPI redisAPI; 60 | 61 | Verticle(RedisAPI redisApi) { 62 | this.redisAPI = redisApi; 63 | } 64 | 65 | @Override 66 | public Future start() { 67 | return vertx.createHttpServer() 68 | .requestHandler(req -> { 69 | String threadBeforeRedisCommandExecution = Thread.currentThread().getName(); 70 | redisAPI.info(Collections.emptyList()).onComplete(result -> { 71 | String threadAfterRedisCommandExecution = Thread.currentThread().getName(); 72 | req.response().putHeader("initialThread", threadBeforeRedisCommandExecution) 73 | .putHeader("threadAfterRedisExecution", threadAfterRedisCommandExecution) 74 | .end("Ok"); 75 | }); 76 | }) 77 | .listen(8080); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/client/RedisTracingTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.client; 2 | 3 | import io.vertx.core.Context; 4 | import io.vertx.core.Vertx; 5 | import io.vertx.core.spi.tracing.SpanKind; 6 | import io.vertx.core.spi.tracing.TagExtractor; 7 | import io.vertx.core.spi.tracing.VertxTracer; 8 | import io.vertx.core.tracing.TracingPolicy; 9 | import io.vertx.ext.unit.Async; 10 | import io.vertx.ext.unit.TestContext; 11 | import io.vertx.ext.unit.junit.VertxUnitRunner; 12 | import io.vertx.redis.client.Command; 13 | import io.vertx.redis.client.Redis; 14 | import io.vertx.redis.client.RedisOptions; 15 | import io.vertx.redis.client.Request; 16 | import io.vertx.redis.client.impl.CommandImpl; 17 | import io.vertx.tests.redis.containers.RedisStandalone; 18 | import org.junit.After; 19 | import org.junit.Before; 20 | import org.junit.ClassRule; 21 | import org.junit.Test; 22 | import org.junit.runner.RunWith; 23 | 24 | import java.util.*; 25 | import java.util.function.BiConsumer; 26 | 27 | @RunWith(VertxUnitRunner.class) 28 | public class RedisTracingTest { 29 | @ClassRule 30 | public static final RedisStandalone redis = new RedisStandalone(); 31 | 32 | Vertx vertx; 33 | VertxTracer tracer; 34 | Redis client; 35 | 36 | @Before 37 | public void setup() { 38 | vertx = Vertx.builder() 39 | .withTracer(ignored -> new VertxTracer() { 40 | @Override 41 | public Object sendRequest(Context context, SpanKind kind, TracingPolicy tracingPolicy, Object request, String operation, BiConsumer headers, TagExtractor tagExtractor) { 42 | return tracer.sendRequest(context, kind, tracingPolicy, request, operation, headers, tagExtractor); 43 | } 44 | @Override 45 | public void receiveResponse(Context context, Object response, Object payload, Throwable failure, TagExtractor tagExtractor) { 46 | tracer.receiveResponse(context, response, payload, failure, tagExtractor); 47 | } 48 | }).build(); 49 | client = Redis.createClient(vertx, new RedisOptions().setConnectionString(redis.getRedisUri())); 50 | } 51 | 52 | @After 53 | public void teardown(TestContext test) { 54 | vertx.close().onComplete(test.asyncAssertSuccess()); 55 | } 56 | 57 | @Test 58 | public void success(TestContext test) { 59 | testTracing(test, Request.cmd(Command.PING), true); 60 | } 61 | 62 | @Test 63 | public void failure(TestContext test) { 64 | testTracing(test, Request.cmd(new CommandImpl("NONEXISTING COMMAND", 0, true, false, false)), false); 65 | } 66 | 67 | private void testTracing(TestContext test, Request clientRequest, boolean success) { 68 | Async async = test.async(); 69 | 70 | Object trace = new Object(); 71 | List actions = Collections.synchronizedList(new ArrayList<>()); 72 | 73 | tracer = new VertxTracer() { 74 | @Override 75 | public Object sendRequest(Context context, SpanKind kind, TracingPolicy policy, Object request, String operation, BiConsumer headers, TagExtractor tagExtractor) { 76 | Map tags = tagExtractor.extract(request); 77 | String redisPort = String.valueOf(redis.getPort()); 78 | test.assertEquals("client", tags.get("span.kind")); 79 | test.assertEquals("redis", tags.get("db.system")); 80 | test.assertEquals("127.0.0.1", tags.get("network.peer.address")); 81 | test.assertEquals(redisPort, tags.get("network.peer.port")); 82 | test.assertEquals("localhost", tags.get("server.address")); 83 | test.assertEquals(redisPort, tags.get("server.port")); 84 | test.assertEquals(clientRequest.command().toString(), tags.get("db.operation.name")); 85 | actions.add("sendRequest"); 86 | return trace; 87 | } 88 | 89 | @Override 90 | public void receiveResponse(Context context, Object response, Object payload, Throwable failure, TagExtractor tagExtractor) { 91 | test.assertTrue(payload == trace); 92 | if (success) { 93 | test.assertNotNull(response); 94 | test.assertNull(failure); 95 | } else { 96 | test.assertNull(response); 97 | test.assertNotNull(failure); 98 | } 99 | actions.add("receiveResponse"); 100 | } 101 | }; 102 | 103 | vertx.runOnContext(ignored -> { 104 | client.send(clientRequest).onComplete(result -> { 105 | test.assertEquals(success, result.succeeded()); 106 | test.assertEquals(Arrays.asList("sendRequest", "receiveResponse"), actions); 107 | async.complete(); 108 | }); 109 | }); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/client/SharedRedisConnectionTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.client; 2 | 3 | import io.vertx.core.*; 4 | import io.vertx.ext.unit.Async; 5 | import io.vertx.ext.unit.TestContext; 6 | import io.vertx.ext.unit.junit.VertxUnitRunner; 7 | import io.vertx.redis.client.Redis; 8 | import io.vertx.redis.client.RedisAPI; 9 | import io.vertx.redis.client.RedisOptions; 10 | import io.vertx.redis.client.Response; 11 | import io.vertx.tests.redis.containers.RedisStandalone; 12 | import org.junit.After; 13 | import org.junit.Before; 14 | import org.junit.ClassRule; 15 | import org.junit.Test; 16 | import org.junit.runner.RunWith; 17 | 18 | import java.util.Arrays; 19 | 20 | @RunWith(VertxUnitRunner.class) 21 | public class SharedRedisConnectionTest { 22 | 23 | @ClassRule 24 | public static final RedisStandalone redis = new RedisStandalone(); 25 | 26 | private static final int VERTICLES_COUNT = 10; 27 | private static final int ITERATIONS_COUNT = 1000; 28 | 29 | private static final String REDIS_NUMBER_VALUE_KEY = "user:post:pinned:1372"; 30 | private static final String REDIS_SET_VALUE_KEY = "user:like:post:975"; 31 | 32 | Vertx vertx; 33 | RedisAPI conn; 34 | 35 | @Before 36 | public void setup(TestContext test) { 37 | Async async = test.async(); 38 | vertx = Vertx.vertx(); 39 | RedisOptions options = new RedisOptions() 40 | .setConnectionString(redis.getRedisUri()) 41 | .setMaxWaitingHandlers(VERTICLES_COUNT * ITERATIONS_COUNT * 2); // 2 requests per iteration 42 | Redis.createClient(vertx, options) 43 | .connect() 44 | .map(RedisAPI::api) 45 | .flatMap(api -> { 46 | return api.set(Arrays.asList(REDIS_NUMBER_VALUE_KEY, "42")) 47 | .map(api); 48 | }).flatMap(api -> { 49 | return api.sadd(Arrays.asList(REDIS_SET_VALUE_KEY, "100", "101", "102")) 50 | .map(api); 51 | }) 52 | .onComplete(result -> { 53 | if (result.succeeded()) { 54 | conn = result.result(); 55 | } else { 56 | test.fail(result.cause()); 57 | } 58 | async.complete(); 59 | }); 60 | } 61 | 62 | @After 63 | public void teardown(TestContext test) { 64 | conn.close(); 65 | vertx.close().onComplete(test.asyncAssertSuccess()); 66 | } 67 | 68 | @Test 69 | public void test(TestContext test) { 70 | vertx.deployVerticle(() -> new MyVerticle(conn, test), new DeploymentOptions().setInstances(VERTICLES_COUNT)); 71 | } 72 | 73 | public static class MyVerticle extends VerticleBase { 74 | private final RedisAPI conn; 75 | private final TestContext test; 76 | 77 | public MyVerticle(RedisAPI conn, TestContext test) { 78 | this.conn = conn; 79 | this.test = test; 80 | } 81 | 82 | @Override 83 | public Future start() throws Exception { 84 | Async async = test.async(ITERATIONS_COUNT); 85 | for (int i = 0; i < ITERATIONS_COUNT; i++) { 86 | test() 87 | .onSuccess(ignored -> async.countDown()) 88 | .onFailure(test::fail); 89 | } 90 | return super.start(); 91 | } 92 | 93 | private Future test() { 94 | Future fetchNumberFuture = conn.get(REDIS_NUMBER_VALUE_KEY) 95 | .onSuccess(response -> { 96 | try { 97 | response.toInteger(); 98 | } catch (Exception e) { 99 | test.fail(e); 100 | } 101 | }); 102 | 103 | Future fetchSetFuture = conn.smembers(REDIS_SET_VALUE_KEY) 104 | .onSuccess(response -> { 105 | try { 106 | for (Response part : response) { 107 | part.toInteger(); 108 | } 109 | } catch (Exception e) { 110 | test.fail(e); 111 | } 112 | }); 113 | 114 | return Future.all(fetchNumberFuture, fetchSetFuture); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/client/TestUtils.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.client; 2 | 3 | import io.vertx.core.Completable; 4 | import io.vertx.core.Future; 5 | import io.vertx.core.Promise; 6 | import io.vertx.core.Vertx; 7 | 8 | import java.util.UUID; 9 | import java.util.function.Supplier; 10 | 11 | public class TestUtils { 12 | public static String randomKey() { 13 | return UUID.randomUUID().toString(); 14 | } 15 | 16 | public static Future retryUntilSuccess(Vertx vertx, Supplier> action, int maxRetries) { 17 | Promise promise = Promise.promise(); 18 | retryUntilSuccess(vertx, action, maxRetries, promise); 19 | return promise.future(); 20 | } 21 | 22 | private static void retryUntilSuccess(Vertx vertx, Supplier> action, int maxRetries, Completable promise) { 23 | action.get().onComplete(result -> { 24 | if (result.succeeded()) { 25 | promise.succeed(result.result()); 26 | } else { 27 | if (maxRetries < 1) { 28 | promise.fail(result.cause()); 29 | } else { 30 | vertx.setTimer(500, ignored -> { 31 | retryUntilSuccess(vertx, action, maxRetries - 1, promise); 32 | }); 33 | } 34 | } 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/containers/RedisCluster.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.containers; 2 | 3 | import org.junit.rules.TestRule; 4 | import org.junit.runner.Description; 5 | import org.junit.runners.model.Statement; 6 | import org.testcontainers.containers.FixedHostPortGenericContainer; 7 | import org.testcontainers.containers.GenericContainer; 8 | import org.testcontainers.containers.wait.strategy.Wait; 9 | import org.testcontainers.containers.wait.strategy.WaitAllStrategy; 10 | 11 | import java.io.IOException; 12 | 13 | public class RedisCluster implements TestRule { 14 | private final GenericContainer container = new FixedHostPortGenericContainer<>("quay.io/ladicek/redis-cluster") 15 | .withFixedExposedPort(7000, 7000) 16 | .withFixedExposedPort(7001, 7001) 17 | .withFixedExposedPort(7002, 7002) 18 | .withFixedExposedPort(7003, 7003) 19 | .withFixedExposedPort(7004, 7004) 20 | .withFixedExposedPort(7005, 7005) 21 | .withFixedExposedPort(7006, 7006) // possible extra node, not present by default 22 | .withFixedExposedPort(7007, 7007) // possible extra node, not present by default 23 | .waitingFor(new WaitAllStrategy() 24 | .withStrategy(Wait.forLogMessage(".*Cluster state changed: ok.*", 6)) 25 | .withStrategy(Wait.forSuccessfulCommand("/cluster-slots-expected-lines.sh 7000 7005 30"))); 26 | 27 | @Override 28 | public Statement apply(Statement base, Description description) { 29 | return container.apply(base, description); 30 | } 31 | 32 | public String getRedisNode0Uri() { 33 | return getRedisUri(7000); 34 | } 35 | 36 | public String getRedisNode1Uri() { 37 | return getRedisUri(7001); 38 | } 39 | 40 | public String getRedisNode2Uri() { 41 | return getRedisUri(7002); 42 | } 43 | 44 | public String getRedisNode3Uri() { 45 | return getRedisUri(7003); 46 | } 47 | 48 | public String getRedisNode4Uri() { 49 | return getRedisUri(7004); 50 | } 51 | 52 | public String getRedisNode5Uri() { 53 | return getRedisUri(7005); 54 | } 55 | 56 | private String getRedisUri(int portNumber) { 57 | return "redis://" + container.getHost() + ":" + container.getMappedPort(portNumber); 58 | } 59 | 60 | public void addMaster(int portNumber) { 61 | execute("/cluster-add-master.sh", "" + portNumber); 62 | } 63 | 64 | private void execute(String... command) { 65 | try { 66 | container.execInContainer(command); 67 | } catch (IOException | InterruptedException e) { 68 | throw new RuntimeException(e); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/containers/RedisReplication.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.containers; 2 | 3 | import org.junit.rules.TestRule; 4 | import org.junit.runner.Description; 5 | import org.junit.runners.model.Statement; 6 | import org.testcontainers.containers.FixedHostPortGenericContainer; 7 | import org.testcontainers.containers.GenericContainer; 8 | import org.testcontainers.containers.wait.strategy.Wait; 9 | 10 | public class RedisReplication implements TestRule { 11 | private final GenericContainer container = new FixedHostPortGenericContainer<>("quay.io/ladicek/redis-replication") 12 | .withFixedExposedPort(7000, 7000) 13 | .withFixedExposedPort(7001, 7001) 14 | .withFixedExposedPort(7002, 7002) 15 | .waitingFor(Wait.forLogMessage(".*MASTER <-> REPLICA sync: Finished with success.*", 2)); 16 | 17 | @Override 18 | public Statement apply(Statement base, Description description) { 19 | return container.apply(base, description); 20 | } 21 | 22 | public String getRedisMasterUri() { 23 | return getRedisUri(7000); 24 | } 25 | 26 | public String getRedisReplica0Uri() { 27 | return getRedisUri(7001); 28 | } 29 | 30 | public String getRedisReplica1Uri() { 31 | return getRedisUri(7002); 32 | } 33 | 34 | private String getRedisUri(int portNumber) { 35 | return "redis://" + container.getHost() + ":" + container.getMappedPort(portNumber); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/containers/RedisSentinel.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.containers; 2 | 3 | import org.junit.rules.TestRule; 4 | import org.junit.runner.Description; 5 | import org.junit.runners.model.Statement; 6 | import org.testcontainers.containers.FixedHostPortGenericContainer; 7 | import org.testcontainers.containers.GenericContainer; 8 | import org.testcontainers.containers.wait.strategy.Wait; 9 | 10 | public class RedisSentinel implements TestRule { 11 | private final GenericContainer container = new FixedHostPortGenericContainer<>("quay.io/ladicek/redis-replication") 12 | .withEnv("SENTINEL", "true") 13 | .withFixedExposedPort(7000, 7000) 14 | .withFixedExposedPort(7001, 7001) 15 | .withFixedExposedPort(7002, 7002) 16 | .withFixedExposedPort(5000, 5000) 17 | .withFixedExposedPort(5001, 5001) 18 | .withFixedExposedPort(5002, 5002) 19 | .waitingFor(Wait.forLogMessage(".*\\+slave.*", 6)); 20 | 21 | @Override 22 | public Statement apply(Statement base, Description description) { 23 | return container.apply(base, description); 24 | } 25 | 26 | public String getRedisMasterUri() { 27 | return getRedisUri(7000); 28 | } 29 | 30 | public String getRedisReplica0Uri() { 31 | return getRedisUri(7001); 32 | } 33 | 34 | public String getRedisReplica1Uri() { 35 | return getRedisUri(7002); 36 | } 37 | 38 | public String getRedisSentinel0Uri() { 39 | return getRedisUri(5000); 40 | } 41 | 42 | public String getRedisSentinel1Uri() { 43 | return getRedisUri(5001); 44 | } 45 | 46 | public String getRedisSentinel2Uri() { 47 | return getRedisUri(5002); 48 | } 49 | 50 | private String getRedisUri(int portNumber) { 51 | return "redis://" + container.getHost() + ":" + container.getMappedPort(portNumber); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/containers/RedisStandalone.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.containers; 2 | 3 | import org.junit.rules.TestRule; 4 | import org.junit.runner.Description; 5 | import org.junit.runners.model.Statement; 6 | import org.testcontainers.containers.GenericContainer; 7 | import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; 8 | import org.testcontainers.images.builder.Transferable; 9 | 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | import java.util.Objects; 13 | 14 | public class RedisStandalone implements TestRule { 15 | public static class Builder { 16 | private String version; 17 | private String password; 18 | 19 | private boolean tlsEnabled; 20 | private KeyPairAndCertificate serverKey; 21 | 22 | private boolean mutualTlsEnabled; 23 | private KeyPairAndCertificate clientKey; 24 | 25 | private Builder() { 26 | } 27 | 28 | public Builder setVersion(String version) { 29 | this.version = Objects.requireNonNull(version); 30 | return this; 31 | } 32 | 33 | public Builder setPassword(String password) { 34 | this.password = Objects.requireNonNull(password); 35 | return this; 36 | } 37 | 38 | public Builder enableTls(KeyPairAndCertificate serverKey) { 39 | this.tlsEnabled = true; 40 | this.serverKey = Objects.requireNonNull(serverKey); 41 | return this; 42 | } 43 | 44 | public Builder enableMutualTls(KeyPairAndCertificate clientKey) { 45 | assert tlsEnabled; 46 | this.mutualTlsEnabled = true; 47 | this.clientKey = Objects.requireNonNull(clientKey); 48 | return this; 49 | } 50 | 51 | public RedisStandalone build() { 52 | return new RedisStandalone(this); 53 | } 54 | } 55 | 56 | public static Builder builder() { 57 | return new Builder(); 58 | } 59 | 60 | // --- 61 | 62 | private final GenericContainer container; 63 | 64 | public RedisStandalone() { 65 | this(builder()); 66 | } 67 | 68 | private RedisStandalone(Builder builder) { 69 | String image = "docker.io/bitnami/redis:" + (builder.version != null ? builder.version : "7.2"); 70 | 71 | Map env = new HashMap<>(); 72 | if (builder.password != null) { 73 | env.put("REDIS_PASSWORD", builder.password); 74 | } else { 75 | env.put("ALLOW_EMPTY_PASSWORD", "yes"); 76 | } 77 | 78 | if (builder.tlsEnabled) { 79 | env.put("REDIS_TLS_ENABLED", "yes"); 80 | env.put("REDIS_TLS_KEY_FILE", "/certs/redis.key"); 81 | env.put("REDIS_TLS_CERT_FILE", "/certs/redis.crt"); 82 | 83 | if (builder.mutualTlsEnabled) { 84 | env.put("REDIS_TLS_CA_FILE", "/certs/client.crt"); 85 | } else { 86 | env.put("REDIS_TLS_AUTH_CLIENTS", "no"); 87 | } 88 | } 89 | 90 | this.container = new GenericContainer<>(image) 91 | .withEnv(env) 92 | .withExposedPorts(6379) 93 | .waitingFor(new LogMessageWaitStrategy().withRegEx(".*Ready to accept connections.*")); 94 | 95 | if (builder.tlsEnabled) { 96 | container.withCopyToContainer(Transferable.of(builder.serverKey.privateKeyAsPEM()), "/certs/redis.key"); 97 | container.withCopyToContainer(Transferable.of(builder.serverKey.certificateAsPEM()), "/certs/redis.crt"); 98 | } 99 | if (builder.mutualTlsEnabled) { 100 | container.withCopyToContainer(Transferable.of(builder.clientKey.certificateAsPEM()), "/certs/client.crt"); 101 | } 102 | } 103 | 104 | @Override 105 | public Statement apply(Statement base, Description description) { 106 | return container.apply(base, description); 107 | } 108 | 109 | public String getHost() { 110 | return container.getHost(); 111 | } 112 | 113 | public int getPort() { 114 | return container.getMappedPort(6379); 115 | } 116 | 117 | public String getRedisUri() { 118 | return "redis://" + getHost() + ":" + getPort(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/internal/ArrayQueueTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.internal; 2 | 3 | import io.vertx.redis.client.impl.ArrayQueue; 4 | import org.junit.Ignore; 5 | import org.junit.Test; 6 | 7 | import static org.junit.Assert.assertEquals; 8 | import static org.junit.Assert.assertNotNull; 9 | 10 | public class ArrayQueueTest { 11 | 12 | @Test 13 | @Ignore("This test is very CPU intensive and causes trouble on CI") 14 | public void testOverflow() { 15 | ArrayQueue arrayQueue = new ArrayQueue(10); 16 | 17 | arrayQueue.offer(0); 18 | for (int i = 0; i < Integer.MAX_VALUE; i++) { 19 | arrayQueue.offer(0); 20 | arrayQueue.poll(); 21 | arrayQueue.offer(0); 22 | arrayQueue.poll(); 23 | } 24 | assertEquals(9, arrayQueue.freeSlots()); 25 | assertEquals(0, (int) arrayQueue.poll()); 26 | assertEquals(10, arrayQueue.freeSlots()); 27 | //Overflow for int back 28 | arrayQueue.offer(1); 29 | //Overflow for int front 30 | assertEquals(1, (int) arrayQueue.poll()); 31 | } 32 | 33 | @Test 34 | public void testPollNSE() { 35 | ArrayQueue arrayQueue = new ArrayQueue(10); 36 | arrayQueue.poll(); 37 | arrayQueue.offer("a"); 38 | assertNotNull(arrayQueue.poll()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/internal/BufferTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.internal; 2 | 3 | import io.netty.buffer.Unpooled; 4 | import io.vertx.core.buffer.Buffer; 5 | import io.vertx.core.internal.buffer.BufferInternal; 6 | import io.vertx.redis.client.Command; 7 | import io.vertx.redis.client.Request; 8 | import io.vertx.redis.client.impl.RequestImpl; 9 | import org.junit.Test; 10 | 11 | import java.nio.charset.Charset; 12 | import java.nio.charset.StandardCharsets; 13 | 14 | public class BufferTest { 15 | 16 | private static final int iterations = 50000; 17 | 18 | @Test 19 | public void testAppendString() { 20 | Request hmset = Request.cmd(Command.HMSET); 21 | String key = "mykey"; 22 | 23 | for (int i = 0; i < iterations; i++) { 24 | hmset.arg(key).arg(i); 25 | } 26 | 27 | System.out.println(((RequestImpl) hmset).encode().length()); 28 | 29 | for (int j = 0; j < 5; j++) { 30 | final long t0 = System.nanoTime(); 31 | hmset = Request.cmd(Command.HMSET); 32 | 33 | for (int i = 0; i < iterations; i++) { 34 | hmset.arg(key).arg(i); 35 | } 36 | final long t1 = System.nanoTime(); 37 | 38 | System.out.println(((RequestImpl) hmset).encode().length() + "| t " + (t1 - t0)); 39 | } 40 | System.out.println("---"); 41 | } 42 | 43 | @Test 44 | public void testAppendBytes() { 45 | Request hmset = Request.cmd(Command.HMSET); 46 | byte[] key = "my-key".getBytes(StandardCharsets.US_ASCII); 47 | 48 | for (int i = 0; i < iterations; i++) { 49 | hmset.arg(key).arg(i); 50 | } 51 | 52 | System.out.println(((RequestImpl) hmset).encode().length()); 53 | 54 | for (int j = 0; j < 5; j++) { 55 | final long t0 = System.nanoTime(); 56 | hmset = Request.cmd(Command.HMSET); 57 | 58 | for (int i = 0; i < iterations; i++) { 59 | hmset.arg(key).arg(i); 60 | } 61 | final long t1 = System.nanoTime(); 62 | 63 | System.out.println(((RequestImpl) hmset).encode().length() + "| t " + (t1 - t0)); 64 | } 65 | System.out.println("---"); 66 | } 67 | 68 | @Test 69 | public void testAppendBuffer() { 70 | Request hmset = Request.cmd(Command.HMSET); 71 | Buffer key = Buffer.buffer("my-key"); 72 | 73 | for (int i = 0; i < iterations; i++) { 74 | hmset.arg(key).arg(i); 75 | } 76 | 77 | System.out.println(((RequestImpl) hmset).encode().length()); 78 | 79 | for (int j = 0; j < 5; j++) { 80 | final long t0 = System.nanoTime(); 81 | hmset = Request.cmd(Command.HMSET); 82 | 83 | for (int i = 0; i < iterations; i++) { 84 | hmset.arg(key).arg(i); 85 | } 86 | final long t1 = System.nanoTime(); 87 | 88 | System.out.println(((RequestImpl) hmset).encode().length() + "| t " + (t1 - t0)); 89 | } 90 | System.out.println("---"); 91 | } 92 | 93 | @Test 94 | public void testAppendStringToBytes() { 95 | Request hmset = Request.cmd(Command.HMSET); 96 | String key = "mykey"; 97 | 98 | for (int i = 0; i < iterations; i++) { 99 | hmset.arg(key.getBytes()).arg(i); 100 | } 101 | 102 | System.out.println(((RequestImpl) hmset).encode().length()); 103 | 104 | for (int j = 0; j < 5; j++) { 105 | final long t0 = System.nanoTime(); 106 | hmset = Request.cmd(Command.HMSET); 107 | 108 | for (int i = 0; i < iterations; i++) { 109 | hmset.arg(key.getBytes()).arg(i); 110 | } 111 | final long t1 = System.nanoTime(); 112 | 113 | System.out.println(((RequestImpl) hmset).encode().length() + "| t " + (t1 - t0)); 114 | } 115 | System.out.println("---"); 116 | } 117 | 118 | @Test 119 | public void testAppendBufferWrapped() { 120 | Charset UTF8 = StandardCharsets.UTF_8; 121 | Request hmset = Request.cmd(Command.HMSET); 122 | Buffer key = BufferInternal.buffer(Unpooled.wrappedBuffer(UTF8.encode("my-key"))); 123 | 124 | for (int i = 0; i < iterations; i++) { 125 | hmset.arg(key).arg(i); 126 | } 127 | 128 | System.out.println(((RequestImpl) hmset).encode().length()); 129 | 130 | for (int j = 0; j < 5; j++) { 131 | final long t0 = System.nanoTime(); 132 | hmset = Request.cmd(Command.HMSET); 133 | 134 | for (int i = 0; i < iterations; i++) { 135 | hmset.arg(key).arg(i); 136 | } 137 | final long t1 = System.nanoTime(); 138 | 139 | System.out.println(((RequestImpl) hmset).encode().length() + "| t " + (t1 - t0)); 140 | } 141 | System.out.println("---"); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/internal/CommandNormalizationTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.internal; 2 | 3 | import io.vertx.redis.client.Command; 4 | import io.vertx.redis.client.Request; 5 | import io.vertx.redis.client.impl.CommandImpl; 6 | import io.vertx.redis.client.impl.RequestImpl; 7 | import org.junit.Test; 8 | 9 | import java.nio.charset.StandardCharsets; 10 | 11 | import static org.junit.Assert.assertEquals; 12 | import static org.junit.Assert.assertFalse; 13 | import static org.junit.Assert.assertNotEquals; 14 | 15 | public class CommandNormalizationTest { 16 | @Test 17 | public void test() { 18 | RequestImpl req = (RequestImpl) Request.cmd(Command.create("hset")).arg("key").arg("field").arg("value"); 19 | CommandImpl cmd = (CommandImpl) req.command(); 20 | assertEquals("hset", cmd.toString()); 21 | assertNotEquals(-1, cmd.getArity()); 22 | assertFalse(cmd.isReadOnly(req.getArgs())); 23 | assertFalse(cmd.needsGetKeys()); 24 | assertEquals(1, req.keys().size()); 25 | assertEquals("key", new String(req.keys().get(0), StandardCharsets.UTF_8)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/internal/ConnectionRecyclingTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.internal; 2 | 3 | import io.vertx.core.Vertx; 4 | import io.vertx.core.internal.pool.ConnectionPool; 5 | import io.vertx.core.internal.resource.ResourceManager; 6 | import io.vertx.ext.unit.Async; 7 | import io.vertx.ext.unit.TestContext; 8 | import io.vertx.ext.unit.junit.VertxUnitRunner; 9 | import io.vertx.redis.client.Command; 10 | import io.vertx.redis.client.Redis; 11 | import io.vertx.redis.client.RedisConnection; 12 | import io.vertx.redis.client.RedisOptions; 13 | import io.vertx.redis.client.Request; 14 | import io.vertx.redis.client.impl.RedisClient; 15 | import io.vertx.redis.client.impl.RedisConnectionInternal; 16 | import io.vertx.redis.client.impl.RedisConnectionManager; 17 | import io.vertx.redis.client.impl.RedisConnectionManager.ConnectionKey; 18 | import io.vertx.redis.client.impl.RedisConnectionManager.RedisEndpoint; 19 | import io.vertx.tests.redis.containers.RedisStandalone; 20 | import org.junit.After; 21 | import org.junit.Before; 22 | import org.junit.ClassRule; 23 | import org.junit.Test; 24 | import org.junit.runner.RunWith; 25 | 26 | import java.lang.reflect.Field; 27 | 28 | @RunWith(VertxUnitRunner.class) 29 | public class ConnectionRecyclingTest { 30 | 31 | @ClassRule 32 | public static final RedisStandalone redis = new RedisStandalone(); 33 | 34 | Vertx vertx; 35 | Redis client; 36 | 37 | @Before 38 | public void setup() { 39 | vertx = Vertx.vertx(); 40 | RedisOptions options = new RedisOptions() 41 | .setConnectionString(redis.getRedisUri()) 42 | .setMaxPoolSize(2) 43 | .setPoolCleanerInterval(100) 44 | .setPoolRecycleTimeout(1000); 45 | client = Redis.createClient(vertx, options); 46 | } 47 | 48 | @After 49 | public void teardown(TestContext test) { 50 | client.close(); 51 | vertx.close().onComplete(test.asyncAssertSuccess()); 52 | } 53 | 54 | @Test 55 | public void testUsageShorterThanRecycleTimeout(TestContext test) { 56 | Async async = test.async(); 57 | 58 | assertConnectionPool(test, 0); 59 | 60 | client.connect() 61 | .flatMap(conn -> { 62 | assertConnectionPool(test, 1); 63 | return conn.close(); 64 | }).onComplete(test.asyncAssertSuccess(ignored -> { 65 | assertConnectionPool(test, 1); 66 | 67 | vertx.setTimer(2000, ignored2 -> { 68 | assertConnectionPool(test, 0); 69 | async.complete(); 70 | }); 71 | })); 72 | } 73 | 74 | @Test 75 | public void testUsageLongerThanRecycleTimeout(TestContext test) { 76 | Async async = test.async(); 77 | 78 | assertConnectionPool(test, 0); 79 | 80 | client.connect().onComplete(test.asyncAssertSuccess(conn -> { 81 | assertConnectionPool(test, 1); 82 | useConnectionForLongTime(conn, System.currentTimeMillis() + 2000); 83 | })); 84 | 85 | vertx.setTimer(2500, ignored -> { 86 | assertConnectionPool(test, 1); 87 | 88 | vertx.setTimer(1000, ignored2 -> { 89 | assertConnectionPool(test, 0); 90 | async.complete(); 91 | }); 92 | }); 93 | } 94 | 95 | private void useConnectionForLongTime(RedisConnection conn, long endTime) { 96 | if (endTime < System.currentTimeMillis()) { 97 | conn.close(); 98 | return; 99 | } 100 | 101 | conn.send(Request.cmd(Command.INFO)) 102 | .onSuccess(ignored -> { 103 | vertx.setTimer(100, ignored2 -> { 104 | useConnectionForLongTime(conn, endTime); 105 | }); 106 | }); 107 | } 108 | 109 | private void assertConnectionPool(TestContext test, int expectedSize) { 110 | IntBox size = new IntBox(0); 111 | 112 | try { 113 | RedisConnectionManager connManager = ((RedisClient) client).connectionManager(); 114 | Field field = RedisConnectionManager.class.getDeclaredField("pooledConnectionManager"); 115 | field.setAccessible(true); 116 | ResourceManager endpointManager = 117 | (ResourceManager) field.get(connManager); 118 | endpointManager.forEach(endpoint -> { 119 | ConnectionPool pool = ((RedisEndpoint) endpoint).pool(); 120 | size.value += pool.size(); 121 | }); 122 | } catch (ReflectiveOperationException e) { 123 | throw new RuntimeException(e); 124 | } 125 | 126 | test.assertEquals(expectedSize, size.value); 127 | } 128 | 129 | static class IntBox { 130 | int value; 131 | 132 | IntBox(int value) { 133 | this.value = value; 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/internal/RequestImplTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.internal; 2 | 3 | import io.vertx.redis.client.Command; 4 | import io.vertx.redis.client.Request; 5 | import io.vertx.redis.client.impl.RequestImpl; 6 | import org.junit.Test; 7 | 8 | public class RequestImplTest { 9 | 10 | @Test 11 | public void testSimple() { 12 | RequestImpl r = (RequestImpl) Request.cmd(Command.PING); 13 | System.out.println(r.encode()); 14 | } 15 | 16 | @Test 17 | public void testWithArgs() { 18 | RequestImpl r = (RequestImpl) Request.cmd(Command.LLEN).arg("mylist"); 19 | System.out.println(r.encode()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/test/java/io/vertx/tests/redis/internal/ZModemTest.java: -------------------------------------------------------------------------------- 1 | package io.vertx.tests.redis.internal; 2 | 3 | import io.vertx.redis.client.impl.ZModem; 4 | import org.junit.Test; 5 | 6 | import java.util.Arrays; 7 | import java.util.HashSet; 8 | import java.util.Set; 9 | 10 | import static org.junit.Assert.assertEquals; 11 | 12 | public class ZModemTest { 13 | 14 | @Test 15 | public void testGenerate() { 16 | assertEquals(12325, ZModem.generate("foobar")); 17 | assertEquals(9132, ZModem.generate("abcdefghijklmnopqrstuvwxyz")); 18 | assertEquals(15532, ZModem.generate("gsdfhan$%^&*(sdgsdnhshcs")); 19 | assertEquals(12325, ZModem.generate("abc{foobar}")); 20 | assertEquals(12325, ZModem.generate("{foobar}")); 21 | assertEquals(12325, ZModem.generate("h8a9sd{foobar}}{asd}}")); 22 | assertEquals(16235, ZModem.generate("{foobar")); 23 | assertEquals(4435, ZModem.generate("foobar{}")); 24 | assertEquals(16235, ZModem.generate("{{foobar}")); 25 | assertEquals(13690, ZModem.generate("éêe")); 26 | assertEquals(3872, ZModem.generate("àâa")); 27 | assertEquals(14191, ZModem.generate("漢字")); 28 | assertEquals(16196, ZModem.generate("汉字")); 29 | assertEquals(4350, ZModem.generate("호텔")); 30 | assertEquals(9284, ZModem.generate("\uD83D\uDC80")); 31 | assertEquals(11620, ZModem.generate("\uD800\uDC00")); 32 | } 33 | 34 | @Test 35 | public void testGenerateMulti() { 36 | assertEquals(9132, ZModem.generateMulti(Arrays.asList( 37 | "abcdefghijklmnopqrstuvwxyz", 38 | "abcdefghijklmnopqrstuvwxyz", 39 | "abcdefghijklmnopqrstuvwxyz", 40 | "abcdefghijklmnopqrstuvwxyz", 41 | "abcdefghijklmnopqrstuvwxyz", 42 | "abcdefghijklmnopqrstuvwxyz", 43 | "abcdefghijklmnopqrstuvwxyz", 44 | "abcdefghijklmnopqrstuvwxyz" 45 | ))); 46 | } 47 | 48 | @Test 49 | public void testAll() { 50 | final Set slots = new HashSet<>(); 51 | for (int i = 0; i < Math.pow(2, 17); i++) { 52 | slots.add(ZModem.generate("" + i)); 53 | } 54 | 55 | assertEquals(16384, slots.size()); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/resources/logging.properties: -------------------------------------------------------------------------------- 1 | handlers=java.util.logging.ConsoleHandler 2 | .level=INFO 3 | java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter 4 | java.util.logging.SimpleFormatter.format=%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS,%1$tL %4$-5s [%3$s] %5$s%6$s%n 5 | -------------------------------------------------------------------------------- /src/test/resources/server-keystore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vert-x3/vertx-redis-client/c53883492679c8b729a521eb8eefe7652496945d/src/test/resources/server-keystore.jks -------------------------------------------------------------------------------- /tools/commands.js: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import * as redis from 'redis' 3 | import Handlebars from 'handlebars' 4 | import HandlebarsHelpers from 'handlebars-helpers' 5 | 6 | const excludedPubSub = ['PUBSUB', 'PUBLISH', 'SPUBLISH'] 7 | 8 | let client = await redis.createClient().connect() 9 | 10 | let version = null 11 | let infoResponse = await client.INFO() 12 | for (let line of infoResponse.split("\r\n")) { 13 | if (line.startsWith('redis_version')) { 14 | version = line 15 | } 16 | } 17 | 18 | let docs = {} 19 | let commandDocsResponse = await client.sendCommand(['COMMAND', 'DOCS']) 20 | for (let i = 0; i < commandDocsResponse.length; i += 2) { 21 | let name = commandDocsResponse[i] 22 | let docResponse = commandDocsResponse[i + 1] 23 | 24 | let doc = {} 25 | for (let j = 0; j < docResponse.length; j += 2) { 26 | doc[docResponse[j]] = docResponse[j + 1] 27 | } 28 | 29 | let summary = doc['summary'] || null 30 | if (summary != null) { 31 | summary = summary.trim() 32 | if (!summary.endsWith('.')) { 33 | summary += '.' 34 | } 35 | } 36 | 37 | let deprecated = false 38 | if (doc['doc_flags']) { 39 | for (let flag of doc['doc_flags']) { 40 | if (flag === 'deprecated') { 41 | deprecated = true 42 | break 43 | } 44 | } 45 | } 46 | let deprecatedSince = null 47 | let replacedBy = null 48 | if (deprecated) { 49 | deprecatedSince = doc['deprecated_since'] || 'unknown' 50 | replacedBy = doc['replaced_by'] || 'unknown' 51 | replacedBy = replacedBy.replaceAll(/`(.*?)`/g, '{@code $1}') 52 | } 53 | 54 | docs[name] = { 55 | summary, 56 | deprecatedSince, 57 | replacedBy 58 | } 59 | } 60 | 61 | let commands = [] 62 | let commandInfoResponse = await client.sendCommand(['COMMAND', 'INFO']) 63 | commandInfoResponse.sort((a, b) => { 64 | return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0 65 | }) 66 | for (let cmd of commandInfoResponse) { 67 | let types = "" 68 | let args = "" 69 | let argLen = 0 70 | 71 | let identifier = cmd[0].replace('.', '_').replace('-', '_').replace(':', '').toUpperCase() 72 | 73 | if (cmd[1] > 0) { 74 | for (let i = 0; i < cmd[1] - 1; i++) { 75 | if (i !== 0) { 76 | args += ', ' 77 | types += ', ' 78 | } 79 | types += ("String arg" + i) 80 | args += ("arg" + i) 81 | } 82 | // arg len includes the command name 83 | argLen = cmd[1] 84 | if (argLen) { 85 | argLen-- 86 | } 87 | } 88 | 89 | if (cmd[1] < 0) { 90 | types = "List args" 91 | args = "args" 92 | argLen = Math.abs(cmd[1]) 93 | } 94 | 95 | commands.push({ 96 | enum: identifier, 97 | name: cmd[0], 98 | safename: cmd[0].replace('-', ' ').replace(':', '').toUpperCase(), 99 | arity: cmd[1], 100 | variable: cmd[1] < 0, 101 | argLen: argLen, 102 | args: args, 103 | types: types, 104 | firstKey: cmd[3], 105 | lastKey: cmd[4], 106 | multiKey: cmd[4] < 0, 107 | interval: cmd[5], 108 | keyless: cmd[5] === 0 && cmd[2].indexOf('movablekeys') === -1, 109 | write: cmd[2].indexOf('write') !== -1, 110 | readOnly: cmd[2].indexOf('readonly') !== -1, 111 | movable: cmd[2].indexOf('movablekeys') !== -1, 112 | pubsub: cmd[2].indexOf('pubsub') !== -1 && !excludedPubSub.includes(identifier), 113 | docsSummary: docs[cmd[0]].summary, 114 | docsDeprecatedSince: docs[cmd[0]].deprecatedSince, 115 | docsReplacedBy: docs[cmd[0]].replacedBy, 116 | }) 117 | } 118 | 119 | await client.disconnect() 120 | 121 | HandlebarsHelpers() 122 | let template = Handlebars.compile(fs.readFileSync('redis-api.hbs', 'utf8')) 123 | let rendered = template({ 124 | commands, 125 | version, 126 | }) 127 | fs.writeFileSync('../src/main/java/io/vertx/redis/client/RedisAPI.java', rendered) 128 | -------------------------------------------------------------------------------- /tools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "commands-generator", 3 | "private": true, 4 | "type": "module", 5 | "main": "commands.js", 6 | "dependencies": { 7 | "handlebars": "^4.7.8", 8 | "handlebars-helpers": "^0.10.0", 9 | "redis": "^4.7.0" 10 | }, 11 | "scripts": { 12 | "--prestart": "docker run --rm --net=host redis/redis-stack-server:7.0.6-RC9", 13 | "start": "node commands.js" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tools/redis-api.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Red Hat, Inc. 3 | * 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * and Apache License v2.0 which accompanies this distribution. 7 | * 8 | * The Eclipse Public License is available at 9 | * http://www.eclipse.org/legal/epl-v10.html 10 | * 11 | * The Apache License v2.0 is available at 12 | * http://www.opensource.org/licenses/apache2.0.php 13 | * 14 | * You may elect to redistribute this code under either of these licenses. 15 | */ 16 | package io.vertx.redis.client; 17 | 18 | import io.vertx.codegen.annotations.GenIgnore; 19 | import io.vertx.codegen.annotations.Nullable; 20 | import io.vertx.codegen.annotations.VertxGen; 21 | import io.vertx.core.Future; 22 | import io.vertx.redis.client.impl.RedisAPIImpl; 23 | 24 | import java.util.List; 25 | 26 | import static io.vertx.codegen.annotations.GenIgnore.PERMITTED_TYPE; 27 | 28 | /** 29 | * Auto generated Redis API client wrapper. 30 | * @version {{ version }} 31 | */ 32 | @VertxGen 33 | public interface RedisAPI { 34 | 35 | @GenIgnore(PERMITTED_TYPE) 36 | static RedisAPI api(Redis client) { 37 | return new RedisAPIImpl(client); 38 | } 39 | 40 | @GenIgnore(PERMITTED_TYPE) 41 | static RedisAPI api(RedisConnection connection) { 42 | return new RedisAPIImpl(connection); 43 | } 44 | 45 | void close(); 46 | {{#each commands}} 47 | 48 | /** 49 | {{#if docsSummary }} 50 | * {{docsSummary}} 51 | *

52 | {{/if}} 53 | * Redis command {{ uppercase name }}. 54 | * @return Future response. 55 | {{#if docsDeprecatedSince}} 56 | * @deprecated since: {{docsDeprecatedSince}}, replaced by: {{docsReplacedBy}} 57 | {{/if}} 58 | */ 59 | {{#if docsDeprecatedSince}} 60 | @Deprecated 61 | {{/if}} 62 | default Future<@Nullable Response> {{ camelcase safename }}({{{ types }}}) { 63 | return send(Command.{{ enum }}{{#if argLen}}, {{{ args }}}{{#if variable}}.toArray(new String[0]){{/if}}{{/if}}); 64 | } 65 | {{/each}} 66 | 67 | /** 68 | * Send untyped command to redis. 69 | * 70 | * @param cmd the command 71 | * @param args var args 72 | * @return Future response. 73 | */ 74 | @GenIgnore 75 | Future<@Nullable Response> send(Command cmd, String... args); 76 | } 77 | --------------------------------------------------------------------------------