diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 77b1708308..7783b1329f 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing to Lettuce -Lettuce is released under the Apache 2.0 license. If you would like to contribute something, or simply want to hack on the code this document should help you get started. +Lettuce is released under the MIT license. If you would like to contribute something, or simply want to hack on the code this document should help you get started. ## Code of Conduct @@ -57,17 +57,16 @@ If you have a question, then check one of the following places first as GitHub i **Checkout the docs** -* [Reference documentation](https://lettuce.io/docs/) -* [Wiki](https://github.com/lettuce-io/lettuce-core/wiki) -* [Javadoc](https://lettuce.io/core/release/api/) +* [Reference documentation](https://redis.github.io/lettuce/) +* [Javadoc](https://www.javadoc.io/doc/io.lettuce/lettuce-core/latest/index.html) **Communication** -* GitHub Discussions (Q&A, Ideas, General discussion): https://github.com/lettuce-io/lettuce-core/discussions +* [GitHub Discussions](https://github.com/redis/lettuce/discussions) (Q&A, Ideas, General discussion) * Stack Overflow (Questions): [https://stackoverflow.com/questions/tagged/lettuce](https://stackoverflow.com/questions/tagged/lettuce) -* Gitter (chat): [![Join the chat at https://gitter.im/lettuce-io/Lobby](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/lettuce-io/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -* Twitter: [@LettuceDriver](https://twitter.com/LettuceDriver) -* [GitHub Issues](https://github.com/lettuce-io/lettuce-core/issues) (Bug reports, feature requests) +* Discord: [![Discord](https://img.shields.io/discord/697882427875393627.svg?style=social&logo=discord)](https://discord.gg/redis) +* Twitter: [![Twitter](https://img.shields.io/twitter/follow/redisinc?style=social)](https://twitter.com/redisinc) +* [GitHub Issues](https://github.com/redis/lettuce/issues) (Bug reports, feature requests) ### Building from Source diff --git a/.github/codecov.yml b/.github/codecov.yml new file mode 100644 index 0000000000..4b4f2fe041 --- /dev/null +++ b/.github/codecov.yml @@ -0,0 +1,65 @@ +codecov: # see https://docs.codecov.com/docs/codecovyml-reference + branch: main + +coverage: + status: # see https://docs.codecov.com/docs/commit-status + project: + default: + target: auto # minimum coverage ratio that the commit must meet to be considered a success + threshold: 5 # Allow the coverage to drop by %, and posting a success status + branches: + - main + - '[0-9].*' + +comment: # see https://docs.codecov.com/docs/pull-request-comments + layout: "condensed_header, condensed_files, condensed_footer" + behavior: new + require_changes: true # Only post comment if there are changes in coverage (positive or negative) + +component_management: # see https://docs.codecov.com/docs/components + individual_components: + - component_id: module_json + name: Lettuce JSON + paths: + - src/main/java/**/json/** + - component_id: module_api + name: Lettuce API + paths: + - src/main/java/**/api/** + - component_id: module_sentinel + name: Lettuce Sentinel + paths: + - src/main/java/**/sentinel/** + - component_id: module_cluster + name: Lettuce Cluster + paths: + - src/main/java/**/cluster/** + - component_id: module_pubsub + name: Lettuce PubSub + paths: + - src/main/java/**/pubsub/** + - component_id: module_masterreplica + name: Lettuce Master/Replica + paths: + - src/main/java/**/masterreplica/** + - src/main/java/**/masterslave/** + - component_id: module_metrics + name: Lettuce Metrics & Tracing + paths: + - src/main/java/**/metrics/** + - src/main/java/**/tracing/** + - component_id: module_core + name: Lettuce Core + paths: + - src/main/java/**/core/* + - src/main/java/**/codec/** + - src/main/java/**/dynamic/** + - src/main/java/**/event/** + - src/main/java/**/internal/** + - src/main/java/**/protocol/** + - src/main/java/**/resource/** + - src/main/java/**/support/** + - component_id: module_kotlin + name: Lettuce Kotlin + paths: + - src/main/kotlin/** \ No newline at end of file diff --git a/.github/wordlist.txt b/.github/wordlist.txt index 2714717cb1..5c11d89f82 100644 --- a/.github/wordlist.txt +++ b/.github/wordlist.txt @@ -80,3 +80,195 @@ DnsResolver dnsResolver evalReadOnly gg +ACL +AOT +APIs +API’s +Akka +Async +AsyncCommand +Asynchronicity +Backpressure +CamelCase +Charset +ClientResources +CommandLatencyCollector +CommandWrapper +CompletionStage +Config +Coroutine +Coroutines +Customizer +DNS +DSL +EPoll +ElastiCache +EventExecutorGroup +EventLoop +EventLoopGroup +EventPublisher +Failover +GZIP +Graal +GraalVM +Graal's +HdrHistogram +IPs +Iterable +JDK +JFR +JIT +JNI +KeyStreamingChannel +KeyValueStreamingChannel +Kops +Kqueue +Kryo +LatencyUtils +Luascripts +MasterReplica +Misconfiguring +Mult +NIO +Netty’s +NodeSelection +OpenSSL +PEM +POSIX +Plaintext +RTT +Reconnection +RedisClient +RedisClusterClient +RedisURIs +RxJava +SHA +SPI +ScoredValueStreamingChannel +Serializer +Sharded +Sharding +SomeClient +StartTLS +StreamingChannel +StreamingChannels +SubstrateVM +TCP +TLS +TimedScheduler +TransactionalCommand +URIs +Un +ValueStreamingChannel +aggregable +amongst +analytics +args +assignability +async +asynchronicity +backoff +backpressure +boolean +broadcasted +bytecode +cancelation +channelId +charset +classpath +codecs +config +coroutines +customizable +customizer +dataset +deserialization +desynchronize +desynchronizes +encodings +epId +epoll +executables +extensibility +failover +fromExecutor +gradle +Graal's +hasNext +hostnames +idempotency +integrations +interoperable +interoperate +invoker +json +keyspace +kotlinx +kqueue +latencies +lifecycle +localhost +macOS +microservices +misconfiguration +multithreaded +natively +netty's +newSingle +nodeId +nodeIds +nodeId's +nullability +onCompleted +onError +onNext +oss +parametrized +pipelining +pluggable +pre +preconfigured +predefine +reconnection +redirections +replicaN +retrigger +runtimes +se +sharding +stateful +subclasses +subcommand +synthetization +th +throwable +topologies +transcoding +typesafe +un +unconfigured +unix +uring +whitespace +xml +RedisJSON +MkDocs +ClientOptions +TimeoutOptions +timeoutOptions +ClusterCommand +completeExceptionally +spublish +BitSet +RedisClusterNode's +allOf +reentrant +jacoco +apache +failsafe +hdrhistogram +bom +ubuntu +behaviour +databind +jackson \ No newline at end of file diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000000..9d8400e426 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,36 @@ +name: Publish Docs +on: + push: + branches: ["main"] +permissions: + contents: read + pages: write + id-token: write +concurrency: + group: "pages" + cancel-in-progress: false +jobs: + build-and-deploy: + concurrency: ci-${{ github.ref }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: 3.9 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install mkdocs mkdocs-material pymdown-extensions mkdocs-macros-plugin + - name: Build docs + run: | + mkdocs build -d docsbuild + - name: Setup Pages + uses: actions/configure-pages@v3 + - name: Upload artifact + uses: actions/upload-pages-artifact@v1 + with: + path: 'docsbuild' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v2 \ No newline at end of file diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 938cc30f48..9213271307 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -20,7 +20,7 @@ jobs: build: name: Build and Test - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout project uses: actions/checkout@v4 @@ -54,6 +54,10 @@ jobs: JVM_OPTS: -Xmx3200m TERM: dumb - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v4.0.1 + uses: codecov/test-results-action@v1 with: + fail_ci_if_error: false + files: ./target/surefire-reports/TEST*,./target/failsafe-reports/TEST* + codecov_yml_path: ./.github/codecov.yml + verbose: ${{ runner.debug }} token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 8461e501fe..a0c6d54045 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-latest steps: # Drafts your next Release notes as Pull Requests are merged into "master" - - uses: release-drafter/release-drafter@v5 + - uses: release-drafter/release-drafter@v6 with: # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml config-name: release-drafter-config.yml diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml index f3239f240b..bf102d541c 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/spellcheck.yml @@ -6,7 +6,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Check Spelling uses: rojopolis/spellcheck-github-actions@0.36.0 with: diff --git a/README.md b/README.md index a1484030eb..3fcbcc63b5 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,17 @@ Lettuce - Advanced Java Redis client =============================== - [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.lettuce/lettuce-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.lettuce/lettuce-core) + [![Integration](https://github.com/redis/lettuce/actions/workflows/integration.yml/badge.svg?branch=main)](https://github.com/redis/lettuce/actions/workflows/integration.yml) + [![codecov](https://codecov.io/gh/redis/lettuce/branch/main/graph/badge.svg?token=pAstxAAjYo)](https://codecov.io/gh/redis/lettuce) + [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE.txt) + [![Maven Central](https://img.shields.io/maven-central/v/io.lettuce/lettuce-core?versionSuffix=RELEASE&logo=redis + )](https://maven-badges.herokuapp.com/maven-central/io.lettuce/lettuce-core) + [![Javadocs](https://www.javadoc.io/badge/io.lettuce/lettuce-core.svg)](https://www.javadoc.io/doc/io.lettuce/lettuce-core) + +[![Discord](https://img.shields.io/discord/697882427875393627.svg?style=social&logo=discord)](https://discord.gg/redis) +[![Twitch](https://img.shields.io/twitch/status/redisinc?style=social)](https://www.twitch.tv/redisinc) +[![YouTube](https://img.shields.io/youtube/channel/views/UCD78lHSwYqMlyetR0_P4Vig?style=social)](https://www.youtube.com/redisinc) +[![Twitter](https://img.shields.io/twitter/follow/redisinc?style=social)](https://twitter.com/redisinc) Lettuce is a scalable thread-safe Redis client for synchronous, asynchronous and reactive usage. Multiple threads may share one connection if they avoid blocking and transactional @@ -11,57 +21,43 @@ Supports advanced Redis features such as Sentinel, Cluster, Pipelining, Auto-Rec This version of Lettuce has been tested against the latest Redis source-build. -* [synchronous](https://github.com/lettuce-io/lettuce-core/wiki/Basic-usage), [asynchronous](https://github.com/lettuce-io/lettuce-core/wiki/Asynchronous-API-%284.0%29) and [reactive](https://github.com/lettuce-io/lettuce-core/wiki/Reactive-API-%285.0%29) usage -* [Redis Sentinel](https://github.com/lettuce-io/lettuce-core/wiki/Redis-Sentinel) -* [Redis Cluster](https://github.com/lettuce-io/lettuce-core/wiki/Redis-Cluster) -* [SSL](https://github.com/lettuce-io/lettuce-core/wiki/SSL-Connections) and [Unix Domain Socket](https://github.com/lettuce-io/lettuce-core/wiki/Unix-Domain-Sockets) connections -* [Streaming API](https://github.com/lettuce-io/lettuce-core/wiki/Streaming-API) -* [CDI](https://github.com/lettuce-io/lettuce-core/wiki/CDI-Support) and [Spring](https://github.com/lettuce-io/lettuce-core/wiki/Spring-Support) integration -* [Codecs](https://github.com/lettuce-io/lettuce-core/wiki/Codecs) (for UTF8/bit/JSON etc. representation of your data) -* multiple [Command Interfaces](https://github.com/lettuce-io/lettuce-core/wiki/Command-Interfaces-%284.0%29) -* Support for [Native Transports](https://github.com/lettuce-io/lettuce-core/wiki/Native-Transports) +* [synchronous](https://redis.github.io/lettuce/user-guide/connecting-redis/#basic-usage), [asynchronous](https://redis.github.io/lettuce/user-guide/async-api/) and [reactive](https://redis.github.io/lettuce/user-guide/reactive-api/) usage +* [Redis Sentinel](https://redis.github.io/lettuce/ha-sharding/#redis-sentinel_1) +* [Redis Cluster](https://redis.github.io/lettuce/ha-sharding/#redis-cluster) +* [SSL](https://redis.github.io/lettuce/advanced-usage/#ssl-connections) and [Unix Domain Socket](https://redis.github.io/lettuce/advanced-usage/#unix-domain-sockets) connections +* [Streaming API](https://redis.github.io/lettuce/advanced-usage/#streaming-api) +* [CDI](https://redis.github.io/lettuce/integration-extension/#cdi-support) +* [Codecs](https://redis.github.io/lettuce/integration-extension/#codecss) (for UTF8/bit/JSON etc. representation of your data) +* multiple [Command Interfaces](https://github.com/redis/lettuce/wiki/Command-Interfaces-%284.0%29) +* Support for [Native Transports](https://redis.github.io/lettuce/advanced-usage/#native-transports) * Compatible with Java 8++ (implicit automatic module w/o descriptors) -See the [reference documentation](https://lettuce.io/docs/) and [Wiki](https://github.com/lettuce-io/lettuce-core/wiki) for more details. +See the [reference documentation](https://redis.github.io/lettuce/) and [API Reference](https://www.javadoc.io/doc/io.lettuce/lettuce-core/latest/index.html) for more details. ## How do I Redis? -[Learn for free at Redis University](https://university.redis.com/) +[Learn for free at Redis University](https://university.redis.io/academy) -[Build faster with the Redis Launchpad](https://launchpad.redis.com/) +[Try the Redis Cloud](https://redis.io/try-free/) -[Try the Redis Cloud](https://redis.com/try-free/) +[Dive in developer tutorials](https://redis.io/learn/) -[Dive in developer tutorials](https://developer.redis.com/) - -[Join the Redis community](https://redis.com/community/) - -[Work at Redis](https://redis.com/company/careers/jobs/) - -Communication ---------------- - -* [GitHub Discussions](https://github.com/lettuce-io/lettuce-core/discussions) (Q&A, Ideas, General discussion) -* Stack Overflow (Questions): [https://stackoverflow.com/questions/tagged/lettuce](https://stackoverflow.com/questions/tagged/lettuce) -* Discord: [![Discord](https://img.shields.io/discord/697882427875393627.svg?style=social&logo=discord)](https://discord.gg/redis) -* Twitter: [![Twitter](https://img.shields.io/twitter/follow/redisinc?style=social)](https://twitter.com/redisinc) -* [GitHub Issues](https://github.com/lettuce-io/lettuce-core/issues) (Bug reports, feature requests) +[Join the Redis community](https://redis.io/community/) +[Work at Redis](https://redis.io/careers/jobs/) Documentation --------------- -* [Reference documentation](https://lettuce.io/docs/) -* [Wiki](https://github.com/lettuce-io/lettuce-core/wiki) -* [Javadoc](https://lettuce.io/core/release/api/) - +* [Reference documentation](https://redis.github.io/lettuce/) +* [Javadoc](https://www.javadoc.io/doc/io.lettuce/lettuce-core/latest/index.html) Binaries/Download ---------------- Binaries and dependency information for Maven, Ivy, Gradle and others can be found at http://search.maven.org. -Releases of lettuce are available in the Maven Central repository. Take also a look at the [Releases](https://github.com/lettuce-io/lettuce-core/releases). +Releases of lettuce are available in the Maven Central repository. Take also a look at the [Releases](https://github.com/redis/lettuce/releases). Example for Maven: @@ -109,7 +105,7 @@ to the lowercase Redis command name. Complex commands with multiple modifiers that change the result type include the CamelCased modifier as part of the command name, e.g. zrangebyscore and zrangebyscoreWithScores. -See [Basic usage](https://github.com/lettuce-io/lettuce-core/wiki/Basic-usage) for further details. +See [Basic usage](https://redis.github.io/lettuce/user-guide/connecting-redis/#basic-usage) for further details. Asynchronous API ------------------------ @@ -117,8 +113,8 @@ Asynchronous API ```java StatefulRedisConnection connection = client.connect(); RedisStringAsyncCommands async = connection.async(); -RedisFuture set = async.set("key", "value") -RedisFuture get = async.get("key") +RedisFuture set = async.set("key", "value"); +RedisFuture get = async.get("key"); LettuceFutures.awaitAll(set, get) == true @@ -126,7 +122,7 @@ set.get() == "OK" get.get() == "value" ``` -See [Asynchronous API](https://github.com/lettuce-io/lettuce-core/wiki/Asynchronous-API-%284.0%29) for further details. +See [Asynchronous API](https://redis.github.io/lettuce/user-guide/async-api/) for further details. Reactive API ------------------------ @@ -142,7 +138,7 @@ set.subscribe(); get.block() == "value" ``` -See [Reactive API](https://github.com/lettuce-io/lettuce-core/wiki/Reactive-API-%285.0%29) for further details. +See [Reactive API](https://redis.github.io/lettuce/user-guide/reactive-api/) for further details. Pub/Sub ------- @@ -150,7 +146,7 @@ Pub/Sub ```java RedisPubSubCommands connection = client.connectPubSub().sync(); connection.getStatefulConnection().addListener(new RedisPubSubListener() { ... }) -connection.subscribe("channel") +connection.subscribe("channel"); ``` Building @@ -162,7 +158,7 @@ are configured using a ```Makefile```. Tests run by default against Redis `unsta To build: ``` -$ git clone https://github.com/lettuce-io/lettuce-core.git +$ git clone https://github.com/redis/lettuce.git $ cd lettuce/ $ make prepare ssl-keys $ make test @@ -177,7 +173,7 @@ $ make test Bugs and Feedback ----------- -For bugs, questions and discussions please use the [GitHub Issues](https://github.com/lettuce-io/lettuce-core/issues). +For bugs, questions and discussions please use the [GitHub Issues](https://github.com/redis/lettuce/issues). License ------- @@ -189,4 +185,4 @@ Contributing ------- Github is for social coding: if you want to write code, I encourage contributions through pull requests from forks of this repository. -Create Github tickets for bugs and new features and comment on the ones that you are interested in and take a look into [CONTRIBUTING.md](https://github.com/lettuce-io/lettuce-core/blob/main/.github/CONTRIBUTING.md) +Create Github tickets for bugs and new features and comment on the ones that you are interested in and take a look into [CONTRIBUTING.md](https://github.com/redis/lettuce/blob/main/.github/CONTRIBUTING.md) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 481c2b4cf8..5f8b73369f 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,11 +1,10 @@ -Lettuce 6.4.0 RELEASE NOTES +Lettuce 6.5.0 RELEASE NOTES ============================== -The Redis team is delighted to announce general availability of Lettuce 6.4. +The Redis team is delighted to announce the general availability of Lettuce 6.5. -This Lettuce driver is now going to be shipped under the MIT licensing scheme. The `CLIENT SETINFO` -is now working in a fire-and-forget mode to allow better compatibility with Redis servers that do -not support this command. +Great news, everyone! Lettuce 6.5.0 comes with RedisJSON support enabled. +For more on that, please consult with the [RedisJSON documentation](https://redis.io/docs/latest/develop/data-types/json/) and the [Lettuce guide on RedisJSON](https://redis.github.io/lettuce/user-guide/redis-json/). Lettuce 6 supports Redis 2.6+ up to Redis 7.x. In terms of Java runtime, Lettuce requires at least Java 8 and works with Java 21. @@ -16,45 +15,43 @@ If you need any support, meet Lettuce at * GitHub Discussions: https://github.com/lettuce-io/lettuce-core/discussions * Stack Overflow (Questions): https://stackoverflow.com/questions/tagged/lettuce -* Join the chat at https://discord.gg/redis for general discussion -* GitHub Issues (Bug reports, feature - requests): https://github.com/lettuce-io/lettuce-core/issues -* Documentation: https://lettuce.io/core/6.4.0.RELEASE/reference/ -* Javadoc: https://lettuce.io/core/6.4.0.RELEASE/api/ +* Join the chat at https://discord.gg/redis and look for the "Help:Tools Lettuce" channel +* GitHub Issues (Bug reports, feature requests): https://github.com/lettuce-io/lettuce-core/issues +* Documentation: https://lettuce.io/core/6.5.0.RELEASE/reference/ +* Javadoc: https://lettuce.io/core/6.5.0.RELEASE/api/ Commands -------- -* Add `PUBSUB` shard channel commands `SHARDCHANNELS` #2756, `SHARDNUMSUB` #2776 -* Add `PUBSUB` shard channel commands `SPUBLISH` #2757, `SSUBSCRIBE` #2758 and `SUNSUBSCRIBE` #2758 -* Add support for `CLIENT KILL [MAXAGE]` #2782 -* Hash field expiration commands `HEXPIRE`, `HEXPIREAT`, `HEXPIRETIME` and `HPERSIST` #2836 -* Hash field expiration commands `HPEXPIRE`, `HPEXPIREAT`, `HPEXPIRETIME`, `HTTL` and `HPTTL` #2857 +* Add `CLUSTER MYSHARDID` in #2920 and `CLUSTER LINKS` in #2986 +* Add `CLIENT TRACKINGINFO` in #2862 Enhancements ------------ -* Add support for `HSCAN NOVALUES` #2763 -* Send the `CLIENT SETINFO` command in a fire-and-forget way #2082 -* Change the license to more permissive MIT #2173 -* Add a evalReadOnly overload that accepts the script as a String #2868 -* `XREAD` support for reading last message from stream #2863 -* Mark dnsResolver(DnsResolver) as deprecated #2855 -* Remove connection-related methods from commands API #2027 -* Move connection-related commands from BaseRedisCommands to RedisConnectionCommands #2031 +* Default ClientOptions.timeoutOptions to TimeoutOptions.enabled() (#2927) +* Update completeExceptionally on ClusterCommand using super (#2980) Fixes ----- +* fix(2971): spublish typo fix (#2972) +* Initialize slots with empty BitSet in RedisClusterNode's constructors (#2341) +* Add defensive copy for Futures allOf() method (#2943) +* fix:deadlock when reentrant exclusive lock (#2961) -* None Other ----- -* Bump `org.apache.commons:commons-pool2` from 2.11.1 to 2.12.0 #2877 -* Bump `org.openjdk.jmh:jmh-generator-annprocess` from 1.21 to 1.37 #2876 -* Bump `org.apache.maven.plugins:maven-jar-plugin` from 3.3.0 to 3.4.1 #2875 -* Bump `org.codehaus.mojo:flatten-maven-plugin from` 1.5.0 to 1.6.0 #2874 -* Bump `org.apache.maven.plugins:maven-javadoc-plugin` from 3.6.3 to 3.7.0 #2873 -* Applying code formatter each time we run a Maven build #2841 -* Bump `setup-java` to v4 #2807 +* Add badges to the README.md file (#2939) +* Convert wiki to markdown docs (#2944) +* Bump org.jacoco:jacoco-maven-plugin from 0.8.9 to 0.8.12 (#2921) +* Bump org.apache.maven.plugins:maven-surefire-plugin from 3.2.5 to 3.3.1 (#2922) +* Bump org.apache.maven.plugins:maven-failsafe-plugin from 3.2.5 to 3.3.1 (#2958) +* Bump org.apache.maven.plugins:maven-javadoc-plugin from 3.7.0 to 3.8.0 (#2957) +* Bump org.apache.maven.plugins:maven-surefire-plugin from 3.3.1 to 3.4.0 (#2968) +* Bump org.hdrhistogram:HdrHistogram from 2.1.12 to 2.2.2 (#2966) +* Bump org.apache.maven.plugins:maven-compiler-plugin from 3.12.1 to 3.13.0 (#2978) +* Bump org.apache.logging.log4j:log4j-bom from 2.17.2 to 2.24.0 (#2988) +* Bump io.netty:netty-bom from 4.1.107.Final to 4.1.113.Final (#2990) +* Suspected change in ubuntu causing CI failures (#2949) diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md new file mode 100644 index 0000000000..19e8c2b9d5 --- /dev/null +++ b/docs/advanced-usage.md @@ -0,0 +1,2634 @@ +# Advanced usage + +## Configuring Client resources + +Client resources are configuration settings for the client related to +performance, concurrency, and events. A vast part of Client resources +consists of thread pools (`EventLoopGroup`s and a `EventExecutorGroup`) +which build the infrastructure for the connection workers. In general, +it is a good idea to reuse instances of `ClientResources` across +multiple clients. + +Client resources are stateful and need to be shut down if they are +supplied from outside the client. + +### Creating Client resources + +Client resources are required to be immutable. You can create instances +using two different patterns: + +**The `create()` factory method** + +By using the `create()` method on `DefaultClientResources` you create +`ClientResources` with default settings: + +``` java +ClientResources res = DefaultClientResources.create(); +``` + +This approach fits the most needs. + +**Resources builder** + +You can build instances of `DefaultClientResources` by using the +embedded builder. It is designed to configure the resources to your +needs. The builder accepts the configuration in a fluent fashion and +then creates the ClientResources at the end: + +``` java +ClientResources res = DefaultClientResources.builder() + .ioThreadPoolSize(4) + .computationThreadPoolSize(4) + .build() +``` + +### Using and reusing `ClientResources` + +A `RedisClient` and `RedisClusterClient` can be created without passing +`ClientResources` upon creation. The resources are exclusive to the +client and are managed itself by the client. When calling `shutdown()` +of the client instance `ClientResources` are shut down. + +``` java +RedisClient client = RedisClient.create(); +... +client.shutdown(); +``` + +If you require multiple instances of a client or you want to provide +existing thread infrastructure, you can configure a shared +`ClientResources` instance using the builder. The shared Client +resources can be passed upon client creation: + +``` java +ClientResources res = DefaultClientResources.create(); +RedisClient client = RedisClient.create(res); +RedisClusterClient clusterClient = RedisClusterClient.create(res, seedUris); +... +client.shutdown(); +clusterClient.shutdown(); +res.shutdown(); +``` + +Shared `ClientResources` are never shut down by the client. Same applies +for shared `EventLoopGroupProvider`s that are an abstraction to provide +`EventLoopGroup`s. + +#### Why `Runtime.getRuntime().availableProcessors()` \* 3? + +Netty requires different `EventLoopGroup`s for NIO (TCP) and for EPoll +(Unix Domain Socket) connections. One additional `EventExecutorGroup` is +used to perform computation tasks. `EventLoopGroup`s are started lazily +to allocate Threads on-demand. + +#### Shutdown + +Every client instance requires a call to `shutdown()` to clear used +resources. Clients with dedicated `ClientResources` (i.e. no +`ClientResources` passed within the constructor/`create`-method) will +shut down `ClientResources` on their own. + +Client instances with using shared `ClientResources` (i.e. +`ClientResources` passed using the constructor/`create`-method) won’t +shut down the `ClientResources` on their own. The `ClientResources` +instance needs to be shut down once it’s not used anymore. + +### Configuration settings + +The basic configuration options are listed in the table below: + +| Name | Method | Default | +|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------|------------------------| +| **I/O Thread Pool Size** | `ioThreadPoolSize` | `Number of processors` | +| The number of threads in the I/O thread pools. The number defaults to the number of available processors that the runtime returns (which, as a well-known fact, sometimes does not represent the actual number of processors). Every thread represents an internal event loop where all I/O tasks are run. The number does not reflect the actual number of I/O threads because the client requires different thread pools for Network (NIO) and Unix Domain Socket (EPoll) connections. The minimum I/O threads are `3`. A pool with fewer threads can cause undefined behavior. | | | +| **Computation Thread Pool Size** | `comput ationThreadPoolSize` | `Number of processors` | +| The number of threads in the computation thread pool. The number defaults to the number of available processors that the runtime returns (which, as a well-known fact, sometimes does not represent the actual number of processors). Every thread represents an internal event loop where all computation tasks are run. The minimum computation threads are `3`. A pool with fewer threads can cause undefined behavior. | | | + +### Advanced settings + +Values for the advanced options are listed in the table below and should +not be changed unless there is a truly good reason to do so. + + +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameMethodDefault
Provider for EventLoopGroupeventLoopGroupProvidernone
For those who want to reuse existing netty infrastructure or the +total control over the thread pools, the +EventLoopGroupProvider API provides a way to do so. +EventLoopGroups are obtained and managed by an +EventLoopGroupProvider. A provided +EventLoopGroupProvider is not managed by the client and +needs to be shut down once you no longer need the resources.
Provided EventExecutorGroupeventExecutorGroupnone
For those who want to reuse existing netty infrastructure or the +total control over the thread pools can provide an existing +EventExecutorGroup to the Client resources. A provided +EventExecutorGroup is not managed by the client and needs +to be shut down once you do not longer need the resources.
Event buseventBusDefaultEventBus
The event bus system is used to transport events from the client to +subscribers. Events are about connection state changes, metrics, and +more. Events are published using a RxJava subject and the default +implementation drops events on backpressure. Learn more about the Reactive API. You can also publish your own +events. If you wish to do so, make sure that your events implement the +Event marker interface.
Command latency collector optionscommandLatencyCollectorOptionsDefaultCommandLatencyCollectorOptions
The client can collect latency metrics during while dispatching +commands. The options allow configuring the percentiles, level of +metrics (per connection or server) and whether the metrics are +cumulative or reset after obtaining these. Command latency collection is +enabled by default and can be disabled by setting +commandLatencyPublisherOptions(…) to +DefaultEventPublisherOptions.disabled(). Latency +collector requires LatencyUtils to be on your class +path.
Command latency collectorcommandLatencyCollectorDefaultCommandLatencyCollector
The client can collect latency metrics during while dispatching +commands. Command latency metrics is collected on connection or server +level. Command latency collection is enabled by default and can be +disabled by setting commandLatency CollectorOptions(…) to +DefaultCom mandLatencyCollector Options.disabled().
Latency event publisher optionscommandLatencyPublisherOptionsDefaultEventPublisherOptions
Command latencies can be published using the event bus. Latency +events are emitted by default every 10 minutes. Event publishing can be +disabled by setting commandLatencyPublisherOptions(…) to +DefaultEventPublisherOptions.disabled().
DNS ResolverdnsResolverDnsResolvers.JVM_DEFAULT ( or netty if present)

Since: 3.5, 4.2. Deprecated: 6.4

+

Configures a DNS resolver to resolve hostnames to a +java.net.InetAddress. Defaults to the JVM DNS resolution +that uses blocking hostname resolution and caching of lookup results. +Users of DNS-based Redis-HA setups (e.g. AWS ElastiCache) might want to +configure a different DNS resolver. Lettuce comes with +DirContextDnsResolver that uses Java’s +DnsContextFactory to resolve hostnames. +DirContextDnsResolver allows using either the system DNS +or custom DNS servers without caching of results so each hostname lookup +yields in a DNS lookup.

+

Since 4.4: Defaults to DnsResolvers.UNRESOLVED to use +netty's AddressResolver that resolves DNS names on +Bootstrap.connect() (requires netty 4.1)

Address Resolver GroupaddressResolverGroupDefaultAddressResolverGroup.INSTANCE ( or netty DnsAddressResolverGroup if present)

Since: 6.1

+

Sets the AddressResolverGroup for DNS resolution. This option is only effective if +DnsResolvers#UNRESOLVED is used as DnsResolver. Defaults to +io.netty.resolver.DefaultAddressResolverGroup#INSTANCE if netty-dns-resolver +is not available, otherwise defaults to io.netty.resolver.dns.DnsAddressResolverGroup

+

Users of DNS-based Redis-HA setups (e.g. AWS ElastiCache) might want to configure a different DNS +resolver group. For example: + +```java +new DnsAddressResolverGroup( + new DnsNameResolverBuilder(dnsEventLoop) + .channelType(NioDatagramChannel.class) + .resolveCache(NoopDnsCache.INSTANCE) + .cnameCache(NoopDnsCnameCache.INSTANCE) + .authoritativeDnsServerCache(NoopAuthoritativeDnsServerCache.INSTANCE) + .consolidateCacheSize(0) +); +``` + +

+
Reconnect DelayreconnectDelayDelay.exponential()

Since: 4.2

+

Configures a reconnect delay used to delay reconnect attempts. +Defaults to binary exponential delay with an upper boundary of +30 SECONDS. See Delay for more delay +implementations.

Netty CustomizerNettyCustomizernone

Since: 4.4

+

Configures a netty customizer to enhance netty components. Allows +customization of Bootstrap after Bootstrap +configuration by Lettuce and Channel customization after +all Lettuce handlers are added to Channel. The customizer +allows custom SSL configuration (requires RedisURI in plain-text mode, +otherwise Lettuce’s configures SSL), adding custom handlers or setting +customized Bootstrap options. Misconfiguring +Bootstrap or Channel can cause connection +failures or undesired behavior.

Tracingtracingdisabled

Since: 5.1

+

Configures a tracing instance to trace Redis calls. +Lettuce wraps Brave data models to support tracing in a vendor-agnostic +way if Brave is on the class path. A Brave tracing instance +can be created using BraveTracing.create(clientTracing);, +where clientTracing is a created or existent Brave tracing +instance .

+ +## Client Options + +Client options allow controlling behavior for some specific features. + +Client options are immutable. Connections inherit the current options at +the moment the connection is created. Changes to options will not affect +existing connections. + +``` java +client.setOptions(ClientOptions.builder() + .autoReconnect(false) + .pingBeforeActivateConnection(true) + .build()); +``` + + +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameMethodDefault
PING before activating connectionpingBefor eActivateConnectiontrue

Since: 3.1, 4.0

+

Perform a lightweight PING connection handshake when +establishing a Redis connection. If true (default is true), +every connection and reconnect will issue a PING command +and await its response before the connection is activated and enabled +for use. If the check fails, the connect/reconnect is treated as a +failure. This option has no effect unless forced to use the RESP 2 +protocol version. RESP 3/protocol discovery performs a +HELLO handshake.

+

Failed PING's on reconnect are handled as protocol +errors and can suspend reconnection if +suspendReconnectOnProtocolFailure is enabled.

+

The PING handshake validates whether the other end of +the connected socket is a service that behaves like a Redis +server.

Auto-ReconnectautoReconnecttrue

Since: 3.1, 4.0

+

Controls auto-reconnect behavior on connections. As soon as a +connection gets closed/reset without the intention to close it, the +client will try to reconnect, activate the connection and re-issue any +queued commands.

+

This flag also has the effect that disconnected connections will +refuse commands and cancel these with an exception.

Cancel commands on reconnect failurecancelCommand sOnReconnectFailurefalse

Since: 3.1, 4.0

+

This flag is deprecated and should not be used as it can lead +to race conditions and protocol offsets. SSL is natively supported by +Lettuce and does no longer requires the use of SSL tunnels where +protocol traffic can get out of sync.

+

If this flag is true any queued commands will be +canceled when a reconnect fails within the activation sequence. The +reconnect itself has two phases: Socket connection and +protocol/connection activation. In case a connect timeout occurs, a +connection reset, host lookup fails, this does not affect the +cancellation of commands. In contrast, where the protocol/connection +activation fails due to SSL errors or PING before activating connection +failure, queued commands are canceled.

Policy how to reclaim decode buffer memorydecodeBufferPolicyratio-based at 75%

Since: 6.0

+

Policy to discard read bytes from the decoding aggregation buffer to +reclaim memory. See DecodeBufferPolicies for available +strategies.

Suspend reconnect on protocol failuresuspendReconnectOnProtocolFailurefalse (was introduced in 3. 1 with default true)

Since: 3.1, 4.0

+

If this flag is true the reconnect will be suspended on +protocol errors. The reconnect itself has two phases: Socket connection +and protocol/connection activation. In case a connect timeout occurs, a +connection reset, host lookup fails, this does not affect the +cancellation of commands. In contrast, where the protocol/connection +activation fails due to SSL errors or PING before activating connection +failure, queued commands are canceled.

+

Reconnection can be activated again, but there is no public API to +obtain the ConnectionWatchdog instance.

Request queue sizerequestQueueSize2147483647 (Integer#MAX_VALUE)

Since: 3.4, 4.1

+

Controls the per-connection request queue size. The command +invocation will lead to a RedisException if the queue size +is exceeded. Setting the requestQueueSize to a lower value +will lead earlier to exceptions during overload or while the connection +is in a disconnected state. A higher value means hitting the boundary +will take longer to occur, but more requests will potentially be queued, +and more heap space is used.

Disconnected behaviordisconnectedBehaviorDEFAULT

Since: 3.4, 4.1

+

A connection can behave in a disconnected state in various ways. The +auto-connect feature allows in particular to retrigger commands that +have been queued while a connection is disconnected. The disconnected +behavior setting allows fine-grained control over the behavior. +Following settings are available:

+

DEFAULT: Accept commands when auto-reconnect is enabled, +reject commands when auto-reconnect is disabled.

+

ACCEPT_COMMANDS: Accept commands in disconnected +state.

+

REJECT_COMMANDS: Reject commands in disconnected +state.

Protocol VersionprotocolVersionLatest/Auto-discovery

Since: 6.0

+

Configuration of which protocol version (RESP2/RESP3) to use. Leaving +this option unconfigured performs a protocol discovery to use the +latest available protocol.

Script CharsetscriptCharsetUTF-8

Since: 6.0

+

Charset to use for Luascripts.

Socket OptionssocketOptions10 seconds Connecti on-Timeout, no keep-a live, no TCP noDelay

Since: 4.3

+

Options to configure low-level socket options for the connections +kept to Redis servers.

SSL OptionssslOptions(non e), use JDK defaults

Since: 4.3

+

Configure SSL options regarding SSL providers (JDK/OpenSSL) and key +store/trust store.

Timeout OptionstimeoutOptionsDo n ot timeout commands.

Since: 5.1

+

Options to configure command timeouts applied to timeout commands +after dispatching these (active connections, queued while disconnected, +batch buffer). By default, the synchronous API times out commands using +RedisURI.getTimeout().

Publish Reactive Signals on SchedulerpublishOnSchedulerUse I/O thread.

Since: 5.1.4

+

Use a dedicated Scheduler to emit reactive data signals. +Enabling this option can be useful for reactive sequences that require a +significant amount of processing with a single/a few Redis connections +performance suffers from a single-thread-like behavior. Enabling this +option uses EventExecutorGroup configured through +ClientResources for data/completion signals. The used +Thread is sticky across all signals for a single +Publisher instance.

+ +### Cluster-specific options + +Cluster client options extend the regular client options by some cluster +specifics. + +Cluster client options are immutable. Connections inherit the current +options at the moment the connection is created. Changes to options will +not affect existing connections. + +``` java +ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder() + .enablePeriodicRefresh(refreshPeriod(10, TimeUnit.MINUTES)) + .enableAllAdaptiveRefreshTriggers() + .build(); + +client.setOptions(ClusterClientOptions.builder() + .topologyRefreshOptions(topologyRefreshOptions) + .build()); +``` + + +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameMethodDefault
Periodic cluster topology refreshen ablePeriodicRefreshfalse

Since: 3.1, 4.0

+

Enables or disables periodic cluster topology refresh. The refresh is +handled in the background. Partitions, the view on the Redis cluster +topology, are valid for a whole RedisClusterClient +instance, not a connection. All connections created by this client +operate on the one cluster topology.

+

The refresh job is regularly executed, the period between the runs +can be set with refreshPeriod. The refresh job starts after +either opening the first connection with the job enabled or by calling +reloadPartitions. The job can be disabled without +discarding the full client by setting new client options.

Cluster topology refresh periodrefreshPeriod60 SECONDS

Since: 3.1, 4.0

+

Set the period between the refresh job runs. The effective interval +cannot be changed once the refresh job is active. Changes to the value +will be ignored.

Adaptive cluster topology refreshenableAda ptiveRefreshTrigger(none)

Since: 4.2

+

Enables selectively adaptive topology refresh triggers. Adaptive +refresh triggers initiate topology view updates based on events happened +during Redis Cluster operations. Adaptive triggers lead to an immediate +topology refresh. These refreshes are rate-limited using a timeout since +events can happen on a large scale. Adaptive refresh triggers are +disabled by default. Following triggers can be enabled:

+

MOVED_REDIRECT, ASK_REDIRECT, +PER SISTENT_RECONNECTS, UNKNOWN_NODE (since +5.1), and UNCOVERED_SLOT (since 5.2) (see also reconnect +attempts for the reconnect trigger)

Adaptive refresh triggers timeoutadaptiveRef reshTriggersTimeout30 SECONDS

Since: 4.2

+

Set the timeout between the adaptive refresh job runs. Multiple +triggers within the timeout will be ignored, only the first enabled +trigger leads to a topology refresh. The effective period cannot be +changed once the refresh job is active. Changes to the value will be +ignored.

Reconnect attempts (Adaptive topology refresh trigger)refreshTrigge rsReconnectAttempts5

Since: 4.2

+

Set the threshold for the PE RSISTENT_RECONNECTS refresh +trigger. Topology updates based on persistent reconnects lead only to a +refresh if the reconnect process tries at least the number of specified +attempts. The first reconnect attempt starts with +1.

Dynamic topology refresh sourcesdy namicRefreshSourcestrue

Since: 4.2

+

Discover cluster nodes from the topology and use only the discovered +nodes as the source for the cluster topology. Using dynamic refresh will +query all discovered nodes for the cluster topology details. If set to +false, only the initial seed nodes will be used as sources +for topology discovery and the number of clients will be obtained only +for the initial seed nodes. This can be useful when using Redis Cluster +with many nodes.

+

Note that enabling dynamic topology refresh sources uses node +addresses reported by Redis CLUSTER NODES output which +typically contains IP addresses.

Close stale connectionscl oseStaleConnectionstrue

Since: 3.3, 4.1

+

Stale connections are existing connections to nodes which are no +longer part of the Redis Cluster. If this flag is set to +true, then stale connections are closed upon topology +refreshes. It’s strongly advised to close stale connections as open +connections will attempt to reconnect nodes if the node is no longer +available and open connections require system resources.

Limitation of cluster redirectsmaxRedirects5

Since: 3.1, 4.0

+

When the assignment of a slot-hash is moved in a Redis Cluster and a +client requests a key that is located on the moved slot-hash, the +Cluster node responds with a -MOVED response. In this case, +the client follows the redirection and queries the cluster specified +within the redirection. Under some circumstances, the redirection can be +endless. To protect the client and also the Cluster, a limit of max +redirects can be configured. Once the limit is reached, the +-MOVED error is returned to the caller. This limit also +applies for -ASK redirections in case a slot is set to +MIGRATING state.

Filter nodes from TopologynodeFilterno filter

Since: 6.1.6

+

When providing a nodeFilter, then +RedisClusterNodes can be filtered from the topology view to +remove unwanted nodes (e.g. failed replicas). Note that the filter is +applied only after obtaining the topology so the filter does not prevent +trying to connect the node during topology discovery.

Validate cluster node membershipvalidateCl usterNodeMembershiptrue

Since: 3.3, 4.0

+

Validate the cluster node membership before allowing connections to +that node. The current implementation performs redirects using +MOVED and ASK and allows obtaining connections +to the particular cluster nodes. The validation was introduced during +the development of version 3.3 to prevent security breaches and only +allow connections to the known hosts of the CLUSTER NODES +output.

+

There are some scenarios, where the strict validation is an +obstruction:

+

MOVED/ASK redirection but the cluster +topology view is stale Connecting to cluster nodes using different +IPs/hostnames (e.g. private/public IPs)

+

Connecting to non-cluster members to reconfigure those while using +the RedisClusterClient connection.

+ +### Request queue size and cluster + +Clustered operations use multiple connections. The resulting +overall-queue limit is +`requestQueueSize * ((number of cluster nodes * 2) + 1)`. + +## SSL Connections + +Lettuce supports SSL connections since version 3.1 on Redis Standalone +connections and since version 4.2 on Redis Cluster. [Redis supports SSL since version 6.0](https://redis.io/docs/latest/operate/oss_and_stack/management/security/encryption/). + +First, you need to [enable SSL on your Redis server](https://redis.io/docs/latest/operate/oss_and_stack/management/security/encryption/). + +Next step is connecting lettuce over SSL to Redis. + +``` java +RedisURI redisUri = RedisURI.Builder.redis("localhost") + .withSsl(true) + .withPassword("authentication") + .withDatabase(2) + .build(); + +RedisClient client = RedisClient.create(redisUri); +``` + +``` java +RedisURI redisUri = RedisURI.create("rediss://authentication@localhost/2"); +RedisClient client = RedisClient.create(redisUri); +``` + +``` java +RedisURI redisUri = RedisURI.Builder.redis("localhost") + .withSsl(true) + .withPassword("authentication") + .build(); + +RedisClusterClient client = RedisClusterClient.create(redisUri); +``` + +### Limitations + +Lettuce supports SSL only on Redis Standalone and Redis Cluster +connections and since 5.2, also for Master resolution using Redis +Sentinel or Redis Master/Replicas. + +### Connection Procedure and Reconnect + +When connecting using SSL, Lettuce performs an SSL handshake before you +can use the connection. Plain text connections do not perform a +handshake. Errors during the handshake throw +`RedisConnectionException`s. + +Reconnection behavior is also different to plain text connections. If an +SSL handshake fails on reconnect (because of peer/certification +verification or peer does not talk SSL) reconnection will be disabled +for the connection. You will also find an error log entry within your +logs. + +### Certificate Chains/Root Certificate/Self-Signed Certificates + +Lettuce uses Java defaults for the trust store that is usually `cacerts` +in your `jre/lib/security` directory and comes with customizable SSL +options via [client options](#client-options). If you need to add you +own root certificate, so you can configure `SslOptions`, import it +either to `cacerts` or you provide an own trust store and set the +necessary system properties: + +``` java +SslOptions sslOptions = SslOptions.builder() + .jdkSslProvider() + .truststore(new File("yourtruststore.jks"), "changeit") + .build(); + +ClientOptions clientOptions = ClientOptions.builder().sslOptions(sslOptions).build(); +``` + +``` java +System.setProperty("javax.net.ssl.trustStore", "yourtruststore.jks"); +System.setProperty("javax.net.ssl.trustStorePassword", "changeit"); +``` + +### Host/Peer Verification + +By default, Lettuce verifies the certificate against the validity and +the common name (Name validation not supported on Java 1.6, only +available on Java 1.7 and higher) of the Redis host you are connecting +to. This behavior can be turned off: + +``` java +RedisURI redisUri = ... +redisUri.setVerifyPeer(false); +``` + +or + +``` java +RedisURI redisUri = RedisURI.Builder.redis(host(), sslPort()) + .withSsl(true) + .withVerifyPeer(false) + .build(); +``` + +### StartTLS + +If you need to issue a StartTLS before you can use SSL, set the +`startTLS` property of `RedisURI` to `true`. StartTLS is disabled by +default. + +``` java +RedisURI redisUri = ... +redisUri.setStartTls(true); +``` + +or + +``` java +RedisURI redisUri = RedisURI.Builder.redis(host(), sslPort()) + .withSsl(true) + .withStartTls(true) + .build(); +``` + +## Native Transports + +Netty provides three platform-specific JNI transports: + +- epoll on Linux + +- io_uring on Linux (Incubator) + +- kqueue on macOS/BSD + +Lettuce defaults to native transports if the appropriate library is +available within its runtime. Using a native transport adds features +specific to a particular platform, generate less garbage and generally +improve performance when compared to the NIO based transport. Native +transports are required to connect to Redis via [Unix Domain +Sockets](#unix-domain-sockets) and are suitable for TCP connections as +well. + +Native transports are available with: + +- Linux **epoll** x86_64 systems with a minimum netty version of + `4.0.26.Final`, requiring `netty-transport-native-epoll`, classifier + `linux-x86_64` + + ``` xml + + io.netty + netty-transport-native-epoll + ${netty-version} + linux-x86_64 + + ``` + +- Linux **io_uring** x86_64 systems with a minimum netty version of + `4.1.54.Final`, requiring `netty-incubator-transport-native-io_uring`, + classifier `linux-x86_64`. Note that this transport is still + experimental. + + ``` xml + + io.netty.incubator + netty-incubator-transport-native-io_uring + 0.0.1.Final + linux-x86_64 + + ``` + +- macOS **kqueue** x86_64 systems with a minimum netty version of + `4.1.11.Final`, requiring `netty-transport-native-kqueue`, classifier + `osx-x86_64` + + ``` xml + + io.netty + netty-transport-native-kqueue + ${netty-version} + osx-x86_64 + + ``` + +You can disable native transport use through system properties. Set +`io.lettuce.core.epoll`, `io.lettuce.core.iouring` respective +`io.lettuce.core.kqueue` to `false` (default is `true`, if unset). + +### Limitations + +Native transport support does not work with the shaded version of +Lettuce because of two reasons: + +1. `netty-transport-native-epoll` and `netty-transport-native-kqueue` + are not packaged into the shaded jar. So adding the jar to the + classpath will resolve in different netty base classes (such as + `io.netty.channel.EventLoopGroup` instead of + `com.lambdaworks.io.netty.channel.EventLoopGroup`) + +2. Support for using epoll/kqueue with shaded netty requires netty 4.1 + and all parts of netty to be shaded. + +See also Netty [documentation on native +transports](http://netty.io/wiki/native-transports.html). + +## Unix Domain Sockets + +Lettuce supports since version 3.2 Unix Domain Sockets for local Redis +connections. + +``` java +RedisURI redisUri = RedisURI.Builder + .socket("/tmp/redis") + .withPassword("authentication") + .withDatabase(2) + .build(); + +RedisClient client = RedisClient.create(redisUri); +``` + +``` java +RedisURI redisUri = RedisURI.create("redis-socket:///tmp/redis"); +RedisClient client = RedisClient.create(redisUri); +``` + +Unix Domain Sockets are inter-process communication channels on POSIX +compliant systems. They allow exchanging data between processes on the +same host operating system. When using Redis, which is usually a network +service, Unix Domain Sockets are usable only if connecting locally to a +single instance. Redis Sentinel and Redis Cluster, maintain tables of +remote or local nodes and act therefore as a registry. Unix Domain +Sockets are not beneficial with Redis Sentinel and Redis Cluster. + +Using `RedisClusterClient` with Unix Domain Sockets would connect to the +local node using a socket and open TCP connections to all the other +hosts. A good example is connecting locally to a standalone or a single +cluster node to gain performance. + +See [Native Transports](#native-transports) for more details and +limitations. + +## Streaming API + +Redis can contain a huge set of data. Collections can burst your memory, +when the amount of data is too massive for your heap. Lettuce can return +your collection data either as List/Set/Map or can push the data on +`StreamingChannel` interfaces. + +`StreamingChannel`s are similar to callback methods. Every method, which +can return bulk data (except transactions/multi and some config methods) +specifies beside a regular method with a collection return class also +method which accepts a `StreamingChannel`. Lettuce interacts with a +`StreamingChannel` as the data arrives so data can be processed while +the command is running and is not yet completed. + +There are 4 StreamingChannels accepting different data types: + +- [KeyStreamingChannel](https://www.javadoc.io/static/io.lettuce/lettuce-core/6.4.0.RELEASE/io/lettuce/core/output/KeyStreamingChannel.html) + +- [ValueStreamingChannel](https://www.javadoc.io/static/io.lettuce/lettuce-core/6.4.0.RELEASE/io/lettuce/core/output/ValueStreamingChannel.html) + +- [KeyValueStreamingChannel](https://www.javadoc.io/static/io.lettuce/lettuce-core/6.4.0.RELEASE/io/lettuce/core/output/KeyValueStreamingChannel.html) + +- [ScoredValueStreamingChannel](https://www.javadoc.io/static/io.lettuce/lettuce-core/6.4.0.RELEASE/io/lettuce/core/output/ScoredValueStreamingChannel.html) + +The result of the steaming methods is the count of keys/values/key-value +pairs as `long` value. + +!!! NOTE + Don’t issue blocking calls (includes synchronous API calls to Lettuce) + from inside of callbacks such as the streaming API as this would block + the EventLoop. If you need to fetch data from Redis from inside a + `StreamingChannel` callback, please use the asynchronous API or use + the reactive API directly. + +``` java +Long count = redis.hgetall(new KeyValueStreamingChannel() + { + @Override + public void onKeyValue(String key, String value) + { + ... + } + }, key); +``` + +Streaming happens real-time to the redis responses. The method call +(future) completes after the last call to the StreamingChannel. + +### Examples + +``` java +redis.lpush("key", "one") +redis.lpush("key", "two") +redis.lpush("key", "three") + +Long count = redis.lrange(new ValueStreamingChannel() + { + @Override + public void onValue(String value) + { + System.out.println("Value: " + value); + } + }, "key", 0, -1); + +System.out.println("Count: " + count); +``` + +will produce the following output: + + Value: one + Value: two + Value: three + Count: 3 + +## Events + +### Before 3.4/4.1 + +lettuce can notify its users of certain events: + +- Connected + +- Disconnected + +- Exceptions in the connection handler pipeline + +You can subscribe to these events using `RedisClient#addListener()` and +unsubscribe with `RedisClient.removeListener()`. Both methods accept a +`RedisConnectionStateListener`. + +`RedisConnectionStateListener` receives as connection the async +implementation of the connection. This means if you use a sync way (e. +g. `RedisConnection`) you will receive the `RedisAsyncConnectionImpl` +instance + +**Example** + +``` java +RedisClient client = new RedisClient(host, port); +client.addListener(new RedisConnectionStateListener() +{ + @Override + public void onRedisConnected(RedisChannelHandler connection) + { + + } + @Override + public void onRedisDisconnected(RedisChannelHandler connection) + { + + } + @Override + public void onRedisExceptionCaught(RedisChannelHandler connection, Throwable cause) + { + + } +}); +``` + +### Since 3.4/4.1 + +The client produces events during its operation and uses an event bus +for the transport. The `EventBus` can be configured and obtained from +the [client resources](#configuring-client-resources) and is used for +client- and custom events. + +Following events are sent by the client: + +- Connection events + +- Metrics events + +- Cluster topology events + +#### Subscribing to events + +The simple-most approach to subscribing to the client events is +obtaining the event bus from the client’s client resources. + +``` java +RedisClient client = RedisClient.create() +EventBus eventBus = client.getresources().eventBus(); + +eventBus.get().subscribe(e -> System.out.println(event)); + +... +client.shutdown(); +``` + +Calls to the `subscribe()` method will return a `Subscription`. If you +plan to unsubscribe from the event stream, you can do so by calling the +`Subscription.unsubscribe()` method. The event bus utilizes +[RxJava](http://reactivex.io) and the {reactive-api} to transport events +from the publisher to its subscribers. + +A thread of the computation thread pool (can be configured using [client +resources](#configuring-client-resources)) transports the events. + +#### Connection events + +When working with events, multiple events occur. These can be used to +monitor connections or react to these. Connection events transport the +local and the remote connection points. The regular order of connection +events is: + +1. Connected: The transport-layer connection is established (TCP or + Unix Domain Socket connection established). Event type: + `ConnectedEvent` + +2. Connection activated: The logical connection is activated and can be + used to dispatch Redis commands (SSL handshake complete, PING before + activating response received). Event type: + `ConnectionActivatedEvent` + +3. Disconnected: The transport-layer connection is closed/reset. That + event occurs on regular connection shutdowns and connection + interruptions (outage). Event type: `DisconnectedEvent` + +4. Connection deactivated: The logical connection is deactivated. The + internal processing state is reset and the `isOpen()` flag is set to + `false` That event occurs on regular connection shutdowns and + connection interruptions (outage). Event type: + `ConnectionDeactivatedEvent` + +5. Since 5.3: Reconnect failed: A reconnect attempt failed. Contains + the reconnect failure and and the retry counter. Event type: + `ReconnectFailedEvent` + +#### Metrics events + +Client command metrics is published using the event bus. The current +event carries command latency metrics. Latency metrics is segregated by +connection or server and command which means you can get detailed +statistics on every command. Connection distinction allows seeing how +particular connections perform. Server distinction how particular +servers perform. You can configure metrics collection using [client +resources](#configuring-client-resources). + +In detail, two command latencies are recorded: + +1. RTT from dispatching the command until the first command response is + processed (first response) + +2. RTT from dispatching the command until the full command response is + processed and at the moment the command is completed (completion) + +The latency metrics provide following statistics: + +- Number of commands + +- min latency + +- max latency + +- latency percentiles + +**First Response Latency** + +The first response latency measuring begins at the moment the command +sending begins (command flush on the netty event loop). That is not the +time at when at which the command was issued from the client API. The +latency time recording ends at the moment the client receives the first +command bytes and starts to process the command response. Both +conditions must be met to end the latency recording. The client could be +busy with processing the previous command while the first bytes are +already available to read. That scenario would be a good time to file an +[issue](https://github.com/mp911de/lettuce/issues) for improving the +client performance. The first response latency value is good to +determine the lag/network performance and can give a hint on the client +and server performance. + +**Completion Latency** + +The completion latency begins at the same time as the first response +latency but lasts until the time where the client is just about to call +the `complete()` method to signal command completion. That means all +command response bytes arrived and were decoded/processed, and the +response data structures are ready for consumption for the user of the +client. On completion callback duration (such as async or observable +callbacks) are not part of the completion latency. + +#### Cluster events + +When using Redis Cluster, you might want to know when the cluster +topology changes. As soon as the cluster client discovers the cluster +topology change, a `ClusterTopologyChangedEvent` event is published to +the event bus. The time at which the event is published is not +necessarily the time the topology change occurred. That is because the +client polls the topology from the cluster. + +The cluster topology changed event carries the topology view before and +after the change. + +Make sure, you enabled cluster topology refresh in the [Client +options](#cluster-specific-options). + +### Java Flight Recorder Events (since 6.1) + +Lettuce emits Connection and Cluster events as Java Flight Recorder +events. `EventBus` emits all events to `EventRecorder` and the actual +event bus. + +`EventRecorder` verifies whether your runtime provides the required JFR +classes (available as of JDK 8 update 262 or later) and if so, then it +creates Flight Recorder variants of the event and commits these to JFR. + +The following events are supported out of the box: + +**Redis Connection Events** + +- Connection Attempt + +- Connect, Disconnect, Connection Activated, Connection Deactivated + +- Reconnect Attempt and Reconnect Failed + +**Redis Cluster Events** + +- Topology Refresh initiated + +- Topology Changed + +- ASK and MOVED redirects + +**Redis Master/Replica Events** + +- Sentinel Topology Refresh initiated + +- Master/Replica Topology Changed + +Events come with a rich set of event attributes such as channelId, epId +(endpoint Id), Redis URI and many more. + +You can record data by starting your application with: + +``` shell +java -XX:StartFlightRecording:filename=recording.jfr,duration=10s … +``` + +You can disable JFR events use through system properties. Set +`io.lettuce.core.jfr` to `false`. + +## Observability + +The following section explains Lettuces metrics and tracing +capabilities. + +### Metrics + +Command latency metrics give insight into command execution and +latencies. Metrics are collected for every completed command. Lettuce +has two mechanisms to collect latency metrics: + +- [Built-in](#built-in-latency-tracking) (since version 3.4 using + HdrHistogram and LatencyUtils. Enabled by default if both libraries + are available on the classpath.) + +- [Micrometer](#micrometer) (since version 6.1) + +### Built-in latency tracking + +Each command is tracked with: + +- Execution count + +- Latency to first response (min, max, percentiles) + +- Latency to complete (min, max, percentiles) + +Command latencies are tracked on remote endpoint (distinction by host +and port or socket path) and command type level (`GET`, `SET`, …​). It is +possible to track command latencies on a per-connection level (see +`DefaultCommandLatencyCollectorOptions`). + +Command latencies are transported using Events on the `EventBus`. The +`EventBus` can be obtained from the [client +resources](#configuring-client-resources) of the client instance. Please +keep in mind that the `EventBus` is used for various event types. Filter +on the event type if you’re interested only in particular event types. + +``` java +RedisClient client = RedisClient.create(); +EventBus eventBus = client.getResources().eventBus(); + +Subscription subscription = eventBus.get() + .filter(redisEvent -> redisEvent instanceof CommandLatencyEvent) + .cast(CommandLatencyEvent.class) + .subscribe(e -> System.out.println(e.getLatencies())); +``` + +The `EventBus` uses Reactor Processors to publish events. This example +prints the received latencies to `stdout`. The interval and the +collection of command latency metrics can be configured in the +`ClientResources`. + +#### Prerequisites + +Lettuce requires the LatencyUtils dependency (at least 2.0) to provide +latency metrics. Make sure to include that dependency on your classpath. +Otherwise, you won’t be able using latency metrics. + +If using Maven, add the following dependency to your pom.xml: + +``` xml + + org.latencyutils + LatencyUtils + 2.0.3 + +``` + +#### Disabling command latency metrics + +To disable metrics collection, use own `ClientResources` with a disabled +`DefaultCommandLatencyCollectorOptions`: + +``` java +ClientResources res = DefaultClientResources + .builder() + .commandLatencyCollectorOptions( DefaultCommandLatencyCollectorOptions.disabled()) + .build(); + +RedisClient client = RedisClient.create(res); +``` + +#### CommandLatencyCollector Options + +The following settings are available to configure from +`DefaultCommandLatencyCollectorOptions`: + +| Name | Method | Default | +|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------|---------------------------------| +| **Disable metrics tracking** | `disable` | `false` | +| Disables tracking of command latency metrics. | | | +| **Latency time unit** | `targetUnit` | `MICROSECONDS` | +| The target unit for command latency values. All values in the `CommandLatencyEvent` and a `CommandMetrics` instance are `long` values scaled to the `targetUnit`. | | | +| **Latency percentiles** | `targetPercentiles` | `50.0, 90 .0, 95.0, 99.0, 99.9` | +| A `double`-array of percentiles for latency metrics. The `CommandMetrics` contains a map that holds the percentile value and the latency value according to the percentile. Note that percentiles here must be specified in the range between 0 and 100. | | | +| **Reset latencies after publish** | `reset LatenciesAfterEvent` | `true` | +| Allows controlling whether the latency metrics are reset to zero one they were published. Setting `reset LatenciesAfterEvent` allows accumulating metrics over a long period for long-term analytics. | | | +| **Local socket distinction** | `localDistinction` | `false` | +| Enables per connection metrics tracking instead of per host/port. If `true`, multiple connections to the same host/connection point will be recorded separately which allows to inspection of every connection individually. If `false`, multiple connections to the same host/connection point will be recorded together. This allows a consolidated view on one particular service. | | | + +#### EventPublisher Options + +The following settings are available to configure from +`DefaultEventPublisherOptions`: + +| Name | Method | Default | +|---------------------------------------------------|--------------------------|-----------| +| **Disable event publisher** | `disable` | `false` | +| Disables event publishing. | | | +| **Event publishing time unit** | `ev entEmitIntervalUnit` | `MINUTES` | +| The `TimeUnit` for the event publishing interval. | | | +| **Event publishing interval** | `eventEmitInterval` | `10` | +| The interval for the event publishing. | | | + +### Micrometer + +Commands are tracked by using two Micrometer `Timer`s: +`lettuce.command.firstresponse` and `lettuce.command.completion`. The +following tags are attached to each timer: + +- `command`: Name of the command (`GET`, `SET`, …​) + +- `local`: Local socket (`localhost/127.0.0.1:45243` or `ANY` when local + distinction is disabled, which is the default behavior) + +- `remote`: Remote socket (`localhost/127.0.0.1:6379`) + +Command latencies are reported using the provided `MeterRegistry`. + +``` java +MeterRegistry meterRegistry = …; +MicrometerOptions options = MicrometerOptions.create(); +ClientResources resources = ClientResources.builder().commandLatencyRecorder(new MicrometerCommandLatencyRecorder(meterRegistry, options)).build(); + +RedisClient client = RedisClient.create(resources); +``` + +#### Prerequisites + +Lettuce requires Micrometer (`micrometer-core`) to integrate with +Micrometer. + +If using Maven, add the following dependency to your pom.xml: + +``` xml + + io.micrometer + micrometer-core + ${micrometer.version} + +``` + +#### Micrometer Options + +The following settings are available to configure from +`MicrometerOptions`: + +| Name | Method | Default | +|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------|------------------------------------------------------------------------------------| +| **Disable metrics tracking** | `disable` | `false` | +| Disables tracking of command latency metrics. | | | +| **Histogram** | `histogram` | `false` | +| Enable histogram buckets used to generate aggregable percentile approximations in monitoring systems that have query facilities to do so. | | | +| **Local socket distinction** | `localDistinction` | `false` | +| Enables per connection metrics tracking instead of per host/port. If `true`, multiple connections to the same host/connection point will be recorded separately which allows inspection of every connection individually. If `false`, multiple connections to the same host/connection point will be recorded together. This allows a consolidated view on one particular service. | | | +| **Maximum Latency** | `maxLatency` | `5 Minutes` | +| Sets the maximum value that this timer is expected to observe. Applies only if Histogram publishing is enabled. | | | +| **Minimum Latency** | `minLatency` | `1ms` | +| Sets the minimum value that this timer is expected to observe. Applies only if Histogram publishing is enabled. | | | +| **Additional Tags** | `tags` | `Tags.empty()` | +| Extra tags to add to the generated metrics. | | | +| **Latency percentiles** | `targetPercentiles` | `0.5, 0.9, 0.95, 0.99, 0.999 (corresp onding with 50.0, 90. 0, 95.0, 99.0, 99.9)` | +| A `double`-array of percentiles for latency metrics. Values must be supplied in the range of `0.0` (0th percentile) up to `1.0` (100th percentile). The `CommandMetrics` contains a map that holds the percentile value and the latency value according to the percentile. This applies only if Histogram publishing is enabled. | | | + +### Tracing + +Tracing gives insights about individual Redis commands sent to Redis to +trace their frequency, duration and to trace of which commands a +particular activity consists. Lettuce provides a tracing SPI to avoid +mandatory tracing library dependencies. Lettuce ships integrations with +[Micrometer Tracing](https://github.com/micrometer-metrics/tracing) and +[Brave](https://github.com/openzipkin/brave) which can be configured +through [client resources](#configuring-client-resources). + +#### Micrometer Tracing + +With Micrometer tracing enabled, Lettuce creates an observation for each +Redis command resulting in spans per Command and corresponding Meters if +configured in Micrometer’s `ObservationContext`. + +##### Prerequisites + +Lettuce requires the Micrometer Tracing dependency to provide Tracing +functionality. Make sure to include that dependency on your classpath. + +If using Maven, add the following dependency to your pom.xml: + +``` xml + + io.micrometer + micrometer-tracing + +``` + +The following example shows how to configure tracing through +`ClientResources`: + +``` java +ObservationRegistry observationRegistry = …; + +MicrometerTracing tracing = new MicrometerTracing(observationRegistry, "Redis"); + +ClientResources resources = ClientResources.builder().tracing(tracing).build(); +``` + +#### Brave + +With Brave tracing enabled, Lettuce creates a span for each Redis +command. The following options can be configured: + +- `serviceName` (defaults to `redis`). + +- `Endpoint` customizer. This option can be used together with a custom + `SocketAddressResolver` to attach custom endpoint details. + +- `Span` customizer. Allows for customization of spans based on the + actual Redis `Command` object. + +- Inclusion/Exclusion of all command arguments in a span. By default, + all arguments are included. + +##### Prerequisites + +Lettuce requires the Brave dependency (at least 5.1) to provide Tracing +functionality. Make sure to include that dependency on your classpath. + +If using Maven, add the following dependency to your pom.xml: + +``` xml + + io.zipkin.brave + brave + +``` + +The following example shows how to configure tracing through +`ClientResources`: + +``` java +brave.Tracing clientTracing = …; + +BraveTracing tracing = BraveTracing.builder().tracing(clientTracing) + .excludeCommandArgsFromSpanTags() + .serviceName("custom-service-name-goes-here") + .spanCustomizer((command, span) -> span.tag("cmd", command.getType().name())) + .build(); + +ClientResources resources = ClientResources.builder().tracing(tracing).build(); +``` + +Lettuce ships with a Tracing SPI in `io.lettuce.core.tracing` that +allows custom tracer implementations. + +## Pipelining and command flushing + +Redis is a TCP server using the client-server model and what is called a +Request/Response protocol. This means that usually a request is +accomplished with the following steps: + +- The client sends a query to the server and reads from the socket, + usually in a blocking way, for the server response. + +- The server processes the command and sends the response back to the + client. + +A request/response server can be implemented so that it is able to +process new requests even if the client did not already read the old +responses. This way it is possible to send multiple commands to the +server without waiting for the replies at all, and finally read the +replies in a single step. + +Using the synchronous API, in general, the program flow is blocked until +the response is accomplished. The underlying connection is busy with +sending the request and receiving its response. Blocking, in this case, +applies only from a current Thread perspective, not from a global +perspective. + +To understand why using a synchronous API does not block on a global +level we need to understand what this means. Lettuce is a non-blocking +and asynchronous client. It provides a synchronous API to achieve a +blocking behavior on a per-Thread basis to create await (synchronize) a +command response. Blocking does not affect other Threads per se. Lettuce +is designed to operate in a pipelining way. Multiple threads can share +one connection. While one Thread may process one command, the other +Thread can send a new command. As soon as the first request returns, the +first Thread’s program flow continues, while the second request is +processed by Redis and comes back at a certain point in time. + +Lettuce is built on top of netty decouple reading from writing and to +provide thread-safe connections. The result is, that reading and writing +can be handled by different threads and commands are written and read +independent of each other but in sequence. You can find more details +about [message ordering](#message-ordering) to learn +about command ordering rules in single- and multi-threaded arrangements. +The transport and command execution layer does not block the processing +until a command is written, processed and while its response is read. +Lettuce sends commands at the moment they are invoked. + +A good example is the [async API](user-guide/async-api.md). Every +invocation on the [async API](user-guide/async-api.md) returns a +`Future` (response handle) after the command is written to the netty +pipeline. A write to the pipeline does not mean, the command is written +to the underlying transport. Multiple commands can be written without +awaiting the response. Invocations to the API (sync, async and starting +with `4.0` also reactive API) can be performed by multiple threads. + +Sharing a connection between threads is possible but keep in mind: + +**The longer commands need for processing, the longer other invoker wait +for their results** + +You should not use transactional commands (`MULTI`) on shared +connection. If you use Redis-blocking commands (e. g. `BLPOP`) all +invocations of the shared connection will be blocked until the blocking +command returns which impacts the performance of other threads. Blocking +commands can be a reason to use multiple connections. + +### Command flushing + +!!! NOTE + Command flushing is an advanced topic and in most cases (i.e. unless + your use-case is a single-threaded mass import application) you won’t + need it as Lettuce uses pipelining by default. + +The normal operation mode of Lettuce is to flush every command which +means, that every command is written to the transport after it was +issued. Any regular user desires this behavior. You can control command +flushing since Version `3.3`. + +Why would you want to do this? A flush is an [expensive system +call](https://github.com/netty/netty/issues/1759) and impacts +performance. Batching, disabling auto-flushing, can be used under +certain conditions and is recommended if: + +- You perform multiple calls to Redis and you’re not depending + immediately on the result of the call + +- You’re bulk-importing + +Controlling the flush behavior is only available on the async API. The +sync API emulates blocking calls, and as soon as you invoke a command, +you can no longer interact with the connection until the blocking call +ends. + +The `AutoFlushCommands` state is set per connection and, therefore +visible to all threads using a shared connection. If you want to omit +this effect, use dedicated connections. The `AutoFlushCommands` state +cannot be set on pooled connections by the Lettuce connection pooling. + +!!! WARNING + Do not use `setAutoFlushCommands(…)` when sharing a connection across + threads, at least not without proper synchronization. According to the + many questions and (invalid) bug reports using + `setAutoFlushCommands(…)` in a multi-threaded scenario causes a lot of + complexity overhead and is very likely to cause issues on your side. + `setAutoFlushCommands(…)` can only be reliably used on single-threaded + connection usage in scenarios like bulk-loading. + +``` java +StatefulRedisConnection connection = client.connect(); +RedisAsyncCommands commands = connection.async(); + +// disable auto-flushing +commands.setAutoFlushCommands(false); + +// perform a series of independent calls +List> futures = Lists.newArrayList(); +for (int i = 0; i < iterations; i++) { + futures.add(commands.set("key-" + i, "value-" + i)); + futures.add(commands.expire("key-" + i, 3600)); +} + +// write all commands to the transport layer +commands.flushCommands(); + +// synchronization example: Wait until all futures complete +boolean result = LettuceFutures.awaitAll(5, TimeUnit.SECONDS, + futures.toArray(new RedisFuture[futures.size()])); + +// later +connection.close(); +``` + +#### Performance impact + +Commands invoked in the default flush-after-write mode perform in an +order of about 100Kops/sec (async/multithreaded execution). Grouping +multiple commands in a batch (size depends on your environment, but +batches between 50 and 1000 work nice during performance tests) can +increase the throughput up to a factor of 5x. + +Pipelining within the Redis docs: + +## Connection Pooling + +Lettuce connections are designed to be thread-safe so one connection can +be shared amongst multiple threads and Lettuce connections +[auto-reconnection](#client-options) by default. While connection +pooling is not necessary in most cases it can be helpful in certain use +cases. Lettuce provides generic connection pooling support. + +### Is connection pooling necessary? + +Lettuce is thread-safe by design which is sufficient for most cases. All +Redis user operations are executed single-threaded. Using multiple +connections does not impact the performance of an application in a +positive way. The use of blocking operations usually goes hand in hand +with worker threads that get their dedicated connection. The use of +Redis Transactions is the typical use case for dynamic connection +pooling as the number of threads requiring a dedicated connection tends +to be dynamic. That said, the requirement for dynamic connection pooling +is limited. Connection pooling always comes with a cost of complexity +and maintenance. + +### Execution Models + +Lettuce supports two execution models for pooling: + +- Synchronous/Blocking via Apache Commons Pool 2 + +- Asynchronous/Non-Blocking via a Lettuce-specific pool implementation + (since version 5.1) + +### Synchronous Connection Pooling + +Using imperative programming models, synchronous connection pooling is +the right choice as it carries out all operations on the thread that is +used to execute the code. + +#### Prerequisites + +Lettuce requires Apache’s +[common-pool2](https://commons.apache.org/proper/commons-pool/) +dependency (at least 2.2) to provide connection pooling. Make sure to +include that dependency on your classpath. Otherwise, you won’t be able +using connection pooling. + +If using Maven, add the following dependency to your `pom.xml`: + +``` xml + + org.apache.commons + commons-pool2 + 2.4.3 + +``` + +#### Connection pool support + +Lettuce provides generic connection pool support. It requires a +connection `Supplier` that is used to create connections of any +supported type (Redis Standalone, Pub/Sub, Sentinel, Master/Replica, +Redis Cluster). `ConnectionPoolSupport` will create a +`GenericObjectPool` or `SoftReferenceObjectPool`, depending on your +needs. The pool can allocate either wrapped or direct connections. + +- Wrapped instances will return the connection back to the pool when + called `StatefulConnection.close()`. + +- Regular connections need to be returned to the pool with + `GenericObjectPool.returnObject(…)`. + +**Basic usage** + +``` java +RedisClient client = RedisClient.create(RedisURI.create(host, port)); + +GenericObjectPool> pool = ConnectionPoolSupport + .createGenericObjectPool(() -> client.connect(), new GenericObjectPoolConfig()); + +// executing work +try (StatefulRedisConnection connection = pool.borrowObject()) { + + RedisCommands commands = connection.sync(); + commands.multi(); + commands.set("key", "value"); + commands.set("key2", "value2"); + commands.exec(); +} + +// terminating +pool.close(); +client.shutdown(); +``` + +**Cluster usage** + +``` java +RedisClusterClient clusterClient = RedisClusterClient.create(RedisURI.create(host, port)); + +GenericObjectPool> pool = ConnectionPoolSupport + .createGenericObjectPool(() -> clusterClient.connect(), new GenericObjectPoolConfig()); + +// execute work +try (StatefulRedisClusterConnection connection = pool.borrowObject()) { + connection.sync().set("key", "value"); + connection.sync().blpop(10, "list"); +} + +// terminating +pool.close(); +clusterClient.shutdown(); +``` + +### Asynchronous Connection Pooling + +Asynchronous/non-blocking programming models require a non-blocking API +to obtain Redis connections. A blocking connection pool can easily lead +to a state that blocks the event loop and prevents your application from +progress in processing. + +Lettuce comes with an asynchronous, non-blocking pool implementation to +be used with Lettuces asynchronous connection methods. It does not +require additional dependencies. + +#### Asynchronous Connection pool support + +Lettuce provides asynchronous connection pool support. It requires a +connection `Supplier` that is used to asynchronously connect to any +supported type (Redis Standalone, Pub/Sub, Sentinel, Master/Replica, +Redis Cluster). `AsyncConnectionPoolSupport` will create a +`BoundedAsyncPool`. The pool can allocate either wrapped or direct +connections. + +- Wrapped instances will return the connection back to the pool when + called `StatefulConnection.closeAsync()`. + +- Regular connections need to be returned to the pool with + `AsyncPool.release(…)`. + +**Basic usage** + +``` java +RedisClient client = RedisClient.create(); + +CompletionStage>> poolFuture = AsyncConnectionPoolSupport.createBoundedObjectPoolAsync( + () -> client.connectAsync(StringCodec.UTF8, RedisURI.create(host, port)), BoundedPoolConfig.create()); + +// await poolFuture initialization to avoid NoSuchElementException: Pool exhausted when starting your application + +// execute work +CompletableFuture transactionResult = pool.acquire().thenCompose(connection -> { + + RedisAsyncCommands async = connection.async(); + + async.multi(); + async.set("key", "value"); + async.set("key2", "value2"); + return async.exec().whenComplete((s, throwable) -> pool.release(connection)); +}); + +// terminating +pool.closeAsync(); + +// after pool completion +client.shutdownAsync(); +``` + +**Cluster usage** + +``` java +RedisClusterClient clusterClient = RedisClusterClient.create(RedisURI.create(host, port)); + +CompletionStage>> poolFuture = AsyncConnectionPoolSupport.createBoundedObjectPoolAsync( + () -> clusterClient.connectAsync(StringCodec.UTF8), BoundedPoolConfig.create()); + +// execute work +CompletableFuture setResult = pool.acquire().thenCompose(connection -> { + + RedisAdvancedClusterAsyncCommands async = connection.async(); + + async.set("key", "value"); + return async.set("key2", "value2").whenComplete((s, throwable) -> pool.release(connection)); +}); + +// terminating +pool.closeAsync(); + +// after pool completion +clusterClient.shutdownAsync(); +``` + +## Custom commands + +Lettuce covers nearly all Redis commands. Redis development is an +ongoing process and the Redis Module system is intended to introduce new +commands which are not part of the Redis Core. This requirement +introduces the need to invoke custom commands or use custom outputs. +Custom commands can be dispatched on the one hand using Lua and the +`eval()` command, on the other side Lettuce 4.x allows you to trigger +own commands. That API is used by Lettuce itself to dispatch commands +and requires some knowledge of how commands are constructed and +dispatched within Lettuce. + +Lettuce provides two levels of command dispatching: + +1. Using the synchronous, asynchronous or reactive API wrappers which + invoke commands according to their nature + +2. Using the bare connection to influence the command nature and + synchronization (advanced) + +**Example using `dispatch()` on the synchronous API** + +``` java +RedisCodec codec = StringCodec.UTF8; +RedisCommands commands = ... + +String response = redis.dispatch(CommandType.SET, new StatusOutput<>(codec), + new CommandArgs<>(codec) + .addKey(key) + .addValue(value)); +``` + +**Example using `dispatch()` on the asynchronous API** + +``` java +RedisCodec codec = StringCodec.UTF8; +RedisAsyncCommands commands = ... + +RedisFuture response = redis.dispatch(CommandType.SET, new StatusOutput<>(codec), + new CommandArgs<>(codec) + .addKey(key) + .addValue(value)); +``` + +**Example using `dispatch()` on the reactive API** + +``` java +RedisCodec codec = StringCodec.UTF8; +RedisReactiveCommands commands = ... + +Observable response = redis.dispatch(CommandType.SET, new StatusOutput<>(codec), + new CommandArgs<>(codec) + .addKey(key) + .addValue(value)); +``` + +**Example using a `RedisFuture` command wrapper** + +``` java +StatefulRedisConnection connection = redis.getStatefulConnection(); + +RedisCommand command = new Command<>(CommandType.PING, + new StatusOutput<>(StringCodec.UTF8)); + +AsyncCommand async = new AsyncCommand<>(command); +connection.dispatch(async); + +// async instanceof CompletableFuture == true +``` + +### Mechanics of Lettuce commands + +Lettuce uses the command pattern to implement to execute commands. Every +time a command is invoked, Lettuce creates a command object (`Command` +or types implementing `RedisCommand`). Commands can carry arguments +(`CommandArgs`) and an output (subclasses of `CommandOutput`). Both are +optional. The two mandatory properties are the command type (see +`CommandType` or a type implementing `ProtocolKeyword`) and a +`RedisCodec`. If you dispatch commands by yourself, do not reuse command +instances to dispatch commands more than once. Commands that were +executed once have the completed flag set and cannot be reused. + +#### Arguments + +`CommandArgs` is a container for command arguments that follow the +command keyword (`CommandType`). A `PING` or `QUIT` command do not +require commands whereas the `GET` or `SET` commands require arguments +in the form of keys and values. + +**The `PING` command** + +``` java +RedisCommand command = new Command<>(CommandType.PING, + new StatusOutput<>(StringCodec.UTF8)); +``` + +**The `SET` command** + +``` java +StringCodec codec = StringCodec.UTF8; +RedisCommand command = new Command<>(CommandType.SET, + new StatusOutput<>(codec), new CommandArgs<>(codec) + .addKey("key") + .addValue("value")); +``` + +`CommandArgs` allow to add one or more: + +- key and arrays of keys + +- value and arrays of values + +- `String`, `long` (the Redis integer), `double` + +- byte array + +- `CommandType`, `CommandKeyword` and generic `ProtocolKeyword` + +The sequence of args and keywords is not validated by Lettuce beyond the +supported data types, meaning Redis will report errors if the command +syntax is not correct. + +#### Outputs + +Commands producing an output are required to consume the output. Lettuce +supports type-safe conversion of the response into the appropriate +result types. The output handlers derive from the `CommandOutput` base +class. Lettuce provides a wide range of output types (see the +`com.lambdaworks.redis.output` package for details). Command outputs are +mostly used to return the result as the whole object. The response is +available as soon as the whole command output is processed. There are +cases, where you might want to stream the response instead of allocating +a significant amount of memory and return the whole response as one. +These types are called streaming outputs. Following implementations ship +with Lettuce: + +- `KeyStreamingOutput` + +- `KeyValueScanStreamingOutput` + +- `KeyValueStreamingOutput` + +- `ScoredValueStreamingOutput` + +- `ValueScanStreamingOutput` + +- `ValueStreamingOutput` + +Those outputs take a streaming channel (see `ValueStreamingChannel`) and +invoke the callback method (e.g. `onValue(V value)`) for every data +element. + +Implementing an own output is, in general, a good idea when you want to +support a different data type, or you want to work with different types +than the basic collection, map, String, and primitive types. You might +get an impression of the custom types idea by taking a look on +`GeoWithinListOutput`, which takes a bunch of strings and nested lists +to construct a list of `GeoWithin` instances. + +Please note that using an output that does not fit the command output +can jam the response processing and lead to not usable connections. Use +either `ArrayOutput` or `NestedMultiOutput` when in doubt, so you +receive a list of objects (nested lists). + +**Output for the `PING` command** + +``` java +Command command = new Command<>(CommandType.PING, + new StatusOutput<>(StringCodec.UTF8)); +``` + +**Output for the `HGETALL` command** + +``` java +StringCodec codec = StringCodec.UTF8; +Command> command = new Command<>(CommandType.HGETALL, + new MapOutput<>(codec), + new CommandArgs<>(codec).addKey(key)); +``` + +**Output for the `HKEYS` command** + +``` java +StringCodec codec = StringCodec.UTF8; +Command> command = new Command<>(CommandType.HKEYS, + new KeyListOutput<>(codec), + new CommandArgs<>(codec).addKey(key)); +``` + +### Synchronous, asynchronous and reactive + +Great, that you made it up to here. You might want to know now, how to +synchronize the command completion, work with `Future`s or how about the +reactive API. The simple way is using the `dispatch(…)` method of the +according wrapper. If this is not sufficient, then continue on reading. + +The `dispatch()` method on a stateful Redis connection is not +opinionated at all how you are using Lettuce, whether it is synchronous +or reactive. The only thing this method does is dispatching the command. +The response handler handles decoding the command and completing the +command once it’s done. The asynchronous command processing is the only +operating mode of Lettuce. + +The `RedisCommand` interface provides methods to `complete()`, +`cancel()` and `completeExceptionally()` the command. The `complete()` +methods are called by the response handler as soon as the command is +completed. Redis commands can be wrapped and augmented by that way. +Wrapping is used when using transactions (`MULTI`) or Redis Cluster. + +You are free to implement your command type or use one of the provided +commands: + +- Command (default implementation) + +- AsyncCommand (the `CompleteableFuture` wrapper for `RedisCommand`) + +- CommandWrapper (generic wrapper) + +- TransactionalCommand (wraps `RedisCommand`s when `MULTI` is active) + +#### Fire & Forget + +Fire&Forget is the simple-most way to dispatch commands. You just +trigger it and then you do not care what happens, whether the command +completes or not, and you don’t have access to the command output: + +``` java +StatefulRedisConnection connection = redis.getStatefulConnection(); + +connection.dispatch(CommandType.PING, VoidOutput.create()); +``` + +!!! NOTE + `VoidOutput.create()` swallows also Redis error responses. If you want + to just avoid response decoding, create a `VoidCodec` instance using + its constructor to retain error response decoding. + +#### Asynchronous + +The asynchronous API works in general with the `AsyncCommand` wrapper +that extends `CompleteableFuture`. `AsyncCommand` can be synchronized by +`await()` or `get()` which corresponds with the asynchronous pull style. +By using the methods from the `CompletionStage` interface (such as +`handle()` or `thenAccept()`) the response handler will trigger the +functions ("listeners") on command completion. Lear more about +asynchronous usage in the [Asynchronous API](user-guide/async-api.md) topic. + +``` java +StatefulRedisConnection connection = redis.getStatefulConnection(); + +RedisCommand command = new Command<>(CommandType.PING, + new StatusOutput<>(StringCodec.UTF8)); + +AsyncCommand async = new AsyncCommand<>(command); +connection.dispatch(async); + +// async instanceof CompletableFuture == true +``` + +#### Synchronous + +The synchronous API of Lettuce uses future synchronization to provide a +synchronous view. + +#### Reactive + +Reactive commands are dispatched at the moment of subscription (see +[Reactive API](user-guide/reactive-api.md) for more details on reactive APIs). In the +context of Lettuce this means, you need to start before calling the +`dispatch()` method. The reactive API uses internally an +`ObservableCommand`, but that is internal stuff. If you want to dispatch +commands the reactive way, you’ll need to wrap commands (or better: +command supplier to be able to retry commands) with the +`ReactiveCommandDispatcher`. The dispatcher implements the `OnSubscribe` +API to create an `Observable`, handles command dispatching at the +time of subscription and can dissolve collection types to particular +elements. An instance of `ReactiveCommandDispatcher` allows creating +multiple `Observable`s as long as you use a `Supplier`. +Commands that were executed once have the completed flag set and cannot +be reused. + +``` java +StatefulRedisConnection connection = redis.getStatefulConnection(); + +RedisCommand command = new Command<>(CommandType.PING, + new StatusOutput<>(StringCodec.UTF8)); +ReactiveCommandDispatcher dispatcher = new ReactiveCommandDispatcher<>(command, + connection, false); + +Observable observable = Observable.create(dispatcher); +String result = observable.toBlocking().first(); + +result == "PONG" +``` + +## Graal Native Image + +This section explains how to use Lettuce with Graal Native Image +compilation. + +### Why Create a Native Image? + +The GraalVM +[`native-image`](http://www.graalvm.org/docs/reference-manual/aot-compilation/) +tool enables ahead-of-time (AOT) compilation of Java applications into +native executables or shared libraries. While traditional Java code is +just-in-time (JIT) compiled at run time, AOT compilation has two main +advantages: + +1. First, it improves the start-up time since the code is already + pre-compiled into efficient machine code. + +2. Second, it reduces the memory footprint of Java applications since + it eliminates the need to include infrastructure to load and + optimize code at run time. + +There are additional advantages such as more predictable performance and +less total CPU usage. + +### Building Native Images + +Native images assume a closed world principle in which all code needs to +be known at the time the native image is built. Graal's SubstrateVM +analyzes class files during native image build-time to determine what +bytecode needs to be translated into a native image. While this task can +be achieved to a good extent by analyzing static bytecode, it’s harder +for dynamic parts of the code such as reflection. When using reflective +access or Java proxies, the native image build process requires a little +bit of help so it can include parts that are required during runtime. + +Lettuce ships with configuration files that specifically describe which +classes are used by Lettuce during runtime and which Java proxies get +created. + +Starting as of Lettuce 5.3.2, the following configuration files are +available: + +- `META-INF/native-image/io.lettuce/lettuce-core/native-image.properties` + +- `META-INF/native-image/io.lettuce/lettuce-core/proxy-config.json` + +- `META-INF/native-image/io.lettuce/lettuce-core/reflect-config.json` + +Those cover Lettuce operations for `RedisClient` and +`RedisClusterClient`. + +Depending on your configuration you might need additional configuration +for Netty, HdrHistogram (metrics collection), Reactive Libraries, and +dynamic Redis Command interfaces. + +### HdrHistogram/Command Latency Metrics + +Lettuce uses HdrHistogram and LatencyUtils to accumulate metrics. You +can use your application without these. If you want to use Command +Latency Metrics, please add the following lines to your own +`reflect-config.json` file: + +``` json + { + "name": "org.HdrHistogram.Histogram" + }, + { + "name": "org.LatencyUtils.PauseDetector" + } +``` + +### Dynamic Command Interfaces + +You can use Dynamic Command Interfaces when compiling your code to a +GraalVM Native Image. GraalVM requires two information as Lettuce +inspects command interfaces using reflection and it creates a Java +proxy: + +1. Add the command interface class name to your `reflect-config.json` + using ideally `allDeclaredMethods:true`. + +2. Add the command interface class name to your `proxy-config.json` + +
+ +**`reflect-config.json`** + +
+ +``` json +[ + { + "name": "com.example.MyCommands", + "allDeclaredMethods": true + }, +] +``` + +
+ +**`proxy-config.json`** + +
+ +``` json +[ + ["com.example.MyCommands"] +] +``` + +#### Reactive Libraries + +If you decide to use a specific reactive library with dynamic command +interfaces, please add the following lines to your `reflect-config.json` +file, depending on the presence of Rx Java 1-3: + +``` json + { + "name": "rx.Completable" + }, + { + "name": "io.reactivex.Flowable" + }, + { + "name": "io.reactivex.rxjava3.core.Flowable" + } +``` + +### Limitations + +For now, native images must be compiled with +`--report-unsupported-elements-at-runtime` to ignore missing Method +Handles and annotation synthetization failures. + +#### Netty Config + +To properly start up the netty stack, the following reflection +configuration is required for netty and the JDK in +`reflect-config.json`: + +``` json + { + "name":"io.netty.util.internal.shaded.org.jctools.queues.BaseMpscLinkedArrayQueueColdProducerFields", + "fields":[{"name":"producerLimit","allowUnsafeAccess" : true}] + }, + { + "name":"io.netty.util.internal.shaded.org.jctools.queues.BaseMpscLinkedArrayQueueConsumerFields", + "fields":[{"name":"consumerIndex","allowUnsafeAccess" : true}] + }, + { + "name":"io.netty.util.internal.shaded.org.jctools.queues.BaseMpscLinkedArrayQueueProducerFields", + "fields":[{"name":"producerIndex", "allowUnsafeAccess" : true}] + }, + { + "name":"io.netty.util.internal.shaded.org.jctools.queues.MpscArrayQueueConsumerIndexField", + "fields":[{"name":"consumerIndex", "allowUnsafeAccess" : true}] + }, + { + "name":"io.netty.util.internal.shaded.org.jctools.queues.MpscArrayQueueProducerIndexField", + "fields":[{"name":"producerIndex", "allowUnsafeAccess" : true}] + }, + { + "name":"io.netty.util.internal.shaded.org.jctools.queues.MpscArrayQueueProducerLimitField", + "fields":[{"name":"producerLimit","allowUnsafeAccess" : true}] + }, + { + "name":"java.nio.Buffer", + "fields":[{"name":"address", "allowUnsafeAccess":true}] + }, + { + "name":"java.nio.DirectByteBuffer", + "fields":[{"name":"cleaner", "allowUnsafeAccess":true}], + "methods":[{"name":"","parameterTypes":["long","int"] }] + }, + { + "name":"io.netty.buffer.AbstractReferenceCountedByteBuf", + "fields":[{"name":"refCnt", "allowUnsafeAccess":true}] + }, + { + "name":"io.netty.buffer.AbstractByteBufAllocator", + "allPublicMethods": true, + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true + }, + { + "name":"io.netty.buffer.PooledByteBufAllocator", + "allPublicMethods": true, + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true + }, + { + "name":"io.netty.channel.ChannelDuplexHandler", + "allPublicMethods": true, + "allDeclaredConstructors":true + }, + { + "name":"io.netty.channel.ChannelHandlerAdapter", + "allPublicMethods": true, + "allDeclaredConstructors":true + }, + { + "name": "io.netty.channel.ChannelInboundHandlerAdapter", + "allPublicMethods": true, + "allDeclaredConstructors":true + }, + { + "name": "io.netty.channel.ChannelInitializer", + "allPublicMethods": true, + "allDeclaredConstructors":true + }, + { + "name": "io.netty.channel.ChannelOutboundHandlerAdapter", + "allPublicMethods": true, + "allDeclaredConstructors":true + }, + { + "name": "io.netty.channel.DefaultChannelPipeline$HeadContext", + "allPublicMethods": true, + "allDeclaredConstructors":true + }, + { + "name": "io.netty.channel.DefaultChannelPipeline$TailContext", + "allPublicMethods": true, + "allDeclaredConstructors":true + }, + { + "name": "io.netty.channel.socket.nio.NioSocketChannel", + "allPublicMethods": true, + "allDeclaredConstructors":true + }, + { + "name": "io.netty.handler.codec.MessageToByteEncoder", + "allPublicMethods": true, + "allDeclaredConstructors":true + }, + { + "name":"io.netty.util.ReferenceCountUtil", + "allPublicMethods": true, + "allDeclaredConstructors":true + } +``` + +#### Functionality + +We don’t have found a way yet to invoke default interface methods on +proxies without `MethodHandle`. Hence the `NodeSelection` API +(`masters()`, `all()` and others on `RedisAdvancedClusterCommands` and +`RedisAdvancedClusterAsyncCommands`) do not work. + +## Command execution reliability + +Lettuce is a thread-safe and scalable Redis client that allows multiple +independent connections to Redis. + +### General + +Lettuce provides two levels of consistency; these are the rules for +Redis command sends: + +Depending on the chosen consistency level: + +- **at-most-once execution**, i. e. no guaranteed execution + +- **at-least-once execution**, i. e. guaranteed execution (with [some + exceptions](#exceptions-to-at-least-once)) + +Always: + +- command ordering in the order of invocations + +### What does *at-most-once* mean? + +When it comes to describing the semantics of an execution mechanism, +there are three basic categories: + +- **at-most-once** execution means that for each command handed to the + mechanism, that command is execution zero or one time; in more casual + terms it means that commands may be lost. + +- **at-least-once** execution means that for each command handed to the + mechanism potentially multiple attempts are made at execution it, such + that at least one succeeds; again, in more casual terms this means + that commands may be duplicated but not lost. + +- **exactly-once** execution means that for each command handed to the + mechanism exactly one execution is made; the command can neither be + lost nor duplicated. + +The first one is the cheapest - the highest performance, least +implementation overhead - because it can be done without tracking +whether the command was sent or got lost within the transport mechanism. +The second one requires retries to counter transport losses, which means +keeping the state at the sending end and having an acknowledgment +mechanism at the receiving end. The third is most expensive—and has +consequently worst performance—because also to the second it requires a +state to be kept at the receiving end to filter out duplicate +executions. + +### Why No Guaranteed Delivery? + +At the core of the problem lies the question what exactly this guarantee +shall mean: + +1. The command is sent out on the network? + +2. The command is received by the other host? + +3. The command is processed by Redis? + +4. The command response is sent by the other host? + +5. The command response is received by the network? + +6. The command response is processed successfully? + +Each one of these have different challenges and costs, and it is obvious +that there are conditions under which any command sending library would +be unable to comply. Think for example about how a network partition +would affect point three, or even what it would mean to decide upon the +“successfully” part of point six. + +The only meaningful way for a client to know whether an interaction was +successful is by receiving a business-level acknowledgment command, +which is not something Lettuce could make up on its own. + +Lettuce allows two levels of consistency; each one has its costs and +benefits, and therefore it does not try to lie and emulate a leaky +abstraction. + +### Message Ordering + +The rule more specifically is that commands sent are not be executed +out-of-order. + +The following illustrates the guarantee: + +- Thread `T1` sends commands `C1`, `C2`, `C3` to Redis + +- Thread `T2` sends commands `C4`, `C5`, `C6` to Redis + +This means that: + +- If `C1` is executed, it must be executed before `C2` and `C3`. + +- If `C2` is executed, it must be executed before `C3`. + +- If `C4` is executed, it must be executed before `C5` and `C6`. + +- If `C5` is executed, it must be executed before `C6`. + +- Redis executes commands from `T1` interleaved with commands from `T2`. + +- If there is no guaranteed delivery, any of the commands may be + dropped, i.e. not arrive at Redis. + +### Failures and *at-least-once* execution + +Lettuce’s *at-least-once* execution is scoped to the lifecycle of a +logical connection. Redis commands are not persisted to be executed +after a JVM or client restart. All Redis command state is held in +memory. A retry mechanism re-executes commands that are not successfully +completed if a network failure occurs. In more casual terms, when Redis +is available again, the retry mechanism fires all queued commands. +Commands that are issued as long as the failure persists are buffered. + +*at-least-once* execution ensures a higher consistency level than +*at-most-once* but comes with some caveats: + +- Commands can be executed more than once + +- Higher usage of resources since commands are buffered and sent again + after reconnect + +#### Exceptions to *at-least-once* + +Lettuce does not loose commands while sending them. A command execution +can, however, fail for the same reasons as a normal method call can on +the JVM: + +- `StackOverflowError` + +- `OutOfMemoryError` + +- other `Error`s + +Also, executions can fail in specific ways: + +- The command runs into a timeout + +- The command cannot be encoded + +- The command cannot be decoded, because: + +- The output is not compatible with the command output + +- Exceptions occur while command decoding/processing. This may happen a + `StreamingChannel` results in an error, or a consumer of Pub/Sub + events fails while listener notification. + +While the first is clearly a matter of configuration, the second +deserves some thought: The command execution does not get feedback if +there was a timeout. This is in general not distinguishable from a lost +message. By using the Sync API, commands that exceeded their timeout are +canceled. This behavior cannot be changed. When using the Async API, +users can decide, how to proceed with the command, whether the command +should be canceled. + +Commands which run into `Exception`s while encoding or decoding reach a +non-recoverable state. Commands that cannot be *encoded* are **not** +executed but get canceled. Commands that cannot be *decoded* were +already executed; only the result is not available. These errors are +caused mostly due to a wrong implementation. The result of a command, +which cannot be *decoded* is that the command gets canceled, and the +causing `Exception` is available in the result. The command is cleared +from the response queue, and the connection stays usable. + +In general, when `Errors` occur while operating on a connection, you +should close the connection and use a new one. Connections, that +experienced such severe failures get into a unrecoverable state, and no +further response processing is possible. + +Executing commands more than once + +In terms of consistency, Redis commands can be grouped into two +categories: + +- Idempotent commands + +- Non-idempotent commands + +Idempotent commands are commands that lead to the same state if they are +executed more than once. Read commands are a good example for +idempotency since they do not change the state of data. Another set of +idempotent commands are commands that write a whole data structure/entry +at once such as `SET`, `DEL` or `CLIENT SETNAME`. Those commands change +the data to the desired state. Subsequent executions of the same command +leave the data in the same state. + +Non-idempotent commands change the state with every execution. This +means, if you execute a command twice, each resulting state is different +in comparison to the previous. Examples for non-idempotent Redis +commands are such as `LPUSH`, `PUBLISH` or `INCR`. + +Note: When using master-replica replication, different rules apply to +*at-least-once* consistency. Replication between Redis nodes works +asynchronously. A command can be processed successfully from Lettuce’s +client perspective, but the result is not necessarily replicated to the +replica yet. If a failover occurs at that moment, a replica takes over, +and the not yet replicated data is lost. Replication behavior is +Redis-specific. Further documentation about failover and consistency +from Redis perspective is available within the Redis docs: + + +### Switching between *at-least-once* and *at-most-once* operations + +Lettuce’s consistency levels are bound to retries on reconnects and the +connection state. By default, Lettuce operates in the *at-least-once* +mode. Auto-reconnect is enabled and as soon as the connection is +re-established, queued commands are re-sent for execution. While a +connection failure persists, issued commands are buffered. + +To change into *at-most-once* consistency level, disable auto-reconnect +mode. Connections cannot be longer reconnected and thus no retries are +issued. Not successfully commands are canceled. New commands are +rejected. + +### Clustered operations + +Lettuce sticks in clustered operations to the same rules as for +standalone operations but with one exception: + +Command execution on master nodes, which is rejected by a `MOVED` +response are tried to re-execute with the appropriate connection. +`MOVED` errors occur on master nodes when a slot’s responsibility is +moved from one cluster node to another node. Afterwards *at-least-once* +and *at-most-once* rules apply. + +When the cluster topology changes, generally spoken, the cluster slots +or master/replica state is reconfigured, following rules apply: + +- **at-most-once** If the connection is disconnected, queued commands + are canceled and buffered commands, which were not sent, are executed + by using the new cluster view + +- **at-least-once** If the connection is disconnected, queued and + buffered commands, which were not sent, are executed by using the new + cluster view + +- If the connection is not disconnected, queued commands are finished + and buffered commands, which were not sent, are executed by using the + new cluster view + diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 0000000000..4bb14401d2 --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,171 @@ +# Frequently Asked Questions + +## I’m seeing `RedisCommandTimeoutException` + +**Symptoms:** + +`RedisCommandTimeoutException` with a stack trace like: + + io.lettuce.core.RedisCommandTimeoutException: Command timed out after 1 minute(s) + at io.lettuce.core.ExceptionFactory.createTimeoutException(ExceptionFactory.java:51) + at io.lettuce.core.LettuceFutures.awaitOrCancel(LettuceFutures.java:114) + at io.lettuce.core.FutureSyncInvocationHandler.handleInvocation(FutureSyncInvocationHandler.java:69) + at io.lettuce.core.internal.AbstractInvocationHandler.invoke(AbstractInvocationHandler.java:80) + at com.sun.proxy.$Proxy94.set(Unknown Source) + +**Diagnosis:** + +1. Check the debug log (log level `DEBUG` or `TRACE` for the logger + `io.lettuce.core.protocol`) + +2. Take a Thread dump to investigate Thread activity + +3. Investigate Lettuce usage, specifically for + `setAutoFlushCommands(false)` calls + +4. Do you use a custom `RedisCodec`? + +**Cause:** + +Command timeouts are caused by the fact that a command was not completed +within the configured timeout. Timeouts may be caused for various +reasons: + +1. Redis server has crashed/network partition happened and your Redis + service didn’t recover within the configured timeout + +2. Command was not finished in time. This can happen if your Redis + server is overloaded or if the connection is blocked by a command + (e.g. `BLPOP 0`, long-running Lua script). See also + [gives](#blpopdurationzero--gives-rediscommandtimeoutexception). + +3. Configured timeout does not match Redis’s performance. + +4. If you block the `EventLoop` (e.g. calling blocking methods in a + `RedisFuture` callback or in a Reactive pipeline). That can easily + happen when calling Redis commands in a Pub/Sub listener or a + `RedisConnectionStateListener`. + +5. If you manually control the flushing behavior of commands + (`setAutoFlushCommands(true/false)`), you should have a good reason + to do so. In multi-threaded environments, race conditions may easily + happen, and commands are not flushed. Updating a missing or + misplaced `flushCommands()` call might solve the problem. + +6. If you’re using a custom `RedisCodec` that can fail during encoding, + this will desynchronize the protocol state. + +**Action:** + +Check for the causes above. If the configured timeout does not match +your Redis latency characteristics, consider increasing the timeout. +Never block the `EventLoop` from your code. Make sure that your +`RedisCodec` doesn’t fail on encode. + +## `blpop(Duration.ZERO, …)` gives `RedisCommandTimeoutException` + +**Symptoms:** + +Calling `blpop`, `brpop` or any other blocking command followed by +`RedisCommandTimeoutException` with a stack trace like: + + io.lettuce.core.RedisCommandTimeoutException: Command timed out after 1 minute(s) + at io.lettuce.core.ExceptionFactory.createTimeoutException(ExceptionFactory.java:51) + at io.lettuce.core.LettuceFutures.awaitOrCancel(LettuceFutures.java:114) + at io.lettuce.core.FutureSyncInvocationHandler.handleInvocation(FutureSyncInvocationHandler.java:69) + at io.lettuce.core.internal.AbstractInvocationHandler.invoke(AbstractInvocationHandler.java:80) + at com.sun.proxy.$Proxy94.set(Unknown Source) + +**Cause:** + +The configured command timeout applies without considering +command-specific timeouts. + +**Action:** + +There are various options: + +1. Configure a higher default timeout. + +2. Consider a timeout that meets the default timeout when calling + blocking commands. + +3. Configure `TimeoutOptions` with a custom `TimeoutSource` + +``` java +TimeoutOptions timeoutOptions = TimeoutOptions.builder().timeoutSource(new TimeoutSource() { + @Override + public long getTimeout(RedisCommand command) { + + if (command.getType() == CommandType.BLPOP) { + return TimeUnit.MILLISECONDS.toNanos(CommandArgsAccessor.getFirstInteger(command.getArgs())); + } + + // -1 indicates fallback to the default timeout + return -1; + } +}).build(); +``` + +Note that commands that timed out may block the connection until either +the timeout exceeds or Redis sends a response. + +## Excessive Memory Usage or `RedisException` while disconnected + +**Symptoms:** + +`RedisException` with one of the following messages: + + io.lettuce.core.RedisException: Request queue size exceeded: n. Commands are not accepted until the queue size drops. + + io.lettuce.core.RedisException: Internal stack size exceeded: n. Commands are not accepted until the stack size drops. + +Or excessive memory allocation. + +**Diagnosis:** + +1. Check Redis connectivity + +2. Inspect memory usage + +**Cause:** + +Lettuce auto-reconnects by default to Redis to minimize service +disruption. Commands issued while there’s no Redis connection are +buffered and replayed once the server connection is reestablished. By +default, the queue is unbounded which can lead to memory exhaustion. + +**Action:** + +You can configure disconnected behavior and the request queue size +through `ClientOptions` for your workload profile. See [Client +Options](advanced-usage.md#client-options) for further reference. + +## Performance Degradation using the Reactive API with a single connection + +**Symptoms:** + +Performance degradation when using the Reactive API with a single +connection (i.e. non-pooled connection arrangement). + +**Diagnosis:** + +1. Inspect Thread affinity of reactive signals + +**Cause:** + +Netty’s threading model assigns a single Thread to each connection which +makes I/O for a single `Channel` effectively single-threaded. With a +significant computation load and without further thread switching, the +system leverages a single thread and therefore leads to contention. + +**Action:** + +You can configure signal multiplexing for the reactive API through +`ClientOptions` by enabling `publishOnScheduler(true)`. See [Client +Options](advanced-usage.md#client-options) for further reference. Alternatively, you can +configure `Scheduler` on each result stream through +`publishOn(Scheduler)`. Note that the asynchronous API features the same +behavior and you might want to use `then…Async(…)`, `run…Async(…)`, +`apply…Async(…)`, or `handleAsync(…)` methods along with an `Executor` +object. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000000..a9bdc97fd1 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,87 @@ +# Getting Started + +You can get started with Lettuce in various ways. + +## 1. Get it + +### For Maven users + +Add these lines to file pom.xml: + +``` xml + + io.lettuce + lettuce-core + 6.5.0.RELEASE + +``` + +### For Ivy users + +Add these lines to file ivy.xml: + +``` xml + + + + + +``` + +### For Gradle users + +Add these lines to file build.gradle: + +``` groovy +dependencies { + implementation 'io.lettuce:lettuce-core:6.5.0.RELEASE' +} +``` + +### Plain Java + +Download the latest binary package from + and extract the +archive. + +## 2. Start coding + +So easy! No more boring routines, we can start. + +Import required classes: + +``` java +import io.lettuce.core.*; +``` + +and now, write your code: + +``` java +RedisClient redisClient = RedisClient.create("redis://password@localhost:6379/0"); +StatefulRedisConnection connection = redisClient.connect(); +RedisCommands syncCommands = connection.sync(); + +syncCommands.set("key", "Hello, Redis!"); + +connection.close(); +redisClient.shutdown(); +``` + +Done! + +Do you want to see working examples? + +- [Standalone Redis](https://github.com/redis/lettuce/blob/main/src/test/java/io/lettuce/examples/ConnectToRedis.java) + +- [Standalone Redis with SSL](https://github.com/redis/lettuce/blob/main/src/test/java/io/lettuce/examples/ConnectToRedisSSL.java) + +- [Redis Sentinel](https://github.com/redis/lettuce/blob/main/src/test/java/io/lettuce/examples/ConnectToRedisUsingRedisSentinel.java) + +- [Redis Cluster](https://github.com/redis/lettuce/blob/main/src/test/java/io/lettuce/examples/ConnectToRedisCluster.java) + +- [Connecting to a ElastiCache Master](https://github.com/redis/lettuce/blob/main/src/test/java/io/lettuce/examples/ConnectToElastiCacheMaster.java) + +- [Connecting to ElastiCache with Master/Replica](https://github.com/redis/lettuce/blob/main/src/test/java/io/lettuce/examples/ConnectToMasterSlaveUsingElastiCacheCluster.java) + +- [Connecting to Azure Redis Cluster](https://github.com/redis/lettuce/blob/main/src/test/java/io/lettuce/examples/ConnectToRedisClusterSSL.java) + diff --git a/docs/ha-sharding.md b/docs/ha-sharding.md new file mode 100644 index 0000000000..67c547b65d --- /dev/null +++ b/docs/ha-sharding.md @@ -0,0 +1,670 @@ +# High-Availability and Sharding + +## Master/Replica + +Redis can increase availability and read throughput by using +replication. Lettuce provides dedicated Master/Replica support since 4.2 +for topologies and ReadFrom-Settings. + +Redis Master/Replica can be run standalone or together with Redis +Sentinel, which provides automated failover and master promotion. +Failover and master promotion is supported in Lettuce already since +version 3.1 for master connections. + +Connections can be obtained from the `MasterReplica` connection provider +by supplying the client, Codec, and one or multiple RedisURIs. + +### Redis Sentinel + +Master/Replica using Redis Sentinel uses Redis +Sentinel as registry and notification source for topology events. +Details about the master and its replicas are obtained from Redis +Sentinel. Lettuce subscribes to Redis Sentinel events for notifications to all supplied +Sentinels. + +### Standalone Master/Replica + +Running a Standalone Master/Replica setup requires one seed address to +establish a Redis connection. Providing one `RedisURI` will discover +other nodes which belong to the Master/Replica setup and use the +discovered addresses for connections. The initial URI can point either +to a master or a replica node. + +### Static Master/Replica with predefined node addresses + +In some cases, topology discovery shouldn’t be enabled, or the +discovered Redis addresses are not suited for connections. AWS +ElastiCache falls into this category. Lettuce allows to specify one or +more Redis addresses as `List` and predefine the node topology. +Master/Replica URIs will be treated in this case as static topology, and +no additional hosts are discovered in such case. Redis Standalone +Master/Replica will discover the roles of the supplied `RedisURI`s and +issue commands to the appropriate node. + +### Topology discovery + +Master-Replica topologies are either static or semi-static. Redis +Standalone instances with attached replicas provide no failover/HA +mechanism. Redis Sentinel managed instances are controlled by Redis +Sentinel and allow failover (which include master promotion). The +`MasterReplica` API supports both mechanisms. The topology is provided +by a `TopologyProvider`: + +- `MasterReplicaTopologyProvider`: Dynamic topology lookup using the + `INFO REPLICATION` output. Replicas are listed as replicaN=…​ entries. + The initial connection can either point to a master or a replica, and + the topology provider will discover nodes. The connection needs to be + re-established outside of Lettuce in a case of a Master/Replica + failover or topology changes. + +- `StaticMasterReplicaTopologyProvider`: Topology is defined by the list + of URIs and the ROLE output. MasterReplica uses only the supplied + nodes and won’t discover additional nodes in the setup. The connection + needs to be re-established outside of Lettuce in case of a + Master/Replica failover or topology changes. + +- `SentinelTopologyProvider`: Dynamic topology lookup using the Redis + Sentinel API. In particular, `SENTINEL MASTER` and `SENTINEL REPLICAS` + output. Master/Replica failover is handled by Lettuce. + +### Topology Updates + +- Standalone Master/Replica: Performs a one-time topology lookup which + remains static afterward + +- Redis Sentinel: Subscribes to all Sentinels and listens for Pub/Sub + messages to trigger topology refreshing + +#### Transactions + +Since version 5.1, transactions and commands during a transaction are +routed to the master node to ensure atomic transaction execution on a +single node. Transactions can contain read- and write-operations so the +driver cannot decide upfront which node can be used to run the actual +transaction. + +#### Examples + +``` java +RedisClient redisClient = RedisClient.create(); + +StatefulRedisMasterReplicaConnection connection = MasterReplica.connect(redisClient, StringCodec.UTF8, + RedisURI.create("redis://localhost")); +connection.setReadFrom(ReadFrom.MASTER_PREFERRED); + +System.out.println("Connected to Redis"); + +connection.close(); +redisClient.shutdown(); +``` + +``` java +RedisClient redisClient = RedisClient.create(); + +StatefulRedisMasterReplicaConnection connection = MasterReplica.connect(redisClient, StringCodec.UTF8, + RedisURI.create("redis-sentinel://localhost:26379,localhost:26380/0#mymaster")); +connection.setReadFrom(ReadFrom.MASTER_PREFERRED); + +System.out.println("Connected to Redis"); + +connection.close(); +redisClient.shutdown(); +``` + +``` java +RedisClient redisClient = RedisClient.create(); + +List nodes = Arrays.asList(RedisURI.create("redis://host1"), + RedisURI.create("redis://host2"), + RedisURI.create("redis://host3")); + +StatefulRedisMasterReplicaConnection connection = MasterReplica + .connect(redisClient, StringCodec.UTF8, nodes); +connection.setReadFrom(ReadFrom.MASTER_PREFERRED); + +System.out.println("Connected to Redis"); + +connection.close(); +redisClient.shutdown(); +``` + +## Redis Sentinel + +When using Lettuce, you can interact with Redis Sentinel and Redis +Sentinel-managed nodes in multiple ways: + +1. [Direct connection to Redis + Sentinel](#direct-connection-redis-sentinel-nodes), for issuing + Redis Sentinel commands + +2. Using Redis Sentinel to [connect to a + master](#redis-discovery-using-redis-sentinel) + +3. Using Redis Sentinel to connect to master nodes and replicas through + the {master-replica-api-link}. + +In both cases, you need to supply a `RedisURI` since the Redis Sentinel +integration supports multiple Sentinel hosts to provide high +availability. + +Please note: Redis Sentinel (Lettuce 3.x) integration provides only +asynchronous connections and no connection pooling. + +### Direct connection Redis Sentinel nodes + +Lettuce exposes an API to interact with Redis Sentinel nodes directly. +This is useful for performing administrative tasks using Lettuce. You +can monitor new master nodes, query master addresses, replicas and much +more. A connection to a Redis Sentinel node is established by +`RedisClient.connectSentinel()`. Use a [Publish/Subscribe +connection](user-guide/pubsub.md) to subscribe to Sentinel events. + +### Redis discovery using Redis Sentinel + +One or more Redis Sentinels can monitor Redis instances . These Redis +instances are usually operated together with a replica of the Redis +instance. Once the master goes down, the replica is promoted to a +master. Once a master instance is not reachable anymore, the failover +process is started by the Redis Sentinels. Usually, the client +connection is terminated. The disconnect can result in any of the +following options: + +1. The master comes back: The connection is restored to the Redis + instance + +2. A replica is promoted to a master: Lettuce performs an address + lookup using the `masterId`. As soon as the Redis Sentinel provides + an address the connection is restored to the new Redis instance + +Read more at + +### Examples + +``` java +RedisURI redisUri = RedisURI.create("redis://sentinelhost1:26379"); +RedisClient client = new RedisClient(redisUri); + +RedisSentinelAsyncConnection connection = client.connectSentinelAsync(); + +Map map = connection.master("mymaster").get(); +``` + +``` java +RedisURI redisUri = RedisURI.Builder.sentinel("sentinelhost1", "mymaster").withSentinel("sentinelhost2").build(); +RedisClient client = RedisClient.create(redisUri); + +RedisConnection connection = client.connect(); +``` + +!!! NOTE + Every time you connect to a Redis instance using Redis Sentinel, the + Redis master is looked up using a new connection to a Redis Sentinel. + This can be time-consuming, especially when multiple Redis Sentinels + are used and one or more of them are not reachable. + +## Redis Cluster + +Lettuce supports Redis Cluster with: + +- Support of all `CLUSTER` commands + +- Command routing based on the hash slot of the commands' key + +- High-level abstraction for selected cluster commands + +- Execution of commands on multiple cluster nodes + +- `MOVED` and `ASK` redirection handling + +- Obtaining direct connections to cluster nodes by slot and host/port + (since 3.3) + +- SSL and authentication (since 4.2) + +- Periodic and adaptive cluster topology updates + +- Publish/Subscribe + +Connecting to a Redis Cluster requires one or more initial seed nodes. +The full cluster topology view (partitions) is obtained on the first +connection so you’re not required to specify all cluster nodes. +Specifying multiple seed nodes helps to improve resiliency as Lettuce is +able to connect the cluster even if a seed node is not available. +Lettuce holds multiple connections, which are opened on demand. You are +free to operate on these connections. + +Connections can be bound to specific hosts or nodeIds. Connections bound +to a nodeId will always stick to the nodeId, even if the nodeId is +handled by a different host. Requests to unknown nodeId’s or host/ports +that are not part of the cluster are rejected. Do not close the +connections. Otherwise, unpredictable behavior will occur. Keep also in +mind that the node connections are used by the cluster connection itself +to perform cluster operations: If you block one connection all other +users of the cluster connection might be affected. + +### Command routing + +The [concept of Redis Cluster](https://redis.io/docs/latest/operate/oss_and_stack/management/scaling/) +bases on sharding. Every master node within the cluster handles one or +more slots. Slots are the [unit of +sharding](https://redis.io/docs/latest/operate/oss_and_stack/management/scaling/#redis-cluster-data-sharding) +and calculated from the commands' key using `CRC16 MOD 16384`. Hash +slots can also be specified using hash tags such as `{user:1000}.foo`. + +Every request, which incorporates at least one key is routed based on +its hash slot to the corresponding node. Commands without a key are +executed on the *default* connection that points most likely to the +first provided `RedisURI`. The same rule applies to commands operating +on multiple keys but with the limitation that all keys have to be in the +same slot. Commands operating on multiple slots will be terminated with +a `CROSSSLOT` error. + +### Cross-slot command execution and cluster-wide execution for selected commands + +Regular Redis Cluster commands are limited to single-slot keys operation +– either single key commands or multi-key commands that share the same +hash slot. + +The cross slot limitation can be mitigated by using the advanced cluster +API for *a set of selected* multi-key commands. Commands that operate on +keys with different slots are decomposed into multiple commands. The +single commands are fired in a fork/join fashion. The commands are +issued concurrently to avoid synchronous chaining. Results are +synchronized before the command is completed. + +Following commands are supported for cross-slot command execution: + +- `DEL`: Delete the `KEY`s. Returns the number of keys that were + removed. + +- `EXISTS`: Count the number of `KEY`s that exist across the master + nodes being responsible for the particular key. + +- `MGET`: Get the values of all given `KEY`s. Returns the values in the + order of the keys. + +- `MSET`: Set multiple key/value pairs for all given `KEY`s. Returns + always `OK`. + +- `TOUCH`: Alters the last access time of all given `KEY`s. Returns the + number of keys that were touched. + +- `UNLINK`: Delete the `KEY`s and reclaiming memory in a different + thread. Returns the number of keys that were removed. + +Following commands are executed on multiple cluster nodes operations: + +- `CLIENT SETNAME`: Set the client name on all known cluster node + connections. Returns always `OK`. + +- `KEYS`: Return/Stream all keys that are stored on all masters. + +- `DBSIZE`: Return the number of keys that are stored on all masters. + +- `FLUSHALL`: Flush all data on the cluster masters. Returns always + `OK`. + +- `FLUSHDB`: Flush all data on the cluster masters. Returns always `OK`. + +- `RANDOMKEY`: Return a random key from a random master. + +- `SCAN`: Scan the keyspace across the whole cluster according to + `ReadFrom` settings. + +- `SCRIPT FLUSH`: Remove all the scripts from the script cache on all + cluster nodes. + +- `SCRIPT LOAD`: Load the script into the Lua script cache on all nodes. + +- `SCRIPT KILL`: Kill the script currently in execution on all cluster + nodes. This call does not fail even if no scripts are running. + +- `SHUTDOWN`: Synchronously save the dataset to disk and then shut down + all nodes of the cluster. + +Cross-slot command execution is available on the following APIs: + +- `RedisAdvancedClusterCommands` + +- `RedisAdvancedClusterAsyncCommands` + +- `RedisAdvancedClusterReactiveCommands` + +### Execution of commands on one or multiple cluster nodes + +Sometimes commands have to be executed on multiple cluster nodes. The +advanced cluster API allows to select a set of nodes (e.g. all masters, +all replicas) and trigger a command on this set. + +``` java +RedisAdvancedClusterAsyncCommands async = clusterClient.connect().async(); +AsyncNodeSelection replicas = connection.slaves(); + +AsyncExecutions> executions = replicas.commands().keys("*"); +executions.forEach(result -> result.thenAccept(keys -> System.out.println(keys))); +``` + +The commands are triggered concurrently. This API is currently only +available for async commands. Commands are dispatched to the nodes +within the selection, the result (CompletionStage) is available through +`AsyncExecutions`. + +A node selection can be either dynamic or static. A dynamic node +selection updates its node set upon a [cluster topology view +refresh](#refreshing-the-cluster-topology-view). Node +selections can be constructed by the following presets: + +- masters + +- replicas (operate on connections with activated `READONLY` mode) + +- all nodes + +A custom selection of nodes is available by implementing [custom +predicates](https://www.javadoc.io/static/io.lettuce/lettuce-core/6.4.0.RELEASE/io/lettuce/core/cluster/api/async/RedisAdvancedClusterAsyncCommands.html#nodes-java.util.function.Predicate-) +or lambdas. + +The particular results map to a cluster node (`RedisClusterNode`) that +was involved in the node selection. You can obtain the set of involved +`RedisClusterNode`s and all results as `CompletableFuture` from +`AsyncExecutions`. + +The node selection API is a technical preview and can change at any +time. That approach allows powerful operations but it requires further +feedback from the users. So feel free to contribute. + +### Refreshing the cluster topology view + +The Redis Cluster configuration may change at runtime. New nodes can be +added, the master for a specific slot can change. Lettuce handles +`MOVED` and `ASK` redirects transparently but in case too many commands +run into redirects, you should refresh the cluster topology view. The +topology is bound to a `RedisClusterClient` instance. All cluster +connections that are created by one `RedisClusterClient` instance share +the same cluster topology view. The view can be updated in three ways: + +1. Either by calling `RedisClusterClient.reloadPartitions` + +2. [Periodic updates](advanced-usage.md#cluster-specific-options) in the background + based on an interval + +3. [Adaptive updates](advanced-usage.md#cluster-specific-options) in the background + based on persistent disconnects and `MOVED`/`ASK` redirections + +By default, commands follow `-ASK` and `-MOVED` redirects [up to 5 +times](advanced-usage.md#cluster-specific-options) until the command execution is +considered to be failed. Background topology updating starts with the +first connection obtained through `RedisClusterClient`. + +### Connection Count for a Redis Cluster Connection Object + +With Standalone Redis, a single connection object correlates with a +single transport connection. Redis Cluster works differently: A +connection object with Redis Cluster consists of multiple transport +connections. These are: + +- Default connection object (Used for key-less commands and for Pub/Sub + message publication) + +- Connection per node (read/write connection to communicate with + individual Cluster nodes) + +- When using `ReadFrom`: Read-only connection per read replica node + (read-only connection to read data from read replicas) + +Connections are allocated on demand and not up-front to start with a +minimal set of connections. Formula to calculate the maximum number of +transport connections for a single connection object: + + 1 + (N * 2) + +Where `N` is the number of cluster nodes. + +Apart of connection objects, `RedisClusterClient` uses additional +connections for topology refresh. These are created on topology refresh +and closed after obtaining the topology: + +- Set of connections for cluster topology refresh (a connection to each + cluster node) + +### Client-options + +See [Cluster-specific Client options](advanced-usage.md#cluster-specific-options). + +#### Examples + +``` java +RedisURI redisUri = RedisURI.Builder.redis("localhost").withPassword("authentication").build(); + +RedisClusterClient clusterClient = RedisClusterClient.create(redisUri); +StatefulRedisClusterConnection connection = clusterClient.connect(); +RedisAdvancedClusterCommands syncCommands = connection.sync(); + +... + +connection.close(); +clusterClient.shutdown(); +``` + +``` java +RedisURI node1 = RedisURI.create("node1", 6379); +RedisURI node2 = RedisURI.create("node2", 6379); + +RedisClusterClient clusterClient = RedisClusterClient.create(Arrays.asList(node1, node2)); +StatefulRedisClusterConnection connection = clusterClient.connect(); +RedisAdvancedClusterCommands syncCommands = connection.sync(); + +... + +connection.close(); +clusterClient.shutdown(); +``` + +``` java +RedisClusterClient clusterClient = RedisClusterClient.create(RedisURI.create("localhost", 6379)); + +ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder() + .enablePeriodicRefresh(10, TimeUnit.MINUTES) + .build(); + +clusterClient.setOptions(ClusterClientOptions.builder() + .topologyRefreshOptions(topologyRefreshOptions) + .build()); +... + +clusterClient.shutdown(); +``` + +``` java +RedisURI node1 = RedisURI.create("node1", 6379); +RedisURI node2 = RedisURI.create("node2", 6379); + +RedisClusterClient clusterClient = RedisClusterClient.create(Arrays.asList(node1, node2)); + +ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder() + .enableAdaptiveRefreshTrigger(RefreshTrigger.MOVED_REDIRECT, RefreshTrigger.PERSISTENT_RECONNECTS) + .adaptiveRefreshTriggersTimeout(30, TimeUnit.SECONDS) + .build(); + +clusterClient.setOptions(ClusterClientOptions.builder() + .topologyRefreshOptions(topologyRefreshOptions) + .build()); +... + +clusterClient.shutdown(); +``` + +``` java +RedisURI node1 = RedisURI.create("node1", 6379); +RedisURI node2 = RedisURI.create("node2", 6379); + +RedisClusterClient clusterClient = RedisClusterClient.create(Arrays.asList(node1, node2)); +StatefulRedisClusterConnection connection = clusterClient.connect(); + +RedisClusterCommands node1 = connection.getConnection("host", 7379).sync(); + +... +// do not close node1 + +connection.close(); +clusterClient.shutdown(); +``` + +## ReadFrom Settings + +The ReadFrom setting describes how Lettuce routes read operations to +replica nodes. + +By default, Lettuce routes its read operations in multi-node connections +to the master node. Reading from the master returns the most recent +version of the data because write operations are issued to the single +master node. Reading from masters guarantees strong consistency. + +You can reduce latency or improve read throughput by distributing reads +to replica members for applications that do not require fully up-to-date +data. + +Be careful if using other ReadFrom settings than `MASTER`. Settings +other than `MASTER` may return stale data because the replication is +asynchronous. Data in the replicas may not hold the most recent data. + +### Redis Cluster + +Redis Cluster is a multi-node operated Redis setup that uses one or more +master nodes and allows to setup replica nodes. Redis Cluster +connections allow to set a `ReadFrom` setting on connection level. This +setting applies for all read operations on this connection. + +``` java +RedisClusterClient client = RedisClusterClient.create(RedisURI.create("host", 7379)); +StatefulRedisClusterConnection connection = client.connect(); +connection.setReadFrom(ReadFrom.REPLICA); + +RedisAdvancedClusterCommands sync = connection.sync(); +sync.set(key, "value"); + +sync.get(key); // replica read + +connection.close(); +client.shutdown(); +``` + +### Master/Replica connections ("Master/Slave") + +Redis nodes can be operated in a Master/Replica setup to achieve +availability and performance. Master/Replica setups can be run either +Standalone or managed using Redis Sentinel. Lettuce allows to use +replica nodes for read operations by using the `MasterReplica` API that +supports both Master/Replica setups: + +1. Redis Standalone Master/Replica (no failover) + +2. Redis Sentinel Master/Replica (Sentinel-managed failover) + +The resulting connection uses in any case the primary connection-point +to dispatch non-read operations. + +#### Redis Sentinel + +Master/Replica with Redis Sentinel is very similar to regular Redis +Sentinel operations. When the master fails over, a replica is promoted +by Redis Sentinel to the new master and the client obtains the new +topology from Redis Sentinel. + +Connections to Master/Replica require one or more Redis Sentinel +connection points and a master name. The primary connection point is the +Sentinel monitored master node. + +``` java +RedisURI sentinelUri = RedisURI.Builder.sentinel("sentinel-host", 26379, "master-name").build(); +RedisClient client = RedisClient.create(); + +StatefulRedisMasterReplicaConnection connection = MasterReplica.connect( + client, + StringCodec.UTF8 + sentinelUri); + +connection.setReadFrom(ReadFrom.REPLICA); + +connection.sync().get("key"); // Replica read + +connection.close(); +client.shutdown(); +``` + +#### Redis Standalone + +Master/Replica with Redis Standalone is very similar to regular Redis +Standalone operations. A Redis Standalone Master/Replica setup is static +and provides no built-in failover. Replicas are read from the Redis +master node’s `INFO` command. + +Connecting to Redis Standalone Master/Replica nodes requires connections +to use the Redis master for the `RedisURI`. The node used within the +`RedisURI` is the primary connection point. + +``` java +RedisURI masterUri = RedisURI.Builder.redis("master-host", 6379).build(); +RedisClient client = RedisClient.create(); + +StatefulRedisMasterReplicaConnection connection = MasterReplica.connect( + client, + StringCodec.UTF8, + masterUri); + +connection.setReadFrom(ReadFrom.REPLICA); + +connection.sync().get("key"); // Replica read + +connection.close(); +client.shutdown(); +``` + +### Use Cases for non-master reads + +The following use cases are common for using non-master read settings +and encourage eventual consistency: + +- Providing local reads for geographically distributed applications. If + you have Redis and application servers in multiple data centers, you + may consider having a geographically distributed cluster. Using the + `LOWEST_LATENCY` setting allows the client to read from the + lowest-latency members, rather than always reading from the master + node. + +- Maintaining availability during a failover. Use `MASTER_PREFERRED` if + you want an application to read from the master by default, but to + allow stale reads from replicas when the master node is unavailable. + `MASTER_PREFERRED` allows a "read-only mode" for your application + during a failover. + +- Increase read throughput by allowing stale reads If you want to + increase your read throughput by adding additional replica nodes to + your cluster Use `REPLICA` to read explicitly from replicas and reduce + read load on the master node. Using replica reads can highly lead to + stale reads. + +### Read from settings + +All `ReadFrom` settings except `MASTER` may return stale data because +replicas replication is asynchronous and requires some delay. You need +to ensure that your application can tolerate stale data. + +| Setting | Description | +|---------------------|--------------------------------------------------------------------------------| +| `MASTER` | Default mode. Read from the current master node. | +| `MASTER_PREFERRED` | Read from the master, but if it is unavailable, read from replica nodes. | +| `REPLICA` | Read from replica nodes. | +| `REPLICA_PREFERRED` | Read from the replica nodes, but if none is unavailable, read from the master. | +| `LOWEST_LATENCY` | Read from any node of the cluster with the lowest latency. | +| `ANY` | Read from any node of the cluster. | +| `ANY_REPLICA` | Read from any replica of the cluster. | + +!!! TIP + The latency of the nodes is determined upon the cluster topology + refresh. If the topology view is never refreshed, values from the + initial cluster nodes read are used. + +Custom read settings can be implemented by extending the +`io.lettuce.core.ReadFrom` class. + diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000000..074a48bf84 --- /dev/null +++ b/docs/index.md @@ -0,0 +1 @@ +{% include 'README.md' %} \ No newline at end of file diff --git a/docs/integration-extension.md b/docs/integration-extension.md new file mode 100644 index 0000000000..d58004ca69 --- /dev/null +++ b/docs/integration-extension.md @@ -0,0 +1,280 @@ +# Integration and Extension + +## Codecs + +Codecs are a pluggable mechanism for transcoding keys and values between +your application and Redis. The default codec supports UTF-8 encoded +String keys and values. + +Each connection may have its codec passed to the extended +`RedisClient.connect` methods: + +``` java +StatefulRedisConnection connect(RedisCodec codec) +StatefulRedisPubSubConnection connectPubSub(RedisCodec codec) +``` + +Lettuce ships with predefined codecs: + +- `io.lettuce.core.codec.ByteArrayCodec` - use `byte[]` for keys and + values + +- `io.lettuce.core.codec.StringCodec` - use Strings for keys and values. + Using the default charset or a specified `Charset` with improved + support for `US_ASCII` and `UTF-8`. + +- `io.lettuce.core.codec.CipherCodec` - used for transparent encryption + of values. + +- `io.lettuce.core.codec.CompressionCodec` - apply `GZIP` or `DEFLATE` + compression to values. + +Publish/Subscribe connections use channel names and patterns for keys; +messages are treated as values. + +Keys and values can be encoded independently from each other which means +the key can be a `java.lang.String` while the value is a `byte[]`. Many +other constellations are possible like: + +- Representing your data as JSON if your data is mapped to a particular + Java type. Different types are complex to map since the codec applies + to all operations. + +- Serialize your data using the Java Serializer + (`ObjectInputStream`/`ObjectOutputStream`). Allows type-safe + conversions but is less interoperable with other languages + +- Serializing your data using + [Kryo](https://github.com/EsotericSoftware/kryo) for improved + type-safe serialization. + +- Any specialized codecs like the `BitStringCodec` (see below) + +### Exception handling during Encoding + +Codecs should be designed in a way that doesn’t allow encoding +exceptions except for Out-of-Memory scenarios. Encoding of keys and +values happens on the Event Loop after registering a command in the +protocol stack and sending a command to the write queue. Exceptions at +that stage will leave the command in the protocol stack while a command +might have not been sent to Redis because encoding has failed. Such a +state desynchronizes the protocol state and your commands will fail +with: +`Cannot encode command. Please close the connection as the connection state may be out of sync.`. + +JSON and JDK serialization can fail because the underlying object graph +cannot be serialized (i.e. an object does not implement `Serializable` +or Jackson cannot serialize a value because of misconfiguration). If you +want to remain safe (and remove encoding load from the Event Loop), +rather serialize such objects beforehand and use the resulting `byte[]` +as value input to Redis commands. + +### Why `ByteBuffer` instead of `byte[]` + +The `RedisCodec` interface accepts and returns `ByteBuffer`s for data +interchange. A `ByteBuffer` is not opinionated about the source of the +underlying bytes. The `byte[]` interface of Lettuce 3.x required the +user to provide an array with the exact data for interchange. So if you +have an array where you want to use only a subset, you’re required to +create a new instance of a byte array and copy the data. The same +applies if you have a different byte source (e.g. netty's `ByteBuf` or +an NIO `ByteBuffer`). The `ByteBuffer`s for decoding are pointers to the +underlying data. `ByteBuffer`s for encoding data can be either pure +pointers or allocated memory. Lettuce does not free any memory (such as +pooled buffers). + +### Diversity in Codecs + +As in every other segment of technology, there is no one-fits-it-all +solution when it comes to Codecs. Redis data structures provide a +variety of The key and value limitation of codecs is intentionally and a +balance amongst convenience and simplicity. The Redis API allows much +more variance in encoding and decoding particular data elements. A good +example is Redis hashes. A hash is identified by its key but stores +another key/value pairs. The keys of the key-value pairs could be +encoded using a different approach than the key of the hash. Another +different approach might be to use different encodings between lists and +sets. Using a base codec (such as UTF-8 or byte array) and performing an +own conversion on top of the base codec is often the better idea. + +### Multi-Threading + +A key point in Codecs is that Codecs are shared resources and can be +used by multiple threads. Your Codec needs to be thread-safe (by +shared-nothing, pooling or synchronization). Every logical Lettuce +connection uses its codec instance. Codec instances are shared as soon +as multiple threads are issuing commands or if you use Redis Cluster. + +### Compression + +Compression can be a good idea when storing larger chunks of data within +Redis. Any textual data structures (such as JSON or XML) are suited for +compression. Compression is handled at Codec-level which means you do +not have to change your application to apply compression. The +`CompressionCodec` provides basic and transparent compression for values +using either GZIP or Deflate compression: + +``` java +StatefulRedisConnection connection = client.connect( + CompressionCodec.valueCompressor(new SerializedObjectCodec(), CompressionCodec.CompressionType.GZIP)).sync(); + +StatefulRedisConnection connection = client.connect( + CompressionCodec.valueCompressor(StringCodec.UTF8, CompressionCodec.CompressionType.DEFLATE)).sync(); +``` + +Compression can be used with any codec, the compressor just wraps the +inner `RedisCodec` and compresses/decompresses the data that is +interchanged. You can build your own compressor the same way as you can +provide own codecs. + +### Examples + +``` java +public class BitStringCodec extends StringCodec { + @Override + public String decodeValue(ByteBuffer bytes) { + StringBuilder bits = new StringBuilder(bytes.remaining() * 8); + while (bytes.remaining() > 0) { + byte b = bytes.get(); + for (int i = 0; i < 8; i++) { + bits.append(Integer.valueOf(b >>> i & 1)); + } + } + return bits.toString(); + } +} + +StatefulRedisConnection connection = client.connect(new BitStringCodec()); +RedisCommands redis = connection.sync(); + +redis.setbit(key, 0, 1); +redis.setbit(key, 1, 1); +redis.setbit(key, 2, 0); +redis.setbit(key, 3, 0); +redis.setbit(key, 4, 0); +redis.setbit(key, 5, 1); + +redis.get(key) == "00100011" +``` + +``` java +public class SerializedObjectCodec implements RedisCodec { + private Charset charset = Charset.forName("UTF-8"); + + @Override + public String decodeKey(ByteBuffer bytes) { + return charset.decode(bytes).toString(); + } + + @Override + public Object decodeValue(ByteBuffer bytes) { + try { + byte[] array = new byte[bytes.remaining()]; + bytes.get(array); + ObjectInputStream is = new ObjectInputStream(new ByteArrayInputStream(array)); + return is.readObject(); + } catch (Exception e) { + return null; + } + } + + @Override + public ByteBuffer encodeKey(String key) { + return charset.encode(key); + } + + @Override + public ByteBuffer encodeValue(Object value) { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + ObjectOutputStream os = new ObjectOutputStream(bytes); + os.writeObject(value); + return ByteBuffer.wrap(bytes.toByteArray()); + } catch (IOException e) { + return ByteBuffer.wrap(new byte[0]); + } + } +} +``` + +## CDI Support + +CDI support for Lettuce is available for `RedisClient` and +`RedisClusterClient`. You need to provide a `RedisURI` in order to get +Lettuce injected. + +### RedisURI producer + +Implement a simple producer (either field producer or producer method) +of `RedisURI`: + +``` java +@Produces +public RedisURI redisURI() { + return RedisURI.Builder.redis("localhost").build(); +} +``` + +Lettuce also supports qualified `RedisURI`'s: + +``` java +@Produces +@PersonDB +public RedisURI redisURI() { + return RedisURI.Builder.redis("localhost").build(); +} +``` + +### Injection + +After declaring your `RedisURI`'s you can start using Lettuce in your +classes: + +``` java +public class InjectedClient { + + @Inject + private RedisClient redisClient; + + @Inject + private RedisClusterClient redisClusterClient; + + @Inject + @PersonDB + private RedisClient redisClient; + + private RedisConnection connection; + + @PostConstruct + public void postConstruct() { + connection = redisClient.connect(); + } + + public void pingRedis() { + connection.ping(); + } + + @PreDestroy + public void preDestroy() { + if (connection != null) { + connection.close(); + } + } +} +``` + +### Activating Lettuce’s CDI extension + +By default, you just drop Lettuce on your classpath and declare at least +one `RedisURI` bean. That’s all. + +The CDI extension registers one bean pair (`RedisClient` and +`RedisClusterClient`) per discovered `RedisURI`. This means, if you do +not declare any `RedisURI` producers, the CDI extension won’t be +activated at all. This way you can use Lettuce in CDI-capable containers +without even activating the CDI extension. + +All produced beans (`RedisClient` and `RedisClusterClient`) remain +active as long as your application is running since the beans are +`@ApplicationScoped`. + diff --git a/docs/new-features.md b/docs/new-features.md new file mode 100644 index 0000000000..c67cd1089d --- /dev/null +++ b/docs/new-features.md @@ -0,0 +1,185 @@ +# New & Noteworthy + +## What’s new in Lettuce 6.5 + +- [RedisJSON support](user-guide/redis-json.md) through `RedisJSONCommands` and the respective reactive, async and Kotlin APIs +- Complete support for all `CLUSTER` commands (added `CLUSTER MYSHARDID` and `CLUSTER LINKS`) +- Added support for the `CLIENT TRACKING` command +- Migrated the documentation to [MkDocs](https://www.mkdocs.org/) + +## What’s new in Lettuce 6.4 + +- [Hash Field Expiration](https://redis.io/docs/latest/develop/data-types/hashes/#field-expiration) is now fully supported +- [Sharded Pub/Sub](https://redis.io/docs/latest/develop/interact/pubsub/#sharded-pubsub) is now fully supported +- Support `CLIENT KILL` with `[MAXAGE]` parameter and `HSCAN` with `NOVALUES` parameter + +## What’s new in Lettuce 6.3 + +- [Redis Function support](user-guide/redis-functions.md) (`fcall` and `FUNCTION` + commands). + +- Support for Library Name and Version through `LettuceVersion`. + Automated registration of the Lettuce library version upon connection + handshake. + +- Support for Micrometer Tracing to trace observations (distributed + tracing and metrics). + +## What’s new in Lettuce 6.2 + +- [`RedisCredentialsProvider`](user-guide/connecting-redis.md#authentication) abstraction to + externalize credentials and credentials rotation. + +- Retrieval of Redis Cluster node connections using `ConnectionIntent` + to obtain read-only connections. + +- Master/Replica now uses `SENTINEL REPLICAS` to discover replicas + instead of `SENTINEL SLAVES`. + +## What’s new in Lettuce 6.1 + +- Kotlin Coroutines support for `SCAN`/`HSCAN`/`SSCAN`/`ZSCAN` through + `ScanFlow`. + +- Command Listener API through + `RedisClient.addListener(CommandListener)`. + +- [Micrometer support](advanced-usage.md#micrometer) through + `MicrometerCommandLatencyRecorder`. + +- [Experimental support for `io_uring`](advanced-usage.md#native-transports). + +- Configuration of extended Keep-Alive options through + `KeepAliveOptions` (only available for some transports/Java versions). + +- Configuration of netty's `AddressResolverGroup` through + `ClientResources`. Uses `DnsAddressResolverGroup` when + `netty-resolver-dns` is on the classpath. + +- Add support for Redis ACL commands. + +- [Java Flight Recorder Events](advanced-usage.md#java-flight-recorder-events-since-61) + +## What’s new in Lettuce 6.0 + +- Support for RESP3 usage with Redis 6 along with RESP2/RESP3 handshake + and protocol version discovery. + +- ACL authentication using username and password or password-only + authentication. + +- Cluster topology refresh is now non-blocking. + +- [Kotlin Coroutine Extensions](user-guide/kotlin-api.md). + +- RxJava 3 support. + +- Refined Scripting API accepting the Lua script either as `byte[]` or + `String`. + +- Connection and Queue failures now no longer throw an exception but + properly associate the failure with the Future handle. + +- Removal of deprecated API including timeout methods accepting + `TimeUnit`. Use methods accepting `Duration` instead. + +- Lots of internal refinements. + +- `xpending` methods return now `List` and + `PendingMessages` + +- Spring support removed. Use Spring Data Redis for a seamless Spring + integration with Lettuce. + +- `AsyncConnectionPoolSupport.createBoundedObjectPool(…)` methods are + now blocking to await pool initialization. + +- `DecodeBufferPolicy` for fine-grained memory reclaim control. + +- `RedisURI.toString()` renders masked password. + +- `ClientResources.commandLatencyCollector(…)` refactored into + `ClientResources.commandLatencyRecorder(…)` returning + `CommandLatencyRecorder`. + +## What’s new in Lettuce 5.3 + +- Improved SSL configuration supporting Cipher suite selection and + PEM-encoded certificates. + +- Fixed method signature for `randomkey()`. + +- Un-deprecated `ClientOptions.pingBeforeActivateConnection` to allow + connection verification during connection handshake. + +## What’s new in Lettuce 5.2 + +- Allow randomization of read candidates using Redis Cluster. + +- SSL support for Redis Sentinel. + +## What’s new in Lettuce 5.1 + +- Add support for `ZPOPMIN`, `ZPOPMAX`, `BZPOPMIN`, `BZPOPMAX` commands. + +- Add support for Redis Command Tracing through Brave, see [Configuring + Client resources](advanced-usage.md#configuring-client-resources). + +- Add support for [Redis + Streams](https://redis.io/topics/streams-intro). + +- Asynchronous `connect()` for Master/Replica connections. + +- [Asynchronous Connection Pooling](advanced-usage.md#asynchronous-connection-pooling) + through `AsyncConnectionPoolSupport` and `AsyncPool`. + +- Dedicated exceptions for Redis `LOADING`, `BUSY`, and `NOSCRIPT` + responses. + +- Commands in at-most-once mode (auto-reconnect disabled) are now + canceled already on disconnect. + +- Global command timeouts (also for reactive and asynchronous API usage) + configurable through [Client Options](advanced-usage.md#client-options). + +- Host and port mappers for Lettuce usage behind connection + tunnels/proxies through `SocketAddressResolver`, see [Configuring + Client resources](advanced-usage.md#configuring-client-resources). + +- `SCRIPT LOAD` dispatch to all cluster nodes when issued through + `RedisAdvancedClusterCommands`. + +- Reactive `ScanStream` to iterate over the keyspace using `SCAN` + commands. + +- Transactions using Master/Replica connections are bound to the master + node. + +## What’s new in Lettuce 5.0 + +- New artifact coordinates: `io.lettuce:lettuce-core` and packages moved + from `com.lambdaworks.redis` to `io.lettuce.core`. + +- [Reactive API](user-guide/reactive-api.md) now Reactive Streams-based using + [Project Reactor](https://projectreactor.io/). + +- [Redis Command + Interfaces](redis-command-interfaces.md) supporting + dynamic command invocation and Redis Modules. + +- Enhanced, immutable Key-Value objects. + +- Asynchronous Cluster connect. + +- Native transport support for Kqueue on macOS systems. + +- Removal of support for Guava. + +- Removal of deprecated `RedisConnection` and `RedisAsyncConnection` + interfaces. + +- Java 9 compatibility. + +- HTML and PDF reference documentation along with a new project website: + . + diff --git a/docs/overview.md b/docs/overview.md new file mode 100644 index 0000000000..780cc189aa --- /dev/null +++ b/docs/overview.md @@ -0,0 +1,68 @@ +# Overview + +This document is the reference guide for Lettuce. It explains how to use +Lettuce, its concepts, semantics, and the syntax. + +You can read this reference guide in a linear fashion, or you can skip +sections if something does not interest you. + +This section provides some basic introduction to Redis. The rest of the +document refers only to Lettuce features and assumes the user is +familiar with Redis concepts. + +## Knowing Redis + +If you are new to Redis, you can find a good introduction to Redis on [redis.io](https://redis.io/docs/latest/develop/) + +## Project Reactor + +[Reactor](https://projectreactor.io) is a highly optimized reactive +library for building efficient, non-blocking applications on the JVM +based on the [Reactive Streams +Specification](https://github.com/reactive-streams/reactive-streams-jvm). +Reactor based applications can sustain very high throughput message +rates and operate with a very low memory footprint, making it suitable +for building efficient event-driven applications using the microservices +architecture. + +Reactor implements two publishers +[Flux\](https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html) +and +[Mono\](https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Mono.html), +both of which support non-blocking back-pressure. This enables exchange +of data between threads with well-defined memory usage, avoiding +unnecessary intermediate buffering or blocking. + +## Non-blocking API for Redis + +Lettuce is a scalable thread-safe Redis client based on +[netty](https://netty.io) and Reactor. Lettuce provides +[synchronous](user-guide/connecting-redis.md#basic-usage), [asynchronous](user-guide/async-api.md) and +[reactive](user-guide/reactive-api.md) APIs to interact with Redis. + +## Requirements + +Lettuce 6.x binaries require JDK level 8.0 and above. + +In terms of [Redis](https://redis.io/), at least 2.6. + +## Where to go from here + +- Head to [Getting Started](getting-started.md) if you feel like jumping + straight into the code. + +- Go to [High-Availability and Sharding](ha-sharding.md) for Master/Replica + ("Master/Slave"), Redis Sentinel and Redis Cluster topics. + +- In order to dig deeper into the core features of Reactor: + + - If you’re looking for client configuration options, performance + related behavior and how to use various transports, go to [Advanced usage](advanced-usage.md). + + - See [Integration and Extension](integration-extension.md) for + extending Lettuce with codecs or integrate it in your CDI/Spring + application. + + - You want to know more about **at-least-once** and **at-most-once**? + Take a look into [Command execution reliability](advanced-usage.md#command-execution-reliability). + diff --git a/docs/redis-command-interfaces.md b/docs/redis-command-interfaces.md new file mode 100644 index 0000000000..6a644bdc85 --- /dev/null +++ b/docs/redis-command-interfaces.md @@ -0,0 +1,584 @@ +# Working with dynamic Redis Command Interfaces + +The Redis Command Interface abstraction provides a dynamic way for +typesafe Redis command invocation. It allows you to declare an interface +with command methods to significantly reduce boilerplate code required +to invoke a Redis command. + +## Introduction + +Redis is a data store supporting over 190 documented commands and over +450 command permutations. The community supports actively Redis +development; each major Redis release comes with new commands. Command +growth and keeping track with upcoming modules are challenging for +client developers and Redis user as there is no full command coverage +for each module in a single Redis client. + +The central interface in Lettuce Command Interface abstraction is +`Commands`. This interface acts primarily as a marker interface to help +you to discover interfaces that extend this one. The `KeyCommands` +interface below declares some command methods. + +``` java +public interface KeyCommands extends Commands { + + String get(String key); + + String set(String key, String value); + + String set(String key, byte[] value); +} +``` + +- Retrieves a key by its name. + +- Sets a key and value. + +- Sets a key and a value by using bytes. + +The interface from above declares several methods. Let’s take a brief +look at `String set(String key, String value)`. We can derive from that +declaration certain things: + +- It should be executed synchronously – there’s no + [asynchronous](#asynchronous-future-execution) or + [reactive](#reactive-execution) wrapper declared in the result type. + +- The Redis command method returns a `String` - that reveals something + regarding the command result expectation. This command expects a reply + that can be represented as `String`. + +- The method is named `set` so the derived command will be named `set`. + +- There are two parameters defined: `String key` and `String value`. + Although Redis does not take any other parameter types than bulk + strings, we still can apply a transformation to the parameters – we + can conclude their serialization from the declared type. + +The `set` command from above called would look like: + + commands.set("key", "value"); + +This command translates to: + + SET key value + +## Command methods + +With Lettuce, declaring command methods becomes a four-step process: + +1. Declare an interface extending `Commands`. + + ``` java + interface KeyCommands extends Commands { … } + ``` + +2. Declare command methods on the interface. + + ``` java + interface KeyCommands extends Commands { + String get(String key); + } + ``` + +3. Set up Lettuce to create proxy instances for those interfaces. + + ``` java + RedisClient client = … + RedisCommandFactory factory = new RedisCommandFactory(client.connect()); + ``` + +4. Get the commands instance and use it. + + ``` java + public class SomeClient { + + KeyCommands commands; + + public SomeClient(RedisCommandFactory factory) { + commands = factory.getCommands(KeyCommands.class); + } + + public void doSomething() { + String value = commands.get("Walter"); + } + } + ``` + +The sections that follow explain each step in detail. + +## Defining command methods + +As a first step, you define a specific command interface. The interface +must extend `Commands`. + +Command methods are declared inside the commands interface like regular +methods (probably not that much of a surprise). Lettuce derives commands +(name, arguments, and response) from each declared method. + +### Command naming + +The commands proxy has two ways to derive a Redis command from the +method name. It can derive the command name from the method name +directly, or by using a manually defined `@Command` annotation. However, +there’s got to be a strategy that decides what actual command is +created. Let’s have a look at the available options. + +``` java +public interface MixedCommands extends Commands { + + List mget(String... keys); + + @Command("MGET") + List mgetAsValues(String... keys); + + @CommandNaming(strategy = DOT) + double nrRun(String key, int... indexes) +} +``` + +- Plain command method. Lettuce will derive to the `MGET` command. + +- Command method annotated with `@Command`. Lettuce will execute `MGET` + since annotations have a higher precedence than method-based name + derivation. + +- Redis commands consist of one or multiple command parts or follow a + different naming strategy. The recommended pattern for commands + provided by modules is using dot notation. Command methods can derive + from "camel humps" that style by placing a dot (`.`) between name + parts. + +!!! NOTE + Command names are attempted to be resolved against `CommandType` to + participate in settings for known commands. These are primarily used + to determine a command intent (whether a command is a read-only one). + Commands are resolved case-sensitive. Use lower-case command names in + `@Command` to resolve to an unknown command to e.g. enforce + master-routing. + +### CamelCase in method names + +Command methods use by default the method name command type. This is +ideal for commands like `GET`, `SET`, `ZADD` and so on. Some commands, +such as `CLIENT SETNAME` consist of multiple command segments and +passing `SETNAME` as argument to a method `client(…)` feels rather +clunky. + +Camel case is a natural way to express word boundaries in method names. +These "camel humps" (changes in letter casing) can be interpreted in +different ways. The most common case is to translate a change in case +into a space between command segments. + +``` java +interface ServerCommands extends Commands { + String clientSetname(String name); +} +``` + +Invoking `clientSetname(…)` will execute the Redis command +`CLIENT SETNAME name`. + +#### `@CommandNaming` + +Camel humps are translated to whitespace-delimited command segments by +default. Methods and the commands interface can be annotated with +`@CommandNaming` to apply a different strategy. + +``` java +@CommandNaming(strategy = Strategy.DOT) +interface MixedCommands extends Commands { + + @CommandNaming(strategy = Strategy.SPLIT) + String clientSetname(String name); + + @CommandNaming(strategy = Strategy.METHOD_NAME) + String mSet(String key1, String value1, String key2, String value2); + + double nrRun(String key, int... indexes) +} +``` + +You can choose among multiple strategies: + +- `SPLIT`: Splits camel-case method names into multiple command + segments: `clientSetname` executes `CLIENT SETNAME`. This is the + default strategy. + +- `METHOD_NAME`: Uses the method name as-is: `mSet` executes `MSET`. + +- `DOT`: Translates camel-case method names into dot-notation that is + the recommended pattern for module-provided commands. `nrRun` executes + `NR.RUN`. + +### `@Command` annotation + +You already learned, that method names are used as command type any by +default all arguments are appended to the command. Some cases, such as +the example from above, require in Java declaring a method with a +different name because of variance in the return type. `mgetAsValues` +would execute a non-existent command `MGETASVALUES`. + +Annotating command methods with `@Command` lets you take control over +implicit conventions. The annotation value overrides the command name +and provides command segments to command methods. Command segments are +parts of a command that are sent to Redis. The semantics of a command +segment depend on context and the command itself. +`@Command("CLIENT SETNAME")` denotes a subcommand of the `CLIENT` +command while a method annotated with `@Command("SET key")` invokes +`SET`, using `mykey` as key. `@Command` lets you specify whole command +strings and reference [parameters](#parameters) to construct custom +commands. + +``` java +interface MixedCommands extends Commands { + + @Command("CLIENT SETNAME") + String setName(String name); + + @Command("MGET") + List mgetAsValues(String... keys); + + @Command("SET mykey") + String set(String value); + + @Command("NR.OBSERVE ?0 ?1 -> ?2 TRAIN") + List nrObserve(String key, int[] in, int... out) +} +``` + +### Parameters + +Most Redis commands take one or more parameters to operate with your +data. Using command methods with Redis appends all parameters in their +specified order to the command as arguments. You have already seen +commands annotated with `@Command("MGET")` or with no annotation at all. +Commands append their parameters as command arguments as declared in the +method signature. + +``` java +interface MixedCommands extends Commands { + + @Command("SET ?1 ?0") + String set(String value, String key); + + @Command("NR.OBSERVE :key :in -> :out TRAIN") + List nrObserve(@Param("key") String key, @Param("in") int[] in, @Param("out") int... out) +} +``` + +`@Command`-annotated command methods allow references to parameters. You +can use index-based or name-based parameter references. Index-based +references (`?0`, `?1`, …) are zero-based. Name-based parameters +(`:key`, `:in`) reference parameters by their name. Java 8 provides +access to parameter names if the code was compiled with +`javac -parameters`. Parameter names can be supplied alternatively by +`@Param`. Please note that all parameters are required to be annotated +if using `@Param`. + +!!! NOTE + The same parameter can be referenced multiple times. Not referenced + parameters are appended as arguments after the last command segment. + +#### Keys and values + +Redis commands are usually less concerned about key and value type since +all data is bytes anyway. In the context of Redis Cluster, the very +first key affects command routing. Keys and values are discovered by +verifying their declared type assignability to `RedisCodec` key and +value types. In some cases, where keys and values are indistinguishable +from their types, it might be required to hint command methods about +keys and values. You can annotate key and value parameters with `@Key` +and `@Value` to control which parameters should be treated as keys or +values. + +``` java +interface KeyCommands extends Commands { + + String set(@Key String key, @Value String value); +} +``` + +Hinting command method parameters influences +[`RedisCodec`](#codecs) selection. + +#### Parameter types + +Command method parameter types are just limited by the +[`RedisCodec`s](#codecs) that are supplied to +`RedisCommandFactory`. Command methods, however, support a basic set of +parameter types that are agnostic to the selected codec. If a parameter +is identified as key or value and the codec supports that parameter, +this specific parameter is encoded by applying codec conversion. + +Built-in parameter types: + +- `String` - encoded to bytes using `ASCII`. + +- `byte[]` + +- `double`/`Double` + +- `ProtocolKeyword` - using its byte-representation. `ProtocolKeyword` + is useful to declare/reuse commonly used Redis keywords, see + `io.lettuce.core.protocol.CommandType` and + `io.lettuce.core.protocol.CommandKeyword`. + +- `Map` - key and value encoding of key-value pairs using `RedisCodec`. + +- types implementing `io.lettuce.core.CompositeParameter` - Lettuce + comes with a set of command argument types such as `BitFieldArgs`, + `SetArgs`, `SortArgs`, … that can be used as parameter. Providing + `CompositeParameter` will contribute multiple command arguments by + invoking the `CompositeParameter.build(CommandArgs)` method. + +- `Value`, `KeyValue`, and `ScoredValue` that are encoded to their + value, key and value and score and value representation using + `RedisCodec`. + +- `GeoCoordinates` - contribute longitude and latitude command arguments + +- `Limit` - used together with `ZRANGEBYLEX`/`ZRANGEBYSCORE` commands. + Will add `LIMIT (offset) (count)` segments to the command. + +- `Range` - used together with `ZCOUNT`/`ZRANGEBYLEX`/`ZRANGEBYSCORE` + commands. Numerical commands are converted to numerical boundaries + (`` inf`, `(1.0`, `[1.0`). Value-typed `Range` parameters are encoded to their value boundary representation (` ``, + `-`, `[value`, `(value`). + +Command methods accept other, special parameter types such as `Timeout` +or `FlushMode` that control [execution-model +specific](#execution-models) behavior. Those parameters are filtered +from command arguments. + +### Codecs + +Redis command interfaces use `RedisCodec`s for key/value encoding and +decoding. Each command method performs `RedisCodec` resolution so each +command method can use a different `RedisCodec`. Codec resolution is +based on key and value types declared in the command method signature. +Key and value parameters can be annotated with `@Key`/`@Value` +annotations to hint codec resolution to the appropriate types. Codec +resolution checks all annotated parameters for compatibility. If types +are assignable to codec types, the codec is selected for a particular +command method. + +Codec resolution without annotation is based on a compatible type +majority. A command method resolves to the codec accepting the most +compatible types. See also [Keys and values](#keys-and-values) for +details on key/value encoding. Depending on provided codecs and the +command method signature it’s possible that no codec can be resolved. +You need to provide either a compatible `RedisCodec` or adjust parameter +types in the method signature to provide a compatible method signature. +`RedisCommandFactory` uses `StringCodec` (UTF-8) and `ByteArrayCodec` by +default. + +``` java +RedisCommandFactory factory = new RedisCommandFactory(connection, Arrays.asList(new ByteArrayCodec(), new StringCodec(LettuceCharsets.UTF8))); +``` + +The resolved codec is also applied to command response deserialization +that allows you to use parametrized command response types. + +### Response types + +Another aspect of command methods is their response type. Redis command +responses consist of simple strings, bulk strings (byte streams) or +arrays with nested elements depending on the issued command. + +You can choose among various return types that map to a particular +{custom-commands-command-output-link}. A command output can return +either its return type directly (`List` for `StringListOutput`) +or stream individual elements (`String` for `StringListOutput` as it +implements `StreamingOutput`). Command output resolution depends +on whether the declared return type supports streaming. The currently +only supported streaming output are reactive wrappers such as `Flux`. + +`RedisCommandFactory` comes with built-in command outputs that are +resolved from `OutputRegistry`. You can choose from built-in command +output types or register your own `CommandOutput`. + +A command method can return its response directly or wrapped in a +response wrapper. See [Execution models](#execution-models) for +execution-specific wrapper types. + +| `CommandOutput` class | return type | streaming type | +|----|----|----| +| `ListOfMapsOutput` | `List>` | | +| `ArrayOutput` | `List` | | +| `DoubleOutput` | `Double`, `double` | | +| `ByteArrayOutput` | `byte[]` | | +| `IntegerOutput` | `Long`, `long` | | +| `KeyOutput` | `K` (Codec key type) | | +| `KeyListOutput` | `List` (Codec key type) | `K` (Codec key type) | +| `ValueOutput` | `V` (Codec value type) | | +| `ValueListOutput` | `List` (Codec value type) | `V` (Codec value type) | +| `ValueSetOutput` | `Set` (Codec value type) | | +| `MapOutput` | `Map` | | +| `BooleanOutput` | `Boolean`, `boolean` | | +| `BooleanListOutput` | `List` | `Boolean` | +| `GeoCo ordinatesListOutput` | `GeoCoordinates` | | +| `GeoCoordin atesValueListOutput` | `List>` | `V alue` | +| `Sc oredValueListOutput` | `L ist>` | `ScoredValue` | +| `St ringValueListOutput` (ASCII) | `List>` | `Value` | +| `StringListOutput` (ASCII) | `List` | `String` | +| `V alueValueListOutput` | `List>` | `Value` | +| `VoidOutput` | `Void`, `void` | | + +Built-in command output types + +## Execution models + +Each declared command methods requires a synchronization mode, more +specific an execution model. Lettuce uses an event-driven command +execution model to send commands, process responses, and signal +completion. Command methods can execute their commands in a synchronous, +[asynchronous](user-guide/async-api.md) or [reactive](user-guide/reactive-api.md) way. + +The choice of a particular execution model is made on return type level, +more specific on the return type wrapper. Each command method may use a +different execution model so command methods within a command interface +may mix different execution models. + +### Synchronous (Blocking) Execution + +Declaring a non-wrapped return type (like `List`, `String`) will +execute commands synchronously. See +{custom-commands-command-exec-model-link} on more details on synchronous +command execution. + +Blocking command execution applies by default timeouts set on connection +level. Command methods support timeouts on invocation level by defining +a special `Timeout` parameter. The parameter position does not affect +command segments since special parameters are filtered from the command +arguments. Supplying `null` will apply connection defaults. + +``` java +interface KeyCommands extends Commands { + + String get(String key, Timeout timeout); +} + +KeyCommands commands = … + +commands.get("key", Timeout.create(10, TimeUnit.SECONDS)); +``` + +### Asynchronous (Future) Execution + +Command methods wrapping their response in `Future`, +`CompletableFuture`, `CompletionStage` or `RedisFuture` will execute +their commands asynchronously. Invoking an asynchronous command method +will send the command to Redis at invocation time and return a return +handle that allows you to synchronize or chain command execution. + +``` java +interface KeyCommands extends Commands { + + RedisFuture get(String key, Timeout timeout); +} +``` + +### Reactive Execution + +You can declare command methods that wrap their response in a reactive +type for reactive command execution. Invoking a reactive command method +will not send the command to Redis until the resulting subscriber +signals demand for data to its subscription. Using reactive wrapper +types allow [result streaming](#response-types) by emitting data as it’s +received from the I/O channel. + +Currently supported reactive types: + +- Project Reactor `Mono` and `Flux` (native) + +- RxJava 1 `Single` and `Observable` (via `rxjava-reactive-streams`) + +- RxJava 2 `Single`, `Maybe` and `Flowable` (via `rxjava` 2.0) + +See [Reactive API](user-guide/reactive-api.md) for more details. + +``` java +interface KeyCommands extends Commands { + + @Command("GET") + Mono get(String key); + + @Command("GET") + Maybe getRxJava2Maybe(String key); + + Flowable lrange(String key, long start, long stop); +} +``` + +### Batch Execution + +Command interfaces support command batching to collect multiple commands +in a batch queue and flush the batch in a single write to the transport. +Command batching executes commands in a deferred nature. This means that +at the time of invocation no result is available. Batching can be only +used with synchronous methods without a return value (`void`) or +asynchronous methods returning a `RedisFuture`. Reactive command +batching is not supported because reactive executed commands maintain an +own subscription lifecycle that is decoupled from command method +batching. + +Command batching can be enabled on two levels: + +- On class level by annotating the command interface with `@BatchSize`. + All methods participate in command batching. + +- On method level by adding `CommandBatching` to the arguments. Method + participates selectively in command batching. + +``` java +@BatchSize(50) +interface StringCommands extends Commands { + + void set(String key, String value); + + RedisFuture get(String key); + + RedisFuture get(String key, CommandBatching batching); +} + +StringCommands commands = … + +commands.set("key", "value"); // queued until 50 command invocations reached. + // The 50th invocation flushes the queue. + +commands.get("key", CommandBatching.queue()); // invocation-level queueing control +commands.get("key", CommandBatching.flush()); // invocation-level queueing control, + // flushes all queued commands +``` + +Batching can be controlled on per invocation by passing a +`CommandBatching` argument. `CommandBatching` has precedence over +`@BatchSize`. + +To flush queued commands at any time (without further command +invocation), add `BatchExecutor` to your interface definition. + +``` java +@BatchSize(50) +interface StringCommands extends Commands, BatchExecutor { + + RedisFuture get(String key); +} + +StringCommands commands = … + +commands.set("key"); + +commands.flush() // force-flush +``` + +#### Batch execution synchronization + +Queued command batches are flushed either on reaching the batch size or +force flush (via `BatchExecutor.flush()` or `CommandBatching.flush()`). +Errors are transported through `RedisFuture`. Synchronous commands don’t +receive any result/exception signal except if the batch is flushed +through a synchronous method call. Synchronous flushing throws +`BatchException` containing the failed commands. + diff --git a/docs/static/logo-redis.svg b/docs/static/logo-redis.svg new file mode 100644 index 0000000000..a8de68d23c --- /dev/null +++ b/docs/static/logo-redis.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/docs/user-guide/async-api.md b/docs/user-guide/async-api.md new file mode 100644 index 0000000000..c11c350d44 --- /dev/null +++ b/docs/user-guide/async-api.md @@ -0,0 +1,571 @@ +## Asynchronous API + +This guide will give you an impression how and when to use the +asynchronous API provided by Lettuce 4.x. + +### Motivation + +Asynchronous methodologies allow you to utilize better system resources, +instead of wasting threads waiting for network or disk I/O. Threads can +be fully utilized to perform other work instead. Lettuce facilitates +asynchronicity from building the client on top of +[netty](http://netty.io) that is a multithreaded, event-driven I/O +framework. All communication is handled asynchronously. Once the +foundation is able to processes commands concurrently, it is convenient +to take advantage from the asynchronicity. It is way harder to turn a +blocking and synchronous working software into a concurrently processing +system. + +#### Understanding Asynchronicity + +Asynchronicity permits other processing to continue before the +transmission has finished and the response of the transmission is +processed. This means, in the context of Lettuce and especially Redis, +that multiple commands can be issued serially without the need of +waiting to finish the preceding command. This mode of operation is also +known as [Pipelining](https://redis.io/docs/latest/develop/use/pipelining/). The following +example should give you an impression of the mode of operation: + +- Given client *A* and client *B* + +- Client *A* triggers command `SET A=B` + +- Client *B* triggers at the same time of Client *A* command `SET C=D` + +- Redis receives command from Client *A* + +- Redis receives command from Client *B* + +- Redis processes `SET A=B` and responds `OK` to Client *A* + +- Client *A* receives the response and stores the response in the + response handle + +- Redis processes `SET C=D` and responds `OK` to Client *B* + +- Client *B* receives the response and stores the response in the + response handle + +Both clients from the example above can be either two threads or +connections within an application or two physically separated clients. + +Clients can operate concurrently to each other by either being separate +processes, threads, event-loops, actors, fibers, etc. Redis processes +incoming commands serially and operates mostly single-threaded. This +means, commands are processed in the order they are received with some +characteristic that we’ll cover later. + +Let’s take the simplified example and enhance it by some program flow +details: + +- Given client *A* + +- Client *A* triggers command `SET A=B` + +- Client *A* uses the asynchronous API and can perform other processing + +- Redis receives command from Client *A* + +- Redis processes `SET A=B` and responds `OK` to Client *A* + +- Client *A* receives the response and stores the response in the + response handle + +- Client *A* can access now the response to its command without waiting + (non-blocking) + +The Client *A* takes advantage from not waiting on the result of the +command so it can process computational work or issue another Redis +command. The client can work with the command result as soon as the +response is available. + +#### Impact of asynchronicity to the synchronous API + +While this guide helps you to understand the asynchronous API it is +worthwhile to learn the impact on the synchronous API. The general +approach of the synchronous API is no different than the asynchronous +API. In both cases, the same facilities are used to invoke and transport +commands to the Redis server. The only difference is a blocking behavior +of the caller that is using the synchronous API. Blocking happens on +command level and affects only the command completion part, meaning +multiple clients using the synchronous API can invoke commands on the +same connection and at the same time without blocking each other. A call +on the synchronous API is unblocked at the moment a command response was +processed. + +- Given client *A* and client *B* + +- Client *A* triggers command `SET A=B` on the synchronous API and waits + for the result + +- Client *B* triggers at the same time of Client *A* command `SET C=D` + on the synchronous API and waits for the result + +- Redis receives command from Client *A* + +- Redis receives command from Client *B* + +- Redis processes `SET A=B` and responds `OK` to Client *A* + +- Client *A* receives the response and unblocks the program flow of + Client *A* + +- Redis processes `SET C=D` and responds `OK` to Client *B* + +- Client *B* receives the response and unblocks the program flow of + Client *B* + +However, there are some cases you should not share a connection among +threads to avoid side-effects. The cases are: + +- Disabling flush-after-command to improve performance + +- The use of blocking operations like `BLPOP`. Blocking operations are + queued on Redis until they can be executed. While one connection is + blocked, other connections can issue commands to Redis. Once a command + unblocks the blocking command (that said an `LPUSH` or `RPUSH` hits + the list), the blocked connection is unblocked and can proceed after + that. + +- Transactions + +- Using multiple databases + +#### Result handles + +Every command invocation on the asynchronous API creates a +`RedisFuture` that can be canceled, awaited and subscribed +(listener). A `CompleteableFuture` or `RedisFuture` is a pointer +to the result that is initially unknown since the computation of its +value is yet incomplete. A `RedisFuture` provides operations for +synchronization and chaining. + +``` java +CompletableFuture future = new CompletableFuture<>(); + +System.out.println("Current state: " + future.isDone()); + +future.complete("my value"); + +System.out.println("Current state: " + future.isDone()); +System.out.println("Got value: " + future.get()); +``` + +The example prints the following lines: + + Current state: false + Current state: true + Got value: my value + +Attaching a listener to a future allows chaining. Promises can be used +synonymous to futures, but not every future is a promise. A promise +guarantees a callback/notification and thus it has come to its name. + +A simple listener that gets called once the future completes: + +``` java +final CompletableFuture future = new CompletableFuture<>(); + +future.thenRun(new Runnable() { + @Override + public void run() { + try { + System.out.println("Got value: " + future.get()); + } catch (Exception e) { + e.printStackTrace(); + } + + } +}); + +System.out.println("Current state: " + future.isDone()); +future.complete("my value"); +System.out.println("Current state: " + future.isDone()); +``` + +The value processing moves from the caller into a listener that is then +called by whoever completes the future. The example prints the following +lines: + + Current state: false + Got value: my value + Current state: true + +The code from above requires exception handling since calls to the +`get()` method can lead to exceptions. Exceptions raised during the +computation of the `Future` are transported within an +`ExecutionException`. Another exception that may be thrown is the +`InterruptedException`. This is because calls to `get()` are blocking +calls and the blocked thread can be interrupted at any time. Just think +about a system shutdown. + +The `CompletionStage` type allows since Java 8 a much more +sophisticated handling of futures. A `CompletionStage` can consume, +transform and build a chain of value processing. The code from above can +be rewritten in Java 8 in the following style: + +``` java +CompletableFuture future = new CompletableFuture<>(); + +future.thenAccept(new Consumer() { + @Override + public void accept(String value) { + System.out.println("Got value: " + value); + } +}); + +System.out.println("Current state: " + future.isDone()); +future.complete("my value"); +System.out.println("Current state: " + future.isDone()); +``` + +The example prints the following lines: + + Current state: false + Got value: my value + Current state: true + +You can find the full reference for the `CompletionStage` type in the +[Java 8 API +documentation](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CompletionStage.html). + +### Creating futures using Lettuce + +Lettuce futures can be used for initial and chaining operations. When +using Lettuce futures, you will notice the non-blocking behavior. This +is because all I/O and command processing are handled asynchronously +using the netty EventLoop. The Lettuce `RedisFuture` extends a +`CompletionStage` so all methods of the base type are available. + +Lettuce exposes its futures on the Standalone, Sentinel, +Publish/Subscribe and Cluster APIs. + +Connecting to Redis is insanely simple: + +``` java +RedisClient client = RedisClient.create("redis://localhost"); +RedisAsyncCommands commands = client.connect().async(); +``` + +In the next step, obtaining a value from a key requires the `GET` +operation: + +``` java +RedisFuture future = commands.get("key"); +``` + +### Consuming futures + +The first thing you want to do when working with futures is to consume +them. Consuming a futures means obtaining the value. Here is an example +that blocks the calling thread and prints the value: + +``` java +RedisFuture future = commands.get("key"); +String value = future.get(); +System.out.println(value); +``` + +Invocations to the `get()` method (pull-style) block the calling thread +at least until the value is computed but in the worst case indefinitely. +Using timeouts is always a good idea to not exhaust your threads. + +``` java +try { + RedisFuture future = commands.get("key"); + String value = future.get(1, TimeUnit.MINUTES); + System.out.println(value); +} catch (Exception e) { + e.printStackTrace(); +} +``` + +The example will wait at most 1 minute for the future to complete. If +the timeout exceeds, a `TimeoutException` is thrown to signal the +timeout. + +Futures can also be consumed in a push style, meaning when the +`RedisFuture` is completed, a follow-up action is triggered: + +``` java +RedisFuture future = commands.get("key"); + +future.thenAccept(new Consumer() { + @Override + public void accept(String value) { + System.out.println(value); + } +}); +``` + +Alternatively, written in Java 8 lambdas: + +``` java +RedisFuture future = commands.get("key"); + +future.thenAccept(System.out::println); +``` + +Lettuce futures are completed on the netty EventLoop. Consuming and +chaining futures on the default thread is always a good idea except for +one case: Blocking/long-running operations. As a rule of thumb, never +block the event loop. If you need to chain futures using blocking calls, +use the `thenAcceptAsync()`/`thenRunAsync()` methods to fork the +processing to another thread. The `…​async()` methods need a threading +infrastructure for execution, by default the `ForkJoinPool.commonPool()` +is used. The `ForkJoinPool` is statically constructed and does not grow +with increasing load. Using default `Executor`s is almost always the +better idea. + +``` java +Executor sharedExecutor = ... +RedisFuture future = commands.get("key"); + +future.thenAcceptAsync(new Consumer() { + @Override + public void accept(String value) { + System.out.println(value); + } +}, sharedExecutor); +``` + +### Synchronizing futures + +A key point when using futures is the synchronization. Futures are +usually used to: + +1. Trigger multiple invocations without the urge to wait for the + predecessors (Batching) + +2. Invoking a command without awaiting the result at all (Fire&Forget) + +3. Invoking a command and perform other computing in the meantime + (Decoupling) + +4. Adding concurrency to certain computational efforts (Concurrency) + +There are several ways how to wait or get notified in case a future +completes. Certain synchronization techniques apply to some motivations +why you want to use futures. + +#### Blocking synchronization + +Blocking synchronization comes handy if you perform batching/add +concurrency to certain parts of your system. An example to batching can +be setting/retrieving multiple values and awaiting the results before a +certain point within processing. + +``` java +List> futures = new ArrayList>(); + +for (int i = 0; i < 10; i++) { + futures.add(commands.set("key-" + i, "value-" + i)); +} + +LettuceFutures.awaitAll(1, TimeUnit.MINUTES, futures.toArray(new RedisFuture[futures.size()])); +``` + +The code from above does not wait until a certain command completes +before it issues another one. The synchronization is done after all +commands are issued. The example code can easily be turned into a +Fire&Forget pattern by omitting the call to `LettuceFutures.awaitAll()`. + +A single future execution can be also awaited, meaning an opt-in to wait +for a certain time but without raising an exception: + +``` java +RedisFuture future = commands.get("key"); + +if(!future.await(1, TimeUnit.MINUTES)) { + System.out.println("Could not complete within the timeout"); +} +``` + +Calling `await()` is friendlier to call since it throws only an +`InterruptedException` in case the blocked thread is interrupted. You +are already familiar with the `get()` method for synchronization, so we +will not bother you with this one. + +At last, there is another way to synchronize futures in a blocking way. +The major caveat is that you will become responsible to handle thread +interruptions. If you do not handle that aspect, you will not be able to +shut down your system properly if it is in a running state. + +``` java +RedisFuture future = commands.get("key"); +while (!future.isDone()) { + // do something ... +} +``` + +While the `isDone()` method does not aim primarily for synchronization +use, it might come handy to perform other computational efforts while +the command is executed. + +#### Chaining synchronization + +Futures can be synchronized/chained in a non-blocking style to improve +thread utilization. Chaining works very well in systems relying on +event-driven characteristics. Future chaining builds up a chain of one +or more futures that are executed serially, and every chain member +handles a part in the computation. The `CompletionStage` API offers +various methods to chain and transform futures. A simple transformation +of the value can be done using the `thenApply()` method: + +``` java +future.thenApply(new Function() { + @Override + public Integer apply(String value) { + return value.length(); + } +}).thenAccept(new Consumer() { + @Override + public void accept(Integer integer) { + System.out.println("Got value: " + integer); + } +}); +``` + +Alternatively, written in Java 8 lambdas: + +``` java +future.thenApply(String::length) + .thenAccept(integer -> System.out.println("Got value: " + integer)); +``` + +The `thenApply()` method accepts a function that transforms the value +into another one. The final `thenAccept()` method consumes the value for +final processing. + +You have already seen the `thenRun()` method from previous examples. The +`thenRun()` method can be used to handle future completions in case the +data is not crucial to your flow: + +``` java +future.thenRun(new Runnable() { + @Override + public void run() { + System.out.println("Finished the future."); + } +}); +``` + +Keep in mind to execute the `Runnable` on a custom `Executor` if you are +doing blocking calls within the `Runnable`. + +Another chaining method worth mentioning is the either-or chaining. A +couple of `…​Either()` methods are available on a `CompletionStage`, +see the [Java 8 API docs](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CompletionStage.html) +for the full reference. The either-or pattern consumes the value from +the first future that is completed. A good example might be two services +returning the same data, for instance, a Master-Replica scenario, but +you want to return the data as fast as possible: + +``` java +RedisStringAsyncCommands master = masterClient.connect().async(); +RedisStringAsyncCommands replica = replicaClient.connect().async(); + +RedisFuture future = master.get("key"); +future.acceptEither(replica.get("key"), new Consumer() { + @Override + public void accept(String value) { + System.out.println("Got value: " + value); + } +}); +``` + +### Error handling + +Error handling is an indispensable component of every real world +application and should to be considered from the beginning on. Futures +provide some mechanisms to deal with errors. + +In general, you want to react in the following ways: + +- Return a default value instead + +- Use a backup future + +- Retry the future + +`RedisFuture`s transport exceptions if any occurred. Calls to the +`get()` method throw the occurred exception wrapped within an +`ExecutionException` (this is different to Lettuce 3.x). You can find +more details within the Javadoc on +[CompletionStage](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CompletionStage.html). + +The following code falls back to a default value after it runs to an +exception by using the `handle()` method: + +``` java +future.handle(new BiFunction() { + @Override + public Integer apply(String value, Throwable throwable) { + if(throwable != null) { + return "default value"; + } + return value; + } +}).thenAccept(new Consumer() { + @Override + public void accept(String value) { + System.out.println("Got value: " + value); + } +}); +``` + +More sophisticated code could decide on behalf of the throwable type +that value to return, as the shortcut example using the +`exceptionally()` method: + +``` java +future.exceptionally(new Function() { + @Override + public String apply(Throwable throwable) { + if (throwable instanceof IllegalStateException) { + return "default value"; + } + + return "other default value"; + } +}); +``` + +Retrying futures and recovery using futures is not part of the Java 8 +`CompleteableFuture`. See the [Reactive API](reactive-api.md) for +comfortable ways handling with exceptions. + +### Examples + +``` java +RedisAsyncCommands async = client.connect().async(); +RedisFuture set = async.set("key", "value"); +RedisFuture get = async.get("key"); + +set.get() == "OK" +get.get() == "value" +``` + +``` java +RedisAsyncCommands async = client.connect().async(); +RedisFuture set = async.set("key", "value"); +RedisFuture get = async.get("key"); + +set.await(1, SECONDS) == true +set.get() == "OK" +get.get(1, TimeUnit.MINUTES) == "value" +``` + +``` java +RedisStringAsyncCommands async = client.connect().async(); +RedisFuture set = async.set("key", "value"); + +Runnable listener = new Runnable() { + @Override + public void run() { + ...; + } +}; + +set.thenRun(listener); +``` \ No newline at end of file diff --git a/docs/user-guide/connecting-redis.md b/docs/user-guide/connecting-redis.md new file mode 100644 index 0000000000..dac3102b88 --- /dev/null +++ b/docs/user-guide/connecting-redis.md @@ -0,0 +1,239 @@ +# Connecting Redis + +Connections to a Redis Standalone, Sentinel, or Cluster require a +specification of the connection details. The unified form is `RedisURI`. +You can provide the database, password and timeouts within the +`RedisURI`. You have following possibilities to create a `RedisURI`: + +1. Use an URI: + + ``` java + RedisURI.create("redis://localhost/"); + ``` + +2. Use the Builder + + ``` java + RedisURI.Builder.redis("localhost", 6379).auth("password").database(1).build(); + ``` + +3. Set directly the values in `RedisURI` + + ``` java + new RedisURI("localhost", 6379, 60, TimeUnit.SECONDS); + ``` + +## URI syntax + +**Redis Standalone** + + redis :// [[username :] password@] host [:port][/database] + [?[timeout=timeout[d|h|m|s|ms|us|ns]] [&clientName=clientName] + [&libraryName=libraryName] [&libraryVersion=libraryVersion] ] + +**Redis Standalone (SSL)** + + rediss :// [[username :] password@] host [: port][/database] + [?[timeout=timeout[d|h|m|s|ms|us|ns]] [&clientName=clientName] + [&libraryName=libraryName] [&libraryVersion=libraryVersion] ] + +**Redis Standalone (Unix Domain Sockets)** + + redis-socket :// [[username :] password@]path + [?[timeout=timeout[d|h|m|s|ms|us|ns]] [&database=database] + [&clientName=clientName] [&libraryName=libraryName] + [&libraryVersion=libraryVersion] ] + +**Redis Sentinel** + + redis-sentinel :// [[username :] password@] host1[:port1] [, host2[:port2]] [, hostN[:portN]] [/database] + [?[timeout=timeout[d|h|m|s|ms|us|ns]] [&sentinelMasterId=sentinelMasterId] + [&clientName=clientName] [&libraryName=libraryName] + [&libraryVersion=libraryVersion] ] + +**Schemes** + +- `redis` Redis Standalone + +- `rediss` Redis Standalone SSL + +- `redis-socket` Redis Standalone Unix Domain Socket + +- `redis-sentinel` Redis Sentinel + +**Timeout units** + +- `d` Days + +- `h` Hours + +- `m` Minutes + +- `s` Seconds + +- `ms` Milliseconds + +- `us` Microseconds + +- `ns` Nanoseconds + +Hint: The database parameter within the query part has higher precedence +than the database in the path. + +RedisURI supports Redis Standalone, Redis Sentinel and Redis Cluster +with plain, SSL, TLS and unix domain socket connections. + +Hint: The database parameter within the query part has higher precedence +than the database in the path. RedisURI supports Redis Standalone, Redis +Sentinel and Redis Cluster with plain, SSL, TLS and unix domain socket +connections. + +## Authentication + +Redis URIs may contain authentication details that effectively lead to +usernames with passwords, password-only, or no authentication. +Connections are authenticated by using the information provided through +`RedisCredentials`. Credentials are obtained at connection time from +`RedisCredentialsProvider`. When configuring username/password on the +URI statically, then a `StaticCredentialsProvider` holds the configured +information. + +**Notes** + +- When using Redis Sentinel, the password from the URI applies to the + data nodes only. Sentinel authentication must be configured for each + sentinel node. + +- Usernames are supported as of Redis 6. + +- Library name and library version are automatically set on Redis 7.2 or + greater. + +## Basic Usage + +``` java +RedisClient client = RedisClient.create("redis://localhost"); + +StatefulRedisConnection connection = client.connect(); + +RedisCommands commands = connection.sync(); + +String value = commands.get("foo"); + +... + +connection.close(); + +client.shutdown(); +``` + +- Create the `RedisClient` instance and provide a Redis URI pointing to + localhost, Port 6379 (default port). + +- Open a Redis Standalone connection. The endpoint is used from the + initialized `RedisClient` + +- Obtain the command API for synchronous execution. Lettuce supports + asynchronous and reactive execution models, too. + +- Issue a `GET` command to get the key `foo`. + +- Close the connection when you’re done. This happens usually at the + very end of your application. Connections are designed to be + long-lived. + +- Shut down the client instance to free threads and resources. This + happens usually at the very end of your application. + +Each Redis command is implemented by one or more methods with names +identical to the lowercase Redis command name. Complex commands with +multiple modifiers that change the result type include the CamelCased +modifier as part of the command name, e.g. `zrangebyscore` and +`zrangebyscoreWithScores`. + +Redis connections are designed to be long-lived and thread-safe, and if +the connection is lost will reconnect until `close()` is called. Pending +commands that have not timed out will be (re)sent after successful +reconnection. + +All connections inherit a default timeout from their RedisClient and +and will throw a `RedisException` when non-blocking commands fail to +return a result before the timeout expires. The timeout defaults to 60 +seconds and may be changed in the RedisClient or for each connection. +Synchronous methods will throw a `RedisCommandExecutionException` in +case Redis responds with an error. Asynchronous connections do not throw +exceptions when Redis responds with an error. + +### RedisURI + +The RedisURI contains the host/port and can carry +authentication/database details. On a successful connect you get +authenticated, and the database is selected afterward. This applies +also after re-establishing a connection after a connection loss. + +A Redis URI can also be created from an URI string. Supported formats +are: + +- `redis://[password@]host[:port][/databaseNumber]` Plaintext Redis + connection + +- `rediss://[password@]host[:port][/databaseNumber]` [SSL + Connections](../advanced-usage.md#ssl-connections) Redis connection + +- `redis-sentinel://[password@]host[:port][,host2[:port2]][/databaseNumber]#sentinelMasterId` + for using Redis Sentinel + +- `redis-socket:///path/to/socket` [Unix Domain + Sockets](../advanced-usage.md#unix-domain-sockets) connection to Redis + +### Exceptions + +In the case of an exception/error response from Redis, you’ll receive a +`RedisException` containing +the error message. `RedisException` is a `RuntimeException`. + +### Examples + +``` java +RedisClient client = RedisClient.create(RedisURI.create("localhost", 6379)); +client.setDefaultTimeout(20, TimeUnit.SECONDS); + +// … + +client.shutdown(); +``` + +``` java +RedisURI redisUri = RedisURI.Builder.redis("localhost") + .withPassword("authentication") + .withDatabase(2) + .build(); +RedisClient client = RedisClient.create(redisUri); + +// … + +client.shutdown(); +``` + +``` java +RedisURI redisUri = RedisURI.Builder.redis("localhost") + .withSsl(true) + .withPassword("authentication") + .withDatabase(2) + .build(); +RedisClient client = RedisClient.create(redisUri); + +// … + +client.shutdown(); +``` + +``` java +RedisURI redisUri = RedisURI.create("redis://authentication@localhost/2"); +RedisClient client = RedisClient.create(redisUri); + +// … + +client.shutdown(); +``` + diff --git a/docs/user-guide/kotlin-api.md b/docs/user-guide/kotlin-api.md new file mode 100644 index 0000000000..cb39d85c49 --- /dev/null +++ b/docs/user-guide/kotlin-api.md @@ -0,0 +1,90 @@ +## Kotlin API + +Kotlin Coroutines are using Kotlin lightweight threads allowing to write +non-blocking code in an imperative way. On language side, suspending +functions provides an abstraction for asynchronous operations while on +library side kotlinx.coroutines provides functions like `async { }` and +types like `Flow`. + +Lettuce ships with extensions to provide support for idiomatic Kotlin +use. + +### Dependencies + +Coroutines support is available when `kotlinx-coroutines-core` and +`kotlinx-coroutines-reactive` dependencies are on the classpath: + +``` xml + + org.jetbrains.kotlinx + kotlinx-coroutines-core + ${kotlinx-coroutines.version} + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactive + ${kotlinx-coroutines.version} + +``` + +### How does Reactive translate to Coroutines? + +`Flow` is an equivalent to `Flux` in Coroutines world, suitable for hot +or cold streams, finite or infinite streams, with the following main +differences: + +- `Flow` is push-based while `Flux` is a push-pull hybrid + +- Backpressure is implemented via suspending functions + +- `Flow` has only a single suspending collect method and operators are + implemented as extensions + +- Operators are easy to implement thanks to Coroutines + +- Extensions allow to add custom operators to Flow + +- Collect operations are suspending functions + +- `map` operator supports asynchronous operations (no need for + `flatMap`) since it takes a suspending function parameter + +### Coroutines API based on reactive operations + +Example for retrieving commands and using it: + +``` kotlin +val api: RedisCoroutinesCommands = connection.coroutines() + +val foo1 = api.set("foo", "bar") +val foo2 = api.keys("fo*") +``` + +!!! NOTE + Coroutine Extensions are experimental and require opt-in using + `@ExperimentalLettuceCoroutinesApi`. The API ships with a reduced + feature set. Deprecated methods and `StreamingChannel` are left out + intentionally. Expect evolution towards a `Flow`-based API to consume + large Redis responses. + +### Extensions for existing APIs + +#### Transactions DSL + +Example for the synchronous API: + +``` kotlin +val result: TransactionResult = connection.sync().multi { + set("foo", "bar") + get("foo") +} +``` + +Example for async with coroutines: + +``` kotlin +val result: TransactionResult = connection.async().multi { + set("foo", "bar") + get("foo") +} +``` \ No newline at end of file diff --git a/docs/user-guide/lua-scripting.md b/docs/user-guide/lua-scripting.md new file mode 100644 index 0000000000..b31970e500 --- /dev/null +++ b/docs/user-guide/lua-scripting.md @@ -0,0 +1,42 @@ +### Lua Scripting + +[Lua](https://redis.io/topics/lua-api) is a powerful scripting language +that is supported at the core of Redis. Lua scripts can be invoked +dynamically by providing the script contents to Redis or used as stored +procedure by loading the script into Redis and using its digest to +invoke it. + +
+ +``` java +String helloWorld = redis.eval("return ARGV[1]", STATUS, new String[0], "Hello World"); +``` + +
+ +Using Lua scripts is straightforward. Consuming results in Java requires +additional details to consume the result through a matching type. As we +do not know what your script will return, the API uses call-site +generics for you to specify the result type. Additionally, you must +provide a `ScriptOutputType` hint to `EVAL` so that the driver uses the +appropriate output parser. See [Output Formats](redis-functions.md#output-formats) for +further details. + +Lua scripts can be stored on the server for repeated execution. +Dynamically-generated scripts are an anti-pattern as each script is +stored in Redis' script cache. Generating scripts during the application +runtime may, and probably will, exhaust the host’s memory resources for +caching them. Instead, scripts should be as generic as possible and +provide customized execution via their arguments. You can register a +script through `SCRIPT LOAD` and use its SHA digest to invoke it later: + +
+ +``` java +String digest = redis.scriptLoad("return ARGV[1]", STATUS, new String[0], "Hello World"); + +// later +String helloWorld = redis.evalsha(digest, STATUS, new String[0], "Hello World"); +``` + +
\ No newline at end of file diff --git a/docs/user-guide/pubsub.md b/docs/user-guide/pubsub.md new file mode 100644 index 0000000000..96186f4103 --- /dev/null +++ b/docs/user-guide/pubsub.md @@ -0,0 +1,118 @@ +## Publish/Subscribe + +Lettuce provides support for Publish/Subscribe on Redis Standalone and +Redis Cluster connections. The connection is notified on +message/subscribed/unsubscribed events after subscribing to channels or +patterns. [Synchronous](connecting-redis.md#basic-usage), [asynchronous](async-api.md) +and [reactive](reactive-api.md) APIs are provided to interact with Redis +Publish/Subscribe features. + +### Subscribing + +A connection can notify multiple listeners that implement +`RedisPubSubListener` (Lettuce provides a `RedisPubSubAdapter` for +convenience). All listener registrations are kept within the +`StatefulRedisPubSubConnection`/`StatefulRedisClusterConnection`. + +``` java +StatefulRedisPubSubConnection connection = client.connectPubSub() +connection.addListener(new RedisPubSubListener() { ... }) + +RedisPubSubCommands sync = connection.sync(); +sync.subscribe("channel"); + +// application flow continues +``` + +!!! NOTE + Don’t issue blocking calls (includes synchronous API calls to Lettuce) + from inside of Pub/Sub callbacks as this would block the EventLoop. If + you need to fetch data from Redis from inside a callback, please use + the asynchronous API. + +``` java +StatefulRedisPubSubConnection connection = client.connectPubSub() +connection.addListener(new RedisPubSubListener() { ... }) + +RedisPubSubAsyncCommands async = connection.async(); +RedisFuture future = async.subscribe("channel"); + +// application flow continues +``` + +### Reactive API + +The reactive API provides hot `Observable`s to listen on +`ChannelMessage`s and `PatternMessage`s. The `Observable`s receive all +inbound messages. You can do filtering using the observable chain if you +need to filter out the interesting ones, The `Observable` stops +triggering events when the subscriber unsubscribes from it. + +``` java +StatefulRedisPubSubConnection connection = client.connectPubSub() + +RedisPubSubReactiveCommands reactive = connection.reactive(); +reactive.subscribe("channel").subscribe(); + +reactive.observeChannels().doOnNext(patternMessage -> {...}).subscribe() + +// application flow continues +``` + +### Redis Cluster + +Redis Cluster support Publish/Subscribe but requires some attention in +general. User-space Pub/Sub messages (Calling `PUBLISH`) are broadcasted +across the whole cluster regardless of subscriptions to particular +channels/patterns. This behavior allows connecting to an arbitrary +cluster node and registering a subscription. The client isn’t required +to connect to the node where messages were published. + +A cluster-aware Pub/Sub connection is provided by +`RedisClusterClient.connectPubSub()` allowing to listen for cluster +reconfiguration and reconnect if the topology changes. + +``` java +StatefulRedisClusterPubSubConnection connection = clusterClient.connectPubSub() +connection.addListener(new RedisPubSubListener() { ... }) + +RedisPubSubCommands sync = connection.sync(); +sync.subscribe("channel"); +``` + +Redis Cluster also makes a distinction between user-space and key-space +messages. Key-space notifications (Pub/Sub messages for key-activity) +stay node-local and are not broadcasted across the Redis Cluster. A +notification about, e.g. an expiring key, stays local to the node on +which the key expired. + +Clients that are interested in keyspace notifications must subscribe to +the appropriate node (or nodes) to receive these notifications. You can +either use `RedisClient.connectPubSub()` to establish Pub/Sub +connections to the individual nodes or use `RedisClusterClient`'s +message propagation and NodeSelection API to get a managed set of +connections. + +``` java +StatefulRedisClusterPubSubConnection connection = clusterClient.connectPubSub() +connection.addListener(new RedisClusterPubSubListener() { ... }) +connection.setNodeMessagePropagation(true); + +RedisPubSubCommands sync = connection.sync(); +sync.masters().commands().subscribe("__keyspace@0__:*"); +``` + +There are two things to pay special attention to: + +1. Replication: Keys replicated to replica nodes, especially + considering expiry, generate keyspace events on all nodes holding + the key. If a key expires and it is replicated, it will expire on + the master and all replicas. Each Redis server will emit keyspace + events. Subscribing to non-master nodes, therefore, will let your + application see multiple events of the same type for the same key + because of Redis distributed nature. + +2. Topology Changes: Subscriptions are issued either by using the + NodeSelection API or by calling `subscribe(…)` on the individual + cluster node connections. Subscription registrations are not + propagated to new nodes that are added on a topology change. \ No newline at end of file diff --git a/docs/user-guide/reactive-api.md b/docs/user-guide/reactive-api.md new file mode 100644 index 0000000000..3af432a258 --- /dev/null +++ b/docs/user-guide/reactive-api.md @@ -0,0 +1,792 @@ +## Reactive API + +This guide helps you to understand the Reactive Stream pattern and aims +to give you a general understanding of how to build reactive +applications. + +### Motivation + +Asynchronous and reactive methodologies allow you to utilize better +system resources, instead of wasting threads waiting for network or disk +I/O. Threads can be fully utilized to perform other work instead. + +A broad range of technologies exists to facilitate this style of +programming, ranging from the very limited and less usable +`java.util.concurrent.Future` to complete libraries and runtimes like +Akka. [Project Reactor](http://projectreactor.io/), has a very rich set +of operators to compose asynchronous workflows, it has no further +dependencies to other frameworks and supports the very mature Reactive +Streams model. + +### Understanding Reactive Streams + +Reactive Streams is an initiative to provide a standard for asynchronous +stream processing with non-blocking back pressure. This encompasses +efforts aimed at runtime environments (JVM and JavaScript) as well as +network protocols. + +The scope of Reactive Streams is to find a minimal set of interfaces, +methods, and protocols that will describe the necessary operations and +entities to achieve the goal—asynchronous streams of data with +non-blocking back pressure. + +It is an interoperability standard between multiple reactive composition +libraries that allow interaction without the need of bridging between +libraries in application code. + +The integration of Reactive Streams is usually accompanied with the use +of a composition library that hides the complexity of bare +`Publisher` and `Subscriber` types behind an easy-to-use API. +Lettuce uses [Project Reactor](http://projectreactor.io/) that exposes +its publishers as `Mono` and `Flux`. + +For more information about Reactive Streams see +. + +### Understanding Publishers + +Asynchronous processing decouples I/O or computation from the thread +that invoked the operation. A handle to the result is given back, +usually a `java.util.concurrent.Future` or similar, that returns either +a single object, a collection or an exception. Retrieving a result, that +was fetched asynchronously is usually not the end of processing one +flow. Once data is obtained, further requests can be issued, either +always or conditionally. With Java 8 or the Promise pattern, linear +chaining of futures can be set up so that subsequent asynchronous +requests are issued. Once conditional processing is needed, the +asynchronous flow has to be interrupted and synchronized. While this +approach is possible, it does not fully utilize the advantage of +asynchronous processing. + +In contrast to the preceding examples, `Publisher` objects answer the +multiplicity and asynchronous questions in a different fashion: By +inverting the `Pull` pattern into a `Push` pattern. + +**A Publisher is the asynchronous/push “dual” to the synchronous/pull +Iterable** + +| event | Iterable (pull) | Publisher (push) | +|----------------|------------------|--------------------| +| retrieve data | T next() | onNext(T) | +| discover error | throws Exception | onError(Exception) | +| complete | !hasNext() | onCompleted() | + +An `Publisher` supports emission sequences of values or even infinite +streams, not just the emission of single scalar values (as Futures do). +You will very much appreciate this fact once you start to work on +streams instead of single values. Project Reactor uses two types in its +vocabulary: `Mono` and `Flux` that are both publishers. + +A `Mono` can emit `0` to `1` events while a `Flux` can emit `0` to `N` +events. + +A `Publisher` is not biased toward some particular source of +concurrency or asynchronicity and how the underlying code is executed - +synchronous or asynchronous, running within a `ThreadPool`. As a +consumer of a `Publisher`, you leave the actual implementation to the +supplier, who can change it later on without you having to adapt your +code. + +The last key point of a `Publisher` is that the underlying processing +is not started at the time the `Publisher` is obtained, rather its +started at the moment an observer subscribes or signals demand to the +`Publisher`. This is a crucial difference to a +`java.util.concurrent.Future`, which is started somewhere at the time it +is created/obtained. So if no observer ever subscribes to the +`Publisher`, nothing ever will happen. + +### A word on the lettuce Reactive API + +All commands return a `Flux`, `Mono` or `Mono` to which a +`Subscriber` can subscribe to. That subscriber reacts to whatever item +or sequence of items the `Publisher` emits. This pattern facilitates +concurrent operations because it does not need to block while waiting +for the `Publisher` to emit objects. Instead, it creates a sentry in +the form of a `Subscriber` that stands ready to react appropriately at +whatever future time the `Publisher` does so. + +### Consuming `Publisher` + +The first thing you want to do when working with publishers is to +consume them. Consuming a publisher means subscribing to it. Here is an +example that subscribes and prints out all the items emitted: + +``` java +Flux.just("Ben", "Michael", "Mark").subscribe(new Subscriber() { + public void onSubscribe(Subscription s) { + s.request(3); + } + + public void onNext(String s) { + System.out.println("Hello " + s + "!"); + } + + public void onError(Throwable t) { + + } + + public void onComplete() { + System.out.println("Completed"); + } +}); +``` + +The example prints the following lines: + + Hello Ben + Hello Michael + Hello Mark + Completed + +You can see that the Subscriber (or Observer) gets notified of every +event and also receives the completed event. A `Publisher` emits +items until either an exception is raised or the `Publisher` finishes +the emission calling `onCompleted`. No further elements are emitted +after that time. + +A call to the `subscribe` registers a `Subscription` that allows to +cancel and, therefore, do not receive further events. Publishers can +interoperate with the un-subscription and free resources once a +subscriber unsubscribed from the `Publisher`. + +Implementing a `Subscriber` requires implementing numerous methods, +so lets rewrite the code to a simpler form: + +``` java +Flux.just("Ben", "Michael", "Mark").doOnNext(new Consumer() { + public void accept(String s) { + System.out.println("Hello " + s + "!"); + } +}).doOnComplete(new Runnable() { + public void run() { + System.out.println("Completed"); + } +}).subscribe(); +``` + +alternatively, even simpler by using Java 8 Lambdas: + +``` java +Flux.just("Ben", "Michael", "Mark") + .doOnNext(s -> System.out.println("Hello " + s + "!")) + .doOnComplete(() -> System.out.println("Completed")) + .subscribe(); +``` + +You can control the elements that are processed by your `Subscriber` +using operators. The `take()` operator limits the number of emitted +items if you are interested in the first `N` elements only. + +``` java +Flux.just("Ben", "Michael", "Mark") // + .doOnNext(s -> System.out.println("Hello " + s + "!")) + .doOnComplete(() -> System.out.println("Completed")) + .take(2) + .subscribe(); +``` + +The example prints the following lines: + + Hello Ben + Hello Michael + Completed + +Note that the `take` operator implicitly cancels its subscription from +the `Publisher` once the expected count of elements was emitted. + +A subscription to a `Publisher` can be done either by another `Flux` +or a `Subscriber`. Unless you are implementing a custom `Publisher`, +always use `Subscriber`. The used subscriber `Consumer` from the example +above does not handle `Exception`s so once an `Exception` is thrown you +will see a stack trace like this: + + Exception in thread "main" reactor.core.Exceptions$BubblingException: java.lang.RuntimeException: Example exception + at reactor.core.Exceptions.bubble(Exceptions.java:96) + at reactor.core.publisher.Operators.onErrorDropped(Operators.java:296) + at reactor.core.publisher.LambdaSubscriber.onError(LambdaSubscriber.java:117) + ... + Caused by: java.lang.RuntimeException: Example exception + at demos.lambda$example3Lambda$4(demos.java:87) + at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onNext(FluxPeekFuseable.java:157) + ... 23 more + +It is always recommended to implement an error handler right from the +beginning. At a certain point, things can and will go wrong. + +A fully implemented subscriber declares the `onCompleted` and `onError` +methods allowing you to react to these events: + +``` java +Flux.just("Ben", "Michael", "Mark").subscribe(new Subscriber() { + public void onSubscribe(Subscription s) { + s.request(3); + } + + public void onNext(String s) { + System.out.println("Hello " + s + "!"); + } + + public void onError(Throwable t) { + System.out.println("onError: " + t); + } + + public void onComplete() { + System.out.println("Completed"); + } +}); +``` + +### From push to pull + +The examples from above illustrated how publishers can be set up in a +not-opinionated style about blocking or non-blocking execution. A +`Flux` can be converted explicitly into an `Iterable` or +synchronized with `block()`. Avoid calling `block()` in your code as you +start expressing the nature of execution inside your code. Calling +`block()` removes all non-blocking advantages of the reactive chain to +your application. + +``` java +String last = Flux.just("Ben", "Michael", "Mark").last().block(); +System.out.println(last); +``` + +The example prints the following line: + + Mark + +A blocking call can be used to synchronize the publisher chain and find +back a way into the plain and well-known `Pull` pattern. + +``` java +List list = Flux.just("Ben", "Michael", "Mark").collectList().block(); +System.out.println(list); +``` + +The `toList` operator collects all emitted elements and passes the list +through the `BlockingPublisher`. + +The example prints the following line: + + [Ben, Michael, Mark] + +### Creating `Flux` and `Mono` using Lettuce + +There are many ways to establish publishers. You have already seen +`just()`, `take()` and `collectList()`. Refer to the [Project Reactor +documentation](http://projectreactor.io/docs/) for many more methods +that you can use to create `Flux` and `Mono`. + +Lettuce publishers can be used for initial and chaining operations. When +using Lettuce publishers, you will notice the non-blocking behavior. +This is because all I/O and command processing are handled +asynchronously using the netty EventLoop. + +Connecting to Redis is insanely simple: + +``` java +RedisClient client = RedisClient.create("redis://localhost"); +RedisStringReactiveCommands commands = client.connect().reactive(); +``` + +In the next step, obtaining a value from a key requires the `GET` +operation: + +``` java +commands.get("key").subscribe(new Consumer() { + + public void accept(String value) { + System.out.println(value); + } +}); +``` + +Alternatively, written in Java 8 lambdas: + +``` java +commands + .get("key") + .subscribe(value -> System.out.println(value)); +``` + +The execution is handled asynchronously, and the invoking Thread can be +used to processed in processing while the operation is completed on the +Netty EventLoop threads. Due to its decoupled nature, the calling method +can be left before the execution of the `Publisher` is finished. + +Lettuce publishers can be used within the context of chaining to load +multiple keys asynchronously: + +``` java +Flux.just("Ben", "Michael", "Mark"). + flatMap(key -> commands.get(key)). + subscribe(value -> System.out.println("Got value: " + value)); +``` + +### Hot and Cold Publishers + +There is a distinction between Publishers that was not covered yet: + +- A cold Publishers waits for a subscription until it emits values and + does this freshly for every subscriber. + +- A hot Publishers begins emitting values upfront and presents them to + every subscriber subsequently. + +All Publishers returned from the Redis Standalone, Redis Cluster, and +Redis Sentinel API are cold, meaning that no I/O happens until they are +subscribed to. As such a subscriber is guaranteed to see the whole +sequence from the beginning. So just creating a Publisher will not cause +any network I/O thus creating and discarding Publishers is cheap. +Publishers created for a Publish/Subscribe emit `PatternMessage`s and +`ChannelMessage`s once they are subscribed to. Publishers guarantee +however to emit all items from the beginning until their end. While this +is true for Publish/Subscribe publishers, the nature of subscribing to a +Channel/Pattern allows missed messages due to its subscription nature +and less to the Hot/Cold distinction of publishers. + +### Transforming publishers + +Publishers can transform the emitted values in various ways. One of the +most basic transformations is `flatMap()` which you have seen from the +examples above that converts the incoming value into a different one. +Another one is `map()`. The difference between `map()` and `flatMap()` +is that `flatMap()` allows you to do those transformations with +`Publisher` calls. + +``` java +Flux.just("Ben", "Michael", "Mark") + .flatMap(commands::get) + .flatMap(value -> commands.rpush("result", value)) + .subscribe(); +``` + +The first `flatMap()` function is used to retrieve a value and the +second `flatMap()` function appends the value to a Redis list named +`result`. The `flatMap()` function returns a Publisher whereas the +normal map just returns ``. You will use `flatMap()` a lot when +dealing with flows like this, you’ll become good friends. + +An aggregation of values can be achieved using the `reduce()` +transformation. It applies a function to each value emitted by a +`Publisher`, sequentially and emits each successive value. We can use +it to aggregate values, to count the number of elements in multiple +Redis sets: + +``` java +Flux.just("Ben", "Michael", "Mark") + .flatMap(commands::scard) + .reduce((sum, current) -> sum + current) + .subscribe(result -> System.out.println("Number of elements in sets: " + result)); +``` + +The aggregation function of `reduce()` is applied on each emitted value, +so three times in the example above. If you want to get the last value, +which denotes the final result containing the number of elements in all +Redis sets, apply the `last()` transformation: + +``` java +Flux.just("Ben", "Michael", "Mark") + .flatMap(commands::scard) + .reduce((sum, current) -> sum + current) + .last() + .subscribe(result -> System.out.println("Number of elements in sets: " + result)); +``` + +Now let’s take a look at grouping emitted items. The following example +emits three items and groups them by the beginning character. + +``` java +Flux.just("Ben", "Michael", "Mark") + .groupBy(key -> key.substring(0, 1)) + .subscribe( + groupedFlux -> { + groupedFlux.collectList().subscribe(list -> { + System.out.println("First character: " + groupedFlux.key() + ", elements: " + list); + }); + } +); +``` + +The example prints the following lines: + + First character: B, elements: [Ben] + First character: M, elements: [Michael, Mark] + +### Absent values + +The presence and absence of values is an essential part of reactive +programming. Traditional approaches consider `null` as an absence of a +particular value. With Java 8, `Optional` was introduced to +encapsulate nullability. Reactive Streams prohibits the use of `null` +values. + +In the scope of Redis, an absent value is an empty list, a non-existent +key or any other empty data structure. Reactive programming discourages +the use of `null` as value. The reactive answer to absent values is just +not emitting any value that is possible due the `0` to `N` nature of +`Publisher`. + +Suppose we have the keys `Ben` and `Michael` set each to the value +`value`. We query those and another, absent key with the following code: + +``` java +Flux.just("Ben", "Michael", "Mark") + .flatMap(commands::get) + .doOnNext(value -> System.out.println(value)) + .subscribe(); +``` + +The example prints the following lines: + + value + value + +The output is just two values. The `GET` to the absent key `Mark` does +not emit a value. + +The reactive API provides operators to work with empty results when you +require a value. You can use one of the following operators: + +- `defaultIfEmpty`: Emit a default value if the `Publisher` did not + emit any value at all + +- `switchIfEmpty`: Switch to a fallback `Publisher` to emit values + +- `Flux.hasElements`/`Flux.hasElement`: Emit a `Mono` that + contains a flag whether the original `Publisher` is empty + +- `next`/`last`/`elementAt`: Positional operators to retrieve the + first/last/`N`th element or emit a default value + +### Filtering items + +The values emitted by a `Publisher` can be filtered in case you need +only specific results. Filtering does not change the emitted values +itself. Filters affect how many items and at which point (and if at all) +they are emitted. + +``` java +Flux.just("Ben", "Michael", "Mark") + .filter(s -> s.startsWith("M")) + .flatMap(commands::get) + .subscribe(value -> System.out.println("Got value: " + value)); +``` + +The code will fetch only the keys `Michael` and `Mark` but not `Ben`. +The filter criteria are whether the `key` starts with a `M`. + +You already met the `last()` filter to retrieve the last value: + +``` java +Flux.just("Ben", "Michael", "Mark") + .last() + .subscribe(value -> System.out.println("Got value: " + value)); +``` + +the extended variant of `last()` allows you to take the last `N` values: + +``` java +Flux.just("Ben", "Michael", "Mark") + .takeLast(3) + .subscribe(value -> System.out.println("Got value: " + value)); +``` + +The example from above takes the last `2` values. + +The opposite to `next()` is the `first()` filter that is used to +retrieve the next value: + +``` java +Flux.just("Ben", "Michael", "Mark") + .next() + .subscribe(value -> System.out.println("Got value: " + value)); +``` + +### Error handling + +Error handling is an indispensable component of every real world +application and should to be considered from the beginning on. Project +Reactor provides several mechanisms to deal with errors. + +In general, you want to react in the following ways: + +- Return a default value instead + +- Use a backup publisher + +- Retry the Publisher (immediately or with delay) + +The following code falls back to a default value after it throws an +exception at the first emitted item: + +``` java +Flux.just("Ben", "Michael", "Mark") + .doOnNext(value -> { + throw new IllegalStateException("Takes way too long"); + }) + .onErrorReturn("Default value") + .subscribe(); +``` + +You can use a backup `Publisher` which will be called if the first +one fails. + +``` java +Flux.just("Ben", "Michael", "Mark") + .doOnNext(value -> { + throw new IllegalStateException("Takes way too long"); + }) + .switchOnError(commands.get("Default Key")) + .subscribe(); +``` + +It is possible to retry the publisher by re-subscribing. Re-subscribing +can be done as soon as possible, or with a wait interval, which is +preferred when external resources are involved. + +``` java +Flux.just("Ben", "Michael", "Mark") + .flatMap(commands::get) + .retry() + .subscribe(); +``` + +Use the following code if you want to retry with backoff: + +``` java +Flux.just("Ben", "Michael", "Mark") + .doOnNext(v -> { + if (new Random().nextInt(10) + 1 == 5) { + throw new RuntimeException("Boo!"); + } + }) + .doOnSubscribe(subscription -> + { + System.out.println(subscription); + }) + .retryWhen(throwableFlux -> Flux.range(1, 5) + .flatMap(i -> { + System.out.println(i); + return Flux.just(i) + .delay(Duration.of(i, ChronoUnit.SECONDS)); + })) + .blockLast(); +``` + +The attempts get passed into the `retryWhen()` method delayed with the +number of seconds to wait. The delay method is used to complete once its +timer is done. + +### Schedulers and threads + +Schedulers in Project Reactor are used to instruct multi-threading. Some +operators have variants that take a Scheduler as a parameter. These +instruct the operator to do some or all of its work on a particular +Scheduler. + +Project Reactor ships with a set of preconfigured Schedulers, which are +all accessible through the `Schedulers` class: + +- Schedulers.parallel(): Executes the computational work such as + event-loops and callback processing. + +- Schedulers.immediate(): Executes the work immediately in the current + thread + +- Schedulers.elastic(): Executes the I/O-bound work such as asynchronous + performance of blocking I/O, this scheduler is backed by a thread-pool + that will grow as needed + +- Schedulers.newSingle(): Executes the work on a new thread + +- Schedulers.fromExecutor(): Create a scheduler from a + `java.util.concurrent.Executor` + +- Schedulers.timer(): Create or reuse a hash-wheel based TimedScheduler + with a resolution of 50ms. + +Do not use the computational scheduler for I/O. + +Publishers can be executed by a scheduler in the following different +ways: + +- Using an operator that makes use of a scheduler + +- Explicitly by passing the Scheduler to such an operator + +- By using `subscribeOn(Scheduler)` + +- By using `publishOn(Scheduler)` + +Operators like `buffer`, `replay`, `skip`, `delay`, `parallel`, and so +forth use a Scheduler by default if not instructed otherwise. + +All of the listed operators allow you to pass in a custom scheduler if +needed. Sticking most of the time with the defaults is a good idea. + +If you want the subscribe chain to be executed on a specific scheduler, +you use the `subscribeOn()` operator. The code is executed on the main +thread without a scheduler set: + +``` java +Flux.just("Ben", "Michael", "Mark").flatMap(key -> { + System.out.println("Map 1: " + key + " (" + Thread.currentThread().getName() + ")"); + return Flux.just(key); + } +).flatMap(value -> { + System.out.println("Map 2: " + value + " (" + Thread.currentThread().getName() + ")"); + return Flux.just(value); + } +).subscribe(); +``` + +The example prints the following lines: + + Map 1: Ben (main) + Map 2: Ben (main) + Map 1: Michael (main) + Map 2: Michael (main) + Map 1: Mark (main) + Map 2: Mark (main) + +This example shows the `subscribeOn()` method added to the flow (it does +not matter where you add it): + +``` java +Flux.just("Ben", "Michael", "Mark").flatMap(key -> { + System.out.println("Map 1: " + key + " (" + Thread.currentThread().getName() + ")"); + return Flux.just(key); + } +).flatMap(value -> { + System.out.println("Map 2: " + value + " (" + Thread.currentThread().getName() + ")"); + return Flux.just(value); + } +).subscribeOn(Schedulers.parallel()).subscribe(); +``` + +The output of the example shows the effect of `subscribeOn()`. You can +see that the Publisher is executed on the same thread, but on the +computation thread pool: + + Map 1: Ben (parallel-1) + Map 2: Ben (parallel-1) + Map 1: Michael (parallel-1) + Map 2: Michael (parallel-1) + Map 1: Mark (parallel-1) + Map 2: Mark (parallel-1) + +If you apply the same code to Lettuce, you will notice a difference in +the threads on which the second `flatMap()` is executed: + +``` java +Flux.just("Ben", "Michael", "Mark").flatMap(key -> { + System.out.println("Map 1: " + key + " (" + Thread.currentThread().getName() + ")"); + return commands.set(key, key); +}).flatMap(value -> { + System.out.println("Map 2: " + value + " (" + Thread.currentThread().getName() + ")"); + return Flux.just(value); +}).subscribeOn(Schedulers.parallel()).subscribe(); +``` + +The example prints the following lines: + + Map 1: Ben (parallel-1) + Map 1: Michael (parallel-1) + Map 1: Mark (parallel-1) + Map 2: OK (lettuce-nioEventLoop-3-1) + Map 2: OK (lettuce-nioEventLoop-3-1) + Map 2: OK (lettuce-nioEventLoop-3-1) + +Two things differ from the standalone examples: + +1. The values are set rather concurrently than sequentially + +2. The second `flatMap()` transformation prints the netty EventLoop + thread name + +This is because Lettuce publishers are executed and completed on the +netty EventLoop threads by default. + +`publishOn` instructs an Publisher to call its observer’s `onNext`, +`onError`, and `onCompleted` methods on a particular Scheduler. Here, +the order matters: + +``` java +Flux.just("Ben", "Michael", "Mark").flatMap(key -> { + System.out.println("Map 1: " + key + " (" + Thread.currentThread().getName() + ")"); + return commands.set(key, key); +}).publishOn(Schedulers.parallel()).flatMap(value -> { + System.out.println("Map 2: " + value + " (" + Thread.currentThread().getName() + ")"); + return Flux.just(value); +}).subscribe(); +``` + +Everything before the `publishOn()` call is executed in main, everything +below in the scheduler: + + Map 1: Ben (main) + Map 1: Michael (main) + Map 1: Mark (main) + Map 2: OK (parallel-1) + Map 2: OK (parallel-1) + Map 2: OK (parallel-1) + +Schedulers allow direct scheduling of operations. Refer to the [Project +Reactor +documentation](https://projectreactor.io/core/docs/api/reactor/core/scheduler/Schedulers.html) +for further information. + +### Redis Transactions + +Lettuce provides a convenient way to use Redis Transactions in a +reactive way. Commands that should be executed within a transaction can +be executed after the `MULTI` command was executed. Functional chaining +allows to execute commands within a closure, and each command receives +its appropriate response. A cumulative response is also returned with +`TransactionResult` in response to `EXEC`. + +See [Transactions](transactions-multi.md#transactions-using-the-reactive-api) for +further details. + +#### Other examples + +**Blocking example** + +``` java +RedisStringReactiveCommands reactive = client.connect().reactive(); +Mono set = reactive.set("key", "value"); +set.block(); +``` + +**Non-blocking example** + +``` java +RedisStringReactiveCommands reactive = client.connect().reactive(); +Mono set = reactive.set("key", "value"); +set.subscribe(); +``` + +**Functional chaining** + +``` java +RedisStringReactiveCommands reactive = client.connect().reactive(); +Flux.just("Ben", "Michael", "Mark") + .flatMap(key -> commands.sadd("seen", key)) + .flatMap(value -> commands.randomkey()) + .flatMap(commands::type) + .doOnNext(System.out::println).subscribe(); +``` + +**Redis Transaction** + +``` java + RedisReactiveCommands reactive = client.connect().reactive(); + + reactive.multi().doOnSuccess(s -> { + reactive.set("key", "1").doOnNext(s1 -> System.out.println(s1)).subscribe(); + reactive.incr("key").doOnNext(s1 -> System.out.println(s1)).subscribe(); + }).flatMap(s -> reactive.exec()) + .doOnNext(transactionResults -> System.out.println(transactionResults.wasRolledBack())) + .subscribe(); +``` \ No newline at end of file diff --git a/docs/user-guide/redis-functions.md b/docs/user-guide/redis-functions.md new file mode 100644 index 0000000000..f317816731 --- /dev/null +++ b/docs/user-guide/redis-functions.md @@ -0,0 +1,114 @@ +## Redis Functions + +[Redis Functions](https://redis.io/topics/functions-intro) is an +evolution of the scripting API to provide extensibility beyond Lua. +Functions can leverage different engines and follow a model where a +function library registers functionality to be invoked later with the +`FCALL` command. + +
+ +``` java +redis.functionLoad("FUNCTION LOAD "#!lua name=mylib\nredis.register_function('knockknock', function() return 'Who\\'s there?' end)"); + +String response = redis.fcall("knockknock", STATUS); +``` + +
+ +Using Functions is straightforward. Consuming results in Java requires +additional details to consume the result through a matching type. As we +do not know what your function will return, the API uses call-site +generics for you to specify the result type. Additionally, you must +provide a `ScriptOutputType` hint to `EVAL` so that the driver uses the +appropriate output parser. See [Output Formats](#output-formats) for +further details. + +### Output Formats + +You can choose from one of the following: + +- `BOOLEAN`: Boolean output, expects a number `0` or `1` to be converted + to a boolean value. + +- `INTEGER`: 64-bit Integer output, represented as Java `Long`. + +- `MULTI`: List of flat arrays. + +- `STATUS`: Simple status value such as `OK`. The Redis response is + parsed as ASCII. + +- `VALUE`: Value return type decoded through `RedisCodec`. + +- `OBJECT`: RESP3-defined object output supporting all Redis response + structures. + +### Leveraging Scripting and Functions through Command Interfaces + +Using dynamic functionality without a documented response structure can +impose quite some complexity on your application. If you consider using +scripting or functions, then you can use [Command +Interfaces](../redis-command-interfaces.md) to declare +an interface along with methods that represent your scripting or +function landscape. Declaring a method with input arguments and a +response type not only makes it obvious how the script or function is +supposed to be called, but also how the response structure should look +like. + +Let’s take a look at a simple function call first: + +
+ +``` lua +local function my_hlastmodified(keys, args) + local hash = keys[1] + return redis.call('HGET', hash, '_last_modified_') +end +``` + +
+ +
+ +``` java +Long lastModified = redis.fcall("my_hlastmodified", INTEGER, "my_hash"); +``` + +
+ +This example calls the `my_hlastmodified` function expecting some `Long` +response an input argument. Calling a function from a single place in +your code isn’t an issue on its own. The arrangement becomes problematic +once the number of functions grows or you start calling the functions +with different arguments from various places in your code. Without the +function code, it becomes impossible to investigate how the response +mechanics work or determine the argument semantics, as there is no +single place to document the function behavior. + +Let’s apply the Command Interface pattern to see how the the declaration +and call sites change: + +
+ +``` java +interface MyCustomCommands extends Commands { + + /** + * Retrieve the last modified value from the hash key. + * @param hashKey the key of the hash. + * @return the last modified timestamp, can be {@code null}. + */ + @Command("FCALL my_hlastmodified 1 :hashKey") + Long getLastModified(@Param("my_hash") String hashKey); + +} + +MyCustomCommands myCommands = …; +Long lastModified = myCommands.getLastModified("my_hash"); +``` + +
+ +By declaring a command method, you create a place that allows for +storing additional documentation. The method declaration makes clear +what the function call expects and what you get in return. \ No newline at end of file diff --git a/docs/user-guide/redis-json.md b/docs/user-guide/redis-json.md new file mode 100644 index 0000000000..dbdf18bb1a --- /dev/null +++ b/docs/user-guide/redis-json.md @@ -0,0 +1,99 @@ +# RedisJSON support in Lettuce + +Lettuce supports [RedisJSON](https://oss.redis.com/redisjson/) starting from [Lettuce 6.5.0.RELEASE](https://github.com/redis/lettuce/releases/tag/6.5.0.RELEASE). + +The driver generally allows three distinct ways of working with the RedisJSON module: +* (Default mode) - default JSON parsing using Jackson behind the scenes +* (Advanced mode) - custom JSON parsing using a user-provided JSON parser +* (Power-user mode) - unprocessed JSON documents that have not gone through any process of deserialization or serialization + +!!! INFO + In all the above modes, the driver would refrain from processing the JSON document in the main event loop and instead + delegate this to the user thread. This behaviour is consistent when both receiving and sending JSON documents - when + receiving the parsing is done lazily whenever a method is called that requires the JSON to be parsed; when sending the + JSON is serialized immediately after it is passed to any of the commands, but before dispatching the command to the + event loop. + +!!! WARNING + Unless you are using a custom JSON parser you would need to add a dependency to the + [jackson-databind](https://github.com/FasterXML/jackson-databind) library in your project. This is because the + default JSON parser uses Jackson to parse JSON documents to and from string representations. + +## Default mode +Best for: + +* Most typical use-cases where the JSON document is parsed and processed + +### Example usage + +```java +RedisURI redisURI = RedisURI.Builder.redis("acme.com").build(); +RedisClient redisClient = RedisClient.create(redisURI); +try (StatefulRedisConnection connect = redisClient.connect()){ + redis = connect.async(); + JsonPath path = JsonPath.of("$..mountain_bikes[0:2].model"); + + JsonParser parser = redis.getJsonParser(); + JsonObject bikeRecord = parser.createJsonObject(); + JsonObject bikeSpecs = parser.createJsonObject(); + JsonArray bikeColors = parser.createJsonArray(); + + bikeSpecs.put("material", parser.createJsonValue("\"wood\"")); + bikeSpecs.put("weight", parser.createJsonValue("19")); + + bikeColors.add(parser.createJsonValue("\"walnut\"")); + bikeColors.add(parser.createJsonValue("\"chestnut\"")); + + bikeRecord.put("id", parser.createJsonValue("\"bike:13\"")); + bikeRecord.put("model", parser.createJsonValue("\"Woody\"")); + bikeRecord.put("description", parser.createJsonValue("\"The Woody is an environmentally-friendly wooden bike\"")); + bikeRecord.put("price", parser.createJsonValue("\"1112\"")); + bikeRecord.put("specs", bikeSpecs); + bikeRecord.put("colors", bikeColors); + + String result = redis.jsonSet("bikes:inventory", path, bikeRecord).get(); +} +``` + +## Advanced mode +Best for: + +* Applications that want to handle parsing manually - either by using another library or by implementing their own parser + +### Example usage + +```java +RedisURI redisURI = RedisURI.Builder.redis("127.0.0.1").withPort(16379).build(); + +try (RedisClient client = RedisClient.create(redisURI)) { + client.setOptions(ClientOptions.builder().jsonParser(Mono.just(new CustomParser())).build()); + StatefulRedisConnection connection = client.connect(StringCodec.UTF8); + RedisCommands redis = connection.sync(); +} +``` + + + +## Power-user mode +Best for: + +* Applications that do little to no processing on the Java layer + +### Example usage + +```java +JsonPath myPath = JsonPath.of("$..mountain_bikes"); +RedisURI redisURI = RedisURI.Builder.redis("127.0.0.1").withPort(16379).build(); +try (RedisClient client = RedisClient.create(redisURI)) { + RedisAsyncCommands redis = client.connect().async(); + RedisFuture> bikes = redis.jsonGet("bikes:inventory", myPath); + + CompletionStage> stage = bikes.thenApply( + fetchedBikes -> redis.jsonSet("service_bikes", JsonPath.ROOT_PATH, fetchedBikes.get(0))); + + String result = stage.toCompletableFuture().get().get(); +} +``` +!!! NOTE + The power-user mode is not exclusive to using a custom parser (Advanced mode), as long as the custom parser follows + the API contract of the `JsonParser`, `JsonValue`, `JsonArray` and `JsonObject` interfaces. \ No newline at end of file diff --git a/docs/user-guide/transactions-multi.md b/docs/user-guide/transactions-multi.md new file mode 100644 index 0000000000..8fbcb4d6ea --- /dev/null +++ b/docs/user-guide/transactions-multi.md @@ -0,0 +1,168 @@ +## Transactions/Multi + +Transactions allow the execution of a group of commands in a single +step. Transactions can be controlled using `WATCH`, `UNWATCH`, `EXEC`, +`MULTI` and `DISCARD` commands. Synchronous, asynchronous, and reactive +APIs allow the use of transactions. + +!!! note + Transactional use requires external synchronization when a single + connection is used by multiple threads/processes. This can be achieved + either by serializing transactions or by providing a dedicated + connection to each concurrent process. Lettuce itself does not + synchronize transactional/non-transactional invocations regardless of + the used API facade. + +Redis responds to commands invoked during a transaction with a `QUEUED` +response. The response related to the execution of the command is +received at the moment the `EXEC` command is processed, and the +transaction is executed. The particular APIs behave in different ways: + +- Synchronous: Invocations to the commands return `null` while they are + invoked within a transaction. The `MULTI` command carries the response + of the particular commands. + +- Asynchronous: The futures receive their response at the moment the + `EXEC` command is processed. This happens while the `EXEC` response is + received. + +- Reactive: An `Obvervable` triggers `onNext`/`onCompleted` at the + moment the `EXEC` command is processed. This happens while the `EXEC` + response is received. + +As soon as you’re within a transaction, you won’t receive any responses +on triggering the commands + +``` java +redis.multi() == "OK" +redis.set(key, value) == null +redis.exec() == list("OK") +``` + +You’ll receive the transactional response when calling `exec()` on the +end of your transaction. + +``` java +redis.multi() == "OK" +redis.set(key1, value) == null +redis.set(key2, value) == null +redis.exec() == list("OK", "OK") +``` + +### Transactions using the asynchronous API + +Asynchronous use of Redis transactions is very similar to +non-transactional use. The asynchronous API returns `RedisFuture` +instances that eventually complete and they are handles to a future +result. Regular commands complete as soon as Redis sends a response. +Transactional commands complete as soon as the `EXEC` result is +received. + +Each command is completed individually with its own result so users of +`RedisFuture` will see no difference between transactional and +non-transactional `RedisFuture` completion. That said, transactional +command results are available twice: Once via `RedisFuture` of the +command and once through `List` (`TransactionResult` since +Lettuce 5) of the `EXEC` command future. + +``` java +RedisAsyncCommands async = client.connect().async(); + +RedisFuture multi = async.multi(); + +RedisFuture set = async.set("key", "value"); + +RedisFuture> exec = async.exec(); + +List objects = exec.get(); +String setResult = set.get(); + +objects.get(0) == setResult +``` + +### Transactions using the reactive API + +The reactive API can be used to execute multiple commands in a single +step. The nature of the reactive API encourages nesting of commands. It +is essential to understand the time at which an `Observable` emits a +value when working with transactions. Redis responds with `QUEUED` to +commands invoked during a transaction. The response related to the +execution of the command is received at the moment the `EXEC` command is +processed, and the transaction is executed. Subsequent calls in the +processing chain are executed after the transactional end. The following +code starts a transaction, executes two commands within the transaction +and finally executes the transaction. + +``` java +RedisReactiveCommands reactive = client.connect().reactive(); +reactive.multi().subscribe(multiResponse -> { + reactive.set("key", "1").subscribe(); + reactive.incr("key").subscribe(); + reactive.exec().subscribe(); +}); +``` + +### Transactions on clustered connections + +Clustered connections perform a routing by default. This means, that you +can’t be really sure, on which host your command is executed. So if you +are working in a clustered environment, use rather a regular connection +to your node, since then you’ll bound to that node knowing which hash +slots are handled by it. + +### Examples + +**Multi with executing multiple commands** + +``` java +redis.multi(); + +redis.set("one", "1"); +redis.set("two", "2"); +redis.mget("one", "two"); +redis.llen(key); + +redis.exec(); // result: list("OK", "OK", list("1", "2"), 0L) +``` + +**Mult executing multiple asynchronous commands** + +``` java +redis.multi(); + +RedisFuture set1 = redis.set("one", "1"); +RedisFuture set2 = redis.set("two", "2"); +RedisFuture mget = redis.mget("one", "two"); +RedisFuture llen = mgetredis.llen(key); + + +set1.thenAccept(value -> …); // OK +set2.thenAccept(value -> …); // OK + +RedisFuture> exec = redis.exec(); // result: list("OK", "OK", list("1", "2"), 0L) + +mget.get(); // list("1", "2") +llen.thenAccept(value -> …); // 0L +``` + +**Using WATCH** + +``` java +redis.watch(key); + +RedisConnection redis2 = client.connect(); +redis2.set(key, value + "X"); +redis2.close(); + +redis.multi(); +redis.append(key, "foo"); +redis.exec(); // result is an empty list because of the changed key +``` + +## Scripting and Functions + +Redis functionality can be extended through many ways, of which [Lua +Scripting](https://redis.io/topics/eval-intro) and +[Functions](https://redis.io/topics/functions-intro) are two approaches +that do not require specific pre-requisites on the server. + diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000000..14e269e148 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,52 @@ +site_name: Lettuce Reference Guide +repo_url: https://github.com/redis/lettuce +theme: + name: material + logo: static/logo-redis.svg + font: + text: 'Geist' + code: 'Geist Mono' + features: + - content.code.copy + palette: + primary: white + accent: red + +plugins: + - search + - macros: + include_dir: . + +markdown_extensions: + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences + - admonition + - pymdownx.details + - toc: + permalink: true +nav: + - Overview: overview.md + - New & Noteworthy: new-features.md + - Getting Started: getting-started.md + - User Guide: + - Connecting Redis: user-guide/connecting-redis.md + - Asynchronous API: user-guide/async-api.md + - Reactive API: user-guide/reactive-api.md + - Kotlin API: user-guide/kotlin-api.md + - Publish/Subscribe: user-guide/pubsub.md + - Transactions/Multi: user-guide/transactions-multi.md + - Redis JSON: user-guide/redis-json.md + - Redis programmability: + - LUA Scripting: user-guide/lua-scripting.md + - Redis Functions: user-guide/redis-functions.md + - High-Availability and Sharding: ha-sharding.md + - Working with dynamic Redis Command Interfaces: redis-command-interfaces.md + - Advanced Usage: advanced-usage.md + - Integration and Extension: integration-extension.md + - Frequently Asked Questions: faq.md + - API Reference: https://www.javadoc.io/doc/io.lettuce/lettuce-core/latest/index.html \ No newline at end of file diff --git a/pom.xml b/pom.xml index c1c5f9215f..da7a50f5c5 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ io.lettuce lettuce-core - 6.4.0.BUILD-SNAPSHOT + 6.5.0.BUILD-SNAPSHOT jar Lettuce @@ -50,19 +50,20 @@ 5.13.11 3.13.0 2.12.0 + 2.17.0 1.3.2 4.0.1 5.10.2 2.2 - 2.1.12 + 2.2.2 1.7.21 1.5.2 2.0.3 - 2.17.2 + 2.24.0 1.12.4 1.2.4 4.9.0 - 4.1.107.Final + 4.1.113.Final 2.0.27 3.6.6 1.3.8 @@ -238,6 +239,13 @@ true + + com.fasterxml.jackson.core + jackson-databind + ${jackson-version} + true + + @@ -523,6 +531,36 @@ test + + + + org.testcontainers + testcontainers + 1.20.1 + test + + + org.testcontainers + junit-jupiter + 1.20.1 + test + + + + + + org.openjdk.jmh + jmh-core + 1.37 + test + + + org.openjdk.jmh + jmh-generator-annprocess + 1.37 + test + + @@ -577,7 +615,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.12.1 + 3.13.0 @@ -595,7 +633,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.7.0 + 3.8.0 @@ -607,12 +645,12 @@ org.apache.maven.plugins maven-surefire-plugin - 3.2.5 + 3.4.0 org.apache.maven.surefire surefire-junit-platform - 3.2.5 + 3.4.0 @@ -620,12 +658,12 @@ org.apache.maven.plugins maven-failsafe-plugin - 3.2.5 + 3.3.1 org.apache.maven.surefire surefire-junit-platform - 3.2.5 + 3.3.1 @@ -651,7 +689,7 @@ org.jacoco jacoco-maven-plugin - 0.8.9 + 0.8.12 @@ -893,7 +931,7 @@ org.apache.maven.plugins maven-release-plugin - sonatype-oss-release,documentation + sonatype-oss-release deploy true @{project.version} @@ -1238,128 +1276,6 @@ - - - documentation - - - - - - - org.apache.maven.plugins - maven-antrun-plugin - - - - rename-reference-docs - process-resources - - - - - - - run - - - - - - - - org.asciidoctor - asciidoctor-maven-plugin - 2.2.4 - - - org.asciidoctor - asciidoctorj-pdf - 2.3.9 - - - - - - html - generate-resources - - process-asciidoc - - - html5 - - ${project.build.directory}/site/reference/html - - book - - true - true - stylesheets - golo.css - - - - - - pdf - generate-resources - - process-asciidoc - - - pdf - - - - - - - src/main/asciidoc - index.asciidoc - book - - ${project.version} - true - 3 - true - - https://raw.githubusercontent.com/wiki/lettuce-io/lettuce-core/ - - - - - font - coderay - - - - - - org.apache.maven.plugins - maven-assembly-plugin - - - docs - package - - single - - - - src/assembly/docs.xml - - gnu - true - - - - - - - - - diff --git a/src/main/asciidoc/advanced-usage.asciidoc b/src/main/asciidoc/advanced-usage.asciidoc deleted file mode 100644 index 50b657e54b..0000000000 --- a/src/main/asciidoc/advanced-usage.asciidoc +++ /dev/null @@ -1,74 +0,0 @@ -:auto-reconnect-link: <> -:client-options-link: <> -:client-resources-link: <> - -:custom-commands-command-output-link: <> -:custom-commands-command-exec-model-link: <> - -[[advanced-usage]] -== Advanced usage - -[[client-resources]] -=== Configuring Client resources -include::{ext-doc}/Configuring-Client-resources.asciidoc[leveloffset=+2] - -[[client-options]] -=== Client Options -include::{ext-doc}/Client-Options.asciidoc[leveloffset=+2] - -[[ssl]] -=== SSL Connections -include::{ext-doc}/SSL-Connections.asciidoc[leveloffset=+2] - -[[native-transports]] -=== Native Transports -include::{ext-doc}/Native-Transports.asciidoc[leveloffset=+2] - -[[unix-domain-sockets]] -=== Unix Domain Sockets -include::{ext-doc}/Unix-Domain-Sockets.asciidoc[leveloffset=+2] - -[[streaming-api]] -=== Streaming API -include::{ext-doc}/Streaming-API.asciidoc[leveloffset=+1] - -[[events]] -=== Events -include::{ext-doc}/Connection-Events.asciidoc[leveloffset=+2] - -[[observability]] -=== Observability - -The following section explains Lettuces metrics and tracing capabilities. - -[[observability.metrics]] -==== Metrics - -include::{ext-doc}/Command-Latency-Metrics.asciidoc[leveloffset=+2] - -[[observability.tracing]] -==== Tracing - -include::{ext-doc}/Tracing.asciidoc[leveloffset=+2] - -=== Pipelining and command flushing - -include::{ext-doc}/Pipelining-and-command-flushing.asciidoc[leveloffset=+2] - -=== Connection Pooling - -include::{ext-doc}/Connection-Pooling.asciidoc[leveloffset=+2] - -=== Custom commands - -include::{ext-doc}/Custom-commands%2C-outputs-and-command-mechanics.asciidoc[leveloffset=+2] - -=== Graal Native Image - -include::{ext-doc}/Using-Lettuce-with-Native-Images.asciidoc[leveloffset=+2] - -[[command-execution-reliability]] -=== Command execution reliability - -include::{ext-doc}/Command-execution-reliability.asciidoc[leveloffset=+2] - diff --git a/src/main/asciidoc/faq.asciidoc b/src/main/asciidoc/faq.asciidoc deleted file mode 100644 index 1793a75438..0000000000 --- a/src/main/asciidoc/faq.asciidoc +++ /dev/null @@ -1,5 +0,0 @@ -:client-options-link: <> - -[[faq]] -== Frequently Asked Questions -include::{ext-doc}/Frequently-Asked-Questions.asciidoc[leveloffset=+1] diff --git a/src/main/asciidoc/getting-started.asciidoc b/src/main/asciidoc/getting-started.asciidoc deleted file mode 100644 index 54f166c867..0000000000 --- a/src/main/asciidoc/getting-started.asciidoc +++ /dev/null @@ -1,48 +0,0 @@ -:ssl-link: <> -:uds-link: <> -:native-transport-link: <> -:basic-synchronous-link: <> -:asynchronous-api-link: <> -:reactive-api-link: <> -:asynchronous-link: <> -:reactive-link: <> - -[[getting-started]] -== Getting Started -include::{ext-doc}/Getting-started.asciidoc[leveloffset=+1] - -[[connecting-redis]] -== Connecting Redis -include::{ext-doc}/Redis-URI-and-connection-details.asciidoc[] - -[[basic-usage]] -=== Basic Usage -include::{ext-doc}/Basic-usage.asciidoc[leveloffset=+1] - -[[asynchronous-api]] -=== Asynchronous API - -include::{ext-doc}/Asynchronous-API.asciidoc[leveloffset=+2] - -[[reactive-api]] -=== Reactive API - -include::{ext-doc}/Reactive-API.asciidoc[leveloffset=+2] - -[[kotlin]] -=== Kotlin API - -include::kotlin-api.asciidoc[leveloffset=+2] - -=== Publish/Subscribe - -include::{ext-doc}/Pub-Sub.asciidoc[leveloffset=+1] - -=== Transactions/Multi - -include::{ext-doc}/Transactions.asciidoc[leveloffset=+1] - -[[scripting-and-functions]] -=== Scripting and Functions - -include::scripting-and-functions.asciidoc[] diff --git a/src/main/asciidoc/ha-sharding.asciidoc b/src/main/asciidoc/ha-sharding.asciidoc deleted file mode 100644 index aacd855bf3..0000000000 --- a/src/main/asciidoc/ha-sharding.asciidoc +++ /dev/null @@ -1,30 +0,0 @@ -:redis-sentinel-link: <> -:upstream-replica-api-link: <> -:cco-up-to-5-times: <> -:cco-link: <> -:cco-periodic-link: <> -:cco-adaptive-link: <> - -[[ha-sharding]] -== High-Availability and Sharding - -[[master-slave]] -[[master-replica]] -[[upstream-replica]] -=== Master/Replica - -include::{ext-doc}/Master-Replica.asciidoc[leveloffset=+2] - -[[redis-sentinel]] -=== Redis Sentinel - -include::{ext-doc}/Redis-Sentinel.asciidoc[leveloffset=+2] - -[[redis-cluster]] -=== Redis Cluster -include::{ext-doc}/Redis-Cluster.asciidoc[leveloffset=+2] - -[[readfrom-settings]] -=== ReadFrom Settings -include::{ext-doc}/ReadFrom-Settings.asciidoc[leveloffset=+2] - diff --git a/src/main/asciidoc/images/apple-touch-icon-144.png b/src/main/asciidoc/images/apple-touch-icon-144.png deleted file mode 100644 index 8adb9fff09..0000000000 Binary files a/src/main/asciidoc/images/apple-touch-icon-144.png and /dev/null differ diff --git a/src/main/asciidoc/images/apple-touch-icon-180.png b/src/main/asciidoc/images/apple-touch-icon-180.png deleted file mode 100644 index d0928b5316..0000000000 Binary files a/src/main/asciidoc/images/apple-touch-icon-180.png and /dev/null differ diff --git a/src/main/asciidoc/images/lettuce-green-text@2x.png b/src/main/asciidoc/images/lettuce-green-text@2x.png deleted file mode 100644 index adff15525a..0000000000 Binary files a/src/main/asciidoc/images/lettuce-green-text@2x.png and /dev/null differ diff --git a/src/main/asciidoc/images/touch-icon-192x192.png b/src/main/asciidoc/images/touch-icon-192x192.png deleted file mode 100644 index 450da57d8a..0000000000 Binary files a/src/main/asciidoc/images/touch-icon-192x192.png and /dev/null differ diff --git a/src/main/asciidoc/index-docinfo.html b/src/main/asciidoc/index-docinfo.html deleted file mode 100644 index d47a3c38a1..0000000000 --- a/src/main/asciidoc/index-docinfo.html +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/main/asciidoc/index.asciidoc b/src/main/asciidoc/index.asciidoc deleted file mode 100644 index 1bde6343e5..0000000000 --- a/src/main/asciidoc/index.asciidoc +++ /dev/null @@ -1,33 +0,0 @@ -= Lettuce Reference Guide -Mark Paluch ; -:ext-doc: https://raw.githubusercontent.com/wiki/lettuce-io/lettuce-core -{version} -:doctype: book -:icons: font -:toc: -:sectnums: -:sectanchors: -:docinfo: -ifdef::backend-pdf[] -:title-logo-image: images/lettuce-green-text@2x.png -endif::[] - -ifdef::backend-html5[] -image::images/lettuce-green-text@2x.png[width=50%,link=https://lettuce.io] -endif::[] - -include::overview.asciidoc[] - -include::new-features.adoc[leveloffset=+1] - -include::getting-started.asciidoc[] - -include::ha-sharding.asciidoc[] - -include::redis-command-interfaces.asciidoc[] - -include::advanced-usage.asciidoc[] - -include::integration-extension.asciidoc[] - -include::faq.asciidoc[] diff --git a/src/main/asciidoc/integration-extension.asciidoc b/src/main/asciidoc/integration-extension.asciidoc deleted file mode 100644 index 4839d122f8..0000000000 --- a/src/main/asciidoc/integration-extension.asciidoc +++ /dev/null @@ -1,10 +0,0 @@ -[[integration-extension]] -== Integration and Extension - -[[codecs]] -=== Codecs -include::{ext-doc}/Codecs.asciidoc[leveloffset=+1] - -[[cdi-support]] -=== CDI Support -include::{ext-doc}/CDI-Support.asciidoc[leveloffset=+1] diff --git a/src/main/asciidoc/kotlin-api.asciidoc b/src/main/asciidoc/kotlin-api.asciidoc deleted file mode 100644 index e630347ce4..0000000000 --- a/src/main/asciidoc/kotlin-api.asciidoc +++ /dev/null @@ -1,79 +0,0 @@ -Kotlin Coroutines are using Kotlin lightweight threads allowing to write non-blocking code in an imperative way. -On language side, suspending functions provides an abstraction for asynchronous operations while on library side kotlinx.coroutines provides functions like `async { }` and types like `Flow`. - -Lettuce ships with extensions to provide support for idiomatic Kotlin use. - -== Dependencies - -Coroutines support is available when `kotlinx-coroutines-core` and `kotlinx-coroutines-reactive` dependencies are on the classpath: - -.pom.xml -==== -[source,xml] ----- - - org.jetbrains.kotlinx - kotlinx-coroutines-core - ${kotlinx-coroutines.version} - - - org.jetbrains.kotlinx - kotlinx-coroutines-reactive - ${kotlinx-coroutines.version} - ----- -==== - -== How does Reactive translate to Coroutines? - -`Flow` is an equivalent to `Flux` in Coroutines world, suitable for hot or cold streams, finite or infinite streams, with the following main differences: - -* `Flow` is push-based while `Flux` is a push-pull hybrid -* Backpressure is implemented via suspending functions -* `Flow` has only a single suspending collect method and operators are implemented as extensions -* Operators are easy to implement thanks to Coroutines -* Extensions allow to add custom operators to Flow -* Collect operations are suspending functions -* `map` operator supports asynchronous operations (no need for `flatMap`) since it takes a suspending function parameter - -== Coroutines API based on reactive operations - -Example for retrieving commands and using it: - -[source,kotlin] ----- -val api: RedisCoroutinesCommands = connection.coroutines() - -val foo1 = api.set("foo", "bar") -val foo2 = api.keys("fo*") ----- - -NOTE: Coroutine Extensions are experimental and require opt-in using `@ExperimentalLettuceCoroutinesApi`. -The API ships with a reduced feature set. -Deprecated methods and `StreamingChannel` are left out intentionally. -Expect evolution towards a `Flow`-based API to consume large Redis responses. - -== Extensions for existing APIs - -=== Transactions DSL - -Example for the synchronous API: - -[source,kotlin] ----- -val result: TransactionResult = connection.sync().multi { - set("foo", "bar") - get("foo") -} ----- - -Example for async with coroutines: - -[source,kotlin] ----- -val result: TransactionResult = connection.async().multi { - set("foo", "bar") - get("foo") -} ----- - diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc deleted file mode 100644 index 795df139bb..0000000000 --- a/src/main/asciidoc/new-features.adoc +++ /dev/null @@ -1,94 +0,0 @@ -[[new-features]] -= New & Noteworthy - -[[new-features.6-3-0]] -== What's new in Lettuce 6.3 - -* <<_redis_functions,Redis Function support>> (`fcall` and `FUNCTION` commands). -* Support for Library Name and Version through `LettuceVersion`. -Automated registration of the Lettuce library version upon connection handshake. -* Support for Micrometer Tracing to trace observations (distributed tracing and metrics). - -[[new-features.6-2-0]] -== What's new in Lettuce 6.2 - -* <> abstraction to externalize credentials and credentials rotation. -* Retrieval of Redis Cluster node connections using `ConnectionIntent` to obtain read-only connections. -* Master/Replica now uses `SENTINEL REPLICAS` to discover replicas instead of `SENTINEL SLAVES`. - -[[new-features.6-1-0]] -== What's new in Lettuce 6.1 - -* Kotlin Coroutines support for `SCAN`/`HSCAN`/`SSCAN`/`ZSCAN` through `ScanFlow`. -* Command Listener API through `RedisClient.addListener(CommandListener)`. -* <> through `MicrometerCommandLatencyRecorder`. -* <>. -* Configuration of extended Keep-Alive options through `KeepAliveOptions` (only available for some transports/Java versions). -* Configuration of netty's `AddressResolverGroup` through `ClientResources`. -Uses `DnsAddressResolverGroup` when `netty-resolver-dns` is on the classpath. -* Add support for Redis ACL commands. -* <> - -[[new-features.6-0-0]] -== What's new in Lettuce 6.0 - -* Support for RESP3 usage with Redis 6 along with RESP2/RESP3 handshake and protocol version discovery. -* ACL authentication using username and password or password-only authentication. -* Cluster topology refresh is now non-blocking. -* <>. -* RxJava 3 support. -* Refined Scripting API accepting the Lua script either as `byte[]` or `String`. -* Connection and Queue failures now no longer throw an exception but properly associate the failure with the Future handle. -* Removal of deprecated API including timeout methods accepting `TimeUnit`. -Use methods accepting `Duration` instead. -* Lots of internal refinements. -* `xpending` methods return now `List` and `PendingMessages` -* Spring support removed. -Use Spring Data Redis for a seamless Spring integration with Lettuce. -* `AsyncConnectionPoolSupport.createBoundedObjectPool(…)` methods are now blocking to await pool initialization. -* `DecodeBufferPolicy` for fine-grained memory reclaim control. -* `RedisURI.toString()` renders masked password. -* `ClientResources.commandLatencyCollector(…)` refactored into `ClientResources.commandLatencyRecorder(…)` returning `CommandLatencyRecorder`. - -[[new-features.5-3-0]] -== What's new in Lettuce 5.3 - -* Improved SSL configuration supporting Cipher suite selection and PEM-encoded certificates. -* Fixed method signature for `randomkey()`. -* Un-deprecated `ClientOptions.pingBeforeActivateConnection` to allow connection verification during connection handshake. - -[[new-features.5-2-0]] -== What's new in Lettuce 5.2 - -* Allow randomization of read candidates using Redis Cluster. -* SSL support for Redis Sentinel. - -[[new-features.5-1-0]] -== What's new in Lettuce 5.1 - -* Add support for `ZPOPMIN`, `ZPOPMAX`, `BZPOPMIN`, `BZPOPMAX` commands. -* Add support for Redis Command Tracing through Brave, see <>. -* Add support for https://redis.io/topics/streams-intro[Redis Streams]. -* Asynchronous `connect()` for Master/Replica connections. -* <> through `AsyncConnectionPoolSupport` and `AsyncPool`. -* Dedicated exceptions for Redis `LOADING`, `BUSY`, and `NOSCRIPT` responses. -* Commands in at-most-once mode (auto-reconnect disabled) are now canceled already on disconnect. -* Global command timeouts (also for reactive and asynchronous API usage) configurable through <>. -* Host and port mappers for Lettuce usage behind connection tunnels/proxies through `SocketAddressResolver`, see <>. -* `SCRIPT LOAD` dispatch to all cluster nodes when issued through `RedisAdvancedClusterCommands`. -* Reactive `ScanStream` to iterate over the keyspace using `SCAN` commands. -* Transactions using Master/Replica connections are bound to the master node. - -[[new-features.5-0-0]] -== What's new in Lettuce 5.0 - -* New artifact coordinates: `io.lettuce:lettuce-core` and packages moved from `com.lambdaworks.redis` to `io.lettuce.core`. -* <> now Reactive Streams-based using https://projectreactor.io/[Project Reactor]. -* <> supporting dynamic command invocation and Redis Modules. -* Enhanced, immutable Key-Value objects. -* Asynchronous Cluster connect. -* Native transport support for Kqueue on macOS systems. -* Removal of support for Guava. -* Removal of deprecated `RedisConnection` and `RedisAsyncConnection` interfaces. -* Java 9 compatibility. -* HTML and PDF reference documentation along with a new project website: https://lettuce.io. diff --git a/src/main/asciidoc/overview.asciidoc b/src/main/asciidoc/overview.asciidoc deleted file mode 100644 index 83b4f5e771..0000000000 --- a/src/main/asciidoc/overview.asciidoc +++ /dev/null @@ -1,83 +0,0 @@ -[[overview]] -== Overview - -This document is the reference guide for Lettuce. It explains how to use Lettuce, its concepts, semantics, and the syntax. - -You can read this reference guide in a linear fashion, or you can skip sections if something does not interest you. - -This section provides some basic introduction to Redis. The rest of the document refers only to Lettuce features and assumes the user is familiar with Redis concepts. - -[[overview.redis]] -=== Knowing Redis - -NoSQL stores have taken the storage world by storm. -It is a vast domain with a plethora of solutions, terms and patterns (to make things worse even the term itself has multiple https://www.google.com/search?q=nosql+acronym[meanings]). -While some of the principles are common, it is crucial that the user is familiar to some degree with Redis. -The best way to get acquainted to these solutions is to read and follow their documentation - it usually doesn't take more than 5-10 minutes to go through them and if you are coming from an RDMBS-only background many times these exercises can be an eye-opener. - -The jumping off ground for learning about Redis is https://www.redis.io/[redis.io]. -Here is a list of other useful resources: - -* The https://try.redis.io/[interactive tutorial] introduces Redis. -* The https://redis.io/commands[command references] explains Redis commands and contains links to getting started guides, reference documentation and tutorials. - -=== Project Reactor - -https://projectreactor.io[Reactor] is a highly optimized reactive library for building efficient, non-blocking applications on the JVM based on the https://github.com/reactive-streams/reactive-streams-jvm[Reactive Streams Specification]. -Reactor based applications can sustain very high throughput message rates and operate with a very low memory footprint, making it suitable for building efficient event-driven applications using the microservices architecture. - -Reactor implements two publishers https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html[Flux] and -https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Mono.html[Mono], both of which support non-blocking back-pressure. -This enables exchange of data between threads with well-defined memory usage, avoiding unnecessary intermediate buffering or blocking. - -=== Non-blocking API for Redis - -Lettuce is a scalable thread-safe Redis client based on https://netty.io[netty] and Reactor. -Lettuce provides <>, <> and <> APIs to interact with Redis. - -[[overview.requirements]] -=== Requirements - -Lettuce 4.x and 5.x binaries require JDK level 8.0 and above. - -In terms of https://redis.io/[Redis], at least 2.6. - -=== Additional Help Resources - -Learning a new framework is not always straight forward.In this section, we try to provide what we think is an easy-to-follow guide for starting with Lettuce. However, if you encounter issues or you are just looking for an advice, feel free to use one of the links below: - -[[overview.support]] -==== Support - -There are a few support options available: - -* Lettuce on Stackoverflow https://stackoverflow.com/questions/tagged/lettuce[Stackoverflow] is a tag for all Lettuce users to share information and help each other.Note that registration is needed *only* for posting. -* Get in touch with the community on https://gitter.im/lettuce-io/Lobby[Gitter]. -* GitHub Discussions: https://github.com/lettuce-io/lettuce-core/discussions -* Report bugs (or ask questions) in GitHub issues https://github.com/lettuce-io/lettuce-core/issues. - -[[overview.development]] -==== Following Development - -For information on the Lettuce source code repository, nightly builds and snapshot artifacts please see the https://lettuce.io[Lettuce homepage]. -You can help make lettuce best serve the needs of the lettuce community by interacting with developers through the Community on https://stackoverflow.com/questions/tagged/lettuce[Stackoverflow]. -If you encounter a bug or want to suggest an improvement, please create a ticket on the lettuce issue https://github.com/lettuce-io/lettuce-core/issues[tracker]. - -==== Project Metadata - -* Version Control – https://github.com/lettuce-io/lettuce-core -* Releases and Binary Packages – https://github.com/lettuce-io/lettuce-core/releases -* Issue tracker – https://github.com/lettuce-io/lettuce-core/issues -* Release repository – https://repo1.maven.org/maven2/ (Maven Central) -* Snapshot repository – https://oss.sonatype.org/content/repositories/snapshots/ (OSS Sonatype Snapshots) - -=== Where to go from here - -* Head to <> if you feel like jumping straight into the code. -* Go to <> for Master/Replica ("Master/Slave"), Redis Sentinel and Redis Cluster topics. -* In order to dig deeper into the core features of Reactor: -** If you’re looking for client configuration options, performance related behavior and how to use various transports, go to <>. -** See <> for extending Lettuce with codecs or integrate it in your CDI/Spring application. -** You want to know more about *at-least-once* and *at-most-once*? -Take a look into <>. - diff --git a/src/main/asciidoc/redis-command-interfaces.asciidoc b/src/main/asciidoc/redis-command-interfaces.asciidoc deleted file mode 100644 index ae5b750bf6..0000000000 --- a/src/main/asciidoc/redis-command-interfaces.asciidoc +++ /dev/null @@ -1,4 +0,0 @@ - -[[redis-command-interfaces]] -include::{ext-doc}/Redis-Command-Interfaces.asciidoc[leveloffset=+1] - diff --git a/src/main/asciidoc/scripting-and-functions.asciidoc b/src/main/asciidoc/scripting-and-functions.asciidoc deleted file mode 100644 index 73c7f66345..0000000000 --- a/src/main/asciidoc/scripting-and-functions.asciidoc +++ /dev/null @@ -1,4 +0,0 @@ -:command-interfaces-link: <> -[[redis-scripting-and-functions]] -include::{ext-doc}/Scripting-and-Functions.asciidoc[leveloffset=+2] - diff --git a/src/main/asciidoc/stylesheets/golo.css b/src/main/asciidoc/stylesheets/golo.css deleted file mode 100644 index b7699baf53..0000000000 --- a/src/main/asciidoc/stylesheets/golo.css +++ /dev/null @@ -1,1990 +0,0 @@ -@import url('https://fonts.googleapis.com/css?family=Raleway:300:400:700'); -@import url(https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/1.6.2/semantic.min.css); - - -#header .details br+span.author:before { - content: "\00a0\0026\00a0"; - color: rgba(0,0,0,.85); -} - -#header .details br+span.email:before { - content: "("; -} - -#header .details br+span.email:after { - content: ")"; -} - -/*! normalize.css v2.1.2 | MIT License | git.io/normalize */ -/* ========================================================================== HTML5 display definitions ========================================================================== */ -/** Correct `block` display not defined in IE 8/9. */ -@import url(https://cdnjs.cloudflare.com/ajax/libs/font-awesome/3.2.1/css/font-awesome.css); - -article, aside, details, figcaption, figure, footer, header, hgroup, main, nav, section, summary { - display: block; -} - -/** Correct `inline-block` display not defined in IE 8/9. */ -audio, canvas, video { - display: inline-block; -} - -/** Prevent modern browsers from displaying `audio` without controls. Remove excess height in iOS 5 devices. */ -audio:not([controls]) { - display: none; - height: 0; -} - -/** Address `[hidden]` styling not present in IE 8/9. Hide the `template` element in IE, Safari, and Firefox < 22. */ -[hidden], template { - display: none; -} - -script { - display: none !important; -} - -/* ========================================================================== Base ========================================================================== */ -/** 1. Set default font family to sans-serif. 2. Prevent iOS text size adjust after orientation change, without disabling user zoom. */ -html { - font-family: sans-serif; /* 1 */ - -ms-text-size-adjust: 100%; /* 2 */ - -webkit-text-size-adjust: 100%; /* 2 */ -} - -/** Remove default margin. */ -body { - margin: 0; -} - -/* ========================================================================== Links ========================================================================== */ -/** Remove the gray background color from active links in IE 10. */ -a { - background: transparent; -} - -/** Address `outline` inconsistency between Chrome and other browsers. */ -a:focus { - outline: thin dotted; -} - -/** Improve readability when focused and also mouse hovered in all browsers. */ -a:active, a:hover { - outline: 0; -} - -/* ========================================================================== Typography ========================================================================== */ -/** Address variable `h1` font-size and margin within `section` and `article` contexts in Firefox 4+, Safari 5, and Chrome. */ -h1 { - font-size: 2em; - margin: 1.2em 0; -} - -/** Address styling not present in IE 8/9, Safari 5, and Chrome. */ -abbr[title] { - border-bottom: 1px dotted; -} - -/** Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome. */ -b, strong { - font-weight: bold; -} - -/** Address styling not present in Safari 5 and Chrome. */ -dfn { - font-style: italic; -} - -/** Address differences between Firefox and other browsers. */ -hr { - -moz-box-sizing: content-box; - box-sizing: content-box; - height: 0; -} - -/** Address styling not present in IE 8/9. */ -mark { - background: #ff0; - color: #000; -} - -/** Correct font family set oddly in Safari 5 and Chrome. */ -code, kbd, pre, samp { - font-family: Menlo, Monaco, 'Liberation Mono', Consolas, monospace; - font-size: 1em; -} - -/** Improve readability of pre-formatted text in all browsers. */ -pre { - white-space: pre-wrap; -} - -/** Set consistent quote types. */ -q { - quotes: "\201C" "\201D" "\2018" "\2019"; -} - -/** Address inconsistent and variable font size in all browsers. */ -small { - font-size: 80%; -} - -/** Prevent `sub` and `sup` affecting `line-height` in all browsers. */ -sub, sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sup { - top: -0.5em; -} - -sub { - bottom: -0.25em; -} - -/* ========================================================================== Embedded content ========================================================================== */ -/** Remove border when inside `a` element in IE 8/9. */ -img { - border: 0; -} - -/** Correct overflow displayed oddly in IE 9. */ -svg:not(:root) { - overflow: hidden; -} - -/* ========================================================================== Figures ========================================================================== */ -/** Address margin not present in IE 8/9 and Safari 5. */ -figure { - margin: 0; -} - -/* ========================================================================== Forms ========================================================================== */ -/** Define consistent border, margin, and padding. */ -fieldset { - border: 1px solid #c0c0c0; - margin: 0 2px; - padding: 0.35em 0.625em 0.75em; -} - -/** 1. Correct `color` not being inherited in IE 8/9. 2. Remove padding so people aren't caught out if they zero out fieldsets. */ -legend { - border: 0; /* 1 */ - padding: 0; /* 2 */ -} - -/** 1. Correct font family not being inherited in all browsers. 2. Correct font size not being inherited in all browsers. 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome. */ -button, input, select, textarea { - font-family: inherit; /* 1 */ - font-size: 100%; /* 2 */ - margin: 0; /* 3 */ -} - -/** Address Firefox 4+ setting `line-height` on `input` using `!important` in the UA stylesheet. */ -button, input { - line-height: normal; -} - -/** Address inconsistent `text-transform` inheritance for `button` and `select`. All other form control elements do not inherit `text-transform` values. Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+. Correct `select` style inheritance in Firefox 4+ and Opera. */ -button, select { - text-transform: none; -} - -/** 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` and `video` controls. 2. Correct inability to style clickable `input` types in iOS. 3. Improve usability and consistency of cursor style between image-type `input` and others. */ -button, html input[type="button"], input[type="reset"], input[type="submit"] { - -webkit-appearance: button; /* 2 */ - cursor: pointer; /* 3 */ -} - -/** Re-set default cursor for disabled elements. */ -button[disabled], html input[disabled] { - cursor: default; -} - -/** 1. Address box sizing set to `content-box` in IE 8/9. 2. Remove excess padding in IE 8/9. */ -input[type="checkbox"], input[type="radio"] { - box-sizing: border-box; /* 1 */ - padding: 0; /* 2 */ -} - -/** 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome (include `-moz` to future-proof). */ -input[type="search"] { - -webkit-appearance: textfield; /* 1 */ - -moz-box-sizing: content-box; - -webkit-box-sizing: content-box; /* 2 */ - box-sizing: content-box; -} - -/** Remove inner padding and search cancel button in Safari 5 and Chrome on OS X. */ -input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration { - -webkit-appearance: none; -} - -/** Remove inner padding and border in Firefox 4+. */ -button::-moz-focus-inner, input::-moz-focus-inner { - border: 0; - padding: 0; -} - -/** 1. Remove default vertical scrollbar in IE 8/9. 2. Improve readability and alignment in all browsers. */ -textarea { - overflow: auto; /* 1 */ - vertical-align: top; /* 2 */ -} - -/* ========================================================================== Tables ========================================================================== */ -/** Remove most spacing between table cells. */ -table { - border-collapse: collapse; - border-spacing: 0; -} - -meta.foundation-mq-small { - font-family: "only screen and (min-width: 768px)"; - width: 768px; -} - -meta.foundation-mq-medium { - font-family: "only screen and (min-width:1280px)"; - width: 1280px; -} - -meta.foundation-mq-large { - font-family: "only screen and (min-width:1440px)"; - width: 1440px; -} - -*, *:before, *:after { - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - box-sizing: border-box; -} - -html, body { - font-size: 100%; -} - -body { - background: white; - color: #34302d; - padding: 0; - margin: 0; - font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; - font-weight: 400; - font-style: normal; - line-height: 1.8em; - position: relative; - cursor: auto; -} - -#content, #content p { - line-height: 1.8em; - margin-top: 1.5em; -} - -#content li p { - margin-top: 0.25em; -} - -a:hover { - cursor: pointer; -} - -img, object, embed { - max-width: 100%; - height: auto; -} - -object, embed { - height: 100%; -} - -img { - -ms-interpolation-mode: bicubic; -} - -#map_canvas img, #map_canvas embed, #map_canvas object, .map_canvas img, .map_canvas embed, .map_canvas object { - max-width: none !important; -} - -.left { - float: left !important; -} - -.right { - float: right !important; -} - -.text-left { - text-align: left !important; -} - -.text-right { - text-align: right !important; -} - -.text-center { - text-align: center !important; -} - -.text-justify { - text-align: justify !important; -} - -.hide { - display: none; -} - -.antialiased, body { - -webkit-font-smoothing: antialiased; -} - -img { - display: inline-block; - vertical-align: middle; -} - -textarea { - height: auto; - min-height: 50px; -} - -select { - width: 100%; -} - -p.lead, .paragraph.lead > p, #preamble > .sectionbody > .paragraph:first-of-type p { - font-size: 1.21875em; -} - -.subheader, #content #toctitle, .admonitionblock td.content > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .mathblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, .sidebarblock > .title, .tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title, .tableblock > caption { - color: #6db33f; - font-weight: 300; - margin-top: 0.2em; - margin-bottom: 0.5em; -} - -/* Typography resets */ -div, dl, dt, dd, ul, ol, li, h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6, pre, form, p, blockquote, th, td { - margin: 0; - padding: 0; - direction: ltr; -} - -/* Default Link Styles */ -a { - color: #6db33f; - line-height: inherit; - text-decoration: none; -} - -a:hover, a:focus { - color: #6db33f; - text-decoration: underline; -} - -a img { - border: none; -} - -/* Default paragraph styles */ -p { - font-family: inherit; - font-weight: normal; - font-size: 1em; - margin-bottom: 1.25em; - text-rendering: optimizeLegibility; -} - -p aside { - font-size: 0.875em; - font-style: italic; -} - -/* Default header styles */ -h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { - font-family: "Raleway", Arial, sans-serif; - font-weight: normal; - font-style: normal; - color: #34302d; - text-rendering: optimizeLegibility; - margin-top: 1.6em; - margin-bottom: 0.6em; -} - -h1 small, h2 small, h3 small, #toctitle small, .sidebarblock > .content > .title small, h4 small, h5 small, h6 small { - font-size: 60%; - color: #6db33f; - line-height: 0; -} - -h1 { - font-size: 2.125em; - line-height: 2em; -} - -h2 { - font-size: 1.6875em; - line-height: 1.5em; -} - -h3, #toctitle, .sidebarblock > .content > .title { - font-size: 1.375em; - line-height: 1.3em; -} - -h4 { - font-size: 1.125em; -} - -h5 { - font-size: 1.125em; -} - -h6 { - font-size: 1em; -} - -hr { - border: solid #dcd2c9; - border-width: 1px 0 0; - clear: both; - margin: 1.25em 0 1.1875em; - height: 0; -} - -/* Helpful Typography Defaults */ -em, i { - font-style: italic; - line-height: inherit; -} - -strong, b { - font-weight: bold; - line-height: inherit; -} - -small { - font-size: 60%; - line-height: inherit; -} - -code { - font-family: Consolas, "Liberation Mono", Courier, monospace; - font-weight: bold; - color: #305CB5; -} - -/* Lists */ -ul, ol, dl { - font-size: 1em; - margin-bottom: 1.25em; - list-style-position: outside; - font-family: inherit; -} - -ul, ol { - margin-left: 1.5em; -} - -ul.no-bullet, ol.no-bullet { - margin-left: 1.5em; -} - -/* Unordered Lists */ -ul li ul, ul li ol { - margin-left: 1.25em; - margin-bottom: 0; - font-size: 1em; /* Override nested font-size change */ -} - -ul.square li ul, ul.circle li ul, ul.disc li ul { - list-style: inherit; -} - -ul.square { - list-style-type: square; -} - -ul.circle { - list-style-type: circle; -} - -ul.disc { - list-style-type: disc; -} - -ul.no-bullet { - list-style: none; -} - -/* Ordered Lists */ -ol li ul, ol li ol { - margin-left: 1.25em; - margin-bottom: 0; -} - -/* Definition Lists */ -dl dt { - margin-bottom: 0.3125em; - font-weight: bold; -} - -dl dd { - margin-bottom: 1.25em; -} - -/* Abbreviations */ -abbr, acronym { - text-transform: uppercase; - font-size: 90%; - color: #34302d; - border-bottom: 1px dotted #dddddd; - cursor: help; -} - -abbr { - text-transform: none; -} - -/* Blockquotes */ -blockquote { - margin: 0 0 1.25em; - padding: 0.5625em 1.25em 0 1.1875em; - border-left: 1px solid #dddddd; -} - -blockquote cite { - display: block; - font-size: 0.8125em; - color: #655241; -} - -blockquote cite:before { - content: "\2014 \0020"; -} - -blockquote cite a, blockquote cite a:visited { - color: #655241; -} - -blockquote, blockquote p { - color: #34302d; -} - -/* Microformats */ -.vcard { - display: inline-block; - margin: 0 0 1.25em 0; - border: 1px solid #dddddd; - padding: 0.625em 0.75em; -} - -.vcard li { - margin: 0; - display: block; -} - -.vcard .fn { - font-weight: bold; - font-size: 0.9375em; -} - -.vevent .summary { - font-weight: bold; -} - -.vevent abbr { - cursor: auto; - text-decoration: none; - font-weight: bold; - border: none; - padding: 0 0.0625em; -} - -@media only screen and (min-width: 768px) { - h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { - } - - h1 { - font-size: 2.75em; - } - - h2 { - font-size: 2.3125em; - } - - h3, #toctitle, .sidebarblock > .content > .title { - font-size: 1.6875em; - } - - h4 { - font-size: 1.4375em; - } -} - -/* Print styles. Inlined to avoid required HTTP connection: www.phpied.com/delay-loading-your-print-css/ Credit to Paul Irish and HTML5 Boilerplate (html5boilerplate.com) -*/ -.print-only { - display: none !important; -} - -@media print { - * { - background: transparent !important; - color: #000 !important; /* Black prints faster: h5bp.com/s */ - box-shadow: none !important; - text-shadow: none !important; - } - - a, a:visited { - text-decoration: underline; - } - - a[href]:after { - content: " (" attr(href) ")"; - } - - abbr[title]:after { - content: " (" attr(title) ")"; - } - - .ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { - content: ""; - } - - pre, blockquote { - border: 1px solid #999; - page-break-inside: avoid; - } - - thead { - display: table-header-group; /* h5bp.com/t */ - } - - tr, img { - page-break-inside: avoid; - } - - img { - max-width: 100% !important; - } - - @page { - margin: 0.5cm; - } - - p, h2, h3, #toctitle, .sidebarblock > .content > .title { - orphans: 3; - widows: 3; - } - - h2, h3, #toctitle, .sidebarblock > .content > .title { - page-break-after: avoid; - } - - .hide-on-print { - display: none !important; - } - - .print-only { - display: block !important; - } - - .hide-for-print { - display: none !important; - } - - .show-for-print { - display: inherit !important; - } -} - -/* Tables */ -table { - background: white; - margin-bottom: 1.25em; - border: solid 1px #34302d; -} - -table thead, table tfoot { - font-weight: bold; -} - -table thead tr th, table thead tr td, table tfoot tr th, table tfoot tr td { - padding: 0.5em 0.625em 0.625em; - font-size: inherit; - color: #34302d; - text-align: left; -} - -table thead tr th { - color: white; - background: #34302d; -} - -table tr th, table tr td { - padding: 0.5625em 0.625em; - font-size: inherit; - color: #34302d; - border: 0 none; -} - -table tr.even, table tr.alt, table tr:nth-of-type(even) { - background: #f2F2F2; -} - -table thead tr th, table tfoot tr th, table tbody tr td, table tr td, table tfoot tr td { - display: table-cell; -} - -.clearfix:before, .clearfix:after, .float-group:before, .float-group:after { - content: " "; - display: table; -} - -.clearfix:after, .float-group:after { - clear: both; -} - -*:not(pre) > code { - font-size: inherit; - padding: 0; - white-space: nowrap; - background-color: inherit; - border: 0 solid #dddddd; - -webkit-border-radius: 6px; - border-radius: 6px; - text-shadow: none; -} - -pre, pre > code { - color: black; - font-family: monospace, serif; - font-weight: normal; -} - -.keyseq { - color: #774417; -} - -kbd:not(.keyseq) { - display: inline-block; - color: #211306; - font-size: 0.75em; - background-color: #F7F7F7; - border: 1px solid #ccc; - -webkit-border-radius: 3px; - border-radius: 3px; - -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 2px white inset; - box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 2px white inset; - margin: -0.15em 0.15em 0 0.15em; - padding: 0.2em 0.6em 0.2em 0.5em; - vertical-align: middle; - white-space: nowrap; -} - -.keyseq kbd:first-child { - margin-left: 0; -} - -.keyseq kbd:last-child { - margin-right: 0; -} - -.menuseq, .menu { - color: black; -} - -b.button:before, b.button:after { - position: relative; - top: -1px; - font-weight: normal; -} - -b.button:before { - content: "["; - padding: 0 3px 0 2px; -} - -b.button:after { - content: "]"; - padding: 0 2px 0 3px; -} - -p a > code:hover { - color: #541312; -} - -#header, #content, #footnotes, #footer { - width: 100%; - margin-left: auto; - margin-right: auto; - margin-top: 0; - margin-bottom: 0; - max-width: 62.5em; - *zoom: 1; - position: relative; - padding-left: 4em; - padding-right: 4em; -} - -#header:before, #header:after, #content:before, #content:after, #footnotes:before, #footnotes:after, #footer:before, #footer:after { - content: " "; - display: table; -} - -#header:after, #content:after, #footnotes:after, #footer:after { - clear: both; -} - -#header { - margin-bottom: 2.5em; -} - -#header > h1 { - color: #34302d; - font-weight: 400; -} - -#header span { - color: #34302d; -} - -#header #revnumber { - text-transform: capitalize; -} - -#header br { - display: none; -} - -#header br + span { -} - -#revdate { - display: block; -} - -#toc { - border-bottom: 1px solid #e6dfd8; - padding-bottom: 1.25em; -} - -#toc > ul { - margin-left: 0.25em; -} - -#toc ul.sectlevel0 > li > a { - font-style: italic; -} - -#toc ul.sectlevel0 ul.sectlevel1 { - margin-left: 0; - margin-top: 0.5em; - margin-bottom: 0.5em; -} - -#toc ul { - list-style-type: none; -} - -#toctitle { - color: #385dbd; -} - -@media only screen and (min-width: 768px) { - body.toc2 { - padding-left: 15em; - padding-right: 0; - } - - #toc.toc2 { - position: fixed; - width: 15em; - left: 0; - border-bottom: 0; - z-index: 1000; - padding: 1em; - height: 100%; - top: 0px; - background: #F1F1F1; - overflow: auto; - - -moz-transition-property: top; - -o-transition-property: top; - -webkit-transition-property: top; - transition-property: top; - -moz-transition-duration: 0.4s; - -o-transition-duration: 0.4s; - -webkit-transition-duration: 0.4s; - transition-duration: 0.4s; - } - - #reactor-header { - position: fixed; - top: -75px; - left: 0; - right: 0; - height: 75px; - - - -moz-transition-property: top; - -o-transition-property: top; - -webkit-transition-property: top; - transition-property: top; - -moz-transition-duration: 0.4s; - -o-transition-duration: 0.4s; - -webkit-transition-duration: 0.4s; - transition-duration: 0.4s; - } - - body.head-show #toc.toc2 { - top: 75px; - } - body.head-show #reactor-header { - top: 0; - } - - #toc.toc2 a { - color: #34302d; - font-family: "Raleway", Arial, sans-serif; - } - - #toc.toc2 #toctitle { - margin-top: 0; - font-size: 1.2em; - } - - #toc.toc2 > ul { - font-size: .90em; - } - - #toc.toc2 ul ul { - margin-left: 0; - padding-left: 0.4em; - } - - #toc.toc2 ul.sectlevel0 ul.sectlevel1 { - padding-left: 0; - margin-top: 0.5em; - margin-bottom: 0.5em; - } - - body.toc2.toc-right { - padding-left: 0; - padding-right: 15em; - } - - body.toc2.toc-right #toc.toc2 { - border-right: 0; - border-left: 1px solid #e6dfd8; - left: auto; - right: 0; - } -} - -@media only screen and (min-width: 1280px) { - body.toc2 { - padding-left: 20em; - padding-right: 0; - } - - #toc.toc2 { - width: 20em; - } - - #toc.toc2 #toctitle { - font-size: 1.375em; - } - - #toc.toc2 > ul { - font-size: 0.95em; - } - - #toc.toc2 ul ul { - padding-left: 1.25em; - } - - body.toc2.toc-right { - padding-left: 0; - padding-right: 20em; - } -} - -#content #toc { - border-style: solid; - border-width: 1px; - border-color: #d9d9d9; - margin-bottom: 1.25em; - padding: 1.25em; - background: #f2f2f2; - border-width: 0; - -webkit-border-radius: 6px; - border-radius: 6px; -} - -#content #toc > :first-child { - margin-top: 0; -} - -#content #toc > :last-child { - margin-bottom: 0; -} - -#content #toc a { - text-decoration: none; -} - -#content #toctitle { - font-weight: bold; - font-family: "Raleway", Arial, sans-serif; - font-size: 1em; - padding-left: 0.125em; -} - -#footer { - max-width: 100%; - background-color: white; - padding: 1.25em; - color: #CCC; - border-top: 3px solid #F1F1F1; -} - -#footer-text { - color: #444; - line-height: 1.44; -} - -.sect1 { - padding-bottom: 1.25em; -} - -.sect1 + .sect1 { - border-top: 1px solid #e6dfd8; -} - -#content h1 > a.anchor, h2 > a.anchor, h3 > a.anchor, #toctitle > a.anchor, .sidebarblock > .content > .title > a.anchor, h4 > a.anchor, h5 > a.anchor, h6 > a.anchor { - position: absolute; - width: 1em; - margin-left: -1em; - display: block; - text-decoration: none; - visibility: hidden; - text-align: center; - font-weight: normal; -} - -#content h1 > a.anchor:before, h2 > a.anchor:before, h3 > a.anchor:before, #toctitle > a.anchor:before, .sidebarblock > .content > .title > a.anchor:before, h4 > a.anchor:before, h5 > a.anchor:before, h6 > a.anchor:before { - content: '\00A7'; - font-size: .85em; - vertical-align: text-top; - display: block; - margin-top: 0.05em; -} - -#content h1:hover > a.anchor, #content h1 > a.anchor:hover, h2:hover > a.anchor, h2 > a.anchor:hover, h3:hover > a.anchor, #toctitle:hover > a.anchor, .sidebarblock > .content > .title:hover > a.anchor, h3 > a.anchor:hover, #toctitle > a.anchor:hover, .sidebarblock > .content > .title > a.anchor:hover, h4:hover > a.anchor, h4 > a.anchor:hover, h5:hover > a.anchor, h5 > a.anchor:hover, h6:hover > a.anchor, h6 > a.anchor:hover { - visibility: visible; -} - -#content h1 > a.link, h2 > a.link, h3 > a.link, #toctitle > a.link, .sidebarblock > .content > .title > a.link, h4 > a.link, h5 > a.link, h6 > a.link { - color: #34302d; - text-decoration: none; -} - -#content h1 > a.link:hover, h2 > a.link:hover, h3 > a.link:hover, #toctitle > a.link:hover, .sidebarblock > .content > .title > a.link:hover, h4 > a.link:hover, h5 > a.link:hover, h6 > a.link:hover { - color: #34302d; -} - -.imageblock, .literalblock, .listingblock, .mathblock, .verseblock, .videoblock { - margin-bottom: 1.25em; - margin-top: 1.25em; -} - -.admonitionblock td.content > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .mathblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, .sidebarblock > .title, .tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title { - text-align: left; - font-weight: bold; -} - -.tableblock > caption { - text-align: left; - font-weight: bold; - white-space: nowrap; - overflow: visible; - max-width: 0; -} - -table.tableblock #preamble > .sectionbody > .paragraph:first-of-type p { - font-size: inherit; -} - -.admonitionblock > table { - border: 0; - background: none; - width: 100%; -} - -.admonitionblock > table td.icon { - text-align: center; - width: 80px; -} - -.admonitionblock > table td.icon img { - max-width: none; -} - -.admonitionblock > table td.icon .title { - font-weight: bold; - text-transform: uppercase; -} - -.admonitionblock > table td.content { - padding-left: 1.125em; - padding-right: 1.25em; - border-left: 1px solid #dcd2c9; - color: #34302d; -} - -.admonitionblock > table td.content > :last-child > :last-child { - margin-bottom: 0; -} - -.exampleblock > .content { - border-top: 1px solid #6db33f; - border-bottom: 1px solid #6db33f; - margin-bottom: 1.25em; - padding: 1.25em; - background: white; -} - -.exampleblock > .content > :first-child { - margin-top: 0; -} - -.exampleblock > .content > :last-child { - margin-bottom: 0; -} - -.exampleblock > .content h1, .exampleblock > .content h2, .exampleblock > .content h3, .exampleblock > .content #toctitle, .sidebarblock.exampleblock > .content > .title, .exampleblock > .content h4, .exampleblock > .content h5, .exampleblock > .content h6, .exampleblock > .content p { - color: #333333; -} - -.exampleblock > .content h1, .exampleblock > .content h2, .exampleblock > .content h3, .exampleblock > .content #toctitle, .sidebarblock.exampleblock > .content > .title, .exampleblock > .content h4, .exampleblock > .content h5, .exampleblock > .content h6 { - margin-bottom: 0.625em; -} - -.exampleblock > .content h1.subheader, .exampleblock > .content h2.subheader, .exampleblock > .content h3.subheader, .exampleblock > .content .subheader#toctitle, .sidebarblock.exampleblock > .content > .subheader.title, .exampleblock > .content h4.subheader, .exampleblock > .content h5.subheader, .exampleblock > .content h6.subheader { -} - -.exampleblock.result > .content { - -webkit-box-shadow: 0 1px 8px #d9d9d9; - box-shadow: 0 1px 8px #d9d9d9; -} - -.sidebarblock { - padding: 1.25em 2em; - background: #F1F1F1; - margin: 2em -2em; - -} - -.sidebarblock > :first-child { - margin-top: 0; -} - -.sidebarblock > :last-child { - margin-bottom: 0; -} - -.sidebarblock h1, .sidebarblock h2, .sidebarblock h3, .sidebarblock #toctitle, .sidebarblock > .content > .title, .sidebarblock h4, .sidebarblock h5, .sidebarblock h6, .sidebarblock p { - color: #333333; -} - -.sidebarblock h1, .sidebarblock h2, .sidebarblock h3, .sidebarblock #toctitle, .sidebarblock > .content > .title, .sidebarblock h4, .sidebarblock h5, .sidebarblock h6 { - margin-bottom: 0.625em; -} - -.sidebarblock h1.subheader, .sidebarblock h2.subheader, .sidebarblock h3.subheader, .sidebarblock .subheader#toctitle, .sidebarblock > .content > .subheader.title, .sidebarblock h4.subheader, .sidebarblock h5.subheader, .sidebarblock h6.subheader { -} - -.sidebarblock > .content > .title { - color: #6db33f; - margin-top: 0; - font-size: 1.2em; -} - -.exampleblock > .content > :last-child > :last-child, .exampleblock > .content .olist > ol > li:last-child > :last-child, .exampleblock > .content .ulist > ul > li:last-child > :last-child, .exampleblock > .content .qlist > ol > li:last-child > :last-child, .sidebarblock > .content > :last-child > :last-child, .sidebarblock > .content .olist > ol > li:last-child > :last-child, .sidebarblock > .content .ulist > ul > li:last-child > :last-child, .sidebarblock > .content .qlist > ol > li:last-child > :last-child { - margin-bottom: 0; -} - -.literalblock pre:not([class]), .listingblock pre:not([class]) { - background-color:#f2f2f2; -} - -.literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { - border-width: 1px; - border-style: solid; - border-color: rgba(21, 35, 71, 0.1); - -webkit-border-radius: 6px; - border-radius: 6px; - padding: 0.8em; - word-wrap: break-word; -} - -.literalblock pre.nowrap, .literalblock pre[class].nowrap, .listingblock pre.nowrap, .listingblock pre[class].nowrap { - overflow-x: auto; - white-space: pre; - word-wrap: normal; -} - -.literalblock pre > code, .literalblock pre[class] > code, .listingblock pre > code, .listingblock pre[class] > code { - display: block; -} - -@media only screen { - .literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { - font-size: 0.72em; - } -} - -@media only screen and (min-width: 768px) { - .literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { - font-size: 0.81em; - } -} - -@media only screen and (min-width: 1280px) { - .literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { - font-size: 0.9em; - } -} - -.listingblock pre.highlight { - padding: 0; - line-height: 1.4em; -} - -.listingblock pre.highlight > code { - padding: 0.8em; -} - -.listingblock > .content { - position: relative; -} - -.listingblock:hover code[class*=" language-"]:before { - text-transform: uppercase; - font-size: 0.9em; - color: #999; - position: absolute; - top: 0.375em; - right: 0.375em; -} - -.listingblock:hover code.asciidoc:before { - content: "asciidoc"; -} - -.listingblock:hover code.clojure:before { - content: "clojure"; -} - -.listingblock:hover code.css:before { - content: "css"; -} - -.listingblock:hover code.groovy:before { - content: "groovy"; -} - -.listingblock:hover code.html:before { - content: "html"; -} - -.listingblock:hover code.java:before { - content: "java"; -} - -.listingblock:hover code.javascript:before { - content: "javascript"; -} - -.listingblock:hover code.python:before { - content: "python"; -} - -.listingblock:hover code.ruby:before { - content: "ruby"; -} - -.listingblock:hover code.sass:before { - content: "sass"; -} - -.listingblock:hover code.scss:before { - content: "scss"; -} - -.listingblock:hover code.xml:before { - content: "xml"; -} - -.listingblock:hover code.yaml:before { - content: "yaml"; -} - -.listingblock.terminal pre .command:before { - content: attr(data-prompt); - padding-right: 0.5em; - color: #999; -} - -.listingblock.terminal pre .command:not([data-prompt]):before { - content: '$'; -} - -table.pyhltable { - border: 0; - margin-bottom: 0; -} - -table.pyhltable td { - vertical-align: top; - padding-top: 0; - padding-bottom: 0; -} - -table.pyhltable td.code { - padding-left: .75em; - padding-right: 0; -} - -.highlight.pygments .lineno, table.pyhltable td:not(.code) { - color: #999; - padding-left: 0; - padding-right: .5em; - border-right: 1px solid #dcd2c9; -} - -.highlight.pygments .lineno { - display: inline-block; - margin-right: .25em; -} - -table.pyhltable .linenodiv { - background-color: transparent !important; - padding-right: 0 !important; -} - -.quoteblock { - margin: 0 0 1.25em; - padding: 0.5625em 1.25em 0 1.1875em; - border-left: 3px solid #dddddd; -} - -.quoteblock blockquote { - margin: 0 0 1.25em 0; - padding: 0 0 0.5625em 0; - border: 0; -} - -.quoteblock blockquote > .paragraph:last-child p { - margin-bottom: 0; -} - -.quoteblock .attribution { - margin-top: -.25em; - padding-bottom: 0.5625em; - font-size: 0.8125em; -} - -.quoteblock .attribution br { - display: none; -} - -.quoteblock .attribution cite { - display: block; - margin-bottom: 0.625em; -} - -table thead th, table tfoot th { - font-weight: bold; -} - -table.tableblock.grid-all { - border-collapse: separate; - border-radius: 6px; - border-top: 1px solid #34302d; - border-bottom: 1px solid #34302d; -} - -table.tableblock.frame-topbot, table.tableblock.frame-none { - border-left: 0; - border-right: 0; -} - -table.tableblock.frame-sides, table.tableblock.frame-none { - border-top: 0; - border-bottom: 0; -} - -table.tableblock td .paragraph:last-child p > p:last-child, table.tableblock th > p:last-child, table.tableblock td > p:last-child { - margin-bottom: 0; -} - -th.tableblock.halign-left, td.tableblock.halign-left { - text-align: left; -} - -th.tableblock.halign-right, td.tableblock.halign-right { - text-align: right; -} - -th.tableblock.halign-center, td.tableblock.halign-center { - text-align: center; -} - -th.tableblock.valign-top, td.tableblock.valign-top { - vertical-align: top; -} - -th.tableblock.valign-bottom, td.tableblock.valign-bottom { - vertical-align: bottom; -} - -th.tableblock.valign-middle, td.tableblock.valign-middle { - vertical-align: middle; -} - -tbody tr th { - display: table-cell; - background: rgba(105, 60, 22, 0.25); -} - -tbody tr th, tbody tr th p, tfoot tr th, tfoot tr th p { - color: #211306; - font-weight: bold; -} - -td > div.verse { - white-space: pre; -} - -ol { - margin-left: 1.75em; -} - -ul li ol { - margin-left: 1.5em; -} - -dl dd { - margin-left: 1.125em; -} - -dl dd:last-child, dl dd:last-child > :last-child { - margin-bottom: 0; -} - -ol > li p, ul > li p, ul dd, ol dd, .olist .olist, .ulist .ulist, .ulist .olist, .olist .ulist { - margin-bottom: 0.625em; -} - -ul.unstyled, ol.unnumbered, ul.checklist, ul.none { - list-style-type: none; -} - -ul.unstyled, ol.unnumbered, ul.checklist { - margin-left: 0.625em; -} - -ul.checklist li > p:first-child > i[class^="icon-check"]:first-child, ul.checklist li > p:first-child > input[type="checkbox"]:first-child { - margin-right: 0.25em; -} - -ul.checklist li > p:first-child > input[type="checkbox"]:first-child { - position: relative; - top: 1px; -} - -ul.inline { - margin: 0 auto 0.625em auto; - margin-left: -1.375em; - margin-right: 0; - padding: 0; - list-style: none; - overflow: hidden; -} - -ul.inline > li { - list-style: none; - float: left; - margin-left: 1.375em; - display: block; -} - -ul.inline > li > * { - display: block; -} - -.unstyled dl dt { - font-weight: normal; - font-style: normal; -} - -ol.arabic { - list-style-type: decimal; -} - -ol.decimal { - list-style-type: decimal-leading-zero; -} - -ol.loweralpha { - list-style-type: lower-alpha; -} - -ol.upperalpha { - list-style-type: upper-alpha; -} - -ol.lowerroman { - list-style-type: lower-roman; -} - -ol.upperroman { - list-style-type: upper-roman; -} - -ol.lowergreek { - list-style-type: lower-greek; -} - -.hdlist > table, .colist > table { - border: 0; - background: none; -} - -.hdlist > table > tbody > tr, .colist > table > tbody > tr { - background: none; -} - -td.hdlist1 { - padding-right: .75em; - font-weight: bold; -} - -td.hdlist1, td.hdlist2 { - vertical-align: top; -} - -.literalblock + .colist, .listingblock + .colist { - margin-top: -0.5em; -} - -.colist > table tr > td:first-of-type { - padding: 0 .75em; -} - -.colist > table tr > td:last-of-type { - padding: 0.25em 0; -} - -.qanda > ol > li > p > em:only-child { - color: #063f40; -} - -.thumb, .th { - line-height: 0; - display: inline-block; - border: solid 4px white; - -webkit-box-shadow: 0 0 0 1px #dddddd; - box-shadow: 0 0 0 1px #dddddd; -} - -.imageblock.left, .imageblock[style*="float: left"] { - margin: 0.25em 0.625em 1.25em 0; -} - -.imageblock.right, .imageblock[style*="float: right"] { - margin: 0.25em 0 1.25em 0.625em; -} - -.imageblock > .title { - margin-bottom: 0; -} - -.imageblock.thumb, .imageblock.th { - border-width: 6px; -} - -.imageblock.thumb > .title, .imageblock.th > .title { - padding: 0 0.125em; -} - -.image.left, .image.right { - margin-top: 0.25em; - margin-bottom: 0.25em; - display: inline-block; - line-height: 0; -} - -.image.left { - margin-right: 0.625em; -} - -.image.right { - margin-left: 0.625em; -} - -a.image { - text-decoration: none; -} - -span.footnote, span.footnoteref { - vertical-align: super; - font-size: 0.875em; -} - -span.footnote a, span.footnoteref a { - text-decoration: none; -} - -#footnotes { - padding-top: 0.75em; - padding-bottom: 0.75em; - margin-bottom: 0.625em; -} - -#footnotes hr { - width: 20%; - min-width: 6.25em; - margin: -.25em 0 .75em 0; - border-width: 1px 0 0 0; -} - -#footnotes .footnote { - padding: 0 0.375em; - font-size: 0.875em; - margin-left: 1.2em; - text-indent: -1.2em; - margin-bottom: .2em; -} - -#footnotes .footnote a:first-of-type { - font-weight: bold; - text-decoration: none; -} - -#footnotes .footnote:last-of-type { - margin-bottom: 0; -} - -#content #footnotes { - margin-top: -0.625em; - margin-bottom: 0; - padding: 0.75em 0; -} - -.gist .file-data > table { - border: none; - background: #fff; - width: 100%; - margin-bottom: 0; -} - -.gist .file-data > table td.line-data { - width: 99%; -} - -div.unbreakable { - page-break-inside: avoid; -} - -.big { - font-size: larger; -} - -.small { - font-size: smaller; -} - -.underline { - text-decoration: underline; -} - -.overline { - text-decoration: overline; -} - -.line-through { - text-decoration: line-through; -} - -.aqua { - color: #00bfbf; -} - -.aqua-background { - background-color: #00fafa; -} - -.black { - color: black; -} - -.black-background { - background-color: black; -} - -.blue { - color: #0000bf; -} - -.blue-background { - background-color: #0000fa; -} - -.fuchsia { - color: #bf00bf; -} - -.fuchsia-background { - background-color: #fa00fa; -} - -.gray { - color: #606060; -} - -.gray-background { - background-color: #7d7d7d; -} - -.green { - color: #006000; -} - -.green-background { - background-color: #007d00; -} - -.lime { - color: #00bf00; -} - -.lime-background { - background-color: #00fa00; -} - -.maroon { - color: #600000; -} - -.maroon-background { - background-color: #7d0000; -} - -.navy { - color: #000060; -} - -.navy-background { - background-color: #00007d; -} - -.olive { - color: #606000; -} - -.olive-background { - background-color: #7d7d00; -} - -.purple { - color: #600060; -} - -.purple-background { - background-color: #7d007d; -} - -.red { - color: #bf0000; -} - -.red-background { - background-color: #fa0000; -} - -.silver { - color: #909090; -} - -.silver-background { - background-color: #bcbcbc; -} - -.teal { - color: #006060; -} - -.teal-background { - background-color: #007d7d; -} - -.white { - color: #bfbfbf; -} - -.white-background { - background-color: #fafafa; -} - -.yellow { - color: #bfbf00; -} - -.yellow-background { - background-color: #fafa00; -} - -span.icon > [class^="icon-"], span.icon > [class*=" icon-"] { - cursor: default; -} - -.admonitionblock td.icon [class^="icon-"]:before { - font-size: 2.5em; - text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); - cursor: default; -} - -.admonitionblock td.icon .icon-note:before { - content: "\f05a"; - color: #095557; - color: #064042; -} - -.admonitionblock td.icon .icon-tip:before { - content: "\f0eb"; - text-shadow: 1px 1px 2px rgba(155, 155, 0, 0.8); - color: #111; -} - -.admonitionblock td.icon .icon-warning:before { - content: "\f071"; - color: #bf6900; -} - -.admonitionblock td.icon .icon-caution:before { - content: "\f06d"; - color: #bf3400; -} - -.admonitionblock td.icon .icon-important:before { - content: "\f06a"; - color: #bf0000; -} - -.conum { - display: inline-block; - color: white !important; - background-color: #211306; - -webkit-border-radius: 100px; - border-radius: 100px; - text-align: center; - width: 20px; - height: 20px; - font-size: 12px; - font-weight: bold; - line-height: 20px; - font-family: Arial, sans-serif; - font-style: normal; - position: relative; - top: -2px; - letter-spacing: -1px; -} - -.conum * { - color: white !important; -} - -.conum + b { - display: none; -} - -.conum:after { - content: attr(data-value); -} - -.conum:not([data-value]):empty { - display: none; -} - -body { - padding-top: 60px; -} - -#toc.toc2 ul ul { - padding-left: 1em; -} -#toc.toc2 ul ul.sectlevel2 { -} - -#toctitle { - color: #34302d; - display: none; -} - -#header h1 { - font-weight: bold; - position: relative; - left: -0.0625em; -} - -#header h1 span.lo { - color: #dc9424; -} - -#content h2, #content h3, #content #toctitle, #content .sidebarblock > .content > .title, #content h4, #content h5, #content #toctitle { - font-weight: normal; - position: relative; - left: -0.0625em; -} - -#content h2 { - font-weight: bold; -} - -.literalblock .content pre.highlight, .listingblock .content pre.highlight { - background-color:#f2f2f2; -} - -.admonitionblock > table td.content { - border-color: #e6dfd8; -} - -table.tableblock.grid-all { - -webkit-border-radius: 0; - border-radius: 0; -} - -#footer { - background-color: #while; - color: #34302d; -} - -.imageblock .title { - text-align: center; -} - -#content h1.sect0 { - font-size: 48px; -} - -#toc > ul > li > a { - font-size: large; -} diff --git a/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java b/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java index b7da8fb2fd..9c8b52849b 100644 --- a/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java +++ b/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java @@ -26,6 +26,14 @@ import io.lettuce.core.codec.Base16; import io.lettuce.core.codec.RedisCodec; import io.lettuce.core.internal.LettuceAssert; +import io.lettuce.core.json.JsonParser; +import io.lettuce.core.json.JsonType; +import io.lettuce.core.json.JsonValue; +import io.lettuce.core.json.arguments.JsonGetArgs; +import io.lettuce.core.json.arguments.JsonMsetArgs; +import io.lettuce.core.json.JsonPath; +import io.lettuce.core.json.arguments.JsonRangeArgs; +import io.lettuce.core.json.arguments.JsonSetArgs; import io.lettuce.core.models.stream.ClaimedMessages; import io.lettuce.core.models.stream.PendingMessage; import io.lettuce.core.models.stream.PendingMessages; @@ -40,6 +48,7 @@ import io.lettuce.core.protocol.CommandType; import io.lettuce.core.protocol.ProtocolKeyword; import io.lettuce.core.protocol.RedisCommand; +import reactor.core.publisher.Mono; import java.time.Duration; import java.time.Instant; @@ -71,21 +80,27 @@ public abstract class AbstractRedisAsyncCommands implements RedisAclAsyncC RedisKeyAsyncCommands, RedisStringAsyncCommands, RedisListAsyncCommands, RedisSetAsyncCommands, RedisSortedSetAsyncCommands, RedisScriptingAsyncCommands, RedisServerAsyncCommands, RedisHLLAsyncCommands, BaseRedisAsyncCommands, RedisTransactionalAsyncCommands, - RedisGeoAsyncCommands, RedisClusterAsyncCommands { + RedisGeoAsyncCommands, RedisClusterAsyncCommands, RedisJsonAsyncCommands { private final StatefulConnection connection; private final RedisCommandBuilder commandBuilder; + private final RedisJsonCommandBuilder jsonCommandBuilder; + + private final Mono parser; + /** * Initialize a new instance. * * @param connection the connection to operate on * @param codec the codec for command encoding */ - public AbstractRedisAsyncCommands(StatefulConnection connection, RedisCodec codec) { + public AbstractRedisAsyncCommands(StatefulConnection connection, RedisCodec codec, Mono parser) { + this.parser = parser; this.connection = connection; this.commandBuilder = new RedisCommandBuilder<>(codec); + this.jsonCommandBuilder = new RedisJsonCommandBuilder<>(codec, parser); } @Override @@ -389,6 +404,11 @@ public RedisFuture clientTracking(TrackingArgs args) { return dispatch(commandBuilder.clientTracking(args)); } + @Override + public RedisFuture clientTrackinginfo() { + return dispatch(commandBuilder.clientTrackinginfo()); + } + @Override public RedisFuture clientUnblock(long id, UnblockType type) { return dispatch(commandBuilder.clientUnblock(id, type)); @@ -474,6 +494,11 @@ public RedisFuture clusterMyId() { return dispatch(commandBuilder.clusterMyId()); } + @Override + public RedisFuture clusterMyShardId() { + return dispatch(commandBuilder.clusterMyShardId()); + } + @Override public RedisFuture clusterNodes() { return dispatch(commandBuilder.clusterNodes()); @@ -1443,6 +1468,176 @@ public boolean isOpen() { return connection.isOpen(); } + @Override + public RedisFuture> jsonArrappend(K key, JsonPath jsonPath, JsonValue... values) { + return dispatch(jsonCommandBuilder.jsonArrappend(key, jsonPath, values)); + } + + @Override + public RedisFuture> jsonArrappend(K key, JsonValue... values) { + return dispatch(jsonCommandBuilder.jsonArrappend(key, JsonPath.ROOT_PATH, values)); + } + + @Override + public RedisFuture> jsonArrindex(K key, JsonPath jsonPath, JsonValue value, JsonRangeArgs range) { + return dispatch(jsonCommandBuilder.jsonArrindex(key, jsonPath, value, range)); + } + + @Override + public RedisFuture> jsonArrindex(K key, JsonPath jsonPath, JsonValue value) { + return dispatch(jsonCommandBuilder.jsonArrindex(key, jsonPath, value, JsonRangeArgs.Builder.defaults())); + } + + @Override + public RedisFuture> jsonArrinsert(K key, JsonPath jsonPath, int index, JsonValue... values) { + return dispatch(jsonCommandBuilder.jsonArrinsert(key, jsonPath, index, values)); + } + + @Override + public RedisFuture> jsonArrlen(K key, JsonPath jsonPath) { + return dispatch(jsonCommandBuilder.jsonArrlen(key, jsonPath)); + } + + @Override + public RedisFuture> jsonArrlen(K key) { + return dispatch(jsonCommandBuilder.jsonArrlen(key, JsonPath.ROOT_PATH)); + } + + @Override + public RedisFuture> jsonArrpop(K key, JsonPath jsonPath, int index) { + return dispatch(jsonCommandBuilder.jsonArrpop(key, jsonPath, index)); + } + + @Override + public RedisFuture> jsonArrpop(K key, JsonPath jsonPath) { + return dispatch(jsonCommandBuilder.jsonArrpop(key, jsonPath, -1)); + } + + @Override + public RedisFuture> jsonArrpop(K key) { + return dispatch(jsonCommandBuilder.jsonArrpop(key, JsonPath.ROOT_PATH, -1)); + } + + @Override + public RedisFuture> jsonArrtrim(K key, JsonPath jsonPath, JsonRangeArgs range) { + return dispatch(jsonCommandBuilder.jsonArrtrim(key, jsonPath, range)); + } + + @Override + public RedisFuture jsonClear(K key, JsonPath jsonPath) { + return dispatch(jsonCommandBuilder.jsonClear(key, jsonPath)); + } + + @Override + public RedisFuture jsonClear(K key) { + return dispatch(jsonCommandBuilder.jsonClear(key, JsonPath.ROOT_PATH)); + } + + @Override + public RedisFuture jsonDel(K key, JsonPath jsonPath) { + return dispatch(jsonCommandBuilder.jsonDel(key, jsonPath)); + } + + @Override + public RedisFuture jsonDel(K key) { + return dispatch(jsonCommandBuilder.jsonDel(key, JsonPath.ROOT_PATH)); + } + + @Override + public RedisFuture> jsonGet(K key, JsonGetArgs options, JsonPath... jsonPaths) { + return dispatch(jsonCommandBuilder.jsonGet(key, options, jsonPaths)); + } + + @Override + public RedisFuture> jsonGet(K key, JsonPath... jsonPaths) { + return dispatch(jsonCommandBuilder.jsonGet(key, JsonGetArgs.Builder.defaults(), jsonPaths)); + } + + @Override + public RedisFuture jsonMerge(K key, JsonPath jsonPath, JsonValue value) { + return dispatch(jsonCommandBuilder.jsonMerge(key, jsonPath, value)); + } + + @Override + public RedisFuture> jsonMGet(JsonPath jsonPath, K... keys) { + return dispatch(jsonCommandBuilder.jsonMGet(jsonPath, keys)); + } + + @Override + public RedisFuture jsonMSet(List> arguments) { + return dispatch(jsonCommandBuilder.jsonMSet(arguments)); + } + + @Override + public RedisFuture> jsonNumincrby(K key, JsonPath jsonPath, Number number) { + return dispatch(jsonCommandBuilder.jsonNumincrby(key, jsonPath, number)); + } + + @Override + public RedisFuture> jsonObjkeys(K key, JsonPath jsonPath) { + return dispatch(jsonCommandBuilder.jsonObjkeys(key, jsonPath)); + } + + @Override + public RedisFuture> jsonObjkeys(K key) { + return dispatch(jsonCommandBuilder.jsonObjkeys(key, JsonPath.ROOT_PATH)); + } + + @Override + public RedisFuture> jsonObjlen(K key, JsonPath jsonPath) { + return dispatch(jsonCommandBuilder.jsonObjlen(key, jsonPath)); + } + + @Override + public RedisFuture> jsonObjlen(K key) { + return dispatch(jsonCommandBuilder.jsonObjlen(key, JsonPath.ROOT_PATH)); + } + + @Override + public RedisFuture jsonSet(K key, JsonPath jsonPath, JsonValue value, JsonSetArgs options) { + return dispatch(jsonCommandBuilder.jsonSet(key, jsonPath, value, options)); + } + + @Override + public RedisFuture jsonSet(K key, JsonPath jsonPath, JsonValue value) { + return dispatch(jsonCommandBuilder.jsonSet(key, jsonPath, value, JsonSetArgs.Builder.defaults())); + } + + @Override + public RedisFuture> jsonStrappend(K key, JsonPath jsonPath, JsonValue value) { + return dispatch(jsonCommandBuilder.jsonStrappend(key, jsonPath, value)); + } + + @Override + public RedisFuture> jsonStrappend(K key, JsonValue value) { + return dispatch(jsonCommandBuilder.jsonStrappend(key, JsonPath.ROOT_PATH, value)); + } + + @Override + public RedisFuture> jsonStrlen(K key, JsonPath jsonPath) { + return dispatch(jsonCommandBuilder.jsonStrlen(key, jsonPath)); + } + + @Override + public RedisFuture> jsonStrlen(K key) { + return dispatch(jsonCommandBuilder.jsonStrlen(key, JsonPath.ROOT_PATH)); + } + + @Override + public RedisFuture> jsonToggle(K key, JsonPath jsonPath) { + return dispatch(jsonCommandBuilder.jsonToggle(key, jsonPath)); + } + + @Override + public RedisFuture> jsonType(K key, JsonPath jsonPath) { + return dispatch(jsonCommandBuilder.jsonType(key, jsonPath)); + } + + @Override + public RedisFuture> jsonType(K key) { + return dispatch(jsonCommandBuilder.jsonType(key, JsonPath.ROOT_PATH)); + } + @Override public RedisFuture> keys(K pattern) { return dispatch(commandBuilder.keys(pattern)); @@ -3179,6 +3374,16 @@ public RedisFuture zunionstore(K destination, ZStoreArgs zStoreArgs, K... return dispatch(commandBuilder.zunionstore(destination, zStoreArgs, keys)); } + @Override + public RedisFuture>> clusterLinks() { + return dispatch(commandBuilder.clusterLinks()); + } + + @Override + public JsonParser getJsonParser() { + return this.parser.block(); + } + private byte[] encodeFunction(String functionCode) { LettuceAssert.notNull(functionCode, "Function code must not be null"); LettuceAssert.notEmpty(functionCode, "Function code script must not be empty"); diff --git a/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java b/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java index 38404c1ea4..eb7e911ca4 100644 --- a/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java +++ b/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java @@ -26,6 +26,14 @@ import io.lettuce.core.codec.Base16; import io.lettuce.core.codec.RedisCodec; import io.lettuce.core.internal.LettuceAssert; +import io.lettuce.core.json.JsonParser; +import io.lettuce.core.json.JsonPath; +import io.lettuce.core.json.JsonType; +import io.lettuce.core.json.JsonValue; +import io.lettuce.core.json.arguments.JsonGetArgs; +import io.lettuce.core.json.arguments.JsonMsetArgs; +import io.lettuce.core.json.arguments.JsonRangeArgs; +import io.lettuce.core.json.arguments.JsonSetArgs; import io.lettuce.core.models.stream.ClaimedMessages; import io.lettuce.core.models.stream.PendingMessage; import io.lettuce.core.models.stream.PendingMessages; @@ -74,6 +82,7 @@ * @author dengliming * @author Andrey Shlykov * @author Ali Takavci + * @author Tihomir Mateev * @since 4.0 */ public abstract class AbstractRedisReactiveCommands @@ -81,12 +90,16 @@ public abstract class AbstractRedisReactiveCommands RedisStringReactiveCommands, RedisListReactiveCommands, RedisSetReactiveCommands, RedisSortedSetReactiveCommands, RedisScriptingReactiveCommands, RedisServerReactiveCommands, RedisHLLReactiveCommands, BaseRedisReactiveCommands, RedisTransactionalReactiveCommands, - RedisGeoReactiveCommands, RedisClusterReactiveCommands { + RedisGeoReactiveCommands, RedisClusterReactiveCommands, RedisJsonReactiveCommands { private final StatefulConnection connection; private final RedisCommandBuilder commandBuilder; + private final RedisJsonCommandBuilder jsonCommandBuilder; + + private final Mono parser; + private final ClientResources clientResources; private final boolean tracingEnabled; @@ -99,9 +112,11 @@ public abstract class AbstractRedisReactiveCommands * @param connection the connection to operate on. * @param codec the codec for command encoding. */ - public AbstractRedisReactiveCommands(StatefulConnection connection, RedisCodec codec) { + public AbstractRedisReactiveCommands(StatefulConnection connection, RedisCodec codec, Mono parser) { this.connection = connection; + this.parser = parser; this.commandBuilder = new RedisCommandBuilder<>(codec); + this.jsonCommandBuilder = new RedisJsonCommandBuilder<>(codec, parser); this.clientResources = connection.getResources(); this.tracingEnabled = clientResources.tracing().isEnabled(); } @@ -122,6 +137,11 @@ private EventExecutorGroup getScheduler() { return this.scheduler = schedulerToUse; } + @Override + public JsonParser getJsonParser() { + return parser.block(); + } + @Override public Mono> aclCat() { return createMono(commandBuilder::aclCat); @@ -407,6 +427,11 @@ public Mono clientTracking(TrackingArgs args) { return createMono(() -> commandBuilder.clientTracking(args)); } + @Override + public Mono clientTrackinginfo() { + return createMono(commandBuilder::clientTrackinginfo); + } + @Override public Mono clientUnblock(long id, UnblockType type) { return createMono(() -> commandBuilder.clientUnblock(id, type)); @@ -496,6 +521,11 @@ public Mono clusterMyId() { return createMono(commandBuilder::clusterMyId); } + @Override + public Mono clusterMyShardId() { + return createMono(commandBuilder::clusterMyShardId); + } + @Override public Mono clusterNodes() { return createMono(commandBuilder::clusterNodes); @@ -1505,6 +1535,179 @@ public boolean isOpen() { return connection.isOpen(); } + @Override + public Flux jsonArrappend(K key, JsonPath jsonPath, JsonValue... values) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonArrappend(key, jsonPath, values)); + } + + @Override + public Flux jsonArrappend(K key, JsonValue... values) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonArrappend(key, JsonPath.ROOT_PATH, values)); + } + + @Override + public Flux jsonArrindex(K key, JsonPath jsonPath, JsonValue value, JsonRangeArgs range) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonArrindex(key, jsonPath, value, range)); + } + + @Override + public Flux jsonArrindex(K key, JsonPath jsonPath, JsonValue value) { + final JsonRangeArgs args = JsonRangeArgs.Builder.defaults(); + return createDissolvingFlux(() -> jsonCommandBuilder.jsonArrindex(key, jsonPath, value, args)); + } + + @Override + public Flux jsonArrinsert(K key, JsonPath jsonPath, int index, JsonValue... values) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonArrinsert(key, jsonPath, index, values)); + } + + @Override + public Flux jsonArrlen(K key, JsonPath jsonPath) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonArrlen(key, jsonPath)); + } + + @Override + public Flux jsonArrlen(K key) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonArrlen(key, JsonPath.ROOT_PATH)); + } + + @Override + public Flux jsonArrpop(K key, JsonPath jsonPath, int index) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonArrpop(key, jsonPath, index)); + } + + @Override + public Flux jsonArrpop(K key, JsonPath jsonPath) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonArrpop(key, jsonPath, -1)); + } + + @Override + public Flux jsonArrpop(K key) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonArrpop(key, JsonPath.ROOT_PATH, -1)); + } + + @Override + public Flux jsonArrtrim(K key, JsonPath jsonPath, JsonRangeArgs range) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonArrtrim(key, jsonPath, range)); + } + + @Override + public Mono jsonClear(K key, JsonPath jsonPath) { + return createMono(() -> jsonCommandBuilder.jsonClear(key, jsonPath)); + } + + @Override + public Mono jsonClear(K key) { + return createMono(() -> jsonCommandBuilder.jsonClear(key, JsonPath.ROOT_PATH)); + } + + @Override + public Mono jsonDel(K key, JsonPath jsonPath) { + return createMono(() -> jsonCommandBuilder.jsonDel(key, jsonPath)); + } + + @Override + public Mono jsonDel(K key) { + return createMono(() -> jsonCommandBuilder.jsonDel(key, JsonPath.ROOT_PATH)); + } + + @Override + public Flux jsonGet(K key, JsonGetArgs options, JsonPath... jsonPaths) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonGet(key, options, jsonPaths)); + } + + @Override + public Flux jsonGet(K key, JsonPath... jsonPaths) { + final JsonGetArgs args = JsonGetArgs.Builder.defaults(); + return createDissolvingFlux(() -> jsonCommandBuilder.jsonGet(key, args, jsonPaths)); + } + + @Override + public Mono jsonMerge(K key, JsonPath jsonPath, JsonValue value) { + return createMono(() -> jsonCommandBuilder.jsonMerge(key, jsonPath, value)); + } + + @Override + public Flux jsonMGet(JsonPath jsonPath, K... keys) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonMGet(jsonPath, keys)); + } + + @Override + public Mono jsonMSet(List> arguments) { + return createMono(() -> jsonCommandBuilder.jsonMSet(arguments)); + } + + @Override + public Flux jsonNumincrby(K key, JsonPath jsonPath, Number number) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonNumincrby(key, jsonPath, number)); + } + + @Override + public Flux jsonObjkeys(K key, JsonPath jsonPath) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonObjkeys(key, jsonPath)); + } + + @Override + public Flux jsonObjkeys(K key) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonObjkeys(key, JsonPath.ROOT_PATH)); + } + + @Override + public Flux jsonObjlen(K key, JsonPath jsonPath) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonObjlen(key, jsonPath)); + } + + @Override + public Flux jsonObjlen(K key) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonObjlen(key, JsonPath.ROOT_PATH)); + } + + @Override + public Mono jsonSet(K key, JsonPath jsonPath, JsonValue value, JsonSetArgs options) { + return createMono(() -> jsonCommandBuilder.jsonSet(key, jsonPath, value, options)); + } + + @Override + public Mono jsonSet(K key, JsonPath jsonPath, JsonValue value) { + final JsonSetArgs args = JsonSetArgs.Builder.defaults(); + return createMono(() -> jsonCommandBuilder.jsonSet(key, jsonPath, value, args)); + } + + @Override + public Flux jsonStrappend(K key, JsonPath jsonPath, JsonValue value) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonStrappend(key, jsonPath, value)); + } + + @Override + public Flux jsonStrappend(K key, JsonValue value) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonStrappend(key, JsonPath.ROOT_PATH, value)); + } + + @Override + public Flux jsonStrlen(K key, JsonPath jsonPath) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonStrlen(key, jsonPath)); + } + + @Override + public Flux jsonStrlen(K key) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonStrlen(key, JsonPath.ROOT_PATH)); + } + + @Override + public Flux jsonToggle(K key, JsonPath jsonPath) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonToggle(key, jsonPath)); + } + + @Override + public Flux jsonType(K key, JsonPath jsonPath) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonType(key, jsonPath)); + } + + @Override + public Flux jsonType(K key) { + return createDissolvingFlux(() -> jsonCommandBuilder.jsonType(key, JsonPath.ROOT_PATH)); + } + @Override public Flux keys(K pattern) { return createDissolvingFlux(() -> commandBuilder.keys(pattern)); @@ -3247,6 +3450,11 @@ public Mono zunionstore(K destination, ZStoreArgs zStoreArgs, K... keys) { return createMono(() -> commandBuilder.zunionstore(destination, zStoreArgs, keys)); } + @Override + public Mono>> clusterLinks() { + return createMono(commandBuilder::clusterLinks); + } + private byte[] encodeFunction(String functionCode) { LettuceAssert.notNull(functionCode, "Function code must not be null"); LettuceAssert.notEmpty(functionCode, "Function code script must not be empty"); diff --git a/src/main/java/io/lettuce/core/AclSetuserArgs.java b/src/main/java/io/lettuce/core/AclSetuserArgs.java index 4b3e27a69f..c64145c100 100644 --- a/src/main/java/io/lettuce/core/AclSetuserArgs.java +++ b/src/main/java/io/lettuce/core/AclSetuserArgs.java @@ -606,7 +606,7 @@ public void build(CommandArgs args) { @Override public String toString() { - return getClass().getSimpleName() + ": " + value.name(); + return getClass().getSimpleName() + ": " + value.toString(); } } @@ -716,7 +716,7 @@ public void build(CommandArgs args) { if (command.getSubCommand() == null) { args.add("+" + command.getCommand().name()); } else { - args.add("+" + command.getCommand().name() + "|" + command.getSubCommand().name()); + args.add("+" + command.getCommand().name() + "|" + command.getSubCommand().toString()); } } @@ -735,7 +735,7 @@ public void build(CommandArgs args) { if (command.getSubCommand() == null) { args.add("-" + command.getCommand().name()); } else { - args.add("-" + command.getCommand().name() + "|" + command.getSubCommand().name()); + args.add("-" + command.getCommand().name() + "|" + command.getSubCommand().toString()); } } diff --git a/src/main/java/io/lettuce/core/ClientOptions.java b/src/main/java/io/lettuce/core/ClientOptions.java index aa3d2ba188..9f1f1c33d9 100644 --- a/src/main/java/io/lettuce/core/ClientOptions.java +++ b/src/main/java/io/lettuce/core/ClientOptions.java @@ -22,14 +22,20 @@ import java.io.Serializable; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.util.Iterator; +import java.util.ServiceConfigurationError; +import java.util.ServiceLoader; import io.lettuce.core.api.StatefulConnection; import io.lettuce.core.internal.LettuceAssert; +import io.lettuce.core.json.JsonParser; +import io.lettuce.core.json.RedisJsonException; import io.lettuce.core.protocol.DecodeBufferPolicies; import io.lettuce.core.protocol.DecodeBufferPolicy; import io.lettuce.core.protocol.ProtocolVersion; import io.lettuce.core.protocol.ReadOnlyCommands; import io.lettuce.core.resource.ClientResources; +import reactor.core.publisher.Mono; /** * Client Options to control the behavior of {@link RedisClient}. @@ -63,11 +69,21 @@ public class ClientOptions implements Serializable { public static final SocketOptions DEFAULT_SOCKET_OPTIONS = SocketOptions.create(); + public static final Mono DEFAULT_JSON_PARSER = Mono.defer(() -> Mono.fromCallable(() -> { + try { + Iterator services = ServiceLoader.load(JsonParser.class).iterator(); + return services.hasNext() ? services.next() : null; + } catch (ServiceConfigurationError e) { + throw new RedisJsonException("Could not load JsonParser, please consult the guide" + + "at https://redis.github.io/lettuce/user-guide/redis-json/", e); + } + })); + public static final SslOptions DEFAULT_SSL_OPTIONS = SslOptions.create(); public static final boolean DEFAULT_SUSPEND_RECONNECT_PROTO_FAIL = false; - public static final TimeoutOptions DEFAULT_TIMEOUT_OPTIONS = TimeoutOptions.create(); + public static final TimeoutOptions DEFAULT_TIMEOUT_OPTIONS = TimeoutOptions.enabled(); private final boolean autoReconnect; @@ -89,6 +105,8 @@ public class ClientOptions implements Serializable { private final Charset scriptCharset; + private final Mono jsonParser; + private final SocketOptions socketOptions; private final SslOptions sslOptions; @@ -108,6 +126,7 @@ protected ClientOptions(Builder builder) { this.readOnlyCommands = builder.readOnlyCommands; this.requestQueueSize = builder.requestQueueSize; this.scriptCharset = builder.scriptCharset; + this.jsonParser = builder.jsonParser; this.socketOptions = builder.socketOptions; this.sslOptions = builder.sslOptions; this.suspendReconnectOnProtocolFailure = builder.suspendReconnectOnProtocolFailure; @@ -125,6 +144,7 @@ protected ClientOptions(ClientOptions original) { this.readOnlyCommands = original.getReadOnlyCommands(); this.requestQueueSize = original.getRequestQueueSize(); this.scriptCharset = original.getScriptCharset(); + this.jsonParser = original.getJsonParser(); this.socketOptions = original.getSocketOptions(); this.sslOptions = original.getSslOptions(); this.suspendReconnectOnProtocolFailure = original.isSuspendReconnectOnProtocolFailure(); @@ -184,6 +204,8 @@ public static class Builder { private Charset scriptCharset = DEFAULT_SCRIPT_CHARSET; + private Mono jsonParser = DEFAULT_JSON_PARSER; + private SocketOptions socketOptions = DEFAULT_SOCKET_OPTIONS; private SslOptions sslOptions = DEFAULT_SSL_OPTIONS; @@ -369,6 +391,21 @@ public Builder scriptCharset(Charset scriptCharset) { return this; } + /** + * Set a custom implementation for the {@link JsonParser} to use. + * + * @param parser a {@link Mono} that emits the {@link JsonParser} to use. + * @return {@code this} + * @see JsonParser + * @since 6.5 + */ + public Builder jsonParser(Mono parser) { + + LettuceAssert.notNull(parser, "JsonParser must not be null"); + this.jsonParser = parser; + return this; + } + /** * Sets the low-level {@link SocketOptions} for the connections kept to Redis servers. See * {@link #DEFAULT_SOCKET_OPTIONS}. @@ -449,9 +486,9 @@ public ClientOptions.Builder mutate() { .decodeBufferPolicy(getDecodeBufferPolicy()).disconnectedBehavior(getDisconnectedBehavior()) .readOnlyCommands(getReadOnlyCommands()).publishOnScheduler(isPublishOnScheduler()) .pingBeforeActivateConnection(isPingBeforeActivateConnection()).protocolVersion(getConfiguredProtocolVersion()) - .requestQueueSize(getRequestQueueSize()).scriptCharset(getScriptCharset()).socketOptions(getSocketOptions()) - .sslOptions(getSslOptions()).suspendReconnectOnProtocolFailure(isSuspendReconnectOnProtocolFailure()) - .timeoutOptions(getTimeoutOptions()); + .requestQueueSize(getRequestQueueSize()).scriptCharset(getScriptCharset()).jsonParser(getJsonParser()) + .socketOptions(getSocketOptions()).sslOptions(getSslOptions()) + .suspendReconnectOnProtocolFailure(isSuspendReconnectOnProtocolFailure()).timeoutOptions(getTimeoutOptions()); return builder; } @@ -609,6 +646,16 @@ public Charset getScriptCharset() { return scriptCharset; } + /** + * Returns the currently set up {@link JsonParser}. + * + * @return the implementation of the {@link JsonParser} to use. + * @since 6.5 + */ + public Mono getJsonParser() { + return jsonParser; + } + /** * Returns the {@link SocketOptions}. * diff --git a/src/main/java/io/lettuce/core/FutureSyncInvocationHandler.java b/src/main/java/io/lettuce/core/FutureSyncInvocationHandler.java index a080ead113..c251a35310 100644 --- a/src/main/java/io/lettuce/core/FutureSyncInvocationHandler.java +++ b/src/main/java/io/lettuce/core/FutureSyncInvocationHandler.java @@ -107,8 +107,8 @@ private static boolean isTxControlMethod(String methodName, Object[] args) { if (methodName.equals("dispatch") && args.length > 0 && args[0] instanceof ProtocolKeyword) { ProtocolKeyword keyword = (ProtocolKeyword) args[0]; - if (keyword.name().equals(CommandType.MULTI.name()) || keyword.name().equals(CommandType.EXEC.name()) - || keyword.name().equals(CommandType.DISCARD.name())) { + if (keyword.toString().equals(CommandType.MULTI.name()) || keyword.toString().equals(CommandType.EXEC.name()) + || keyword.toString().equals(CommandType.DISCARD.name())) { return true; } } diff --git a/src/main/java/io/lettuce/core/ReadFrom.java b/src/main/java/io/lettuce/core/ReadFrom.java index f247fac947..d4300ccefa 100644 --- a/src/main/java/io/lettuce/core/ReadFrom.java +++ b/src/main/java/io/lettuce/core/ReadFrom.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; import io.lettuce.core.internal.LettuceStrings; import io.lettuce.core.models.role.RedisNodeDescription; @@ -181,9 +182,10 @@ protected boolean isOrderSensitive() { } /** - * Retrieve the {@link ReadFrom} preset by name. + * Retrieve the {@link ReadFrom} preset by name. For complex types like {@code subnet} or {@code regex}, the following + * syntax could be used {@code subnet:192.168.0.0/16,2001:db8:abcd:0000::/52} and {@code regex:.*region-1.*} respectively. * - * @param name the name of the read from setting + * @param name the case-insensitive name of the read from setting * @return the {@link ReadFrom} preset * @throws IllegalArgumentException if {@code name} is empty, {@code null} or the {@link ReadFrom} preset is unknown. */ @@ -193,6 +195,25 @@ public static ReadFrom valueOf(String name) { throw new IllegalArgumentException("Name must not be empty"); } + int index = name.indexOf(':'); + if (index != -1) { + String type = name.substring(0, index); + String value = name.substring(index + 1); + if (LettuceStrings.isEmpty(value)) { + throw new IllegalArgumentException("Value must not be empty for the type '" + type + "'"); + } + if (type.equalsIgnoreCase("subnet")) { + return subnet(value.split(",")); + } + if (type.equalsIgnoreCase("regex")) { + try { + return regex(Pattern.compile(value)); + } catch (PatternSyntaxException ex) { + throw new IllegalArgumentException("Value '" + value + "' is not a valid regular expression", ex); + } + } + } + if (name.equalsIgnoreCase("master")) { return UPSTREAM; } @@ -229,14 +250,6 @@ public static ReadFrom valueOf(String name) { return ANY_REPLICA; } - if (name.equalsIgnoreCase("subnet")) { - throw new IllegalArgumentException("subnet must be created via ReadFrom#subnet"); - } - - if (name.equalsIgnoreCase("regex")) { - throw new IllegalArgumentException("regex must be created via ReadFrom#regex"); - } - throw new IllegalArgumentException("ReadFrom " + name + " not supported"); } diff --git a/src/main/java/io/lettuce/core/RedisAsyncCommandsImpl.java b/src/main/java/io/lettuce/core/RedisAsyncCommandsImpl.java index aeb17c53e8..d45215f082 100644 --- a/src/main/java/io/lettuce/core/RedisAsyncCommandsImpl.java +++ b/src/main/java/io/lettuce/core/RedisAsyncCommandsImpl.java @@ -4,6 +4,8 @@ import io.lettuce.core.api.async.RedisAsyncCommands; import io.lettuce.core.cluster.api.async.RedisClusterAsyncCommands; import io.lettuce.core.codec.RedisCodec; +import io.lettuce.core.json.JsonParser; +import reactor.core.publisher.Mono; /** * An asynchronous and thread-safe API for a Redis connection. @@ -22,8 +24,8 @@ public class RedisAsyncCommandsImpl extends AbstractRedisAsyncCommands connection, RedisCodec codec) { - super(connection, codec); + public RedisAsyncCommandsImpl(StatefulRedisConnection connection, RedisCodec codec, Mono parser) { + super(connection, codec, parser); } @Override diff --git a/src/main/java/io/lettuce/core/RedisClient.java b/src/main/java/io/lettuce/core/RedisClient.java index 550c5bf104..4a2c3e7bd3 100644 --- a/src/main/java/io/lettuce/core/RedisClient.java +++ b/src/main/java/io/lettuce/core/RedisClient.java @@ -38,6 +38,7 @@ import io.lettuce.core.internal.ExceptionFactory; import io.lettuce.core.internal.Futures; import io.lettuce.core.internal.LettuceAssert; +import io.lettuce.core.json.JsonParser; import io.lettuce.core.masterreplica.MasterReplica; import io.lettuce.core.protocol.CommandExpiryWriter; import io.lettuce.core.protocol.CommandHandler; @@ -656,7 +657,7 @@ protected StatefulRedisPubSubConnectionImpl newStatefulRedisPubSubC */ protected StatefulRedisSentinelConnectionImpl newStatefulRedisSentinelConnection( RedisChannelWriter channelWriter, RedisCodec codec, Duration timeout) { - return new StatefulRedisSentinelConnectionImpl<>(channelWriter, codec, timeout); + return new StatefulRedisSentinelConnectionImpl<>(channelWriter, codec, timeout, getOptions().getJsonParser()); } /** @@ -674,7 +675,7 @@ protected StatefulRedisSentinelConnectionImpl newStatefulRedisSenti */ protected StatefulRedisConnectionImpl newStatefulRedisConnection(RedisChannelWriter channelWriter, PushHandler pushHandler, RedisCodec codec, Duration timeout) { - return new StatefulRedisConnectionImpl<>(channelWriter, pushHandler, codec, timeout); + return new StatefulRedisConnectionImpl<>(channelWriter, pushHandler, codec, timeout, getOptions().getJsonParser()); } /** diff --git a/src/main/java/io/lettuce/core/RedisCommandBuilder.java b/src/main/java/io/lettuce/core/RedisCommandBuilder.java index 1a5e4d1645..ff403c6dda 100644 --- a/src/main/java/io/lettuce/core/RedisCommandBuilder.java +++ b/src/main/java/io/lettuce/core/RedisCommandBuilder.java @@ -62,16 +62,6 @@ @SuppressWarnings({ "unchecked", "varargs" }) class RedisCommandBuilder extends BaseRedisCommandBuilder { - private static final String MUST_NOT_CONTAIN_NULL_ELEMENTS = "must not contain null elements"; - - private static final String MUST_NOT_BE_EMPTY = "must not be empty"; - - private static final String MUST_NOT_BE_NULL = "must not be null"; - - private static final byte[] MINUS_BYTES = { '-' }; - - private static final byte[] PLUS_BYTES = { '+' }; - RedisCommandBuilder(RedisCodec codec) { super(codec); } @@ -528,6 +518,12 @@ Command clientTracking(TrackingArgs trackingArgs) { return createCommand(CLIENT, new StatusOutput<>(codec), args); } + Command clientTrackinginfo() { + CommandArgs args = new CommandArgs<>(codec).add(TRACKINGINFO); + + return new Command<>(CLIENT, new ComplexOutput<>(codec, TrackingInfoParser.INSTANCE), args); + } + Command clientUnblock(long id, UnblockType type) { LettuceAssert.notNull(type, "UnblockType " + MUST_NOT_BE_NULL); @@ -656,6 +652,12 @@ Command clusterMyId() { return createCommand(CLUSTER, new StatusOutput<>(codec), args); } + Command clusterMyShardId() { + CommandArgs args = new CommandArgs<>(codec).add(MYSHARDID); + + return createCommand(CLUSTER, new StatusOutput<>(codec), args); + } + Command clusterNodes() { CommandArgs args = new CommandArgs<>(codec).add(NODES); @@ -2907,7 +2909,7 @@ Command stralgoLcs(StrAlgoArgs strAlgoArgs) { CommandArgs args = new CommandArgs<>(codec); strAlgoArgs.build(args); - return createCommand(STRALGO, new StringMatchResultOutput<>(codec, strAlgoArgs.isWithIdx()), args); + return createCommand(STRALGO, new StringMatchResultOutput<>(codec), args); } Command> sunion(K... keys) { @@ -4421,146 +4423,9 @@ Command zunionstore(K destination, ZAggregateArgs aggregateArgs, K.. return createCommand(ZUNIONSTORE, new IntegerOutput<>(codec), args); } - private boolean allElementsInstanceOf(Object[] objects, Class expectedAssignableType) { - - for (Object object : objects) { - if (!expectedAssignableType.isAssignableFrom(object.getClass())) { - return false; - } - } - - return true; - } - - private byte[] maxValue(Range range) { - - Boundary upper = range.getUpper(); - - if (upper.getValue() == null) { - return PLUS_BYTES; - } - - ByteBuffer encoded = codec.encodeValue(upper.getValue()); - ByteBuffer allocated = ByteBuffer.allocate(encoded.remaining() + 1); - allocated.put(upper.isIncluding() ? (byte) '[' : (byte) '(').put(encoded); - - return allocated.array(); - } - - private byte[] minValue(Range range) { - - Boundary lower = range.getLower(); - - if (lower.getValue() == null) { - return MINUS_BYTES; - } - - ByteBuffer encoded = codec.encodeValue(lower.getValue()); - ByteBuffer allocated = ByteBuffer.allocate(encoded.remaining() + 1); - allocated.put(lower.isIncluding() ? (byte) '[' : (byte) '(').put(encoded); - - return allocated.array(); - } - - static void notNull(ScoredValueStreamingChannel channel) { - LettuceAssert.notNull(channel, "ScoredValueStreamingChannel " + MUST_NOT_BE_NULL); - } - - static void notNull(KeyStreamingChannel channel) { - LettuceAssert.notNull(channel, "KeyValueStreamingChannel " + MUST_NOT_BE_NULL); - } - - static void notNull(ValueStreamingChannel channel) { - LettuceAssert.notNull(channel, "ValueStreamingChannel " + MUST_NOT_BE_NULL); - } - - static void notNull(KeyValueStreamingChannel channel) { - LettuceAssert.notNull(channel, "KeyValueStreamingChannel " + MUST_NOT_BE_NULL); - } - - static void notNullMinMax(String min, String max) { - LettuceAssert.notNull(min, "Min " + MUST_NOT_BE_NULL); - LettuceAssert.notNull(max, "Max " + MUST_NOT_BE_NULL); - } - - private static void addLimit(CommandArgs args, Limit limit) { - - if (limit.isLimited()) { - args.add(LIMIT).add(limit.getOffset()).add(limit.getCount()); - } - } - - private static void assertNodeId(String nodeId) { - LettuceAssert.notNull(nodeId, "NodeId " + MUST_NOT_BE_NULL); - LettuceAssert.notEmpty(nodeId, "NodeId " + MUST_NOT_BE_EMPTY); - } - - private static String max(Range range) { - - Boundary upper = range.getUpper(); - - if (upper.getValue() == null - || upper.getValue() instanceof Double && upper.getValue().doubleValue() == Double.POSITIVE_INFINITY) { - return "+inf"; - } - - if (!upper.isIncluding()) { - return "(" + upper.getValue(); - } - - return upper.getValue().toString(); - } - - private static String min(Range range) { - - Boundary lower = range.getLower(); - - if (lower.getValue() == null - || lower.getValue() instanceof Double && lower.getValue().doubleValue() == Double.NEGATIVE_INFINITY) { - return "-inf"; - } - - if (!lower.isIncluding()) { - return "(" + lower.getValue(); - } - - return lower.getValue().toString(); - } - - private static void notEmpty(Object[] keys) { - LettuceAssert.notNull(keys, "Keys " + MUST_NOT_BE_NULL); - LettuceAssert.notEmpty(keys, "Keys " + MUST_NOT_BE_EMPTY); - } - - private static void notEmptySlots(int[] slots) { - LettuceAssert.notNull(slots, "Slots " + MUST_NOT_BE_NULL); - LettuceAssert.notEmpty(slots, "Slots " + MUST_NOT_BE_EMPTY); - } - - private static void notEmptyValues(Object[] values) { - LettuceAssert.notNull(values, "Values " + MUST_NOT_BE_NULL); - LettuceAssert.notEmpty(values, "Values " + MUST_NOT_BE_EMPTY); - } - - private static void notNullKey(Object key) { - LettuceAssert.notNull(key, "Key " + MUST_NOT_BE_NULL); - } - - private static void keyAndFieldsProvided(Object key, Object[] fields) { - LettuceAssert.notNull(key, "Key " + MUST_NOT_BE_NULL); - LettuceAssert.notEmpty(fields, "Fields " + MUST_NOT_BE_EMPTY); - } - - private static void notNullLimit(Limit limit) { - LettuceAssert.notNull(limit, "Limit " + MUST_NOT_BE_NULL); - } - - private static void notNullRange(Range range) { - LettuceAssert.notNull(range, "Range " + MUST_NOT_BE_NULL); - } - - private static void notEmptyRanges(Range[] ranges) { - LettuceAssert.notEmpty(ranges, "Ranges " + MUST_NOT_BE_NULL); + Command>> clusterLinks() { + CommandArgs args = new CommandArgs<>(codec).add(LINKS); + return createCommand(CLUSTER, (CommandOutput) new ObjectOutput<>(StringCodec.UTF8), args); } enum LongCodec implements RedisCodec { diff --git a/src/main/java/io/lettuce/core/RedisJsonCommandBuilder.java b/src/main/java/io/lettuce/core/RedisJsonCommandBuilder.java new file mode 100644 index 0000000000..ee7e8cf97b --- /dev/null +++ b/src/main/java/io/lettuce/core/RedisJsonCommandBuilder.java @@ -0,0 +1,330 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core; + +import io.lettuce.core.codec.RedisCodec; +import io.lettuce.core.json.JsonParser; +import io.lettuce.core.json.JsonType; +import io.lettuce.core.json.JsonValue; +import io.lettuce.core.json.arguments.JsonGetArgs; +import io.lettuce.core.json.arguments.JsonMsetArgs; +import io.lettuce.core.json.JsonPath; +import io.lettuce.core.json.arguments.JsonRangeArgs; +import io.lettuce.core.json.arguments.JsonSetArgs; +import io.lettuce.core.output.*; +import io.lettuce.core.protocol.BaseRedisCommandBuilder; +import io.lettuce.core.protocol.Command; +import io.lettuce.core.protocol.CommandArgs; +import reactor.core.publisher.Mono; + +import java.util.List; + +import static io.lettuce.core.protocol.CommandType.*; + +/** + * Implementation of the {@link BaseRedisCommandBuilder} handling JSON commands. + * + * @author Tihomir Mateev + * @since 6.5 + */ +class RedisJsonCommandBuilder extends BaseRedisCommandBuilder { + + private final Mono parser; + + RedisJsonCommandBuilder(RedisCodec codec, Mono theParser) { + super(codec); + parser = theParser; + } + + Command> jsonArrappend(K key, JsonPath jsonPath, JsonValue... jsonValues) { + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (jsonPath != null && !jsonPath.isRootPath()) { + args.add(jsonPath.toString()); + } + + for (JsonValue value : jsonValues) { + args.add(value.asByteBuffer().array()); + } + + return createCommand(JSON_ARRAPPEND, (CommandOutput) new ArrayOutput<>(codec), args); + } + + Command> jsonArrindex(K key, JsonPath jsonPath, JsonValue value, JsonRangeArgs range) { + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (jsonPath != null && !jsonPath.isRootPath()) { + args.add(jsonPath.toString()); + } + + args.add(value.asByteBuffer().array()); + + if (range != null) { + // OPTIONAL as per API + range.build(args); + } + + return createCommand(JSON_ARRINDEX, (CommandOutput) new ArrayOutput<>(codec), args); + } + + Command> jsonArrinsert(K key, JsonPath jsonPath, int index, JsonValue... values) { + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (jsonPath != null && !jsonPath.isRootPath()) { + args.add(jsonPath.toString()); + } + + args.add(index); + + for (JsonValue value : values) { + args.add(value.asByteBuffer().array()); + } + + return createCommand(JSON_ARRINSERT, (CommandOutput) new ArrayOutput<>(codec), args); + } + + Command> jsonArrlen(K key, JsonPath jsonPath) { + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (jsonPath != null && !jsonPath.isRootPath()) { + args.add(jsonPath.toString()); + } + return createCommand(JSON_ARRLEN, (CommandOutput) new ArrayOutput<>(codec), args); + } + + Command> jsonArrpop(K key, JsonPath jsonPath, int index) { + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (jsonPath != null) { + args.add(jsonPath.toString()); + + if (index != -1) { + args.add(index); + } + } + + return createCommand(JSON_ARRPOP, new JsonValueListOutput<>(codec, parser.block()), args); + } + + Command> jsonArrtrim(K key, JsonPath jsonPath, JsonRangeArgs range) { + + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (jsonPath != null && !jsonPath.isRootPath()) { + args.add(jsonPath.toString()); + } + + if (range != null) { + range.build(args); + } + + return createCommand(JSON_ARRTRIM, (CommandOutput) new ArrayOutput<>(codec), args); + } + + Command jsonClear(K key, JsonPath jsonPath) { + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (jsonPath != null && !jsonPath.isRootPath()) { + args.add(jsonPath.toString()); + } + + return createCommand(JSON_CLEAR, new IntegerOutput<>(codec), args); + } + + Command> jsonGet(K key, JsonGetArgs options, JsonPath... jsonPaths) { + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (options != null) { + options.build(args); + } + + if (jsonPaths != null) { + for (JsonPath jsonPath : jsonPaths) { + if (jsonPath != null) { + args.add(jsonPath.toString()); + } + } + } + + return createCommand(JSON_GET, new JsonValueListOutput<>(codec, parser.block()), args); + } + + Command jsonMerge(K key, JsonPath jsonPath, JsonValue value) { + + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (jsonPath != null && !jsonPath.isRootPath()) { + args.add(jsonPath.toString()); + } + + args.add(value.asByteBuffer().array()); + + return createCommand(JSON_MERGE, new StatusOutput<>(codec), args); + } + + Command> jsonMGet(JsonPath jsonPath, K... keys) { + notEmpty(keys); + + CommandArgs args = new CommandArgs<>(codec).addKeys(keys); + + if (jsonPath != null) { + args.add(jsonPath.toString()); + } + + return createCommand(JSON_MGET, new JsonValueListOutput<>(codec, parser.block()), args); + } + + Command jsonMSet(List> arguments) { + + notEmpty(arguments.toArray()); + + CommandArgs args = new CommandArgs<>(codec); + + for (JsonMsetArgs argument : arguments) { + argument.build(args); + } + + return createCommand(JSON_MSET, new StatusOutput<>(codec), args); + } + + Command> jsonNumincrby(K key, JsonPath jsonPath, Number number) { + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (jsonPath != null && !jsonPath.isRootPath()) { + args.add(jsonPath.toString()); + } + + args.add(number.toString()); + + return createCommand(JSON_NUMINCRBY, new NumberListOutput<>(codec), args); + } + + Command> jsonObjkeys(K key, JsonPath jsonPath) { + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (jsonPath != null && !jsonPath.isRootPath()) { + args.add(jsonPath.toString()); + } + + return createCommand(JSON_OBJKEYS, new ValueListOutput<>(codec), args); + } + + Command> jsonObjlen(K key, JsonPath jsonPath) { + + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (jsonPath != null && !jsonPath.isRootPath()) { + args.add(jsonPath.toString()); + } + + return createCommand(JSON_OBJLEN, (CommandOutput) new ArrayOutput<>(codec), args); + } + + Command jsonSet(K key, JsonPath jsonPath, JsonValue value, JsonSetArgs options) { + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + args.add(jsonPath.toString()); + + args.add(value.asByteBuffer().array()); + + if (options != null) { + options.build(args); + } + + return createCommand(JSON_SET, new StatusOutput<>(codec), args); + } + + Command> jsonStrappend(K key, JsonPath jsonPath, JsonValue value) { + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (jsonPath != null && !jsonPath.isRootPath()) { + args.add(jsonPath.toString()); + } + + args.add(value.asByteBuffer().array()); + + return createCommand(JSON_STRAPPEND, (CommandOutput) new ArrayOutput<>(codec), args); + } + + Command> jsonStrlen(K key, JsonPath jsonPath) { + + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (jsonPath != null && !jsonPath.isRootPath()) { + args.add(jsonPath.toString()); + } + + return createCommand(JSON_STRLEN, (CommandOutput) new ArrayOutput<>(codec), args); + } + + Command> jsonToggle(K key, JsonPath jsonPath) { + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (jsonPath != null && !jsonPath.isRootPath()) { + args.add(jsonPath.toString()); + } + + return createCommand(JSON_TOGGLE, (CommandOutput) new ArrayOutput<>(codec), args); + } + + Command jsonDel(K key, JsonPath jsonPath) { + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (jsonPath != null && !jsonPath.isRootPath()) { + args.add(jsonPath.toString()); + } + return createCommand(JSON_DEL, new IntegerOutput<>(codec), args); + } + + Command> jsonType(K key, JsonPath jsonPath) { + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + + if (jsonPath != null && !jsonPath.isRootPath()) { + args.add(jsonPath.toString()); + } + + return createCommand(JSON_TYPE, new JsonTypeListOutput<>(codec), args); + } + +} diff --git a/src/main/java/io/lettuce/core/RedisReactiveCommandsImpl.java b/src/main/java/io/lettuce/core/RedisReactiveCommandsImpl.java index 6b28b8e051..fae12f611b 100644 --- a/src/main/java/io/lettuce/core/RedisReactiveCommandsImpl.java +++ b/src/main/java/io/lettuce/core/RedisReactiveCommandsImpl.java @@ -4,6 +4,8 @@ import io.lettuce.core.api.reactive.RedisReactiveCommands; import io.lettuce.core.cluster.api.reactive.RedisClusterReactiveCommands; import io.lettuce.core.codec.RedisCodec; +import io.lettuce.core.json.JsonParser; +import reactor.core.publisher.Mono; /** * A reactive and thread-safe API for a Redis Sentinel connection. @@ -22,8 +24,9 @@ public class RedisReactiveCommandsImpl extends AbstractRedisReactiveComman * @param codec the codec for command encoding. * */ - public RedisReactiveCommandsImpl(StatefulRedisConnection connection, RedisCodec codec) { - super(connection, codec); + public RedisReactiveCommandsImpl(StatefulRedisConnection connection, RedisCodec codec, + Mono parser) { + super(connection, codec, parser); } @Override diff --git a/src/main/java/io/lettuce/core/ScanIterator.java b/src/main/java/io/lettuce/core/ScanIterator.java index d3b617d240..0e8e26e81c 100644 --- a/src/main/java/io/lettuce/core/ScanIterator.java +++ b/src/main/java/io/lettuce/core/ScanIterator.java @@ -341,7 +341,7 @@ private ScoredValueScanCursor getNextScanCursor(ScanCursor scanCursor) { * @return a {@link Stream} for this {@link ScanIterator}. */ public Stream stream() { - return StreamSupport.stream(Spliterators.spliterator(this, 0, 0), false); + return StreamSupport.stream(Spliterators.spliterator(this, -1, 0), false); } /** diff --git a/src/main/java/io/lettuce/core/StatefulRedisConnectionImpl.java b/src/main/java/io/lettuce/core/StatefulRedisConnectionImpl.java index 5f64e272e8..aef29c99bb 100644 --- a/src/main/java/io/lettuce/core/StatefulRedisConnectionImpl.java +++ b/src/main/java/io/lettuce/core/StatefulRedisConnectionImpl.java @@ -36,9 +36,11 @@ import io.lettuce.core.cluster.api.sync.RedisClusterCommands; import io.lettuce.core.codec.RedisCodec; import io.lettuce.core.codec.StringCodec; +import io.lettuce.core.json.JsonParser; import io.lettuce.core.output.MultiOutput; import io.lettuce.core.output.StatusOutput; import io.lettuce.core.protocol.*; +import reactor.core.publisher.Mono; /** * A thread-safe connection to a Redis server. Multiple threads may share one {@link StatefulRedisConnectionImpl} @@ -64,6 +66,8 @@ public class StatefulRedisConnectionImpl extends RedisChannelHandler private final PushHandler pushHandler; + private final Mono parser; + protected MultiOutput multi; /** @@ -75,12 +79,13 @@ public class StatefulRedisConnectionImpl extends RedisChannelHandler * @param timeout Maximum time to wait for a response. */ public StatefulRedisConnectionImpl(RedisChannelWriter writer, PushHandler pushHandler, RedisCodec codec, - Duration timeout) { + Duration timeout, Mono parser) { super(writer, timeout); this.pushHandler = pushHandler; this.codec = codec; + this.parser = parser; this.async = newRedisAsyncCommandsImpl(); this.sync = newRedisSyncCommandsImpl(); this.reactive = newRedisReactiveCommandsImpl(); @@ -110,7 +115,7 @@ protected RedisCommands newRedisSyncCommandsImpl() { * @return a new instance */ protected RedisAsyncCommandsImpl newRedisAsyncCommandsImpl() { - return new RedisAsyncCommandsImpl<>(this, codec); + return new RedisAsyncCommandsImpl<>(this, codec, parser); } @Override @@ -124,7 +129,7 @@ public RedisReactiveCommands reactive() { * @return a new instance */ protected RedisReactiveCommandsImpl newRedisReactiveCommandsImpl() { - return new RedisReactiveCommandsImpl<>(this, codec); + return new RedisReactiveCommandsImpl<>(this, codec, parser); } @Override @@ -184,7 +189,7 @@ public RedisCommand dispatch(RedisCommand command) { private void potentiallyEnableMulti(RedisCommand command) { - if (command.getType().name().equals(MULTI.name())) { + if (command.getType().toString().equals(MULTI.name())) { multi = (multi == null ? new MultiOutput<>(codec) : multi); @@ -202,7 +207,7 @@ protected RedisCommand preProcessCommand(RedisCommand comm RedisCommand local = command; - if (local.getType().name().equals(AUTH.name())) { + if (local.getType().toString().equals(AUTH.name())) { local = attachOnComplete(local, status -> { if ("OK".equals(status)) { @@ -219,7 +224,7 @@ protected RedisCommand preProcessCommand(RedisCommand comm }); } - if (local.getType().name().equals(SELECT.name())) { + if (local.getType().toString().equals(SELECT.name())) { local = attachOnComplete(local, status -> { if ("OK".equals(status)) { Long db = CommandArgsAccessor.getFirstInteger(command.getArgs()); @@ -230,7 +235,7 @@ protected RedisCommand preProcessCommand(RedisCommand comm }); } - if (local.getType().name().equals(READONLY.name())) { + if (local.getType().toString().equals(READONLY.name())) { local = attachOnComplete(local, status -> { if ("OK".equals(status)) { state.setReadOnly(true); @@ -238,7 +243,7 @@ protected RedisCommand preProcessCommand(RedisCommand comm }); } - if (local.getType().name().equals(READWRITE.name())) { + if (local.getType().toString().equals(READWRITE.name())) { local = attachOnComplete(local, status -> { if ("OK".equals(status)) { state.setReadOnly(false); @@ -246,14 +251,14 @@ protected RedisCommand preProcessCommand(RedisCommand comm }); } - if (local.getType().name().equals(DISCARD.name())) { + if (local.getType().toString().equals(DISCARD.name())) { if (multi != null) { multi.cancel(); multi = null; } } - if (local.getType().name().equals(EXEC.name())) { + if (local.getType().toString().equals(EXEC.name())) { MultiOutput multiOutput = this.multi; this.multi = null; if (multiOutput == null) { @@ -262,7 +267,7 @@ protected RedisCommand preProcessCommand(RedisCommand comm local.setOutput((MultiOutput) multiOutput); } - if (multi != null && !local.getType().name().equals(MULTI.name())) { + if (multi != null && !local.getType().toString().equals(MULTI.name())) { local = new TransactionalCommand<>(local); multi.add(local); } diff --git a/src/main/java/io/lettuce/core/TrackingInfo.java b/src/main/java/io/lettuce/core/TrackingInfo.java new file mode 100644 index 0000000000..1d69dcd06a --- /dev/null +++ b/src/main/java/io/lettuce/core/TrackingInfo.java @@ -0,0 +1,165 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.List; + +/** + * Contains the output of a CLIENT TRACKINGINFO + * command. + * + * @author Tihomir Mateev + * @since 6.5 + */ +public class TrackingInfo { + + private final Set flags = new HashSet<>(); + + private final long redirect; + + private final List prefixes = new ArrayList<>(); + + /** + * Constructor + * + * @param flags a {@link Set} of {@link TrackingFlag}s that the command returned + * @param redirect the client ID used for notification redirection, -1 when none + * @param prefixes a {@link List} of key prefixes for which notifications are sent to the client + * + * @see TrackingFlag + */ + public TrackingInfo(Set flags, long redirect, List prefixes) { + this.flags.addAll(flags); + this.redirect = redirect; + this.prefixes.addAll(prefixes); + } + + /** + * @return set of all the {@link TrackingFlag}s currently enabled on the client connection + */ + public Set getFlags() { + return Collections.unmodifiableSet(flags); + } + + /** + * @return the client ID used for notification redirection, -1 when none + */ + public long getRedirect() { + return redirect; + } + + /** + * @return a {@link List} of key prefixes for which notifications are sent to the client + */ + public List getPrefixes() { + return Collections.unmodifiableList(prefixes); + } + + @Override + public String toString() { + return "TrackingInfo{" + "flags=" + flags + ", redirect=" + redirect + ", prefixes=" + prefixes + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + TrackingInfo that = (TrackingInfo) o; + return redirect == that.redirect && Objects.equals(flags, that.flags) && Objects.equals(prefixes, that.prefixes); + } + + @Override + public int hashCode() { + return Objects.hash(flags, redirect, prefixes); + } + + /** + * CLIENT TRACKINGINFO flags + * + * @see CLIENT TRACKINGINFO + */ + public enum TrackingFlag { + + /** + * The connection isn't using server assisted client side caching. + */ + OFF, + /** + * Server assisted client side caching is enabled for the connection. + */ + ON, + /** + * The client uses broadcasting mode. + */ + BCAST, + /** + * The client does not cache keys by default. + */ + OPTIN, + /** + * The client caches keys by default. + */ + OPTOUT, + /** + * The next command will cache keys (exists only together with optin). + */ + CACHING_YES, + /** + * The next command won't cache keys (exists only together with optout). + */ + CACHING_NO, + /** + * The client isn't notified about keys modified by itself. + */ + NOLOOP, + /** + * The client ID used for redirection isn't valid anymore. + */ + BROKEN_REDIRECT; + + /** + * Convert a given {@link String} flag to the corresponding {@link TrackingFlag} + * + * @param flag a {@link String} representation of the flag + * @return the resulting {@link TrackingFlag} or {@link IllegalArgumentException} if unrecognized + */ + public static TrackingFlag from(String flag) { + switch (flag.toLowerCase()) { + case "off": + return OFF; + case "on": + return ON; + case "bcast": + return BCAST; + case "optin": + return OPTIN; + case "optout": + return OPTOUT; + case "caching-yes": + return CACHING_YES; + case "caching-no": + return CACHING_NO; + case "noloop": + return NOLOOP; + case "broken_redirect": + return BROKEN_REDIRECT; + default: + throw new RuntimeException("Unsupported flag: " + flag); + } + } + + } + +} diff --git a/src/main/java/io/lettuce/core/api/async/RedisAsyncCommands.java b/src/main/java/io/lettuce/core/api/async/RedisAsyncCommands.java index be442dd70b..6ff3ef9ad1 100644 --- a/src/main/java/io/lettuce/core/api/async/RedisAsyncCommands.java +++ b/src/main/java/io/lettuce/core/api/async/RedisAsyncCommands.java @@ -22,6 +22,7 @@ import io.lettuce.core.RedisFuture; import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.cluster.api.async.RedisClusterAsyncCommands; +import io.lettuce.core.json.JsonParser; /** * A complete asynchronous and thread-safe Redis API with 400+ Methods. @@ -36,7 +37,7 @@ public interface RedisAsyncCommands extends BaseRedisAsyncCommands, RedisHashAsyncCommands, RedisHLLAsyncCommands, RedisKeyAsyncCommands, RedisListAsyncCommands, RedisScriptingAsyncCommands, RedisServerAsyncCommands, RedisSetAsyncCommands, RedisSortedSetAsyncCommands, RedisStreamAsyncCommands, RedisStringAsyncCommands, - RedisTransactionalAsyncCommands { + RedisTransactionalAsyncCommands, RedisJsonAsyncCommands { /** * Authenticate to the server. @@ -81,4 +82,10 @@ public interface RedisAsyncCommands extends BaseRedisAsyncCommands, @Deprecated StatefulRedisConnection getStatefulConnection(); + /** + * @return the currently configured instance of the {@link JsonParser} + * @since 6.5 + */ + JsonParser getJsonParser(); + } diff --git a/src/main/java/io/lettuce/core/api/async/RedisJsonAsyncCommands.java b/src/main/java/io/lettuce/core/api/async/RedisJsonAsyncCommands.java new file mode 100644 index 0000000000..74494e2718 --- /dev/null +++ b/src/main/java/io/lettuce/core/api/async/RedisJsonAsyncCommands.java @@ -0,0 +1,441 @@ +/* + * Copyright 2017-2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ +package io.lettuce.core.api.async; + +import java.util.List; +import io.lettuce.core.RedisFuture; +import io.lettuce.core.json.JsonPath; +import io.lettuce.core.json.JsonType; +import io.lettuce.core.json.JsonValue; +import io.lettuce.core.json.arguments.JsonGetArgs; +import io.lettuce.core.json.arguments.JsonMsetArgs; +import io.lettuce.core.json.arguments.JsonRangeArgs; +import io.lettuce.core.json.arguments.JsonSetArgs; + +/** + * Asynchronous executed commands for JSON documents + * + * @param Key type. + * @param Value type. + * @author Tihomir Mateev + * @see Redis JSON + * @since 6.5 + * @generated by io.lettuce.apigenerator.CreateAsyncApi + */ +public interface RedisJsonAsyncCommands { + + /** + * Append the JSON values into the array at a given {@link JsonPath} after the last element in a said array. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param values one or more {@link JsonValue} to be appended. + * @return Long the resulting size of the arrays after the new data was appended, or null if the path does not exist. + * @since 6.5 + */ + RedisFuture> jsonArrappend(K key, JsonPath jsonPath, JsonValue... values); + + /** + * Append the JSON values into the array at the {@link JsonPath#ROOT_PATH} after the last element in a said array. + * + * @param key the key holding the JSON document. + * @param values one or more {@link JsonValue} to be appended. + * @return Long the resulting size of the arrays after the new data was appended, or null if the path does not exist. + * @since 6.5 + */ + RedisFuture> jsonArrappend(K key, JsonValue... values); + + /** + * Search for the first occurrence of a {@link JsonValue} in an array at a given {@link JsonPath} and return its index. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param value the {@link JsonValue} to search for. + * @param range the {@link JsonRangeArgs} to search within. + * @return Long the index hosting the searched element, -1 if not found or null if the specified path is not an array. + * @since 6.5 + */ + RedisFuture> jsonArrindex(K key, JsonPath jsonPath, JsonValue value, JsonRangeArgs range); + + /** + * Search for the first occurrence of a {@link JsonValue} in an array at a given {@link JsonPath} and return its index. This + * method uses defaults for the start and end indexes, see {@link JsonRangeArgs#DEFAULT_START_INDEX} and + * {@link JsonRangeArgs#DEFAULT_END_INDEX}. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param value the {@link JsonValue} to search for. + * @return Long the index hosting the searched element, -1 if not found or null if the specified path is not an array. + * @since 6.5 + */ + RedisFuture> jsonArrindex(K key, JsonPath jsonPath, JsonValue value); + + /** + * Insert the {@link JsonValue}s into the array at a given {@link JsonPath} before the provided index, shifting the existing + * elements to the right + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param index the index before which the new elements will be inserted. + * @param values one or more {@link JsonValue}s to be inserted. + * @return Long the resulting size of the arrays after the new data was inserted, or null if the path does not exist. + * @since 6.5 + */ + RedisFuture> jsonArrinsert(K key, JsonPath jsonPath, int index, JsonValue... values); + + /** + * Report the length of the JSON array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @return the size of the arrays, or null if the path does not exist. + * @since 6.5 + */ + RedisFuture> jsonArrlen(K key, JsonPath jsonPath); + + /** + * Report the length of the JSON array at a the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return the size of the arrays, or null if the path does not exist. + * @since 6.5 + */ + RedisFuture> jsonArrlen(K key); + + /** + * Remove and return {@link JsonValue} at a given index in the array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param index the index of the element to be removed. Default is -1, meaning the last element. Out-of-range indexes round + * to their respective array ends. Popping an empty array returns null. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + RedisFuture> jsonArrpop(K key, JsonPath jsonPath, int index); + + /** + * Remove and return {@link JsonValue} at index -1 (last element) in the array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + RedisFuture> jsonArrpop(K key, JsonPath jsonPath); + + /** + * Remove and return {@link JsonValue} at index -1 (last element) in the array at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + RedisFuture> jsonArrpop(K key); + + /** + * Trim an array at a given {@link JsonPath} so that it contains only the specified inclusive range of elements. All + * elements with indexes smaller than the start range and all elements with indexes bigger than the end range are trimmed. + *

+ * Behavior as of RedisJSON v2.0: + *

    + *
  • If start is larger than the array's size or start > stop, returns 0 and an empty array.
  • + *
  • If start is < 0, then start from the end of the array.
  • + *
  • If stop is larger than the end of the array, it is treated like the last element.
  • + *
+ * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param range the {@link JsonRangeArgs} to trim by. + * @return Long the resulting size of the arrays after the trimming, or null if the path does not exist. + * @since 6.5 + */ + RedisFuture> jsonArrtrim(K key, JsonPath jsonPath, JsonRangeArgs range); + + /** + * Clear container values (arrays/objects) and set numeric values to 0 + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to clear. + * @return Long the number of values removed plus all the matching JSON numerical values that are zeroed. + * @since 6.5 + */ + RedisFuture jsonClear(K key, JsonPath jsonPath); + + /** + * Clear container values (arrays/objects) and set numeric values to 0 at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return Long the number of values removed plus all the matching JSON numerical values that are zeroed. + * @since 6.5 + */ + RedisFuture jsonClear(K key); + + /** + * Deletes a value inside the JSON document at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to clear. + * @return Long the number of values removed (0 or more). + * @since 6.5 + */ + RedisFuture jsonDel(K key, JsonPath jsonPath); + + /** + * Deletes a value inside the JSON document at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return Long the number of values removed (0 or more). + * @since 6.5 + */ + RedisFuture jsonDel(K key); + + /** + * Return the value at the specified path in JSON serialized form. + *

+ * When using a single JSONPath, the root of the matching values is a JSON string with a top-level array of serialized JSON + * value. In contrast, a legacy path returns a single value. + *

+ * When using multiple JSONPath arguments, the root of the matching values is a JSON string with a top-level object, with + * each object value being a top-level array of serialized JSON value. In contrast, if all paths are legacy paths, each + * object value is a single serialized JSON value. If there are multiple paths that include both legacy path and JSONPath, + * the returned value conforms to the JSONPath version (an array of values). + * + * @param key the key holding the JSON document. + * @param options the {@link JsonGetArgs} to use. + * @param jsonPaths the {@link JsonPath}s to use to identify the values to get. + * @return JsonValue the value at path in JSON serialized form, or null if the path does not exist. + * @since 6.5 + */ + RedisFuture> jsonGet(K key, JsonGetArgs options, JsonPath... jsonPaths); + + /** + * Return the value at the specified path in JSON serialized form. Uses defaults for the {@link JsonGetArgs}. + *

+ * When using a single JSONPath, the root of the matching values is a JSON string with a top-level array of serialized JSON + * value. In contrast, a legacy path returns a single value. + *

+ * When using multiple JSONPath arguments, the root of the matching values is a JSON string with a top-level object, with + * each object value being a top-level array of serialized JSON value. In contrast, if all paths are legacy paths, each + * object value is a single serialized JSON value. If there are multiple paths that include both legacy path and JSONPath, + * the returned value conforms to the JSONPath version (an array of values). + * + * @param key the key holding the JSON document. + * @param jsonPaths the {@link JsonPath}s to use to identify the values to get. + * @return JsonValue the value at path in JSON serialized form, or null if the path does not exist. + * @since 6.5 + */ + RedisFuture> jsonGet(K key, JsonPath... jsonPaths); + + /** + * Merge a given {@link JsonValue} with the value matching {@link JsonPath}. Consequently, JSON values at matching paths are + * updated, deleted, or expanded with new children. + *

+ * Merging is done according to the following rules per JSON value in the value argument while considering the corresponding + * original value if it exists: + *

    + *
  • merging an existing object key with a null value deletes the key
  • + *
  • merging an existing object key with non-null value updates the value
  • + *
  • merging a non-existing object key adds the key and value
  • + *
  • merging an existing array with any merged value, replaces the entire array with the value
  • + *
+ *

+ * This command complies with RFC7396 "Json Merge Patch" + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to merge. + * @param value the {@link JsonValue} to merge. + * @return String "OK" if the set was successful, error if the operation failed. + * @since 6.5 + * @see RFC7396 + */ + RedisFuture jsonMerge(K key, JsonPath jsonPath, JsonValue value); + + /** + * Return the values at the specified path from multiple key arguments. + * + * @param jsonPath the {@link JsonPath} pointing to the value to fetch. + * @param keys the keys holding the {@link JsonValue}s to fetch. + * @return List the values at path, or null if the path does not exist. + * @since 6.5 + */ + RedisFuture> jsonMGet(JsonPath jsonPath, K... keys); + + /** + * Set or update one or more JSON values according to the specified {@link JsonMsetArgs} + *

+ * JSON.MSET is atomic, hence, all given additions or updates are either applied or not. It is not possible for clients to + * see that some keys were updated while others are unchanged. + *

+ * A JSON value is a hierarchical structure. If you change a value in a specific path - nested values are affected. + * + * @param arguments the {@link JsonMsetArgs} specifying the values to change. + * @return "OK" if the operation was successful, error otherwise + * @since 6.5 + */ + RedisFuture jsonMSet(List> arguments); + + /** + * Increment the number value stored at the specified {@link JsonPath} in the JSON document by the provided increment. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to increment. + * @param number the increment value. + * @return a {@link List} of the new values after the increment. + * @since 6.5 + */ + RedisFuture> jsonNumincrby(K key, JsonPath jsonPath, Number number); + + /** + * Return the keys in the JSON document that are referenced by the given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) whose key(s) we want. + * @return List the keys in the JSON document that are referenced by the given {@link JsonPath}. + * @since 6.5 + */ + RedisFuture> jsonObjkeys(K key, JsonPath jsonPath); + + /** + * Return the keys in the JSON document that are referenced by the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return List the keys in the JSON document that are referenced by the given {@link JsonPath}. + * @since 6.5 + */ + RedisFuture> jsonObjkeys(K key); + + /** + * Report the number of keys in the JSON object at the specified {@link JsonPath} and for the provided key + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) whose key(s) we want to count + * @return Long the number of keys in the JSON object at the specified path, or null if the path does not exist. + * @since 6.5 + */ + RedisFuture> jsonObjlen(K key, JsonPath jsonPath); + + /** + * Report the number of keys in the JSON object at the {@link JsonPath#ROOT_PATH} and for the provided key + * + * @param key the key holding the JSON document. + * @return Long the number of keys in the JSON object at the specified path, or null if the path does not exist. + * @since 6.5 + */ + RedisFuture> jsonObjlen(K key); + + /** + * Sets the JSON value at a given {@link JsonPath} in the JSON document. + *

+ * For new Redis keys, the path must be the root. For existing keys, when the entire path exists, the value that it contains + * is replaced with the JSON value. For existing keys, when the path exists, except for the last element, a new child is + * added with the JSON value. + *

+ * Adds a key (with its respective value) to a JSON Object (in a RedisJSON data type key) only if it is the last child in + * the path, or it is the parent of a new child being added in the path. Optional arguments NX and XX modify this behavior + * for both new RedisJSON data type keys and the JSON Object keys in them. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to set the value. + * @param value the {@link JsonValue} to set. + * @param options the {@link JsonSetArgs} the options for setting the value. + * @return String "OK" if the set was successful, null if the {@link JsonSetArgs} conditions are not met. + * @since 6.5 + */ + RedisFuture jsonSet(K key, JsonPath jsonPath, JsonValue value, JsonSetArgs options); + + /** + * Sets the JSON value at a given {@link JsonPath} in the JSON document using defaults for the {@link JsonSetArgs}. + *

+ * For new Redis keys the path must be the root. For existing keys, when the entire path exists, the value that it contains + * is replaced with the JSON value. For existing keys, when the path exists, except for the last element, a new child is + * added with the JSON value. + *

+ * Adds a key (with its respective value) to a JSON Object (in a RedisJSON data type key) only if it is the last child in + * the path, or it is the parent of a new child being added in the path. Optional arguments NX and XX modify this behavior + * for both new RedisJSON data type keys and the JSON Object keys in them. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to set the value. + * @param value the {@link JsonValue} to set. + * @return String "OK" if the set was successful, null if the {@link JsonSetArgs} conditions are not met. + * @since 6.5 + */ + RedisFuture jsonSet(K key, JsonPath jsonPath, JsonValue value); + + /** + * Append the json-string values to the string at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to append the value. + * @param value the {@link JsonValue} to append. + * @return Long the new length of the string, or null if the matching JSON value is not a string. + * @since 6.5 + */ + RedisFuture> jsonStrappend(K key, JsonPath jsonPath, JsonValue value); + + /** + * Append the json-string values to the string at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @param value the {@link JsonValue} to append. + * @return Long the new length of the string, or null if the matching JSON value is not a string. + * @since 6.5 + */ + RedisFuture> jsonStrappend(K key, JsonValue value); + + /** + * Report the length of the JSON String at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return Long (in recursive descent) the length of the JSON String at the provided {@link JsonPath}, or null if the value + * ath the desired path is not a string. + * @since 6.5 + */ + RedisFuture> jsonStrlen(K key, JsonPath jsonPath); + + /** + * Report the length of the JSON String at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @return Long (in recursive descent) the length of the JSON String at the provided {@link JsonPath}, or null if the value + * ath the desired path is not a string. + * @since 6.5 + */ + RedisFuture> jsonStrlen(K key); + + /** + * Toggle a Boolean value stored at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return List the new value after the toggle, 0 for false, 1 for true or null if the path does not exist. + * @since 6.5 + */ + RedisFuture> jsonToggle(K key, JsonPath jsonPath); + + /** + * Report the type of JSON value at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return List the type of JSON value at the provided {@link JsonPath} + * @since 6.5 + */ + RedisFuture> jsonType(K key, JsonPath jsonPath); + + /** + * Report the type of JSON value at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @return List the type of JSON value at the provided {@link JsonPath} + * @since 6.5 + */ + RedisFuture> jsonType(K key); + +} diff --git a/src/main/java/io/lettuce/core/api/async/RedisServerAsyncCommands.java b/src/main/java/io/lettuce/core/api/async/RedisServerAsyncCommands.java index 86714aab9a..a4245c2d54 100644 --- a/src/main/java/io/lettuce/core/api/async/RedisServerAsyncCommands.java +++ b/src/main/java/io/lettuce/core/api/async/RedisServerAsyncCommands.java @@ -30,6 +30,7 @@ import io.lettuce.core.ShutdownArgs; import io.lettuce.core.TrackingArgs; import io.lettuce.core.UnblockType; +import io.lettuce.core.TrackingInfo; import io.lettuce.core.protocol.CommandType; /** @@ -177,6 +178,14 @@ public interface RedisServerAsyncCommands { */ RedisFuture clientTracking(TrackingArgs args); + /** + * Returns information about the current client connection's use of the server assisted client side caching feature. + * + * @return {@link TrackingInfo}, for more information check the documentation + * @since 6.5 + */ + RedisFuture clientTrackinginfo(); + /** * Unblock the specified blocked client. * diff --git a/src/main/java/io/lettuce/core/api/reactive/BaseRedisReactiveCommands.java b/src/main/java/io/lettuce/core/api/reactive/BaseRedisReactiveCommands.java index b0d089b237..098500c1e5 100644 --- a/src/main/java/io/lettuce/core/api/reactive/BaseRedisReactiveCommands.java +++ b/src/main/java/io/lettuce/core/api/reactive/BaseRedisReactiveCommands.java @@ -21,6 +21,7 @@ import java.util.Map; +import io.lettuce.core.json.JsonParser; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import io.lettuce.core.output.CommandOutput; @@ -228,4 +229,10 @@ public interface BaseRedisReactiveCommands { @Deprecated void flushCommands(); + /** + * @return the currently configured instance of the {@link JsonParser} + * @since 6.5 + */ + JsonParser getJsonParser(); + } diff --git a/src/main/java/io/lettuce/core/api/reactive/RedisHashReactiveCommands.java b/src/main/java/io/lettuce/core/api/reactive/RedisHashReactiveCommands.java index c0bc9f073d..99d60e7053 100644 --- a/src/main/java/io/lettuce/core/api/reactive/RedisHashReactiveCommands.java +++ b/src/main/java/io/lettuce/core/api/reactive/RedisHashReactiveCommands.java @@ -314,7 +314,7 @@ public interface RedisHashReactiveCommands { * @param channel streaming channel that receives a call for every key. * @param key the key. * @return StreamScanCursor scan cursor. - * @deprecated since 7.0 in favor of consuming large results through the {@link org.reactivestreams.Publisher} returned by + * @deprecated since 6.4 in favor of consuming large results through the {@link org.reactivestreams.Publisher} returned by * {@link #hscanNovalues}. */ @Deprecated @@ -340,7 +340,7 @@ public interface RedisHashReactiveCommands { * @param key the key. * @param scanArgs scan arguments. * @return StreamScanCursor scan cursor. - * @deprecated since 7.0 in favor of consuming large results through the {@link org.reactivestreams.Publisher} returned by + * @deprecated since 6.4 in favor of consuming large results through the {@link org.reactivestreams.Publisher} returned by * {@link #hscanNovalues}. */ @Deprecated @@ -368,7 +368,7 @@ public interface RedisHashReactiveCommands { * @param scanCursor cursor to resume from a previous scan, must not be {@code null}. * @param scanArgs scan arguments. * @return StreamScanCursor scan cursor. - * @deprecated since 7.0 in favor of consuming large results through the {@link org.reactivestreams.Publisher} returned by + * @deprecated since 6.4 in favor of consuming large results through the {@link org.reactivestreams.Publisher} returned by * {@link #hscanNovalues}. */ @Deprecated @@ -394,7 +394,7 @@ public interface RedisHashReactiveCommands { * @param key the key. * @param scanCursor cursor to resume from a previous scan, must not be {@code null}. * @return StreamScanCursor scan cursor. - * @deprecated since 7.0 in favor of consuming large results through the {@link org.reactivestreams.Publisher} returned by + * @deprecated since 6.4 in favor of consuming large results through the {@link org.reactivestreams.Publisher} returned by * {@link #hscanNovalues}. */ @Deprecated diff --git a/src/main/java/io/lettuce/core/api/reactive/RedisJsonReactiveCommands.java b/src/main/java/io/lettuce/core/api/reactive/RedisJsonReactiveCommands.java new file mode 100644 index 0000000000..de5e060125 --- /dev/null +++ b/src/main/java/io/lettuce/core/api/reactive/RedisJsonReactiveCommands.java @@ -0,0 +1,442 @@ +/* + * Copyright 2017-2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ +package io.lettuce.core.api.reactive; + +import java.util.List; +import io.lettuce.core.json.JsonPath; +import io.lettuce.core.json.JsonType; +import io.lettuce.core.json.JsonValue; +import io.lettuce.core.json.arguments.JsonGetArgs; +import io.lettuce.core.json.arguments.JsonMsetArgs; +import io.lettuce.core.json.arguments.JsonRangeArgs; +import io.lettuce.core.json.arguments.JsonSetArgs; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * Reactive executed commands for JSON documents + * + * @param Key type. + * @param Value type. + * @author Tihomir Mateev + * @see Redis JSON + * @since 6.5 + * @generated by io.lettuce.apigenerator.CreateReactiveApi + */ +public interface RedisJsonReactiveCommands { + + /** + * Append the JSON values into the array at a given {@link JsonPath} after the last element in a said array. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param values one or more {@link JsonValue} to be appended. + * @return Long the resulting size of the arrays after the new data was appended, or null if the path does not exist. + * @since 6.5 + */ + Flux jsonArrappend(K key, JsonPath jsonPath, JsonValue... values); + + /** + * Append the JSON values into the array at the {@link JsonPath#ROOT_PATH} after the last element in a said array. + * + * @param key the key holding the JSON document. + * @param values one or more {@link JsonValue} to be appended. + * @return Long the resulting size of the arrays after the new data was appended, or null if the path does not exist. + * @since 6.5 + */ + Flux jsonArrappend(K key, JsonValue... values); + + /** + * Search for the first occurrence of a {@link JsonValue} in an array at a given {@link JsonPath} and return its index. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param value the {@link JsonValue} to search for. + * @param range the {@link JsonRangeArgs} to search within. + * @return Long the index hosting the searched element, -1 if not found or null if the specified path is not an array. + * @since 6.5 + */ + Flux jsonArrindex(K key, JsonPath jsonPath, JsonValue value, JsonRangeArgs range); + + /** + * Search for the first occurrence of a {@link JsonValue} in an array at a given {@link JsonPath} and return its index. This + * method uses defaults for the start and end indexes, see {@link JsonRangeArgs#DEFAULT_START_INDEX} and + * {@link JsonRangeArgs#DEFAULT_END_INDEX}. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param value the {@link JsonValue} to search for. + * @return Long the index hosting the searched element, -1 if not found or null if the specified path is not an array. + * @since 6.5 + */ + Flux jsonArrindex(K key, JsonPath jsonPath, JsonValue value); + + /** + * Insert the {@link JsonValue}s into the array at a given {@link JsonPath} before the provided index, shifting the existing + * elements to the right + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param index the index before which the new elements will be inserted. + * @param values one or more {@link JsonValue}s to be inserted. + * @return Long the resulting size of the arrays after the new data was inserted, or null if the path does not exist. + * @since 6.5 + */ + Flux jsonArrinsert(K key, JsonPath jsonPath, int index, JsonValue... values); + + /** + * Report the length of the JSON array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @return the size of the arrays, or null if the path does not exist. + * @since 6.5 + */ + Flux jsonArrlen(K key, JsonPath jsonPath); + + /** + * Report the length of the JSON array at a the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return the size of the arrays, or null if the path does not exist. + * @since 6.5 + */ + Flux jsonArrlen(K key); + + /** + * Remove and return {@link JsonValue} at a given index in the array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param index the index of the element to be removed. Default is -1, meaning the last element. Out-of-range indexes round + * to their respective array ends. Popping an empty array returns null. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + Flux jsonArrpop(K key, JsonPath jsonPath, int index); + + /** + * Remove and return {@link JsonValue} at index -1 (last element) in the array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + Flux jsonArrpop(K key, JsonPath jsonPath); + + /** + * Remove and return {@link JsonValue} at index -1 (last element) in the array at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + Flux jsonArrpop(K key); + + /** + * Trim an array at a given {@link JsonPath} so that it contains only the specified inclusive range of elements. All + * elements with indexes smaller than the start range and all elements with indexes bigger than the end range are trimmed. + *

+ * Behavior as of RedisJSON v2.0: + *

    + *
  • If start is larger than the array's size or start > stop, returns 0 and an empty array.
  • + *
  • If start is < 0, then start from the end of the array.
  • + *
  • If stop is larger than the end of the array, it is treated like the last element.
  • + *
+ * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param range the {@link JsonRangeArgs} to trim by. + * @return Long the resulting size of the arrays after the trimming, or null if the path does not exist. + * @since 6.5 + */ + Flux jsonArrtrim(K key, JsonPath jsonPath, JsonRangeArgs range); + + /** + * Clear container values (arrays/objects) and set numeric values to 0 + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to clear. + * @return Long the number of values removed plus all the matching JSON numerical values that are zeroed. + * @since 6.5 + */ + Mono jsonClear(K key, JsonPath jsonPath); + + /** + * Clear container values (arrays/objects) and set numeric values to 0 at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return Long the number of values removed plus all the matching JSON numerical values that are zeroed. + * @since 6.5 + */ + Mono jsonClear(K key); + + /** + * Deletes a value inside the JSON document at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to clear. + * @return Long the number of values removed (0 or more). + * @since 6.5 + */ + Mono jsonDel(K key, JsonPath jsonPath); + + /** + * Deletes a value inside the JSON document at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return Long the number of values removed (0 or more). + * @since 6.5 + */ + Mono jsonDel(K key); + + /** + * Return the value at the specified path in JSON serialized form. + *

+ * When using a single JSONPath, the root of the matching values is a JSON string with a top-level array of serialized JSON + * value. In contrast, a legacy path returns a single value. + *

+ * When using multiple JSONPath arguments, the root of the matching values is a JSON string with a top-level object, with + * each object value being a top-level array of serialized JSON value. In contrast, if all paths are legacy paths, each + * object value is a single serialized JSON value. If there are multiple paths that include both legacy path and JSONPath, + * the returned value conforms to the JSONPath version (an array of values). + * + * @param key the key holding the JSON document. + * @param options the {@link JsonGetArgs} to use. + * @param jsonPaths the {@link JsonPath}s to use to identify the values to get. + * @return JsonValue the value at path in JSON serialized form, or null if the path does not exist. + * @since 6.5 + */ + Flux jsonGet(K key, JsonGetArgs options, JsonPath... jsonPaths); + + /** + * Return the value at the specified path in JSON serialized form. Uses defaults for the {@link JsonGetArgs}. + *

+ * When using a single JSONPath, the root of the matching values is a JSON string with a top-level array of serialized JSON + * value. In contrast, a legacy path returns a single value. + *

+ * When using multiple JSONPath arguments, the root of the matching values is a JSON string with a top-level object, with + * each object value being a top-level array of serialized JSON value. In contrast, if all paths are legacy paths, each + * object value is a single serialized JSON value. If there are multiple paths that include both legacy path and JSONPath, + * the returned value conforms to the JSONPath version (an array of values). + * + * @param key the key holding the JSON document. + * @param jsonPaths the {@link JsonPath}s to use to identify the values to get. + * @return JsonValue the value at path in JSON serialized form, or null if the path does not exist. + * @since 6.5 + */ + Flux jsonGet(K key, JsonPath... jsonPaths); + + /** + * Merge a given {@link JsonValue} with the value matching {@link JsonPath}. Consequently, JSON values at matching paths are + * updated, deleted, or expanded with new children. + *

+ * Merging is done according to the following rules per JSON value in the value argument while considering the corresponding + * original value if it exists: + *

    + *
  • merging an existing object key with a null value deletes the key
  • + *
  • merging an existing object key with non-null value updates the value
  • + *
  • merging a non-existing object key adds the key and value
  • + *
  • merging an existing array with any merged value, replaces the entire array with the value
  • + *
+ *

+ * This command complies with RFC7396 "Json Merge Patch" + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to merge. + * @param value the {@link JsonValue} to merge. + * @return String "OK" if the set was successful, error if the operation failed. + * @since 6.5 + * @see RFC7396 + */ + Mono jsonMerge(K key, JsonPath jsonPath, JsonValue value); + + /** + * Return the values at the specified path from multiple key arguments. + * + * @param jsonPath the {@link JsonPath} pointing to the value to fetch. + * @param keys the keys holding the {@link JsonValue}s to fetch. + * @return List the values at path, or null if the path does not exist. + * @since 6.5 + */ + Flux jsonMGet(JsonPath jsonPath, K... keys); + + /** + * Set or update one or more JSON values according to the specified {@link JsonMsetArgs} + *

+ * JSON.MSET is atomic, hence, all given additions or updates are either applied or not. It is not possible for clients to + * see that some keys were updated while others are unchanged. + *

+ * A JSON value is a hierarchical structure. If you change a value in a specific path - nested values are affected. + * + * @param arguments the {@link JsonMsetArgs} specifying the values to change. + * @return "OK" if the operation was successful, error otherwise + * @since 6.5 + */ + Mono jsonMSet(List> arguments); + + /** + * Increment the number value stored at the specified {@link JsonPath} in the JSON document by the provided increment. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to increment. + * @param number the increment value. + * @return a {@link List} of the new values after the increment. + * @since 6.5 + */ + Flux jsonNumincrby(K key, JsonPath jsonPath, Number number); + + /** + * Return the keys in the JSON document that are referenced by the given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) whose key(s) we want. + * @return List the keys in the JSON document that are referenced by the given {@link JsonPath}. + * @since 6.5 + */ + Flux jsonObjkeys(K key, JsonPath jsonPath); + + /** + * Return the keys in the JSON document that are referenced by the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return List the keys in the JSON document that are referenced by the given {@link JsonPath}. + * @since 6.5 + */ + Flux jsonObjkeys(K key); + + /** + * Report the number of keys in the JSON object at the specified {@link JsonPath} and for the provided key + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) whose key(s) we want to count + * @return Long the number of keys in the JSON object at the specified path, or null if the path does not exist. + * @since 6.5 + */ + Flux jsonObjlen(K key, JsonPath jsonPath); + + /** + * Report the number of keys in the JSON object at the {@link JsonPath#ROOT_PATH} and for the provided key + * + * @param key the key holding the JSON document. + * @return Long the number of keys in the JSON object at the specified path, or null if the path does not exist. + * @since 6.5 + */ + Flux jsonObjlen(K key); + + /** + * Sets the JSON value at a given {@link JsonPath} in the JSON document. + *

+ * For new Redis keys, the path must be the root. For existing keys, when the entire path exists, the value that it contains + * is replaced with the JSON value. For existing keys, when the path exists, except for the last element, a new child is + * added with the JSON value. + *

+ * Adds a key (with its respective value) to a JSON Object (in a RedisJSON data type key) only if it is the last child in + * the path, or it is the parent of a new child being added in the path. Optional arguments NX and XX modify this behavior + * for both new RedisJSON data type keys and the JSON Object keys in them. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to set the value. + * @param value the {@link JsonValue} to set. + * @param options the {@link JsonSetArgs} the options for setting the value. + * @return String "OK" if the set was successful, null if the {@link JsonSetArgs} conditions are not met. + * @since 6.5 + */ + Mono jsonSet(K key, JsonPath jsonPath, JsonValue value, JsonSetArgs options); + + /** + * Sets the JSON value at a given {@link JsonPath} in the JSON document using defaults for the {@link JsonSetArgs}. + *

+ * For new Redis keys the path must be the root. For existing keys, when the entire path exists, the value that it contains + * is replaced with the JSON value. For existing keys, when the path exists, except for the last element, a new child is + * added with the JSON value. + *

+ * Adds a key (with its respective value) to a JSON Object (in a RedisJSON data type key) only if it is the last child in + * the path, or it is the parent of a new child being added in the path. Optional arguments NX and XX modify this behavior + * for both new RedisJSON data type keys and the JSON Object keys in them. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to set the value. + * @param value the {@link JsonValue} to set. + * @return String "OK" if the set was successful, null if the {@link JsonSetArgs} conditions are not met. + * @since 6.5 + */ + Mono jsonSet(K key, JsonPath jsonPath, JsonValue value); + + /** + * Append the json-string values to the string at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to append the value. + * @param value the {@link JsonValue} to append. + * @return Long the new length of the string, or null if the matching JSON value is not a string. + * @since 6.5 + */ + Flux jsonStrappend(K key, JsonPath jsonPath, JsonValue value); + + /** + * Append the json-string values to the string at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @param value the {@link JsonValue} to append. + * @return Long the new length of the string, or null if the matching JSON value is not a string. + * @since 6.5 + */ + Flux jsonStrappend(K key, JsonValue value); + + /** + * Report the length of the JSON String at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return Long (in recursive descent) the length of the JSON String at the provided {@link JsonPath}, or null if the value + * ath the desired path is not a string. + * @since 6.5 + */ + Flux jsonStrlen(K key, JsonPath jsonPath); + + /** + * Report the length of the JSON String at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @return Long (in recursive descent) the length of the JSON String at the provided {@link JsonPath}, or null if the value + * ath the desired path is not a string. + * @since 6.5 + */ + Flux jsonStrlen(K key); + + /** + * Toggle a Boolean value stored at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return List the new value after the toggle, 0 for false, 1 for true or null if the path does not exist. + * @since 6.5 + */ + Flux jsonToggle(K key, JsonPath jsonPath); + + /** + * Report the type of JSON value at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return List the type of JSON value at the provided {@link JsonPath} + * @since 6.5 + */ + Flux jsonType(K key, JsonPath jsonPath); + + /** + * Report the type of JSON value at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @return List the type of JSON value at the provided {@link JsonPath} + * @since 6.5 + */ + Flux jsonType(K key); + +} diff --git a/src/main/java/io/lettuce/core/api/reactive/RedisReactiveCommands.java b/src/main/java/io/lettuce/core/api/reactive/RedisReactiveCommands.java index 346f79b4bf..2f75efcc92 100644 --- a/src/main/java/io/lettuce/core/api/reactive/RedisReactiveCommands.java +++ b/src/main/java/io/lettuce/core/api/reactive/RedisReactiveCommands.java @@ -36,7 +36,7 @@ public interface RedisReactiveCommands extends BaseRedisReactiveCommands, RedisHLLReactiveCommands, RedisKeyReactiveCommands, RedisListReactiveCommands, RedisScriptingReactiveCommands, RedisServerReactiveCommands, RedisSetReactiveCommands, RedisSortedSetReactiveCommands, RedisStreamReactiveCommands, - RedisStringReactiveCommands, RedisTransactionalReactiveCommands { + RedisStringReactiveCommands, RedisTransactionalReactiveCommands, RedisJsonReactiveCommands { /** * Authenticate to the server. diff --git a/src/main/java/io/lettuce/core/api/reactive/RedisServerReactiveCommands.java b/src/main/java/io/lettuce/core/api/reactive/RedisServerReactiveCommands.java index 3e2d0addcc..e8f9c070fd 100644 --- a/src/main/java/io/lettuce/core/api/reactive/RedisServerReactiveCommands.java +++ b/src/main/java/io/lettuce/core/api/reactive/RedisServerReactiveCommands.java @@ -28,6 +28,7 @@ import io.lettuce.core.ShutdownArgs; import io.lettuce.core.TrackingArgs; import io.lettuce.core.UnblockType; +import io.lettuce.core.TrackingInfo; import io.lettuce.core.protocol.CommandType; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -177,6 +178,14 @@ public interface RedisServerReactiveCommands { */ Mono clientTracking(TrackingArgs args); + /** + * Returns information about the current client connection's use of the server assisted client side caching feature. + * + * @return {@link TrackingInfo}, for more information check the documentation + * @since 6.5 + */ + Mono clientTrackinginfo(); + /** * Unblock the specified blocked client. * diff --git a/src/main/java/io/lettuce/core/api/sync/RedisCommands.java b/src/main/java/io/lettuce/core/api/sync/RedisCommands.java index efc6005bb0..98f21b4cb2 100644 --- a/src/main/java/io/lettuce/core/api/sync/RedisCommands.java +++ b/src/main/java/io/lettuce/core/api/sync/RedisCommands.java @@ -21,6 +21,7 @@ import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.cluster.api.sync.RedisClusterCommands; +import io.lettuce.core.json.JsonParser; /** * @@ -31,11 +32,11 @@ * @author Mark Paluch * @since 3.0 */ -public interface RedisCommands - extends BaseRedisCommands, RedisAclCommands, RedisClusterCommands, RedisFunctionCommands, - RedisGeoCommands, RedisHashCommands, RedisHLLCommands, RedisKeyCommands, - RedisListCommands, RedisScriptingCommands, RedisServerCommands, RedisSetCommands, - RedisSortedSetCommands, RedisStreamCommands, RedisStringCommands, RedisTransactionalCommands { +public interface RedisCommands extends BaseRedisCommands, RedisAclCommands, RedisClusterCommands, + RedisFunctionCommands, RedisGeoCommands, RedisHashCommands, RedisHLLCommands, + RedisKeyCommands, RedisListCommands, RedisScriptingCommands, RedisServerCommands, + RedisSetCommands, RedisSortedSetCommands, RedisStreamCommands, RedisStringCommands, + RedisTransactionalCommands, RedisJsonCommands { /** * Authenticate to the server. @@ -80,4 +81,10 @@ public interface RedisCommands @Deprecated StatefulRedisConnection getStatefulConnection(); + /** + * @return the currently configured instance of the {@link JsonParser} + * @since 6.5 + */ + JsonParser getJsonParser(); + } diff --git a/src/main/java/io/lettuce/core/api/sync/RedisJsonCommands.java b/src/main/java/io/lettuce/core/api/sync/RedisJsonCommands.java new file mode 100644 index 0000000000..006e382283 --- /dev/null +++ b/src/main/java/io/lettuce/core/api/sync/RedisJsonCommands.java @@ -0,0 +1,440 @@ +/* + * Copyright 2017-2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ +package io.lettuce.core.api.sync; + +import java.util.List; +import io.lettuce.core.json.JsonPath; +import io.lettuce.core.json.JsonType; +import io.lettuce.core.json.JsonValue; +import io.lettuce.core.json.arguments.JsonGetArgs; +import io.lettuce.core.json.arguments.JsonMsetArgs; +import io.lettuce.core.json.arguments.JsonRangeArgs; +import io.lettuce.core.json.arguments.JsonSetArgs; + +/** + * Synchronous executed commands for JSON documents + * + * @param Key type. + * @param Value type. + * @author Tihomir Mateev + * @see Redis JSON + * @since 6.5 + * @generated by io.lettuce.apigenerator.CreateSyncApi + */ +public interface RedisJsonCommands { + + /** + * Append the JSON values into the array at a given {@link JsonPath} after the last element in a said array. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param values one or more {@link JsonValue} to be appended. + * @return Long the resulting size of the arrays after the new data was appended, or null if the path does not exist. + * @since 6.5 + */ + List jsonArrappend(K key, JsonPath jsonPath, JsonValue... values); + + /** + * Append the JSON values into the array at the {@link JsonPath#ROOT_PATH} after the last element in a said array. + * + * @param key the key holding the JSON document. + * @param values one or more {@link JsonValue} to be appended. + * @return Long the resulting size of the arrays after the new data was appended, or null if the path does not exist. + * @since 6.5 + */ + List jsonArrappend(K key, JsonValue... values); + + /** + * Search for the first occurrence of a {@link JsonValue} in an array at a given {@link JsonPath} and return its index. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param value the {@link JsonValue} to search for. + * @param range the {@link JsonRangeArgs} to search within. + * @return Long the index hosting the searched element, -1 if not found or null if the specified path is not an array. + * @since 6.5 + */ + List jsonArrindex(K key, JsonPath jsonPath, JsonValue value, JsonRangeArgs range); + + /** + * Search for the first occurrence of a {@link JsonValue} in an array at a given {@link JsonPath} and return its index. This + * method uses defaults for the start and end indexes, see {@link JsonRangeArgs#DEFAULT_START_INDEX} and + * {@link JsonRangeArgs#DEFAULT_END_INDEX}. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param value the {@link JsonValue} to search for. + * @return Long the index hosting the searched element, -1 if not found or null if the specified path is not an array. + * @since 6.5 + */ + List jsonArrindex(K key, JsonPath jsonPath, JsonValue value); + + /** + * Insert the {@link JsonValue}s into the array at a given {@link JsonPath} before the provided index, shifting the existing + * elements to the right + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param index the index before which the new elements will be inserted. + * @param values one or more {@link JsonValue}s to be inserted. + * @return Long the resulting size of the arrays after the new data was inserted, or null if the path does not exist. + * @since 6.5 + */ + List jsonArrinsert(K key, JsonPath jsonPath, int index, JsonValue... values); + + /** + * Report the length of the JSON array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @return the size of the arrays, or null if the path does not exist. + * @since 6.5 + */ + List jsonArrlen(K key, JsonPath jsonPath); + + /** + * Report the length of the JSON array at a the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return the size of the arrays, or null if the path does not exist. + * @since 6.5 + */ + List jsonArrlen(K key); + + /** + * Remove and return {@link JsonValue} at a given index in the array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param index the index of the element to be removed. Default is -1, meaning the last element. Out-of-range indexes round + * to their respective array ends. Popping an empty array returns null. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + List jsonArrpop(K key, JsonPath jsonPath, int index); + + /** + * Remove and return {@link JsonValue} at index -1 (last element) in the array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + List jsonArrpop(K key, JsonPath jsonPath); + + /** + * Remove and return {@link JsonValue} at index -1 (last element) in the array at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + List jsonArrpop(K key); + + /** + * Trim an array at a given {@link JsonPath} so that it contains only the specified inclusive range of elements. All + * elements with indexes smaller than the start range and all elements with indexes bigger than the end range are trimmed. + *

+ * Behavior as of RedisJSON v2.0: + *

    + *
  • If start is larger than the array's size or start > stop, returns 0 and an empty array.
  • + *
  • If start is < 0, then start from the end of the array.
  • + *
  • If stop is larger than the end of the array, it is treated like the last element.
  • + *
+ * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param range the {@link JsonRangeArgs} to trim by. + * @return Long the resulting size of the arrays after the trimming, or null if the path does not exist. + * @since 6.5 + */ + List jsonArrtrim(K key, JsonPath jsonPath, JsonRangeArgs range); + + /** + * Clear container values (arrays/objects) and set numeric values to 0 + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to clear. + * @return Long the number of values removed plus all the matching JSON numerical values that are zeroed. + * @since 6.5 + */ + Long jsonClear(K key, JsonPath jsonPath); + + /** + * Clear container values (arrays/objects) and set numeric values to 0 at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return Long the number of values removed plus all the matching JSON numerical values that are zeroed. + * @since 6.5 + */ + Long jsonClear(K key); + + /** + * Deletes a value inside the JSON document at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to clear. + * @return Long the number of values removed (0 or more). + * @since 6.5 + */ + Long jsonDel(K key, JsonPath jsonPath); + + /** + * Deletes a value inside the JSON document at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return Long the number of values removed (0 or more). + * @since 6.5 + */ + Long jsonDel(K key); + + /** + * Return the value at the specified path in JSON serialized form. + *

+ * When using a single JSONPath, the root of the matching values is a JSON string with a top-level array of serialized JSON + * value. In contrast, a legacy path returns a single value. + *

+ * When using multiple JSONPath arguments, the root of the matching values is a JSON string with a top-level object, with + * each object value being a top-level array of serialized JSON value. In contrast, if all paths are legacy paths, each + * object value is a single serialized JSON value. If there are multiple paths that include both legacy path and JSONPath, + * the returned value conforms to the JSONPath version (an array of values). + * + * @param key the key holding the JSON document. + * @param options the {@link JsonGetArgs} to use. + * @param jsonPaths the {@link JsonPath}s to use to identify the values to get. + * @return JsonValue the value at path in JSON serialized form, or null if the path does not exist. + * @since 6.5 + */ + List jsonGet(K key, JsonGetArgs options, JsonPath... jsonPaths); + + /** + * Return the value at the specified path in JSON serialized form. Uses defaults for the {@link JsonGetArgs}. + *

+ * When using a single JSONPath, the root of the matching values is a JSON string with a top-level array of serialized JSON + * value. In contrast, a legacy path returns a single value. + *

+ * When using multiple JSONPath arguments, the root of the matching values is a JSON string with a top-level object, with + * each object value being a top-level array of serialized JSON value. In contrast, if all paths are legacy paths, each + * object value is a single serialized JSON value. If there are multiple paths that include both legacy path and JSONPath, + * the returned value conforms to the JSONPath version (an array of values). + * + * @param key the key holding the JSON document. + * @param jsonPaths the {@link JsonPath}s to use to identify the values to get. + * @return JsonValue the value at path in JSON serialized form, or null if the path does not exist. + * @since 6.5 + */ + List jsonGet(K key, JsonPath... jsonPaths); + + /** + * Merge a given {@link JsonValue} with the value matching {@link JsonPath}. Consequently, JSON values at matching paths are + * updated, deleted, or expanded with new children. + *

+ * Merging is done according to the following rules per JSON value in the value argument while considering the corresponding + * original value if it exists: + *

    + *
  • merging an existing object key with a null value deletes the key
  • + *
  • merging an existing object key with non-null value updates the value
  • + *
  • merging a non-existing object key adds the key and value
  • + *
  • merging an existing array with any merged value, replaces the entire array with the value
  • + *
+ *

+ * This command complies with RFC7396 "Json Merge Patch" + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to merge. + * @param value the {@link JsonValue} to merge. + * @return String "OK" if the set was successful, error if the operation failed. + * @since 6.5 + * @see RFC7396 + */ + String jsonMerge(K key, JsonPath jsonPath, JsonValue value); + + /** + * Return the values at the specified path from multiple key arguments. + * + * @param jsonPath the {@link JsonPath} pointing to the value to fetch. + * @param keys the keys holding the {@link JsonValue}s to fetch. + * @return List the values at path, or null if the path does not exist. + * @since 6.5 + */ + List jsonMGet(JsonPath jsonPath, K... keys); + + /** + * Set or update one or more JSON values according to the specified {@link JsonMsetArgs} + *

+ * JSON.MSET is atomic, hence, all given additions or updates are either applied or not. It is not possible for clients to + * see that some keys were updated while others are unchanged. + *

+ * A JSON value is a hierarchical structure. If you change a value in a specific path - nested values are affected. + * + * @param arguments the {@link JsonMsetArgs} specifying the values to change. + * @return "OK" if the operation was successful, error otherwise + * @since 6.5 + */ + String jsonMSet(List> arguments); + + /** + * Increment the number value stored at the specified {@link JsonPath} in the JSON document by the provided increment. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to increment. + * @param number the increment value. + * @return a {@link List} of the new values after the increment. + * @since 6.5 + */ + List jsonNumincrby(K key, JsonPath jsonPath, Number number); + + /** + * Return the keys in the JSON document that are referenced by the given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) whose key(s) we want. + * @return List the keys in the JSON document that are referenced by the given {@link JsonPath}. + * @since 6.5 + */ + List jsonObjkeys(K key, JsonPath jsonPath); + + /** + * Return the keys in the JSON document that are referenced by the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return List the keys in the JSON document that are referenced by the given {@link JsonPath}. + * @since 6.5 + */ + List jsonObjkeys(K key); + + /** + * Report the number of keys in the JSON object at the specified {@link JsonPath} and for the provided key + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) whose key(s) we want to count + * @return Long the number of keys in the JSON object at the specified path, or null if the path does not exist. + * @since 6.5 + */ + List jsonObjlen(K key, JsonPath jsonPath); + + /** + * Report the number of keys in the JSON object at the {@link JsonPath#ROOT_PATH} and for the provided key + * + * @param key the key holding the JSON document. + * @return Long the number of keys in the JSON object at the specified path, or null if the path does not exist. + * @since 6.5 + */ + List jsonObjlen(K key); + + /** + * Sets the JSON value at a given {@link JsonPath} in the JSON document. + *

+ * For new Redis keys, the path must be the root. For existing keys, when the entire path exists, the value that it contains + * is replaced with the JSON value. For existing keys, when the path exists, except for the last element, a new child is + * added with the JSON value. + *

+ * Adds a key (with its respective value) to a JSON Object (in a RedisJSON data type key) only if it is the last child in + * the path, or it is the parent of a new child being added in the path. Optional arguments NX and XX modify this behavior + * for both new RedisJSON data type keys and the JSON Object keys in them. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to set the value. + * @param value the {@link JsonValue} to set. + * @param options the {@link JsonSetArgs} the options for setting the value. + * @return String "OK" if the set was successful, null if the {@link JsonSetArgs} conditions are not met. + * @since 6.5 + */ + String jsonSet(K key, JsonPath jsonPath, JsonValue value, JsonSetArgs options); + + /** + * Sets the JSON value at a given {@link JsonPath} in the JSON document using defaults for the {@link JsonSetArgs}. + *

+ * For new Redis keys the path must be the root. For existing keys, when the entire path exists, the value that it contains + * is replaced with the JSON value. For existing keys, when the path exists, except for the last element, a new child is + * added with the JSON value. + *

+ * Adds a key (with its respective value) to a JSON Object (in a RedisJSON data type key) only if it is the last child in + * the path, or it is the parent of a new child being added in the path. Optional arguments NX and XX modify this behavior + * for both new RedisJSON data type keys and the JSON Object keys in them. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to set the value. + * @param value the {@link JsonValue} to set. + * @return String "OK" if the set was successful, null if the {@link JsonSetArgs} conditions are not met. + * @since 6.5 + */ + String jsonSet(K key, JsonPath jsonPath, JsonValue value); + + /** + * Append the json-string values to the string at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to append the value. + * @param value the {@link JsonValue} to append. + * @return Long the new length of the string, or null if the matching JSON value is not a string. + * @since 6.5 + */ + List jsonStrappend(K key, JsonPath jsonPath, JsonValue value); + + /** + * Append the json-string values to the string at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @param value the {@link JsonValue} to append. + * @return Long the new length of the string, or null if the matching JSON value is not a string. + * @since 6.5 + */ + List jsonStrappend(K key, JsonValue value); + + /** + * Report the length of the JSON String at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return Long (in recursive descent) the length of the JSON String at the provided {@link JsonPath}, or null if the value + * ath the desired path is not a string. + * @since 6.5 + */ + List jsonStrlen(K key, JsonPath jsonPath); + + /** + * Report the length of the JSON String at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @return Long (in recursive descent) the length of the JSON String at the provided {@link JsonPath}, or null if the value + * ath the desired path is not a string. + * @since 6.5 + */ + List jsonStrlen(K key); + + /** + * Toggle a Boolean value stored at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return List the new value after the toggle, 0 for false, 1 for true or null if the path does not exist. + * @since 6.5 + */ + List jsonToggle(K key, JsonPath jsonPath); + + /** + * Report the type of JSON value at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return List the type of JSON value at the provided {@link JsonPath} + * @since 6.5 + */ + List jsonType(K key, JsonPath jsonPath); + + /** + * Report the type of JSON value at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @return List the type of JSON value at the provided {@link JsonPath} + * @since 6.5 + */ + List jsonType(K key); + +} diff --git a/src/main/java/io/lettuce/core/api/sync/RedisServerCommands.java b/src/main/java/io/lettuce/core/api/sync/RedisServerCommands.java index 7454ce2e6a..28b0530448 100644 --- a/src/main/java/io/lettuce/core/api/sync/RedisServerCommands.java +++ b/src/main/java/io/lettuce/core/api/sync/RedisServerCommands.java @@ -28,6 +28,7 @@ import io.lettuce.core.KillArgs; import io.lettuce.core.ShutdownArgs; import io.lettuce.core.TrackingArgs; +import io.lettuce.core.TrackingInfo; import io.lettuce.core.UnblockType; import io.lettuce.core.protocol.CommandType; @@ -176,6 +177,14 @@ public interface RedisServerCommands { */ String clientTracking(TrackingArgs args); + /** + * Returns information about the current client connection's use of the server assisted client side caching feature. + * + * @return {@link TrackingInfo}, for more information check the documentation + * @since 6.5 + */ + TrackingInfo clientTrackinginfo(); + /** * Unblock the specified blocked client. * diff --git a/src/main/java/io/lettuce/core/cluster/ClusterCommand.java b/src/main/java/io/lettuce/core/cluster/ClusterCommand.java index 563c2e701d..1b884b8f0e 100644 --- a/src/main/java/io/lettuce/core/cluster/ClusterCommand.java +++ b/src/main/java/io/lettuce/core/cluster/ClusterCommand.java @@ -81,9 +81,8 @@ public void encode(ByteBuf buf) { @Override public boolean completeExceptionally(Throwable ex) { - boolean result = command.completeExceptionally(ex); completed = true; - return result; + return super.completeExceptionally(ex); } @Override diff --git a/src/main/java/io/lettuce/core/cluster/ClusterDistributionChannelWriter.java b/src/main/java/io/lettuce/core/cluster/ClusterDistributionChannelWriter.java index a0a80609f1..983ca013eb 100644 --- a/src/main/java/io/lettuce/core/cluster/ClusterDistributionChannelWriter.java +++ b/src/main/java/io/lettuce/core/cluster/ClusterDistributionChannelWriter.java @@ -141,13 +141,13 @@ private RedisCommand doWrite(RedisCommand command) { clusterEventListener.onMovedRedirection(); asking = false; - publish(new MovedRedirectionEvent(clusterCommand.getType().name(), keyAsString, slot, + publish(new MovedRedirectionEvent(clusterCommand.getType().toString(), keyAsString, slot, clusterCommand.getError())); } else { target = getAskTarget(clusterCommand.getError()); asking = true; clusterEventListener.onAskRedirection(); - publish(new AskRedirectionEvent(clusterCommand.getType().name(), keyAsString, slot, + publish(new AskRedirectionEvent(clusterCommand.getType().toString(), keyAsString, slot, clusterCommand.getError())); } diff --git a/src/main/java/io/lettuce/core/cluster/CommandSet.java b/src/main/java/io/lettuce/core/cluster/CommandSet.java index 788da79d30..2bc0d625f9 100644 --- a/src/main/java/io/lettuce/core/cluster/CommandSet.java +++ b/src/main/java/io/lettuce/core/cluster/CommandSet.java @@ -59,7 +59,7 @@ public boolean hasCommand(ProtocolKeyword commandName) { return availableCommands.contains(commandName); } - return commands.containsKey(commandName.name().toLowerCase()); + return commands.containsKey(commandName.toString().toLowerCase()); } } diff --git a/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterAsyncCommandsImpl.java b/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterAsyncCommandsImpl.java index f4d8fc63e9..14295db788 100644 --- a/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterAsyncCommandsImpl.java +++ b/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterAsyncCommandsImpl.java @@ -36,6 +36,7 @@ import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.Collectors; import io.lettuce.core.*; import io.lettuce.core.api.StatefulRedisConnection; @@ -52,6 +53,10 @@ import io.lettuce.core.cluster.models.partitions.Partitions; import io.lettuce.core.cluster.models.partitions.RedisClusterNode; import io.lettuce.core.codec.RedisCodec; +import io.lettuce.core.json.JsonParser; +import io.lettuce.core.json.JsonPath; +import io.lettuce.core.json.JsonValue; +import io.lettuce.core.json.arguments.JsonMsetArgs; import io.lettuce.core.output.IntegerOutput; import io.lettuce.core.output.KeyStreamingChannel; import io.lettuce.core.output.KeyValueStreamingChannel; @@ -59,6 +64,7 @@ import io.lettuce.core.protocol.Command; import io.lettuce.core.protocol.CommandType; import io.lettuce.core.protocol.ConnectionIntent; +import reactor.core.publisher.Mono; /** * An advanced asynchronous and thread-safe API for a Redis Cluster connection. @@ -67,6 +73,7 @@ * @param Value type. * @author Mark Paluch * @author Jon Chambers + * @author Tihomir Mateev * @since 3.3 */ @SuppressWarnings("unchecked") @@ -80,11 +87,13 @@ public class RedisAdvancedClusterAsyncCommandsImpl extends AbstractRedisAs * * @param connection the stateful connection * @param codec Codec used to encode/decode keys and values. - * @deprecated since 5.1, use {@link #RedisAdvancedClusterAsyncCommandsImpl(StatefulRedisClusterConnection, RedisCodec)}. + * @deprecated since 5.1, use + * {@link #RedisAdvancedClusterAsyncCommandsImpl(StatefulRedisClusterConnection, RedisCodec, Mono)}. */ @Deprecated - public RedisAdvancedClusterAsyncCommandsImpl(StatefulRedisClusterConnectionImpl connection, RedisCodec codec) { - super(connection, codec); + public RedisAdvancedClusterAsyncCommandsImpl(StatefulRedisClusterConnectionImpl connection, RedisCodec codec, + Mono parser) { + super(connection, codec, parser); this.codec = codec; } @@ -94,8 +103,9 @@ public RedisAdvancedClusterAsyncCommandsImpl(StatefulRedisClusterConnectionImpl< * @param connection the stateful connection * @param codec Codec used to encode/decode keys and values. */ - public RedisAdvancedClusterAsyncCommandsImpl(StatefulRedisClusterConnection connection, RedisCodec codec) { - super(connection, codec); + public RedisAdvancedClusterAsyncCommandsImpl(StatefulRedisClusterConnection connection, RedisCodec codec, + Mono parser) { + super(connection, codec, parser); this.codec = codec; } @@ -282,6 +292,54 @@ public RedisFuture keys(KeyStreamingChannel channel, K pattern) { return MultiNodeExecution.aggregateAsync(executions); } + @Override + public RedisFuture> jsonMGet(JsonPath jsonPath, K... keys) { + Map> partitioned = SlotHash.partition(codec, Arrays.asList(keys)); + + if (partitioned.size() < 2) { + return super.jsonMGet(jsonPath, keys); + } + + // For a given partition, maps the key to its index within the List in partitioned for faster lookups below + Map> keysToIndexes = mapKeyToIndex(partitioned); + Map slots = SlotHash.getSlots(partitioned); + Map>> executions = new HashMap<>(partitioned.size()); + + for (Map.Entry> entry : partitioned.entrySet()) { + K[] partitionKeys = entry.getValue().toArray((K[]) new Object[entry.getValue().size()]); + RedisFuture> jsonMget = super.jsonMGet(jsonPath, partitionKeys); + executions.put(entry.getKey(), jsonMget); + } + + // restore order of key + return new PipelinedRedisFuture<>(executions, objectPipelinedRedisFuture -> { + List result = new ArrayList<>(slots.size()); + for (K opKey : keys) { + int slot = slots.get(opKey); + + int position = keysToIndexes.get(slot).get(opKey); + RedisFuture> listRedisFuture = executions.get(slot); + result.add(MultiNodeExecution.execute(() -> listRedisFuture.get().get(position))); + } + + return result; + }); + } + + private Map> mapKeyToIndex(Map> partitioned) { + Map> result = new HashMap<>(partitioned.size()); + for (Integer partition : partitioned.keySet()) { + List keysForPartition = partitioned.get(partition); + Map keysToIndexes = new HashMap<>(keysForPartition.size()); + for (int i = 0; i < keysForPartition.size(); i++) { + keysToIndexes.put(keysForPartition.get(i), i); + } + result.put(partition, keysToIndexes); + } + + return result; + } + @Override public RedisFuture>> mget(K... keys) { return mget(Arrays.asList(keys)); @@ -296,15 +354,7 @@ public RedisFuture>> mget(Iterable keys) { } // For a given partition, maps the key to its index within the List in partitioned for faster lookups below - Map> partitionedKeysToIndexes = new HashMap<>(partitioned.size()); - for (Integer partition : partitioned.keySet()) { - List keysForPartition = partitioned.get(partition); - Map keysToIndexes = new HashMap<>(keysForPartition.size()); - for (int i = 0; i < keysForPartition.size(); i++) { - keysToIndexes.put(keysForPartition.get(i), i); - } - partitionedKeysToIndexes.put(partition, keysToIndexes); - } + Map> partitionedKeysToIndexes = mapKeyToIndex(partitioned); Map slots = SlotHash.getSlots(partitioned); Map>>> executions = new HashMap<>(partitioned.size()); @@ -351,6 +401,30 @@ public RedisFuture mget(KeyValueStreamingChannel channel, Iterable jsonMSet(List> arguments) { + List keys = arguments.stream().map(JsonMsetArgs::getKey).collect(Collectors.toList()); + Map>> argsPerKey = arguments.stream().collect(Collectors.groupingBy(JsonMsetArgs::getKey)); + Map> partitioned = SlotHash.partition(codec, keys); + + if (partitioned.size() < 2) { + return super.jsonMSet(arguments); + } + + Map> executions = new HashMap<>(); + + for (Map.Entry> entry : partitioned.entrySet()) { + + List> op = new ArrayList<>(); + entry.getValue().forEach(k -> op.addAll(argsPerKey.get(k))); + + RedisFuture mset = super.jsonMSet(op); + executions.put(entry.getKey(), mset); + } + + return MultiNodeExecution.firstOfAsync(executions); + } + @Override public RedisFuture mset(Map map) { diff --git a/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterReactiveCommandsImpl.java b/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterReactiveCommandsImpl.java index 2df05e254b..0517feb4c4 100644 --- a/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterReactiveCommandsImpl.java +++ b/src/main/java/io/lettuce/core/cluster/RedisAdvancedClusterReactiveCommandsImpl.java @@ -34,6 +34,7 @@ import java.util.function.Predicate; import java.util.stream.Collectors; +import io.lettuce.core.json.JsonParser; import org.reactivestreams.Publisher; import io.lettuce.core.*; @@ -75,12 +76,13 @@ public class RedisAdvancedClusterReactiveCommandsImpl extends AbstractRedi * * @param connection the stateful connection. * @param codec Codec used to encode/decode keys and values. - * @deprecated since 5.2, use {@link #RedisAdvancedClusterReactiveCommandsImpl(StatefulRedisClusterConnection, RedisCodec)}. + * @deprecated since 5.2, use + * {@link #RedisAdvancedClusterReactiveCommandsImpl(StatefulRedisClusterConnection, RedisCodec, Mono)}. */ @Deprecated - public RedisAdvancedClusterReactiveCommandsImpl(StatefulRedisClusterConnectionImpl connection, - RedisCodec codec) { - super(connection, codec); + public RedisAdvancedClusterReactiveCommandsImpl(StatefulRedisClusterConnectionImpl connection, RedisCodec codec, + Mono parser) { + super(connection, codec, parser); this.codec = codec; } @@ -90,8 +92,9 @@ public RedisAdvancedClusterReactiveCommandsImpl(StatefulRedisClusterConnectionIm * @param connection the stateful connection. * @param codec Codec used to encode/decode keys and values. */ - public RedisAdvancedClusterReactiveCommandsImpl(StatefulRedisClusterConnection connection, RedisCodec codec) { - super(connection, codec); + public RedisAdvancedClusterReactiveCommandsImpl(StatefulRedisClusterConnection connection, RedisCodec codec, + Mono parser) { + super(connection, codec, parser); this.codec = codec; } diff --git a/src/main/java/io/lettuce/core/cluster/RedisClusterClient.java b/src/main/java/io/lettuce/core/cluster/RedisClusterClient.java index a60dfd0d82..577689cecc 100644 --- a/src/main/java/io/lettuce/core/cluster/RedisClusterClient.java +++ b/src/main/java/io/lettuce/core/cluster/RedisClusterClient.java @@ -59,6 +59,7 @@ import io.lettuce.core.internal.Futures; import io.lettuce.core.internal.LettuceAssert; import io.lettuce.core.internal.LettuceLists; +import io.lettuce.core.json.JsonParser; import io.lettuce.core.output.KeyValueStreamingChannel; import io.lettuce.core.protocol.CommandExpiryWriter; import io.lettuce.core.protocol.CommandHandler; @@ -553,7 +554,7 @@ ConnectionFuture> connectToNodeAsync(RedisC } StatefulRedisConnectionImpl connection = newStatefulRedisConnection(writer, endpoint, codec, - getFirstUri().getTimeout()); + getFirstUri().getTimeout(), getClusterClientOptions().getJsonParser()); ConnectionFuture> connectionFuture = connectStatefulAsync(connection, endpoint, getFirstUri(), socketAddressSupplier, @@ -575,13 +576,14 @@ ConnectionFuture> connectToNodeAsync(RedisC * @param pushHandler the handler for push notifications * @param codec codec * @param timeout default timeout + * @param parser the JSON parser to be used * @param Key-Type * @param Value Type * @return new instance of StatefulRedisConnectionImpl */ protected StatefulRedisConnectionImpl newStatefulRedisConnection(RedisChannelWriter channelWriter, - PushHandler pushHandler, RedisCodec codec, Duration timeout) { - return new StatefulRedisConnectionImpl<>(channelWriter, pushHandler, codec, timeout); + PushHandler pushHandler, RedisCodec codec, Duration timeout, Mono parser) { + return new StatefulRedisConnectionImpl<>(channelWriter, pushHandler, codec, timeout, parser); } /** @@ -667,7 +669,7 @@ private CompletableFuture> connectCl clusterWriter.setClusterConnectionProvider(pooledClusterConnectionProvider); StatefulRedisClusterConnectionImpl connection = newStatefulRedisClusterConnection(clusterWriter, - pooledClusterConnectionProvider, codec, getFirstUri().getTimeout()); + pooledClusterConnectionProvider, codec, getFirstUri().getTimeout(), getClusterClientOptions().getJsonParser()); connection.setReadFrom(ReadFrom.UPSTREAM); connection.setPartitions(partitions); @@ -699,13 +701,15 @@ private CompletableFuture> connectCl * @param pushHandler the handler for push notifications * @param codec codec * @param timeout default timeout + * @param parser the Json parser to be used * @param Key-Type * @param Value Type * @return new instance of StatefulRedisClusterConnectionImpl */ protected StatefulRedisClusterConnectionImpl newStatefulRedisClusterConnection( - RedisChannelWriter channelWriter, ClusterPushHandler pushHandler, RedisCodec codec, Duration timeout) { - return new StatefulRedisClusterConnectionImpl(channelWriter, pushHandler, codec, timeout); + RedisChannelWriter channelWriter, ClusterPushHandler pushHandler, RedisCodec codec, Duration timeout, + Mono parser) { + return new StatefulRedisClusterConnectionImpl(channelWriter, pushHandler, codec, timeout, parser); } private Mono connect(Mono socketAddressSupplier, DefaultEndpoint endpoint, diff --git a/src/main/java/io/lettuce/core/cluster/StatefulRedisClusterConnectionImpl.java b/src/main/java/io/lettuce/core/cluster/StatefulRedisClusterConnectionImpl.java index 3e89689016..4a27ef1a14 100644 --- a/src/main/java/io/lettuce/core/cluster/StatefulRedisClusterConnectionImpl.java +++ b/src/main/java/io/lettuce/core/cluster/StatefulRedisClusterConnectionImpl.java @@ -52,11 +52,13 @@ import io.lettuce.core.cluster.models.partitions.RedisClusterNode; import io.lettuce.core.codec.RedisCodec; import io.lettuce.core.internal.LettuceAssert; +import io.lettuce.core.json.JsonParser; import io.lettuce.core.protocol.CommandArgsAccessor; import io.lettuce.core.protocol.CompleteableCommand; import io.lettuce.core.protocol.ConnectionIntent; import io.lettuce.core.protocol.ConnectionWatchdog; import io.lettuce.core.protocol.RedisCommand; +import reactor.core.publisher.Mono; /** * A thread-safe connection to a Redis Cluster. Multiple threads may share one {@link StatefulRedisClusterConnectionImpl} @@ -74,6 +76,8 @@ public class StatefulRedisClusterConnectionImpl extends RedisChannelHandle protected final RedisCodec codec; + protected final Mono parser; + protected final RedisAdvancedClusterCommands sync; protected final RedisAdvancedClusterAsyncCommandsImpl async; @@ -93,11 +97,12 @@ public class StatefulRedisClusterConnectionImpl extends RedisChannelHandle * @param timeout Maximum time to wait for a response. */ public StatefulRedisClusterConnectionImpl(RedisChannelWriter writer, ClusterPushHandler pushHandler, RedisCodec codec, - Duration timeout) { + Duration timeout, Mono parser) { super(writer, timeout); this.pushHandler = pushHandler; this.codec = codec; + this.parser = parser; this.async = newRedisAdvancedClusterAsyncCommandsImpl(); this.sync = newRedisAdvancedClusterCommandsImpl(); @@ -105,7 +110,7 @@ public StatefulRedisClusterConnectionImpl(RedisChannelWriter writer, ClusterPush } protected RedisAdvancedClusterReactiveCommandsImpl newRedisAdvancedClusterReactiveCommandsImpl() { - return new RedisAdvancedClusterReactiveCommandsImpl<>((StatefulRedisClusterConnection) this, codec); + return new RedisAdvancedClusterReactiveCommandsImpl<>((StatefulRedisClusterConnection) this, codec, parser); } protected RedisAdvancedClusterCommands newRedisAdvancedClusterCommandsImpl() { @@ -117,7 +122,7 @@ protected T clusterSyncHandler(Class... interfaces) { } protected RedisAdvancedClusterAsyncCommandsImpl newRedisAdvancedClusterAsyncCommandsImpl() { - return new RedisAdvancedClusterAsyncCommandsImpl((StatefulRedisClusterConnection) this, codec); + return new RedisAdvancedClusterAsyncCommandsImpl((StatefulRedisClusterConnection) this, codec, parser); } @Override @@ -235,7 +240,7 @@ private RedisCommand preProcessCommand(RedisCommand comman RedisCommand local = command; - if (local.getType().name().equals(AUTH.name())) { + if (local.getType().toString().equals(AUTH.name())) { local = attachOnComplete(local, status -> { if (status.equals("OK")) { List args = CommandArgsAccessor.getCharArrayArguments(command.getArgs()); @@ -252,7 +257,7 @@ private RedisCommand preProcessCommand(RedisCommand comman }); } - if (local.getType().name().equals(READONLY.name())) { + if (local.getType().toString().equals(READONLY.name())) { local = attachOnComplete(local, status -> { if (status.equals("OK")) { this.connectionState.setReadOnly(true); @@ -260,7 +265,7 @@ private RedisCommand preProcessCommand(RedisCommand comman }); } - if (local.getType().name().equals(READWRITE.name())) { + if (local.getType().toString().equals(READWRITE.name())) { local = attachOnComplete(local, status -> { if (status.equals("OK")) { this.connectionState.setReadOnly(false); diff --git a/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionAsyncCommands.java b/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionAsyncCommands.java index 0d7894b5ca..dbf9b679fa 100644 --- a/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionAsyncCommands.java +++ b/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionAsyncCommands.java @@ -8,9 +8,10 @@ * * @author Mark Paluch */ -public interface NodeSelectionAsyncCommands extends BaseNodeSelectionAsyncCommands, - NodeSelectionFunctionAsyncCommands, NodeSelectionGeoAsyncCommands, NodeSelectionHashAsyncCommands, - NodeSelectionHLLAsyncCommands, NodeSelectionKeyAsyncCommands, NodeSelectionListAsyncCommands, - NodeSelectionScriptingAsyncCommands, NodeSelectionServerAsyncCommands, NodeSelectionSetAsyncCommands, - NodeSelectionSortedSetAsyncCommands, NodeSelectionStreamCommands, NodeSelectionStringAsyncCommands { +public interface NodeSelectionAsyncCommands + extends BaseNodeSelectionAsyncCommands, NodeSelectionFunctionAsyncCommands, + NodeSelectionGeoAsyncCommands, NodeSelectionHashAsyncCommands, NodeSelectionHLLAsyncCommands, + NodeSelectionKeyAsyncCommands, NodeSelectionListAsyncCommands, NodeSelectionScriptingAsyncCommands, + NodeSelectionServerAsyncCommands, NodeSelectionSetAsyncCommands, NodeSelectionSortedSetAsyncCommands, + NodeSelectionStreamCommands, NodeSelectionStringAsyncCommands, NodeSelectionJsonAsyncCommands { } diff --git a/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionJsonAsyncCommands.java b/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionJsonAsyncCommands.java new file mode 100644 index 0000000000..bc4d9229dd --- /dev/null +++ b/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionJsonAsyncCommands.java @@ -0,0 +1,440 @@ +/* + * Copyright 2017-2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ +package io.lettuce.core.cluster.api.async; + +import java.util.List; +import io.lettuce.core.json.JsonPath; +import io.lettuce.core.json.JsonType; +import io.lettuce.core.json.JsonValue; +import io.lettuce.core.json.arguments.JsonGetArgs; +import io.lettuce.core.json.arguments.JsonMsetArgs; +import io.lettuce.core.json.arguments.JsonRangeArgs; +import io.lettuce.core.json.arguments.JsonSetArgs; + +/** + * Asynchronous executed commands on a node selection for JSON documents + * + * @param Key type. + * @param Value type. + * @author Tihomir Mateev + * @see Redis JSON + * @since 6.5 + * @generated by io.lettuce.apigenerator.CreateAsyncNodeSelectionClusterApi + */ +public interface NodeSelectionJsonAsyncCommands { + + /** + * Append the JSON values into the array at a given {@link JsonPath} after the last element in a said array. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param values one or more {@link JsonValue} to be appended. + * @return Long the resulting size of the arrays after the new data was appended, or null if the path does not exist. + * @since 6.5 + */ + AsyncExecutions> jsonArrappend(K key, JsonPath jsonPath, JsonValue... values); + + /** + * Append the JSON values into the array at the {@link JsonPath#ROOT_PATH} after the last element in a said array. + * + * @param key the key holding the JSON document. + * @param values one or more {@link JsonValue} to be appended. + * @return Long the resulting size of the arrays after the new data was appended, or null if the path does not exist. + * @since 6.5 + */ + AsyncExecutions> jsonArrappend(K key, JsonValue... values); + + /** + * Search for the first occurrence of a {@link JsonValue} in an array at a given {@link JsonPath} and return its index. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param value the {@link JsonValue} to search for. + * @param range the {@link JsonRangeArgs} to search within. + * @return Long the index hosting the searched element, -1 if not found or null if the specified path is not an array. + * @since 6.5 + */ + AsyncExecutions> jsonArrindex(K key, JsonPath jsonPath, JsonValue value, JsonRangeArgs range); + + /** + * Search for the first occurrence of a {@link JsonValue} in an array at a given {@link JsonPath} and return its index. This + * method uses defaults for the start and end indexes, see {@link JsonRangeArgs#DEFAULT_START_INDEX} and + * {@link JsonRangeArgs#DEFAULT_END_INDEX}. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param value the {@link JsonValue} to search for. + * @return Long the index hosting the searched element, -1 if not found or null if the specified path is not an array. + * @since 6.5 + */ + AsyncExecutions> jsonArrindex(K key, JsonPath jsonPath, JsonValue value); + + /** + * Insert the {@link JsonValue}s into the array at a given {@link JsonPath} before the provided index, shifting the existing + * elements to the right + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param index the index before which the new elements will be inserted. + * @param values one or more {@link JsonValue}s to be inserted. + * @return Long the resulting size of the arrays after the new data was inserted, or null if the path does not exist. + * @since 6.5 + */ + AsyncExecutions> jsonArrinsert(K key, JsonPath jsonPath, int index, JsonValue... values); + + /** + * Report the length of the JSON array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @return the size of the arrays, or null if the path does not exist. + * @since 6.5 + */ + AsyncExecutions> jsonArrlen(K key, JsonPath jsonPath); + + /** + * Report the length of the JSON array at a the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return the size of the arrays, or null if the path does not exist. + * @since 6.5 + */ + AsyncExecutions> jsonArrlen(K key); + + /** + * Remove and return {@link JsonValue} at a given index in the array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param index the index of the element to be removed. Default is -1, meaning the last element. Out-of-range indexes round + * to their respective array ends. Popping an empty array returns null. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + AsyncExecutions> jsonArrpop(K key, JsonPath jsonPath, int index); + + /** + * Remove and return {@link JsonValue} at index -1 (last element) in the array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + AsyncExecutions> jsonArrpop(K key, JsonPath jsonPath); + + /** + * Remove and return {@link JsonValue} at index -1 (last element) in the array at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + AsyncExecutions> jsonArrpop(K key); + + /** + * Trim an array at a given {@link JsonPath} so that it contains only the specified inclusive range of elements. All + * elements with indexes smaller than the start range and all elements with indexes bigger than the end range are trimmed. + *

+ * Behavior as of RedisJSON v2.0: + *

    + *
  • If start is larger than the array's size or start > stop, returns 0 and an empty array.
  • + *
  • If start is < 0, then start from the end of the array.
  • + *
  • If stop is larger than the end of the array, it is treated like the last element.
  • + *
+ * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param range the {@link JsonRangeArgs} to trim by. + * @return Long the resulting size of the arrays after the trimming, or null if the path does not exist. + * @since 6.5 + */ + AsyncExecutions> jsonArrtrim(K key, JsonPath jsonPath, JsonRangeArgs range); + + /** + * Clear container values (arrays/objects) and set numeric values to 0 + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to clear. + * @return Long the number of values removed plus all the matching JSON numerical values that are zeroed. + * @since 6.5 + */ + AsyncExecutions jsonClear(K key, JsonPath jsonPath); + + /** + * Clear container values (arrays/objects) and set numeric values to 0 at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return Long the number of values removed plus all the matching JSON numerical values that are zeroed. + * @since 6.5 + */ + AsyncExecutions jsonClear(K key); + + /** + * Deletes a value inside the JSON document at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to clear. + * @return Long the number of values removed (0 or more). + * @since 6.5 + */ + AsyncExecutions jsonDel(K key, JsonPath jsonPath); + + /** + * Deletes a value inside the JSON document at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return Long the number of values removed (0 or more). + * @since 6.5 + */ + AsyncExecutions jsonDel(K key); + + /** + * Return the value at the specified path in JSON serialized form. + *

+ * When using a single JSONPath, the root of the matching values is a JSON string with a top-level array of serialized JSON + * value. In contrast, a legacy path returns a single value. + *

+ * When using multiple JSONPath arguments, the root of the matching values is a JSON string with a top-level object, with + * each object value being a top-level array of serialized JSON value. In contrast, if all paths are legacy paths, each + * object value is a single serialized JSON value. If there are multiple paths that include both legacy path and JSONPath, + * the returned value conforms to the JSONPath version (an array of values). + * + * @param key the key holding the JSON document. + * @param options the {@link JsonGetArgs} to use. + * @param jsonPaths the {@link JsonPath}s to use to identify the values to get. + * @return JsonValue the value at path in JSON serialized form, or null if the path does not exist. + * @since 6.5 + */ + AsyncExecutions> jsonGet(K key, JsonGetArgs options, JsonPath... jsonPaths); + + /** + * Return the value at the specified path in JSON serialized form. Uses defaults for the {@link JsonGetArgs}. + *

+ * When using a single JSONPath, the root of the matching values is a JSON string with a top-level array of serialized JSON + * value. In contrast, a legacy path returns a single value. + *

+ * When using multiple JSONPath arguments, the root of the matching values is a JSON string with a top-level object, with + * each object value being a top-level array of serialized JSON value. In contrast, if all paths are legacy paths, each + * object value is a single serialized JSON value. If there are multiple paths that include both legacy path and JSONPath, + * the returned value conforms to the JSONPath version (an array of values). + * + * @param key the key holding the JSON document. + * @param jsonPaths the {@link JsonPath}s to use to identify the values to get. + * @return JsonValue the value at path in JSON serialized form, or null if the path does not exist. + * @since 6.5 + */ + AsyncExecutions> jsonGet(K key, JsonPath... jsonPaths); + + /** + * Merge a given {@link JsonValue} with the value matching {@link JsonPath}. Consequently, JSON values at matching paths are + * updated, deleted, or expanded with new children. + *

+ * Merging is done according to the following rules per JSON value in the value argument while considering the corresponding + * original value if it exists: + *

    + *
  • merging an existing object key with a null value deletes the key
  • + *
  • merging an existing object key with non-null value updates the value
  • + *
  • merging a non-existing object key adds the key and value
  • + *
  • merging an existing array with any merged value, replaces the entire array with the value
  • + *
+ *

+ * This command complies with RFC7396 "Json Merge Patch" + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to merge. + * @param value the {@link JsonValue} to merge. + * @return String "OK" if the set was successful, error if the operation failed. + * @since 6.5 + * @see RFC7396 + */ + AsyncExecutions jsonMerge(K key, JsonPath jsonPath, JsonValue value); + + /** + * Return the values at the specified path from multiple key arguments. + * + * @param jsonPath the {@link JsonPath} pointing to the value to fetch. + * @param keys the keys holding the {@link JsonValue}s to fetch. + * @return List the values at path, or null if the path does not exist. + * @since 6.5 + */ + AsyncExecutions> jsonMGet(JsonPath jsonPath, K... keys); + + /** + * Set or update one or more JSON values according to the specified {@link JsonMsetArgs} + *

+ * JSON.MSET is atomic, hence, all given additions or updates are either applied or not. It is not possible for clients to + * see that some keys were updated while others are unchanged. + *

+ * A JSON value is a hierarchical structure. If you change a value in a specific path - nested values are affected. + * + * @param arguments the {@link JsonMsetArgs} specifying the values to change. + * @return "OK" if the operation was successful, error otherwise + * @since 6.5 + */ + AsyncExecutions jsonMSet(List> arguments); + + /** + * Increment the number value stored at the specified {@link JsonPath} in the JSON document by the provided increment. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to increment. + * @param number the increment value. + * @return a {@link List} of the new values after the increment. + * @since 6.5 + */ + AsyncExecutions> jsonNumincrby(K key, JsonPath jsonPath, Number number); + + /** + * Return the keys in the JSON document that are referenced by the given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) whose key(s) we want. + * @return List the keys in the JSON document that are referenced by the given {@link JsonPath}. + * @since 6.5 + */ + AsyncExecutions> jsonObjkeys(K key, JsonPath jsonPath); + + /** + * Return the keys in the JSON document that are referenced by the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return List the keys in the JSON document that are referenced by the given {@link JsonPath}. + * @since 6.5 + */ + AsyncExecutions> jsonObjkeys(K key); + + /** + * Report the number of keys in the JSON object at the specified {@link JsonPath} and for the provided key + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) whose key(s) we want to count + * @return Long the number of keys in the JSON object at the specified path, or null if the path does not exist. + * @since 6.5 + */ + AsyncExecutions> jsonObjlen(K key, JsonPath jsonPath); + + /** + * Report the number of keys in the JSON object at the {@link JsonPath#ROOT_PATH} and for the provided key + * + * @param key the key holding the JSON document. + * @return Long the number of keys in the JSON object at the specified path, or null if the path does not exist. + * @since 6.5 + */ + AsyncExecutions> jsonObjlen(K key); + + /** + * Sets the JSON value at a given {@link JsonPath} in the JSON document. + *

+ * For new Redis keys, the path must be the root. For existing keys, when the entire path exists, the value that it contains + * is replaced with the JSON value. For existing keys, when the path exists, except for the last element, a new child is + * added with the JSON value. + *

+ * Adds a key (with its respective value) to a JSON Object (in a RedisJSON data type key) only if it is the last child in + * the path, or it is the parent of a new child being added in the path. Optional arguments NX and XX modify this behavior + * for both new RedisJSON data type keys and the JSON Object keys in them. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to set the value. + * @param value the {@link JsonValue} to set. + * @param options the {@link JsonSetArgs} the options for setting the value. + * @return String "OK" if the set was successful, null if the {@link JsonSetArgs} conditions are not met. + * @since 6.5 + */ + AsyncExecutions jsonSet(K key, JsonPath jsonPath, JsonValue value, JsonSetArgs options); + + /** + * Sets the JSON value at a given {@link JsonPath} in the JSON document using defaults for the {@link JsonSetArgs}. + *

+ * For new Redis keys the path must be the root. For existing keys, when the entire path exists, the value that it contains + * is replaced with the JSON value. For existing keys, when the path exists, except for the last element, a new child is + * added with the JSON value. + *

+ * Adds a key (with its respective value) to a JSON Object (in a RedisJSON data type key) only if it is the last child in + * the path, or it is the parent of a new child being added in the path. Optional arguments NX and XX modify this behavior + * for both new RedisJSON data type keys and the JSON Object keys in them. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to set the value. + * @param value the {@link JsonValue} to set. + * @return String "OK" if the set was successful, null if the {@link JsonSetArgs} conditions are not met. + * @since 6.5 + */ + AsyncExecutions jsonSet(K key, JsonPath jsonPath, JsonValue value); + + /** + * Append the json-string values to the string at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to append the value. + * @param value the {@link JsonValue} to append. + * @return Long the new length of the string, or null if the matching JSON value is not a string. + * @since 6.5 + */ + AsyncExecutions> jsonStrappend(K key, JsonPath jsonPath, JsonValue value); + + /** + * Append the json-string values to the string at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @param value the {@link JsonValue} to append. + * @return Long the new length of the string, or null if the matching JSON value is not a string. + * @since 6.5 + */ + AsyncExecutions> jsonStrappend(K key, JsonValue value); + + /** + * Report the length of the JSON String at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return Long (in recursive descent) the length of the JSON String at the provided {@link JsonPath}, or null if the value + * ath the desired path is not a string. + * @since 6.5 + */ + AsyncExecutions> jsonStrlen(K key, JsonPath jsonPath); + + /** + * Report the length of the JSON String at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @return Long (in recursive descent) the length of the JSON String at the provided {@link JsonPath}, or null if the value + * ath the desired path is not a string. + * @since 6.5 + */ + AsyncExecutions> jsonStrlen(K key); + + /** + * Toggle a Boolean value stored at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return List the new value after the toggle, 0 for false, 1 for true or null if the path does not exist. + * @since 6.5 + */ + AsyncExecutions> jsonToggle(K key, JsonPath jsonPath); + + /** + * Report the type of JSON value at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return List the type of JSON value at the provided {@link JsonPath} + * @since 6.5 + */ + AsyncExecutions> jsonType(K key, JsonPath jsonPath); + + /** + * Report the type of JSON value at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @return List the type of JSON value at the provided {@link JsonPath} + * @since 6.5 + */ + AsyncExecutions> jsonType(K key); + +} diff --git a/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionServerAsyncCommands.java b/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionServerAsyncCommands.java index 42b308d470..f3b7b877c0 100644 --- a/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionServerAsyncCommands.java +++ b/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionServerAsyncCommands.java @@ -19,15 +19,15 @@ */ package io.lettuce.core.cluster.api.async; -import java.util.Date; -import java.util.List; import java.util.Map; - +import java.util.List; +import java.util.Date; import io.lettuce.core.ClientListArgs; import io.lettuce.core.FlushMode; import io.lettuce.core.KillArgs; import io.lettuce.core.TrackingArgs; import io.lettuce.core.UnblockType; +import io.lettuce.core.TrackingInfo; import io.lettuce.core.protocol.CommandType; /** @@ -175,6 +175,14 @@ public interface NodeSelectionServerAsyncCommands { */ AsyncExecutions clientTracking(TrackingArgs args); + /** + * Returns information about the current client connection's use of the server assisted client side caching feature. + * + * @return {@link TrackingInfo}, for more information check the documentation + * @since 6.5 + */ + AsyncExecutions clientTrackinginfo(); + /** * Unblock the specified blocked client. * diff --git a/src/main/java/io/lettuce/core/cluster/api/async/RedisClusterAsyncCommands.java b/src/main/java/io/lettuce/core/cluster/api/async/RedisClusterAsyncCommands.java index 6a9c81ade5..9fb75c8b43 100644 --- a/src/main/java/io/lettuce/core/cluster/api/async/RedisClusterAsyncCommands.java +++ b/src/main/java/io/lettuce/core/cluster/api/async/RedisClusterAsyncCommands.java @@ -26,6 +26,7 @@ import io.lettuce.core.Range; import io.lettuce.core.RedisFuture; import io.lettuce.core.api.async.*; +import io.lettuce.core.json.JsonParser; /** * A complete asynchronous and thread-safe cluster Redis API with 400+ Methods. @@ -217,6 +218,16 @@ public interface RedisClusterAsyncCommands extends BaseRedisAsyncCommands< */ RedisFuture clusterMyId(); + /** + * Obtain the shard ID for the currently connected node. + *

+ * The CLUSTER MYSHARDID command returns the unique, auto-generated identifier that is associated with the shard to which + * the connected cluster node belongs. + * + * @return String simple-string-reply + */ + RedisFuture clusterMyShardId(); + /** * Obtain details about all cluster nodes. Can be parsed using * {@link io.lettuce.core.cluster.models.partitions.ClusterPartitionParser#parse} @@ -370,4 +381,17 @@ public interface RedisClusterAsyncCommands extends BaseRedisAsyncCommands< */ RedisFuture readWrite(); + /** + * Retrieves information about the TCP links between nodes in a Redis Cluster. + * + * @return List of maps containing attributes and values for each peer link. + */ + RedisFuture>> clusterLinks(); + + /** + * @return the currently configured instance of the {@link JsonParser} + * @since 6.5 + */ + JsonParser getJsonParser(); + } diff --git a/src/main/java/io/lettuce/core/cluster/api/reactive/RedisClusterReactiveCommands.java b/src/main/java/io/lettuce/core/cluster/api/reactive/RedisClusterReactiveCommands.java index d7e3a9757c..2416b380ec 100644 --- a/src/main/java/io/lettuce/core/cluster/api/reactive/RedisClusterReactiveCommands.java +++ b/src/main/java/io/lettuce/core/cluster/api/reactive/RedisClusterReactiveCommands.java @@ -21,6 +21,7 @@ import java.time.Duration; import java.util.List; +import java.util.Map; import io.lettuce.core.Range; import io.lettuce.core.api.reactive.*; @@ -216,6 +217,16 @@ public interface RedisClusterReactiveCommands extends BaseRedisReactiveCom */ Mono clusterMyId(); + /** + * Obtain the shard ID for the currently connected node. + *

+ * The CLUSTER MYSHARDID command returns the unique, auto-generated identifier that is associated with the shard to which + * the connected cluster node belongs. + * + * @return String simple-string-reply + */ + Mono clusterMyShardId(); + /** * Obtain details about all cluster nodes. Can be parsed using * {@link io.lettuce.core.cluster.models.partitions.ClusterPartitionParser#parse} @@ -358,4 +369,11 @@ public interface RedisClusterReactiveCommands extends BaseRedisReactiveCom */ Mono readWrite(); + /** + * Retrieves information about the TCP links between nodes in a Redis Cluster. + * + * @return List of maps containing attributes and values for each peer link. + */ + Mono>> clusterLinks(); + } diff --git a/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionJsonCommands.java b/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionJsonCommands.java new file mode 100644 index 0000000000..0ca886a8b8 --- /dev/null +++ b/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionJsonCommands.java @@ -0,0 +1,440 @@ +/* + * Copyright 2017-2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ +package io.lettuce.core.cluster.api.sync; + +import java.util.List; +import io.lettuce.core.json.JsonPath; +import io.lettuce.core.json.JsonType; +import io.lettuce.core.json.JsonValue; +import io.lettuce.core.json.arguments.JsonGetArgs; +import io.lettuce.core.json.arguments.JsonMsetArgs; +import io.lettuce.core.json.arguments.JsonRangeArgs; +import io.lettuce.core.json.arguments.JsonSetArgs; + +/** + * Synchronous executed commands on a node selection for JSON documents + * + * @param Key type. + * @param Value type. + * @author Tihomir Mateev + * @see Redis JSON + * @since 6.5 + * @generated by io.lettuce.apigenerator.CreateSyncNodeSelectionClusterApi + */ +public interface NodeSelectionJsonCommands { + + /** + * Append the JSON values into the array at a given {@link JsonPath} after the last element in a said array. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param values one or more {@link JsonValue} to be appended. + * @return Long the resulting size of the arrays after the new data was appended, or null if the path does not exist. + * @since 6.5 + */ + Executions> jsonArrappend(K key, JsonPath jsonPath, JsonValue... values); + + /** + * Append the JSON values into the array at the {@link JsonPath#ROOT_PATH} after the last element in a said array. + * + * @param key the key holding the JSON document. + * @param values one or more {@link JsonValue} to be appended. + * @return Long the resulting size of the arrays after the new data was appended, or null if the path does not exist. + * @since 6.5 + */ + Executions> jsonArrappend(K key, JsonValue... values); + + /** + * Search for the first occurrence of a {@link JsonValue} in an array at a given {@link JsonPath} and return its index. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param value the {@link JsonValue} to search for. + * @param range the {@link JsonRangeArgs} to search within. + * @return Long the index hosting the searched element, -1 if not found or null if the specified path is not an array. + * @since 6.5 + */ + Executions> jsonArrindex(K key, JsonPath jsonPath, JsonValue value, JsonRangeArgs range); + + /** + * Search for the first occurrence of a {@link JsonValue} in an array at a given {@link JsonPath} and return its index. This + * method uses defaults for the start and end indexes, see {@link JsonRangeArgs#DEFAULT_START_INDEX} and + * {@link JsonRangeArgs#DEFAULT_END_INDEX}. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param value the {@link JsonValue} to search for. + * @return Long the index hosting the searched element, -1 if not found or null if the specified path is not an array. + * @since 6.5 + */ + Executions> jsonArrindex(K key, JsonPath jsonPath, JsonValue value); + + /** + * Insert the {@link JsonValue}s into the array at a given {@link JsonPath} before the provided index, shifting the existing + * elements to the right + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param index the index before which the new elements will be inserted. + * @param values one or more {@link JsonValue}s to be inserted. + * @return Long the resulting size of the arrays after the new data was inserted, or null if the path does not exist. + * @since 6.5 + */ + Executions> jsonArrinsert(K key, JsonPath jsonPath, int index, JsonValue... values); + + /** + * Report the length of the JSON array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @return the size of the arrays, or null if the path does not exist. + * @since 6.5 + */ + Executions> jsonArrlen(K key, JsonPath jsonPath); + + /** + * Report the length of the JSON array at a the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return the size of the arrays, or null if the path does not exist. + * @since 6.5 + */ + Executions> jsonArrlen(K key); + + /** + * Remove and return {@link JsonValue} at a given index in the array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param index the index of the element to be removed. Default is -1, meaning the last element. Out-of-range indexes round + * to their respective array ends. Popping an empty array returns null. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + Executions> jsonArrpop(K key, JsonPath jsonPath, int index); + + /** + * Remove and return {@link JsonValue} at index -1 (last element) in the array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + Executions> jsonArrpop(K key, JsonPath jsonPath); + + /** + * Remove and return {@link JsonValue} at index -1 (last element) in the array at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + Executions> jsonArrpop(K key); + + /** + * Trim an array at a given {@link JsonPath} so that it contains only the specified inclusive range of elements. All + * elements with indexes smaller than the start range and all elements with indexes bigger than the end range are trimmed. + *

+ * Behavior as of RedisJSON v2.0: + *

    + *
  • If start is larger than the array's size or start > stop, returns 0 and an empty array.
  • + *
  • If start is < 0, then start from the end of the array.
  • + *
  • If stop is larger than the end of the array, it is treated like the last element.
  • + *
+ * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param range the {@link JsonRangeArgs} to trim by. + * @return Long the resulting size of the arrays after the trimming, or null if the path does not exist. + * @since 6.5 + */ + Executions> jsonArrtrim(K key, JsonPath jsonPath, JsonRangeArgs range); + + /** + * Clear container values (arrays/objects) and set numeric values to 0 + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to clear. + * @return Long the number of values removed plus all the matching JSON numerical values that are zeroed. + * @since 6.5 + */ + Executions jsonClear(K key, JsonPath jsonPath); + + /** + * Clear container values (arrays/objects) and set numeric values to 0 at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return Long the number of values removed plus all the matching JSON numerical values that are zeroed. + * @since 6.5 + */ + Executions jsonClear(K key); + + /** + * Deletes a value inside the JSON document at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to clear. + * @return Long the number of values removed (0 or more). + * @since 6.5 + */ + Executions jsonDel(K key, JsonPath jsonPath); + + /** + * Deletes a value inside the JSON document at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return Long the number of values removed (0 or more). + * @since 6.5 + */ + Executions jsonDel(K key); + + /** + * Return the value at the specified path in JSON serialized form. + *

+ * When using a single JSONPath, the root of the matching values is a JSON string with a top-level array of serialized JSON + * value. In contrast, a legacy path returns a single value. + *

+ * When using multiple JSONPath arguments, the root of the matching values is a JSON string with a top-level object, with + * each object value being a top-level array of serialized JSON value. In contrast, if all paths are legacy paths, each + * object value is a single serialized JSON value. If there are multiple paths that include both legacy path and JSONPath, + * the returned value conforms to the JSONPath version (an array of values). + * + * @param key the key holding the JSON document. + * @param options the {@link JsonGetArgs} to use. + * @param jsonPaths the {@link JsonPath}s to use to identify the values to get. + * @return JsonValue the value at path in JSON serialized form, or null if the path does not exist. + * @since 6.5 + */ + Executions> jsonGet(K key, JsonGetArgs options, JsonPath... jsonPaths); + + /** + * Return the value at the specified path in JSON serialized form. Uses defaults for the {@link JsonGetArgs}. + *

+ * When using a single JSONPath, the root of the matching values is a JSON string with a top-level array of serialized JSON + * value. In contrast, a legacy path returns a single value. + *

+ * When using multiple JSONPath arguments, the root of the matching values is a JSON string with a top-level object, with + * each object value being a top-level array of serialized JSON value. In contrast, if all paths are legacy paths, each + * object value is a single serialized JSON value. If there are multiple paths that include both legacy path and JSONPath, + * the returned value conforms to the JSONPath version (an array of values). + * + * @param key the key holding the JSON document. + * @param jsonPaths the {@link JsonPath}s to use to identify the values to get. + * @return JsonValue the value at path in JSON serialized form, or null if the path does not exist. + * @since 6.5 + */ + Executions> jsonGet(K key, JsonPath... jsonPaths); + + /** + * Merge a given {@link JsonValue} with the value matching {@link JsonPath}. Consequently, JSON values at matching paths are + * updated, deleted, or expanded with new children. + *

+ * Merging is done according to the following rules per JSON value in the value argument while considering the corresponding + * original value if it exists: + *

    + *
  • merging an existing object key with a null value deletes the key
  • + *
  • merging an existing object key with non-null value updates the value
  • + *
  • merging a non-existing object key adds the key and value
  • + *
  • merging an existing array with any merged value, replaces the entire array with the value
  • + *
+ *

+ * This command complies with RFC7396 "Json Merge Patch" + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to merge. + * @param value the {@link JsonValue} to merge. + * @return String "OK" if the set was successful, error if the operation failed. + * @since 6.5 + * @see RFC7396 + */ + Executions jsonMerge(K key, JsonPath jsonPath, JsonValue value); + + /** + * Return the values at the specified path from multiple key arguments. + * + * @param jsonPath the {@link JsonPath} pointing to the value to fetch. + * @param keys the keys holding the {@link JsonValue}s to fetch. + * @return List the values at path, or null if the path does not exist. + * @since 6.5 + */ + Executions> jsonMGet(JsonPath jsonPath, K... keys); + + /** + * Set or update one or more JSON values according to the specified {@link JsonMsetArgs} + *

+ * JSON.MSET is atomic, hence, all given additions or updates are either applied or not. It is not possible for clients to + * see that some keys were updated while others are unchanged. + *

+ * A JSON value is a hierarchical structure. If you change a value in a specific path - nested values are affected. + * + * @param arguments the {@link JsonMsetArgs} specifying the values to change. + * @return "OK" if the operation was successful, error otherwise + * @since 6.5 + */ + Executions jsonMSet(List> arguments); + + /** + * Increment the number value stored at the specified {@link JsonPath} in the JSON document by the provided increment. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to increment. + * @param number the increment value. + * @return a {@link List} of the new values after the increment. + * @since 6.5 + */ + Executions> jsonNumincrby(K key, JsonPath jsonPath, Number number); + + /** + * Return the keys in the JSON document that are referenced by the given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) whose key(s) we want. + * @return List the keys in the JSON document that are referenced by the given {@link JsonPath}. + * @since 6.5 + */ + Executions> jsonObjkeys(K key, JsonPath jsonPath); + + /** + * Return the keys in the JSON document that are referenced by the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return List the keys in the JSON document that are referenced by the given {@link JsonPath}. + * @since 6.5 + */ + Executions> jsonObjkeys(K key); + + /** + * Report the number of keys in the JSON object at the specified {@link JsonPath} and for the provided key + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) whose key(s) we want to count + * @return Long the number of keys in the JSON object at the specified path, or null if the path does not exist. + * @since 6.5 + */ + Executions> jsonObjlen(K key, JsonPath jsonPath); + + /** + * Report the number of keys in the JSON object at the {@link JsonPath#ROOT_PATH} and for the provided key + * + * @param key the key holding the JSON document. + * @return Long the number of keys in the JSON object at the specified path, or null if the path does not exist. + * @since 6.5 + */ + Executions> jsonObjlen(K key); + + /** + * Sets the JSON value at a given {@link JsonPath} in the JSON document. + *

+ * For new Redis keys, the path must be the root. For existing keys, when the entire path exists, the value that it contains + * is replaced with the JSON value. For existing keys, when the path exists, except for the last element, a new child is + * added with the JSON value. + *

+ * Adds a key (with its respective value) to a JSON Object (in a RedisJSON data type key) only if it is the last child in + * the path, or it is the parent of a new child being added in the path. Optional arguments NX and XX modify this behavior + * for both new RedisJSON data type keys and the JSON Object keys in them. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to set the value. + * @param value the {@link JsonValue} to set. + * @param options the {@link JsonSetArgs} the options for setting the value. + * @return String "OK" if the set was successful, null if the {@link JsonSetArgs} conditions are not met. + * @since 6.5 + */ + Executions jsonSet(K key, JsonPath jsonPath, JsonValue value, JsonSetArgs options); + + /** + * Sets the JSON value at a given {@link JsonPath} in the JSON document using defaults for the {@link JsonSetArgs}. + *

+ * For new Redis keys the path must be the root. For existing keys, when the entire path exists, the value that it contains + * is replaced with the JSON value. For existing keys, when the path exists, except for the last element, a new child is + * added with the JSON value. + *

+ * Adds a key (with its respective value) to a JSON Object (in a RedisJSON data type key) only if it is the last child in + * the path, or it is the parent of a new child being added in the path. Optional arguments NX and XX modify this behavior + * for both new RedisJSON data type keys and the JSON Object keys in them. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to set the value. + * @param value the {@link JsonValue} to set. + * @return String "OK" if the set was successful, null if the {@link JsonSetArgs} conditions are not met. + * @since 6.5 + */ + Executions jsonSet(K key, JsonPath jsonPath, JsonValue value); + + /** + * Append the json-string values to the string at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to append the value. + * @param value the {@link JsonValue} to append. + * @return Long the new length of the string, or null if the matching JSON value is not a string. + * @since 6.5 + */ + Executions> jsonStrappend(K key, JsonPath jsonPath, JsonValue value); + + /** + * Append the json-string values to the string at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @param value the {@link JsonValue} to append. + * @return Long the new length of the string, or null if the matching JSON value is not a string. + * @since 6.5 + */ + Executions> jsonStrappend(K key, JsonValue value); + + /** + * Report the length of the JSON String at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return Long (in recursive descent) the length of the JSON String at the provided {@link JsonPath}, or null if the value + * ath the desired path is not a string. + * @since 6.5 + */ + Executions> jsonStrlen(K key, JsonPath jsonPath); + + /** + * Report the length of the JSON String at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @return Long (in recursive descent) the length of the JSON String at the provided {@link JsonPath}, or null if the value + * ath the desired path is not a string. + * @since 6.5 + */ + Executions> jsonStrlen(K key); + + /** + * Toggle a Boolean value stored at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return List the new value after the toggle, 0 for false, 1 for true or null if the path does not exist. + * @since 6.5 + */ + Executions> jsonToggle(K key, JsonPath jsonPath); + + /** + * Report the type of JSON value at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return List the type of JSON value at the provided {@link JsonPath} + * @since 6.5 + */ + Executions> jsonType(K key, JsonPath jsonPath); + + /** + * Report the type of JSON value at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @return List the type of JSON value at the provided {@link JsonPath} + * @since 6.5 + */ + Executions> jsonType(K key); + +} diff --git a/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionServerCommands.java b/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionServerCommands.java index b8179a7d0f..bd6b2e13d3 100644 --- a/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionServerCommands.java +++ b/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionServerCommands.java @@ -28,6 +28,7 @@ import io.lettuce.core.KillArgs; import io.lettuce.core.TrackingArgs; import io.lettuce.core.UnblockType; +import io.lettuce.core.TrackingInfo; import io.lettuce.core.protocol.CommandType; /** @@ -163,7 +164,7 @@ public interface NodeSelectionServerCommands { * @return simple-string-reply {@code OK} if the connection name was successfully set. * @since 6.3 */ - Executions clientSetinfo(String key, V value); + Executions clientSetinfo(String key, String value); /** * Enables the tracking feature of the Redis server, that is used for server assisted client side caching. Tracking messages @@ -175,6 +176,14 @@ public interface NodeSelectionServerCommands { */ Executions clientTracking(TrackingArgs args); + /** + * Returns information about the current client connection's use of the server assisted client side caching feature. + * + * @return {@link TrackingInfo}, for more information check the documentation + * @since 6.5 + */ + Executions clientTrackinginfo(); + /** * Unblock the specified blocked client. * diff --git a/src/main/java/io/lettuce/core/cluster/api/sync/RedisClusterCommands.java b/src/main/java/io/lettuce/core/cluster/api/sync/RedisClusterCommands.java index 597ec53d75..988975740c 100644 --- a/src/main/java/io/lettuce/core/cluster/api/sync/RedisClusterCommands.java +++ b/src/main/java/io/lettuce/core/cluster/api/sync/RedisClusterCommands.java @@ -21,9 +21,11 @@ import java.time.Duration; import java.util.List; +import java.util.Map; import io.lettuce.core.Range; import io.lettuce.core.api.sync.*; +import io.lettuce.core.json.JsonParser; /** * A complete synchronous and thread-safe Redis Cluster API with 400+ Methods. @@ -34,10 +36,11 @@ * @author dengliming * @since 4.0 */ -public interface RedisClusterCommands extends BaseRedisCommands, RedisAclCommands, - RedisFunctionCommands, RedisGeoCommands, RedisHashCommands, RedisHLLCommands, - RedisKeyCommands, RedisListCommands, RedisScriptingCommands, RedisServerCommands, - RedisSetCommands, RedisSortedSetCommands, RedisStreamCommands, RedisStringCommands { +public interface RedisClusterCommands + extends BaseRedisCommands, RedisAclCommands, RedisFunctionCommands, RedisGeoCommands, + RedisHashCommands, RedisHLLCommands, RedisKeyCommands, RedisListCommands, + RedisScriptingCommands, RedisServerCommands, RedisSetCommands, RedisSortedSetCommands, + RedisStreamCommands, RedisStringCommands, RedisJsonCommands { /** * Set the default timeout for operations. A zero timeout value indicates to not time out. @@ -214,6 +217,16 @@ public interface RedisClusterCommands extends BaseRedisCommands, Red */ String clusterMyId(); + /** + * Obtain the shard ID for the currently connected node. + *

+ * The CLUSTER MYSHARDID command returns the unique, auto-generated identifier that is associated with the shard to which + * the connected cluster node belongs. + * + * @return String simple-string-reply + */ + String clusterMyShardId(); + /** * Obtain details about all cluster nodes. Can be parsed using * {@link io.lettuce.core.cluster.models.partitions.ClusterPartitionParser#parse} @@ -358,4 +371,17 @@ public interface RedisClusterCommands extends BaseRedisCommands, Red @Override String readWrite(); + /** + * Retrieves information about the TCP links between nodes in a Redis Cluster. + * + * @return List of maps containing attributes and values for each peer link. + */ + List> clusterLinks(); + + /** + * @return the currently configured instance of the {@link JsonParser} + * @since 6.5 + */ + JsonParser getJsonParser(); + } diff --git a/src/main/java/io/lettuce/core/cluster/models/partitions/RedisClusterNode.java b/src/main/java/io/lettuce/core/cluster/models/partitions/RedisClusterNode.java index e1cef2fdce..c17006f731 100644 --- a/src/main/java/io/lettuce/core/cluster/models/partitions/RedisClusterNode.java +++ b/src/main/java/io/lettuce/core/cluster/models/partitions/RedisClusterNode.java @@ -45,6 +45,7 @@ * * @author Mark Paluch * @author Alessandro Simi + * @author Tony Zhang * @since 3.0 */ @SuppressWarnings("serial") @@ -103,8 +104,11 @@ public RedisClusterNode(RedisURI uri, String nodeId, boolean connected, String s this.configEpoch = configEpoch; this.replOffset = -1; - this.slots = new BitSet(slots.length()); - this.slots.or(slots); + this.slots = new BitSet(SlotHash.SLOT_COUNT); + + if (slots != null) { + this.slots.or(slots); + } setFlags(flags); } @@ -123,8 +127,9 @@ public RedisClusterNode(RedisClusterNode redisClusterNode) { this.replOffset = redisClusterNode.replOffset; this.aliases.addAll(redisClusterNode.aliases); - if (redisClusterNode.slots != null && !redisClusterNode.slots.isEmpty()) { - this.slots = new BitSet(SlotHash.SLOT_COUNT); + this.slots = new BitSet(SlotHash.SLOT_COUNT); + + if (redisClusterNode.slots != null) { this.slots.or(redisClusterNode.slots); } @@ -290,6 +295,15 @@ public List getSlots() { return slots; } + /** + * Checks if the node has no slots assigned. + * + * @return {@code true} if the slots field is null or empty, {@code false} otherwise. + */ + public boolean hasNoSlots() { + return slots == null || slots.isEmpty(); + } + /** * Performs the given action for each slot of this {@link RedisClusterNode} until all elements have been processed or the * action throws an exception. Unless otherwise specified by the implementing class, actions are performed in the order of diff --git a/src/main/java/io/lettuce/core/dynamic/DefaultCommandMethodVerifier.java b/src/main/java/io/lettuce/core/dynamic/DefaultCommandMethodVerifier.java index ea49a9f916..dc44640ac0 100644 --- a/src/main/java/io/lettuce/core/dynamic/DefaultCommandMethodVerifier.java +++ b/src/main/java/io/lettuce/core/dynamic/DefaultCommandMethodVerifier.java @@ -55,10 +55,10 @@ public DefaultCommandMethodVerifier(List commandDetails) { @Override public void validate(CommandSegments commandSegments, CommandMethod commandMethod) throws CommandMethodSyntaxException { - LettuceAssert.notEmpty(commandSegments.getCommandType().name(), "Command name must not be empty"); + LettuceAssert.notEmpty(commandSegments.getCommandType().toString(), "Command name must not be empty"); - CommandDetail commandDetail = findCommandDetail(commandSegments.getCommandType().name()) - .orElseThrow(() -> syntaxException(commandSegments.getCommandType().name(), commandMethod)); + CommandDetail commandDetail = findCommandDetail(commandSegments.getCommandType().toString()) + .orElseThrow(() -> syntaxException(commandSegments.getCommandType().toString(), commandMethod)); validateParameters(commandDetail, commandSegments, commandMethod); } diff --git a/src/main/java/io/lettuce/core/dynamic/output/OutputRegistry.java b/src/main/java/io/lettuce/core/dynamic/output/OutputRegistry.java index 0ce4557ef1..8d59004cf3 100644 --- a/src/main/java/io/lettuce/core/dynamic/output/OutputRegistry.java +++ b/src/main/java/io/lettuce/core/dynamic/output/OutputRegistry.java @@ -56,6 +56,8 @@ public class OutputRegistry { register(registry, StringListOutput.class, StringListOutput::new); register(registry, VoidOutput.class, VoidOutput::new); + register(registry, StringMatchResultOutput.class, StringMatchResultOutput::new); + BUILTIN.putAll(registry); } diff --git a/src/main/java/io/lettuce/core/dynamic/segment/CommandSegments.java b/src/main/java/io/lettuce/core/dynamic/segment/CommandSegments.java index 155edc4b65..5cade513d3 100644 --- a/src/main/java/io/lettuce/core/dynamic/segment/CommandSegments.java +++ b/src/main/java/io/lettuce/core/dynamic/segment/CommandSegments.java @@ -80,14 +80,9 @@ public byte[] getBytes() { return commandTypeBytes; } - @Override - public String name() { - return commandType; - } - @Override public String toString() { - return name(); + return commandType; } @Override @@ -112,7 +107,7 @@ public int hashCode() { @Override public String toString() { StringBuilder sb = new StringBuilder(); - sb.append(getCommandType().name()); + sb.append(getCommandType().toString()); for (CommandSegment segment : segments) { sb.append(' ').append(segment); diff --git a/src/main/java/io/lettuce/core/internal/Futures.java b/src/main/java/io/lettuce/core/internal/Futures.java index 8c3fcf69f5..f8276f8b3b 100644 --- a/src/main/java/io/lettuce/core/internal/Futures.java +++ b/src/main/java/io/lettuce/core/internal/Futures.java @@ -12,6 +12,7 @@ * without further notice. * * @author Mark Paluch + * @author jinkshower * @since 5.1 */ public abstract class Futures { @@ -21,7 +22,7 @@ private Futures() { } /** - * Create a composite {@link CompletableFuture} is composed from the given {@code stages}. + * Create a composite {@link CompletableFuture} that is composed of the given {@code stages}. * * @param stages must not be {@code null}. * @return the composed {@link CompletableFuture}. @@ -32,10 +33,11 @@ public static CompletableFuture allOf(Collection stage : stages) { + for (CompletionStage stage : copies) { futures[index++] = stage.toCompletableFuture(); } diff --git a/src/main/java/io/lettuce/core/json/DefaultJsonParser.java b/src/main/java/io/lettuce/core/json/DefaultJsonParser.java new file mode 100644 index 0000000000..afab7afa70 --- /dev/null +++ b/src/main/java/io/lettuce/core/json/DefaultJsonParser.java @@ -0,0 +1,86 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Default implementation of the {@link JsonParser} that should fit most use cases. Utilizes the Jackson library for maintaining + * the JSON tree model and provides the ability to create new instances of the {@link JsonValue}, {@link JsonArray} and + * {@link JsonObject}. + * + * @since 6.5 + * @author Tihomir Mateev + */ +public class DefaultJsonParser implements JsonParser { + + @Override + public JsonValue loadJsonValue(ByteBuffer bytes) { + return new UnproccessedJsonValue(bytes, this); + } + + @Override + public JsonValue createJsonValue(ByteBuffer bytes) { + return parse(bytes); + } + + @Override + public JsonValue createJsonValue(String value) { + return parse(value); + } + + @Override + public JsonObject createJsonObject() { + return new DelegateJsonObject(); + } + + @Override + public JsonArray createJsonArray() { + return new DelegateJsonArray(); + } + + @Override + public JsonValue fromObject(Object object) { + ObjectMapper objectMapper = new ObjectMapper(); + try { + JsonNode root = objectMapper.valueToTree(object); + return DelegateJsonValue.wrap(root); + } catch (IllegalArgumentException e) { + throw new RedisJsonException("Failed to process the provided object as JSON", e); + } + } + + private JsonValue parse(String value) { + ObjectMapper mapper = new ObjectMapper(); + try { + JsonNode root = mapper.readTree(value); + return DelegateJsonValue.wrap(root); + } catch (JsonProcessingException e) { + throw new RedisJsonException( + "Failed to process the provided value as JSON: " + String.format("%.50s", value) + "...", e); + } + } + + private JsonValue parse(ByteBuffer byteBuffer) { + ObjectMapper mapper = new ObjectMapper(); + try { + byte[] bytes = new byte[byteBuffer.remaining()]; + byteBuffer.get(bytes); + JsonNode root = mapper.readTree(bytes); + return DelegateJsonValue.wrap(root); + } catch (IOException e) { + throw new RedisJsonException("Failed to process the provided value as JSON", e); + } + } + +} diff --git a/src/main/java/io/lettuce/core/json/DelegateJsonArray.java b/src/main/java/io/lettuce/core/json/DelegateJsonArray.java new file mode 100644 index 0000000000..39d63ac75f --- /dev/null +++ b/src/main/java/io/lettuce/core/json/DelegateJsonArray.java @@ -0,0 +1,108 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import io.lettuce.core.internal.LettuceAssert; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Implementation of the {@link DelegateJsonArray} that delegates most of its' functionality to the Jackson {@link ArrayNode}. + * + * @author Tihomir Mateev + */ +class DelegateJsonArray extends DelegateJsonValue implements JsonArray { + + DelegateJsonArray() { + super(new ArrayNode(JsonNodeFactory.instance)); + } + + DelegateJsonArray(JsonNode node) { + super(node); + } + + @Override + public JsonArray add(JsonValue element) { + JsonNode newNode = null; + + if (element != null) { + newNode = ((DelegateJsonValue) element).getNode(); + } + + ((ArrayNode) node).add(newNode); + + return this; + } + + @Override + public void addAll(JsonArray element) { + LettuceAssert.notNull(element, "Element must not be null"); + + ArrayNode otherArray = (ArrayNode) ((DelegateJsonValue) element).getNode(); + ((ArrayNode) node).addAll(otherArray); + } + + @Override + public List asList() { + List result = new ArrayList<>(); + + for (JsonNode jsonNode : node) { + result.add(new DelegateJsonValue(jsonNode)); + } + + return result; + } + + @Override + public JsonValue get(int index) { + JsonNode jsonNode = node.get(index); + + return jsonNode == null ? null : wrap(jsonNode); + } + + @Override + public JsonValue getFirst() { + return get(0); + } + + @Override + public Iterator iterator() { + return asList().iterator(); + } + + @Override + public JsonValue remove(int index) { + JsonNode jsonNode = ((ArrayNode) node).remove(index); + + return wrap(jsonNode); + } + + @Override + public JsonValue replace(int index, JsonValue newElement) { + JsonNode replaceWith = ((DelegateJsonValue) newElement).getNode(); + JsonNode replaced = ((ArrayNode) node).set(index, replaceWith); + + return wrap(replaced); + } + + @Override + public int size() { + return node.size(); + } + + @Override + public JsonArray asJsonArray() { + return this; + } + +} diff --git a/src/main/java/io/lettuce/core/json/DelegateJsonObject.java b/src/main/java/io/lettuce/core/json/DelegateJsonObject.java new file mode 100644 index 0000000000..8159726f61 --- /dev/null +++ b/src/main/java/io/lettuce/core/json/DelegateJsonObject.java @@ -0,0 +1,61 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * Implementation of the {@link DelegateJsonObject} that delegates most of its functionality to the Jackson {@link ObjectNode}. + * + * @author Tihomir Mateev + */ +class DelegateJsonObject extends DelegateJsonValue implements JsonObject { + + DelegateJsonObject() { + super(new ObjectNode(JsonNodeFactory.instance)); + } + + DelegateJsonObject(JsonNode node) { + super(node); + } + + @Override + public JsonObject put(String key, JsonValue element) { + JsonNode newNode = ((DelegateJsonValue) element).getNode(); + + ((ObjectNode) node).replace(key, newNode); + return this; + } + + @Override + public JsonValue get(String key) { + JsonNode value = node.get(key); + + return value == null ? null : wrap(value); + } + + @Override + public JsonValue remove(String key) { + JsonNode value = ((ObjectNode) node).remove(key); + + return wrap(value); + } + + @Override + public int size() { + return node.size(); + } + + @Override + public JsonObject asJsonObject() { + return this; + } + +} diff --git a/src/main/java/io/lettuce/core/json/DelegateJsonValue.java b/src/main/java/io/lettuce/core/json/DelegateJsonValue.java new file mode 100644 index 0000000000..3ca65eb7a7 --- /dev/null +++ b/src/main/java/io/lettuce/core/json/DelegateJsonValue.java @@ -0,0 +1,122 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.nio.ByteBuffer; + +/** + * Implementation of the {@link JsonValue} that delegates most of its functionality to the Jackson {@link JsonNode}. + * + * @author Tihomir Mateev + */ +class DelegateJsonValue implements JsonValue { + + protected JsonNode node; + + DelegateJsonValue(JsonNode node) { + this.node = node; + } + + @Override + public String toString() { + return node.toString(); + } + + @Override + public ByteBuffer asByteBuffer() { + byte[] result = node.toString().getBytes(); + return ByteBuffer.wrap(result); + } + + @Override + public boolean isJsonArray() { + return node.isArray(); + } + + @Override + public JsonArray asJsonArray() { + return null; + } + + @Override + public boolean isJsonObject() { + return node.isObject(); + } + + @Override + public JsonObject asJsonObject() { + return null; + } + + @Override + public boolean isString() { + return node.isTextual(); + } + + @Override + public String asString() { + return node.isTextual() ? node.asText() : null; + } + + @Override + public boolean isNumber() { + return node.isNumber(); + } + + @Override + public Boolean asBoolean() { + + return node.isBoolean() ? node.asBoolean() : null; + } + + @Override + public boolean isBoolean() { + return node.isBoolean(); + } + + public boolean isNull() { + return node.isNull(); + } + + @Override + public Number asNumber() { + if (node.isNull()) { + return null; + } + return node.numberValue(); + } + + protected JsonNode getNode() { + return node; + } + + @Override + public T toObject(Class type) { + ObjectMapper mapper = new ObjectMapper(); + try { + return mapper.treeToValue(node, type); + } catch (IllegalArgumentException | JsonProcessingException e) { + throw new RedisJsonException("Unable to map the provided JsonValue to " + type.getName(), e); + } + } + + static JsonValue wrap(JsonNode root) { + if (root.isObject()) { + return new DelegateJsonObject(root); + } else if (root.isArray()) { + return new DelegateJsonArray(root); + } + + return new DelegateJsonValue(root); + } + +} diff --git a/src/main/java/io/lettuce/core/json/JsonArray.java b/src/main/java/io/lettuce/core/json/JsonArray.java new file mode 100644 index 0000000000..687dbec22f --- /dev/null +++ b/src/main/java/io/lettuce/core/json/JsonArray.java @@ -0,0 +1,90 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +import java.util.Iterator; +import java.util.List; + +/** + * Representation of a JSON array as per RFC 8259 - The + * JavaScript Object Notation (JSON) Data Interchange Format, Section 5. Arrays + *

+ * + * @see JsonValue + * @author Tihomir Mateev + * @since 6.5 + */ +public interface JsonArray extends JsonValue { + + /** + * Add a new {@link JsonValue} to the array. Supports chaining of calls. + * + * @param element the value to add + * @return the updated {@link JsonArray} to allow call chaining + */ + JsonArray add(JsonValue element); + + /** + * Add all elements from the provided {@link JsonArray} to this array. + * + * @param element the array to add all elements from + */ + void addAll(JsonArray element); + + /** + * Get all the {@link JsonValue}s in the array as a {@link List}. + * + * @return the {@link List} of {@link JsonValue}s in the array + */ + List asList(); + + /** + * Get the {@link JsonValue} at the provided index. + * + * @param index the index to get the value for + * @return the {@link JsonValue} at the provided index or {@code null} if no value is found + */ + JsonValue get(int index); + + /** + * Get the first {@link JsonValue} in the array. + * + * @return the first {@link JsonValue} in the array or {@code null} if the array is empty + */ + JsonValue getFirst(); + + /** + * Get an {@link Iterator} allowing access to all the {@link JsonValue}s in the array. + * + * @return the last {@link JsonValue} in the array or {@code null} if the array is empty + */ + Iterator iterator(); + + /** + * Remove the {@link JsonValue} at the provided index. + * + * @param index the index to remove the value for + * @return the removed {@link JsonValue} or {@code null} if no value is found + */ + JsonValue remove(int index); + + /** + * Replace the {@link JsonValue} at the provided index with the provided new {@link JsonValue}. + * + * @param index the index to replace the value for + * @param newElement the new value to replace the old one with + * @return the updated {@link JsonArray} to allow call chaining + */ + JsonValue replace(int index, JsonValue newElement); + + /** + * @return the number of elements in this {@link JsonArray} + */ + int size(); + +} diff --git a/src/main/java/io/lettuce/core/json/JsonObject.java b/src/main/java/io/lettuce/core/json/JsonObject.java new file mode 100644 index 0000000000..4ab4ce0cc8 --- /dev/null +++ b/src/main/java/io/lettuce/core/json/JsonObject.java @@ -0,0 +1,52 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +/** + * Representation of a JSON object as per RFC 8259 - The + * JavaScript Object Notation (JSON) Data Interchange Format, Section 4. Objects + *

+ * + * @see JsonValue + * @author Tihomir Mateev + * @since 6.5 + */ +public interface JsonObject extends JsonValue { + + /** + * Add (if there is no value with the same key already) or replace (if there is) a new {@link JsonValue} to the object under + * the provided key. Supports chaining of calls. + * + * @param key the key of the {@link JsonValue} to add or replace + * @param element the value to add or replace + * @return the updated {@link JsonObject} to allow call chaining + */ + JsonObject put(String key, JsonValue element); + + /** + * Get the {@link JsonValue} under the provided key. + * + * @param key the key to get the value for + * @return the {@link JsonValue} under the provided key or {@code null} if no value is found + */ + JsonValue get(String key); + + /** + * Remove the {@link JsonValue} under the provided key. + * + * @param key the key to remove the value for + * @return the removed {@link JsonValue} or {@code null} if no value is found + */ + JsonValue remove(String key); + + /** + * @return the number of key-value pairs in this {@link JsonObject} + */ + int size(); + +} diff --git a/src/main/java/io/lettuce/core/json/JsonParser.java b/src/main/java/io/lettuce/core/json/JsonParser.java new file mode 100644 index 0000000000..3e6122f3b2 --- /dev/null +++ b/src/main/java/io/lettuce/core/json/JsonParser.java @@ -0,0 +1,78 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +import java.nio.ByteBuffer; + +/** + * The JsonParser is an abstraction that allows transforming a JSON document from a {@link ByteBuffer} to implementations of the + * {@link JsonValue} interface and vice versa. Underneath there could be different implementations that use different JSON + * parser libraries or custom implementations. Respectively the {@link JsonParser} is responsible for building new instances of + * the {@link JsonArray} and {@link JsonObject} interfaces, as they are ultimately tightly coupled with the specific JSON parser + * that is being used. + *

+ * A custom implementation of the {@link JsonParser} can be provided to the {@link io.lettuce.core.ClientOptions} in case the + * default implementation does not fit the requirements. + * + * @since 6.5 + * @author Tihomir Mateev + */ +public interface JsonParser { + + /** + * Loads the provided {@link ByteBuffer} in a new {@link JsonValue}. Does not start the actual processing of the + * {@link ByteBuffer} until a method of the {@link JsonValue} is called. + * + * @param bytes the {@link ByteBuffer} to create the {@link JsonValue} from + * @return the created {@link JsonValue} + * @throws RedisJsonException if the provided {@link ByteBuffer} is not a valid JSON document + */ + JsonValue loadJsonValue(ByteBuffer bytes); + + /** + * Create a new {@link JsonValue} from the provided {@link ByteBuffer}. + * + * @param bytes the {@link ByteBuffer} to create the {@link JsonValue} from + * @return the created {@link JsonValue} + * @throws RedisJsonException if the provided {@link ByteBuffer} is not a valid JSON document + */ + JsonValue createJsonValue(ByteBuffer bytes); + + /** + * Create a new {@link JsonValue} from the provided value. + * + * @param value the value to create the {@link JsonValue} from + * @return the created {@link JsonValue} + * @throws RedisJsonException if the provided value is not a valid JSON document + */ + JsonValue createJsonValue(String value); + + /** + * Create a new empty {@link JsonObject}. + * + * @return the created {@link JsonObject} + */ + JsonObject createJsonObject(); + + /** + * Create a new empty {@link JsonArray}. + * + * @return the created {@link JsonArray} + */ + JsonArray createJsonArray(); + + /** + * Create a new {@link JsonValue} from the provided object. + * + * @param object the object to create the {@link JsonValue} from + * @return the created {@link JsonValue} + * @throws RedisJsonException if the provided object is not a valid JSON document + */ + JsonValue fromObject(Object object); + +} diff --git a/src/main/java/io/lettuce/core/json/JsonPath.java b/src/main/java/io/lettuce/core/json/JsonPath.java new file mode 100644 index 0000000000..abb8afc513 --- /dev/null +++ b/src/main/java/io/lettuce/core/json/JsonPath.java @@ -0,0 +1,124 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +/** + * Describes a path to a certain {@link JsonValue} inside a JSON document. + *

+ *

+ * The Redis server implements its own JSONPath implementation, based on existing technologies. The generic rules to build a + * path string are: + *

    + *
  • $ - The root (outermost JSON element), starts the path.
  • + *
  • . or [] - Selects a child element.
  • + *
  • .. - Recursively descends through the JSON document.
  • + *
  • * - Wildcard, returns all elements.
  • + *
  • [] - Subscript operator, accesses an array element.
  • + *
  • [,]] - Union, selects multiple elements.
  • + *
  • [start:end:step] - Array slice where start, end, and step are indexes.
  • + *
  • ?() - Filters a JSON object or array. Supports comparison operators (==, !=, <, <=, >, >=, =~), logical + * operators (&&, ||), and parenthesis ((, )).
  • + *
  • () - Script expression.
  • + *
  • @ - The current element, used in filter or script expressions.
  • + *
+ *

+ * For example, given the following JSON document: + * + *

+ * {
+ *     "inventory": {
+ *         "mountain_bikes": [
+ *             {
+ *                 "id": "bike:1",
+ *                 "model": "Phoebe",
+ *                 "description": "This is a mid-travel trail slayer that is a fantastic daily...",
+ *                 "price": 1920,
+ *                 "specs": {"material": "carbon", "weight": 13.1},
+ *                 "colors": ["black", "silver"],
+ *             },
+ *             ...
+ *         }
+ *     }
+ *}
+ * 
+ *

+ * To get a list of all the {@code mountain_bikes} inside the {@code inventory} you would write something like: + *

+ * {@code JSON.GET store '$.inventory["mountain_bikes"]' } + * + * @author Tihomir Mateev + * @since 6.5 + * @see JSON Path in Redis docs + */ +public class JsonPath { + + /** + * The root path {@code $} as defined by the second version of the RedisJSON implementation. + *

+ * + * @since 6.5 + */ + public static final JsonPath ROOT_PATH = new JsonPath("$"); + + /** + * The legacy root path {@code .} as defined by the first version of the RedisJSON implementation. + * + * @deprecated since 6.5, use {@link #ROOT_PATH} instead. + */ + public static final JsonPath ROOT_PATH_LEGACY = new JsonPath("."); + + private final String path; + + /** + * Create a new {@link JsonPath} given a path string. + * + * @param pathString the path string, must not be {@literal null} or empty. + */ + public JsonPath(final String pathString) { + + if (pathString == null) { + throw new IllegalArgumentException("Path cannot be null."); + } + + if (pathString.isEmpty()) { + throw new IllegalArgumentException("Path cannot be empty."); + } + + this.path = pathString; + } + + @Override + public String toString() { + return path; + } + + /** + * Create a new {@link JsonPath} given a path string. + * + * @param path the path string, must not be {@literal null} or empty. + * @return the {@link JsonPath}. + */ + public static JsonPath of(final String path) { + return new JsonPath(path); + } + + @Override + public boolean equals(Object obj) { + return this.path.equals(obj); + } + + @Override + public int hashCode() { + return path.hashCode(); + } + + public boolean isRootPath() { + return ROOT_PATH.toString().equals(path) || ROOT_PATH_LEGACY.toString().equals(path); + } + +} diff --git a/src/main/java/io/lettuce/core/json/JsonType.java b/src/main/java/io/lettuce/core/json/JsonType.java new file mode 100644 index 0000000000..0344e8cee2 --- /dev/null +++ b/src/main/java/io/lettuce/core/json/JsonType.java @@ -0,0 +1,40 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +/** + * JSON types as returned by the JSON.TYPE command + * + * @see io.lettuce.core.api.sync.RedisCommands#jsonType + * @since 6.5 + * @author Tihomir Mateev + */ +public enum JsonType { + + OBJECT, ARRAY, STRING, INTEGER, NUMBER, BOOLEAN, UNKNOWN; + + public static JsonType fromString(String s) { + switch (s) { + case "object": + return OBJECT; + case "array": + return ARRAY; + case "string": + return STRING; + case "integer": + return INTEGER; + case "number": + return NUMBER; + case "boolean": + return BOOLEAN; + default: + return UNKNOWN; + } + } + +} diff --git a/src/main/java/io/lettuce/core/json/JsonValue.java b/src/main/java/io/lettuce/core/json/JsonValue.java new file mode 100644 index 0000000000..2ba6ac20a0 --- /dev/null +++ b/src/main/java/io/lettuce/core/json/JsonValue.java @@ -0,0 +1,109 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +import java.nio.ByteBuffer; + +/** + * Representation of a JSON text as per the RFC 8259 - + * The JavaScript Object Notation (JSON) Data Interchange Format, Section 3. Values + *

+ * Implementations of this interface need to make sure parsing of the JSON is not done inside the event loop thread, used to + * process the data coming from the Redis server; otherwise larger JSON documents might cause performance degradation that spans + * across all threads using the driver. + * + * @see JsonObject + * @see JsonArray + * @see RFC 8259 - The JavaScript Object Notation (JSON) Data + * Interchange Format + * @author Tihomir Mateev + * @since 6.5 + */ +public interface JsonValue { + + /** + * Execute any {@link io.lettuce.core.codec.RedisCodec} decoding and fetch the result. + * + * @return the {@link String} representation of this {@link JsonValue} + */ + String toString(); + + /** + * @return the raw JSON text as a {@link ByteBuffer} + */ + ByteBuffer asByteBuffer(); + + /** + * @return {@code true} if this {@link JsonValue} represents a JSON array + */ + boolean isJsonArray(); + + /** + * @return the {@link JsonArray} representation of this {@link JsonValue}, null if this is not a JSON array + * @see #isJsonArray() + */ + JsonArray asJsonArray(); + + /** + * @return {@code true} if this {@link JsonValue} represents a JSON object + */ + boolean isJsonObject(); + + /** + * @return the {@link JsonObject} representation of this {@link JsonValue}, null if this is not a JSON object + * @see #isJsonObject() + */ + JsonObject asJsonObject(); + + /** + * @return {@code true} if this {@link JsonValue} represents a JSON string + */ + boolean isString(); + + /** + * @return the {@link String} representation of this {@link JsonValue}, null if this is not a JSON string + * @see #isString() + */ + String asString(); + + /** + * @return {@code true} if this {@link JsonValue} represents a JSON number + */ + boolean isNumber(); + + /** + * @return the {@link Number} representation of this {@link JsonValue}, null if this is not a JSON number + * @see #isNumber() + */ + Number asNumber(); + + /** + * @return {@code true} if this {@link JsonValue} represents a JSON boolean value + */ + boolean isBoolean(); + + /** + * @return the {@link Boolean} representation of this {@link JsonValue}, null if this is not a JSON boolean value + * @see #isNumber() + */ + Boolean asBoolean(); + + /** + * @return {@code true} if this {@link JsonValue} represents the value of null + */ + boolean isNull(); + + /** + * Given a {@link Class} type, this method will attempt to convert the JSON value to the provided type. + * + * @return the newly created instance of the provided type with the data from the JSON value + * @throws RedisJsonException if the provided type is not a valid JSON document + */ + T toObject(Class type); + +} diff --git a/src/main/java/io/lettuce/core/json/RedisJsonException.java b/src/main/java/io/lettuce/core/json/RedisJsonException.java new file mode 100644 index 0000000000..c394ab8f53 --- /dev/null +++ b/src/main/java/io/lettuce/core/json/RedisJsonException.java @@ -0,0 +1,24 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +public class RedisJsonException extends RuntimeException { + + public RedisJsonException(String message) { + super(message); + } + + public RedisJsonException(String message, Throwable cause) { + super(message, cause); + } + + public RedisJsonException(Throwable cause) { + super(cause); + } + +} diff --git a/src/main/java/io/lettuce/core/json/UnproccessedJsonValue.java b/src/main/java/io/lettuce/core/json/UnproccessedJsonValue.java new file mode 100644 index 0000000000..cfeda343b1 --- /dev/null +++ b/src/main/java/io/lettuce/core/json/UnproccessedJsonValue.java @@ -0,0 +1,166 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +import io.lettuce.core.codec.StringCodec; + +import java.nio.ByteBuffer; + +/** + * A wrapper around any of the implementations of the {@link JsonValue} provided by the implementation of the {@link JsonParser} + * that is currently being used. The purpose of this class is to provide a lazy initialization mechanism and avoid any + * deserialization in the event loop that processes the data coming from the Redis server. + *

+ * This class is thread-safe and can be used in a multi-threaded environment. + * + * @author Tihomir Mateev + */ +class UnproccessedJsonValue implements JsonValue { + + private volatile JsonValue jsonValue; + + private final JsonParser parser; + + private final ByteBuffer unprocessedData; + + /** + * Create a new instance of the {@link UnproccessedJsonValue}. + * + * @param bytes the raw JSON data + * @param theParser the {@link JsonParser} that works with the current instance + */ + public UnproccessedJsonValue(ByteBuffer bytes, JsonParser theParser) { + unprocessedData = bytes; + parser = theParser; + } + + @Override + public String toString() { + if (isDeserialized()) { + return jsonValue.toString(); + } + + synchronized (this) { + if (isDeserialized()) { + return jsonValue.toString(); + } + + // if no deserialization took place, so no modification took place + // in this case we can decode the source data as is + return StringCodec.UTF8.decodeValue(unprocessedData); + } + } + + @Override + public ByteBuffer asByteBuffer() { + if (isDeserialized()) { + return jsonValue.asByteBuffer(); + } + + synchronized (this) { + if (isDeserialized()) { + return jsonValue.asByteBuffer(); + } + + // if no deserialization took place, so no modification took place + // in this case we can decode the source data as is + return unprocessedData; + } + } + + @Override + public boolean isJsonArray() { + lazilyDeserialize(); + return jsonValue.isJsonArray(); + } + + @Override + public JsonArray asJsonArray() { + lazilyDeserialize(); + return jsonValue.asJsonArray(); + } + + @Override + public boolean isJsonObject() { + lazilyDeserialize(); + return jsonValue.isJsonObject(); + } + + @Override + public JsonObject asJsonObject() { + lazilyDeserialize(); + return jsonValue.asJsonObject(); + } + + @Override + public boolean isString() { + lazilyDeserialize(); + return jsonValue.isString(); + } + + @Override + public String asString() { + lazilyDeserialize(); + return jsonValue.asString(); + } + + @Override + public boolean isNumber() { + lazilyDeserialize(); + return jsonValue.isNumber(); + } + + @Override + public Number asNumber() { + lazilyDeserialize(); + return jsonValue.asNumber(); + } + + @Override + public boolean isBoolean() { + lazilyDeserialize(); + return jsonValue.isBoolean(); + } + + @Override + public Boolean asBoolean() { + lazilyDeserialize(); + return jsonValue.asBoolean(); + } + + @Override + public boolean isNull() { + lazilyDeserialize(); + return jsonValue.isNull(); + } + + @Override + public T toObject(Class targetType) { + lazilyDeserialize(); + return jsonValue.toObject(targetType); + } + + private void lazilyDeserialize() { + if (!isDeserialized()) { + synchronized (this) { + if (!isDeserialized()) { + jsonValue = parser.createJsonValue(unprocessedData); + unprocessedData.clear(); + } + } + } + } + + /** + * @return {@code true} if the data has been deserialized + */ + boolean isDeserialized() { + return jsonValue != null; + } + +} diff --git a/src/main/java/io/lettuce/core/json/arguments/JsonGetArgs.java b/src/main/java/io/lettuce/core/json/arguments/JsonGetArgs.java new file mode 100644 index 0000000000..6e3b619956 --- /dev/null +++ b/src/main/java/io/lettuce/core/json/arguments/JsonGetArgs.java @@ -0,0 +1,137 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json.arguments; + +import io.lettuce.core.CompositeArgument; +import io.lettuce.core.protocol.CommandArgs; +import io.lettuce.core.protocol.CommandKeyword; + +/** + * Argument list builder for the Redis JSON.GET command. + *

+ * {@link JsonGetArgs} is a mutable object and instances should be used only once to avoid shared mutable state. + * + * @author Tihomir Mateev + * @since 6.5 + */ +public class JsonGetArgs implements CompositeArgument { + + private String indent; + + private String newline; + + private String space; + + /** + * Builder entry points for {@link JsonGetArgs}. + */ + public static class Builder { + + /** + * Utility constructor. + */ + private Builder() { + } + + /** + * Creates new {@link JsonGetArgs} and sets the string used for indentation. + * + * @return new {@link JsonGetArgs} with indentation set. + */ + public static JsonGetArgs indent(String indent) { + return new JsonGetArgs().indent(indent); + } + + /** + * Creates new {@link JsonGetArgs} and sets the string used for newline. + * + * @return new {@link JsonGetArgs} with newline set. + */ + public static JsonGetArgs newline(String newline) { + return new JsonGetArgs().newline(newline); + } + + /** + * Creates new {@link JsonGetArgs} and sets the string used for spacing. + * + * @return new {@link JsonGetArgs} with spacing set. + */ + public static JsonGetArgs space(String space) { + return new JsonGetArgs().space(space); + } + + /** + * Creates new {@link JsonGetArgs} empty arguments. + * + * @return new {@link JsonGetArgs} with empty arguments set. + */ + public static JsonGetArgs defaults() { + return new JsonGetArgs().defaults(); + } + + } + + /** + * Set the string used for indentation. + * + * @return {@code this}. + */ + public JsonGetArgs indent(String indent) { + + this.indent = indent; + return this; + } + + /** + * Set the string used for newline. + * + * @return {@code this}. + */ + public JsonGetArgs newline(String newline) { + + this.newline = newline; + return this; + } + + /** + * Set the string used for spacing. + * + * @return {@code this}. + */ + public JsonGetArgs space(String space) { + + this.space = space; + return this; + } + + /** + * Set empty arguments. + * + * @return {@code this}. + */ + public JsonGetArgs defaults() { + return this; + } + + @Override + public void build(CommandArgs args) { + + if (indent != null) { + args.add(CommandKeyword.INDENT).add(indent); + } + + if (newline != null) { + args.add(CommandKeyword.NEWLINE).add(newline); + } + + if (space != null) { + args.add(CommandKeyword.SPACE).add(space); + } + } + +} diff --git a/src/main/java/io/lettuce/core/json/arguments/JsonMsetArgs.java b/src/main/java/io/lettuce/core/json/arguments/JsonMsetArgs.java new file mode 100644 index 0000000000..eb5f827aab --- /dev/null +++ b/src/main/java/io/lettuce/core/json/arguments/JsonMsetArgs.java @@ -0,0 +1,67 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json.arguments; + +import io.lettuce.core.CompositeArgument; +import io.lettuce.core.json.JsonValue; +import io.lettuce.core.json.JsonPath; +import io.lettuce.core.protocol.CommandArgs; + +/** + * Argument list builder for the Redis JSON.MSET command. + *

+ * + * @author Tihomir Mateev + * @since 6.5 + */ +public class JsonMsetArgs implements CompositeArgument { + + private final K key; + + private final JsonPath path; + + private final JsonValue element; + + /** + * Creates a new {@link JsonMsetArgs} given a {@code key}, {@code path} and {@code element}. + * + * @param key the key to set the value for + * @param path the path to set the value for + * @param element the value to set + */ + public JsonMsetArgs(K key, JsonPath path, JsonValue element) { + this.key = key; + this.path = path; + this.element = element; + } + + /** + * Return the key associated with this {@link JsonMsetArgs}. + */ + public K getKey() { + return key; + } + + @SuppressWarnings("unchecked") + @Override + public void build(CommandArgs args) { + + if (key != null) { + args.addKey((K) key); + } + + if (path != null) { + args.add(path.toString()); + } + + if (element != null) { + args.add(element.asByteBuffer().array()); + } + } + +} diff --git a/src/main/java/io/lettuce/core/json/arguments/JsonRangeArgs.java b/src/main/java/io/lettuce/core/json/arguments/JsonRangeArgs.java new file mode 100644 index 0000000000..d9a579222c --- /dev/null +++ b/src/main/java/io/lettuce/core/json/arguments/JsonRangeArgs.java @@ -0,0 +1,116 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json.arguments; + +import io.lettuce.core.CompositeArgument; +import io.lettuce.core.protocol.CommandArgs; + +/** + * Argument list builder for the RedisJSON commands that require ranges. By default, start and end indexes are set to 0. + * Modifying these values might have different effects depending on the command they are supplied to. + *

+ * {@link JsonRangeArgs} is a mutable object and instances should be used only once to avoid shared mutable state. + * + * @author Tihomir Mateev + * @since 6.5 + * @see JSON.ARRINDEX + * @see JSON.ARRTRIM + */ +public class JsonRangeArgs implements CompositeArgument { + + /** + * Default start index to indicate where to start slicing the array + */ + public static final int DEFAULT_START_INDEX = 0; + + /** + * Default end index to indicate where to stop slicing the array + */ + public static final int DEFAULT_END_INDEX = 0; + + private long start = DEFAULT_START_INDEX; + + private long stop = DEFAULT_END_INDEX; + + /** + * Builder entry points for {@link JsonRangeArgs}. + */ + public static class Builder { + + /** + * Utility constructor. + */ + private Builder() { + } + + /** + * Creates new {@link JsonRangeArgs} and sets the start index. + * + * @return new {@link JsonRangeArgs} with the start index set. + */ + public static JsonRangeArgs start(long start) { + return new JsonRangeArgs().start(start); + } + + /** + * Creates new {@link JsonRangeArgs} and sets the end index. + * + * @return new {@link JsonRangeArgs} with the end index set. + */ + public static JsonRangeArgs stop(long stop) { + return new JsonRangeArgs().stop(stop); + } + + /** + * Creates new {@link JsonRangeArgs} and sets default values. + *

+ * The default start index is 0 and the default end index is 0. + * + * @return new {@link JsonRangeArgs} with the end index set. + */ + public static JsonRangeArgs defaults() { + return new JsonRangeArgs(); + } + + } + + /** + * Set the start index. + * + * @return {@code this}. + */ + public JsonRangeArgs start(long start) { + + this.start = start; + return this; + } + + /** + * Set the end index. + * + * @return {@code this}. + */ + public JsonRangeArgs stop(long stop) { + + this.stop = stop; + return this; + } + + @Override + public void build(CommandArgs args) { + + if (start != DEFAULT_START_INDEX || stop != DEFAULT_END_INDEX) { + args.add(start); + } + + if (stop != DEFAULT_END_INDEX) { + args.add(stop); + } + } + +} diff --git a/src/main/java/io/lettuce/core/json/arguments/JsonSetArgs.java b/src/main/java/io/lettuce/core/json/arguments/JsonSetArgs.java new file mode 100644 index 0000000000..53925bdf49 --- /dev/null +++ b/src/main/java/io/lettuce/core/json/arguments/JsonSetArgs.java @@ -0,0 +1,110 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json.arguments; + +import io.lettuce.core.CompositeArgument; +import io.lettuce.core.protocol.CommandArgs; +import io.lettuce.core.protocol.CommandKeyword; + +/** + * Argument list builder for the Redis JSON.SET command. + *

+ * {@link JsonSetArgs} is a mutable object and instances should be used only once to avoid shared mutable state. + * + * @author Mark Paluch + * @since 6.5 + */ +public class JsonSetArgs implements CompositeArgument { + + private boolean nx; + + private boolean xx; + + /** + * Builder entry points for {@link JsonSetArgs}. + */ + public static class Builder { + + /** + * Utility constructor. + */ + private Builder() { + } + + /** + * Creates new {@link JsonSetArgs} and sets {@literal NX}. + * + * @return new {@link JsonSetArgs} with {@literal NX} set. + */ + public static JsonSetArgs nx() { + return new JsonSetArgs().nx(); + } + + /** + * Creates new {@link JsonSetArgs} and sets {@literal XX}. + * + * @return new {@link JsonSetArgs} with {@literal XX} set. + */ + public static JsonSetArgs xx() { + return new JsonSetArgs().xx(); + } + + /** + * Creates new empty {@link JsonSetArgs} + * + * @return new {@link JsonSetArgs} with nothing set. + */ + public static JsonSetArgs defaults() { + return new JsonSetArgs().defaults(); + } + + } + + /** + * Set the key only if it does not already exist. + * + * @return {@code this}. + */ + public JsonSetArgs nx() { + + this.nx = true; + return this; + } + + /** + * Set the key only if it already exists. + * + * @return {@code this}. + */ + public JsonSetArgs xx() { + + this.xx = true; + return this; + } + + /** + * Set the key only if it already exists. + * + * @return {@code this}. + */ + public JsonSetArgs defaults() { + + return this; + } + + @Override + public void build(CommandArgs args) { + + if (xx) { + args.add(CommandKeyword.XX); + } else if (nx) { + args.add(CommandKeyword.NX); + } + } + +} diff --git a/src/main/java/io/lettuce/core/json/package-info.java b/src/main/java/io/lettuce/core/json/package-info.java new file mode 100644 index 0000000000..a7b24f7830 --- /dev/null +++ b/src/main/java/io/lettuce/core/json/package-info.java @@ -0,0 +1,4 @@ +/** + * Support for the JSON Redis Module. + */ +package io.lettuce.core.json; diff --git a/src/main/java/io/lettuce/core/masterreplica/AutodiscoveryConnector.java b/src/main/java/io/lettuce/core/masterreplica/AutodiscoveryConnector.java index aad151858c..cb17ec886a 100644 --- a/src/main/java/io/lettuce/core/masterreplica/AutodiscoveryConnector.java +++ b/src/main/java/io/lettuce/core/masterreplica/AutodiscoveryConnector.java @@ -134,7 +134,7 @@ private Mono> initializeConnection(Re redisClient.getResources(), redisClient.getOptions()); StatefulRedisMasterReplicaConnectionImpl connection = new StatefulRedisMasterReplicaConnectionImpl<>( - channelWriter, codec, redisURI.getTimeout()); + channelWriter, codec, redisURI.getTimeout(), redisClient.getOptions().getJsonParser()); connection.setOptions(redisClient.getOptions()); diff --git a/src/main/java/io/lettuce/core/masterreplica/MasterReplicaChannelWriter.java b/src/main/java/io/lettuce/core/masterreplica/MasterReplicaChannelWriter.java index bfa4b4bd28..9ad7a8e451 100644 --- a/src/main/java/io/lettuce/core/masterreplica/MasterReplicaChannelWriter.java +++ b/src/main/java/io/lettuce/core/masterreplica/MasterReplicaChannelWriter.java @@ -271,11 +271,11 @@ private static boolean isSuccessfullyCompleted(CompletableFuture connectFutur } private static boolean isStartTransaction(ProtocolKeyword command) { - return command.name().equals("MULTI"); + return command.toString().equals("MULTI"); } private boolean isEndTransaction(ProtocolKeyword command) { - return command.name().equals("EXEC") || command.name().equals("DISCARD"); + return command.toString().equals("EXEC") || command.toString().equals("DISCARD"); } } diff --git a/src/main/java/io/lettuce/core/masterreplica/SentinelConnector.java b/src/main/java/io/lettuce/core/masterreplica/SentinelConnector.java index afda7cc573..ba44025af9 100644 --- a/src/main/java/io/lettuce/core/masterreplica/SentinelConnector.java +++ b/src/main/java/io/lettuce/core/masterreplica/SentinelConnector.java @@ -98,7 +98,7 @@ public CompletableFuture closeAsync() { }; StatefulRedisMasterReplicaConnectionImpl connection = new StatefulRedisMasterReplicaConnectionImpl<>( - channelWriter, codec, redisURI.getTimeout()); + channelWriter, codec, redisURI.getTimeout(), redisClient.getOptions().getJsonParser()); connection.setOptions(redisClient.getOptions()); CompletionStage bind = sentinelTopologyRefresh.bind(runnable); diff --git a/src/main/java/io/lettuce/core/masterreplica/StatefulRedisMasterReplicaConnectionImpl.java b/src/main/java/io/lettuce/core/masterreplica/StatefulRedisMasterReplicaConnectionImpl.java index 47d793fac4..4908a6afef 100644 --- a/src/main/java/io/lettuce/core/masterreplica/StatefulRedisMasterReplicaConnectionImpl.java +++ b/src/main/java/io/lettuce/core/masterreplica/StatefulRedisMasterReplicaConnectionImpl.java @@ -5,6 +5,8 @@ import io.lettuce.core.ReadFrom; import io.lettuce.core.StatefulRedisConnectionImpl; import io.lettuce.core.codec.RedisCodec; +import io.lettuce.core.json.JsonParser; +import reactor.core.publisher.Mono; /** * @author Mark Paluch @@ -18,9 +20,11 @@ class StatefulRedisMasterReplicaConnectionImpl extends StatefulRedisConnec * @param writer the channel writer * @param codec Codec used to encode/decode keys and values. * @param timeout Maximum time to wait for a response. + * @param parser the JSON parser to use */ - StatefulRedisMasterReplicaConnectionImpl(MasterReplicaChannelWriter writer, RedisCodec codec, Duration timeout) { - super(writer, NoOpPushHandler.INSTANCE, codec, timeout); + StatefulRedisMasterReplicaConnectionImpl(MasterReplicaChannelWriter writer, RedisCodec codec, Duration timeout, + Mono parser) { + super(writer, NoOpPushHandler.INSTANCE, codec, timeout, parser); } @Override diff --git a/src/main/java/io/lettuce/core/masterreplica/StaticMasterReplicaConnector.java b/src/main/java/io/lettuce/core/masterreplica/StaticMasterReplicaConnector.java index df9a33eeca..3b504d1f75 100644 --- a/src/main/java/io/lettuce/core/masterreplica/StaticMasterReplicaConnector.java +++ b/src/main/java/io/lettuce/core/masterreplica/StaticMasterReplicaConnector.java @@ -90,7 +90,7 @@ private Mono> initializeConnection(Re redisClient.getResources(), redisClient.getOptions()); StatefulRedisMasterReplicaConnectionImpl connection = new StatefulRedisMasterReplicaConnectionImpl<>( - channelWriter, codec, seedNode.getTimeout()); + channelWriter, codec, seedNode.getTimeout(), redisClient.getOptions().getJsonParser()); connection.setOptions(redisClient.getOptions()); return Mono.just(connection); diff --git a/src/main/java/io/lettuce/core/metrics/CommandLatencyId.java b/src/main/java/io/lettuce/core/metrics/CommandLatencyId.java index 0cf213ab0d..8e3b4d45fd 100644 --- a/src/main/java/io/lettuce/core/metrics/CommandLatencyId.java +++ b/src/main/java/io/lettuce/core/metrics/CommandLatencyId.java @@ -31,7 +31,7 @@ protected CommandLatencyId(SocketAddress localAddress, SocketAddress remoteAddre this.localAddress = localAddress; this.remoteAddress = remoteAddress; this.commandType = commandType; - this.commandName = commandType.name(); + this.commandName = commandType.toString(); } /** diff --git a/src/main/java/io/lettuce/core/metrics/MicrometerCommandLatencyRecorder.java b/src/main/java/io/lettuce/core/metrics/MicrometerCommandLatencyRecorder.java index fcd47c7fb9..0cb992029c 100644 --- a/src/main/java/io/lettuce/core/metrics/MicrometerCommandLatencyRecorder.java +++ b/src/main/java/io/lettuce/core/metrics/MicrometerCommandLatencyRecorder.java @@ -115,7 +115,7 @@ protected Timer completionTimer(CommandLatencyId commandLatencyId) { Timer.Builder timer = Timer.builder(METRIC_COMPLETION) .description("Latency between command send and command completion (complete response received") - .tag(LABEL_COMMAND, commandLatencyId.commandType().name()) + .tag(LABEL_COMMAND, commandLatencyId.commandType().toString()) .tag(LABEL_LOCAL, commandLatencyId.localAddress().toString()) .tag(LABEL_REMOTE, commandLatencyId.remoteAddress().toString()).tags(options.tags()); @@ -131,7 +131,7 @@ protected Timer firstResponseTimer(CommandLatencyId commandLatencyId) { Timer.Builder timer = Timer.builder(METRIC_FIRST_RESPONSE) .description("Latency between command send and first response (first response received)") - .tag(LABEL_COMMAND, commandLatencyId.commandType().name()) + .tag(LABEL_COMMAND, commandLatencyId.commandType().toString()) .tag(LABEL_LOCAL, commandLatencyId.localAddress().toString()) .tag(LABEL_REMOTE, commandLatencyId.remoteAddress().toString()).tags(options.tags()); diff --git a/src/main/java/io/lettuce/core/metrics/MicrometerOptions.java b/src/main/java/io/lettuce/core/metrics/MicrometerOptions.java index 86886f1a99..185358fca3 100644 --- a/src/main/java/io/lettuce/core/metrics/MicrometerOptions.java +++ b/src/main/java/io/lettuce/core/metrics/MicrometerOptions.java @@ -217,7 +217,7 @@ public Builder enabledCommands(CommandType... commands) { enabledCommands.add(enabledCommand.name()); } - return metricsFilter(command -> enabledCommands.contains(command.getType().name())); + return metricsFilter(command -> enabledCommands.contains(command.getType().toString())); } /** diff --git a/src/main/java/io/lettuce/core/output/ArrayComplexData.java b/src/main/java/io/lettuce/core/output/ArrayComplexData.java new file mode 100644 index 0000000000..d53b21a555 --- /dev/null +++ b/src/main/java/io/lettuce/core/output/ArrayComplexData.java @@ -0,0 +1,80 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.output; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * An implementation of the {@link ComplexData} that handles arrays. + *

+ * For RESP2 calling the {@link ComplexData#getDynamicMap()} would heuristically go over the list of elements assuming every odd + * element is a key and every even object is the value and then adding them to an {@link Map}. The logic would follow the same + * order that was used when the elements were added to the {@link ArrayComplexData}. Similarly calling the + * {@link ComplexData#getDynamicSet()} would return a set of all the elements, adding them in the same order. If - for some + * reason - duplicate elements exist they would be overwritten. + *

+ * All data structures that the implementation returns are unmodifiable + * + * @see ComplexData + * @author Tihomir Mateev + * @since 6.5 + */ +class ArrayComplexData extends ComplexData { + + private final List data; + + public ArrayComplexData(int count) { + data = new ArrayList<>(count); + } + + @Override + public void storeObject(Object value) { + data.add(value); + } + + @Override + public List getDynamicList() { + return Collections.unmodifiableList(data); + } + + @Override + public Set getDynamicSet() { + // RESP2 compatibility mode - assuming the caller is aware that the array really contains a set (because in RESP2 we + // lack support for this data type) we make the conversion here + Set set = new LinkedHashSet<>(data); + return Collections.unmodifiableSet(set); + } + + @Override + public Map getDynamicMap() { + // RESP2 compatibility mode - assuming the caller is aware that the array really contains a map (because in RESP2 we + // lack support for this data type) we make the conversion here + Map map = new LinkedHashMap<>(); + final Boolean[] isKey = { true }; + final Object[] key = new Object[1]; + + data.forEach(element -> { + if (isKey[0]) { + key[0] = element; + isKey[0] = false; + } else { + map.put(key[0], element); + isKey[0] = true; + } + }); + + return Collections.unmodifiableMap(map); + } + +} diff --git a/src/main/java/io/lettuce/core/output/ComplexData.java b/src/main/java/io/lettuce/core/output/ComplexData.java new file mode 100644 index 0000000000..08ef81a20f --- /dev/null +++ b/src/main/java/io/lettuce/core/output/ComplexData.java @@ -0,0 +1,118 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.output; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * The base type of all complex data, collected by a {@link ComplexOutput} + *

+ * Commands typically result in simple types, however some of the commands could return complex nested structures. In these + * cases, and with the help of a {@link ComplexDataParser}, the data gathered by the {@link ComplexOutput} could be parsed to a + * domain object. + *

+ * An complex data object could only be an aggregate data type as per the + * RESP2 and + * RESP3 protocol definitions. Its + * contents, however, could be both the simple and aggregate data types. + *

+ * For RESP2 the only possible aggregation is an array. RESP2 commands could also return sets (obviously, by simply making sure + * the elements of the array are unique) or maps (by sending the keys as odd elements and their values as even elements in the + * right order one after another. + *

+ * For RESP3 all the three aggregate types are supported (and indicated with special characters when the result is returned by + * the server). + *

+ * Aggregate data types could also be nested by using the {@link ComplexData#storeObject(Object)} call. + *

+ * Implementations of this class override the {@link ComplexData#getDynamicSet()}, {@link ComplexData#getDynamicList()} and + * {@link ComplexData#getDynamicMap()} methods to return the data received in the server in a implementation of the Collections + * framework. If a given implementation could not do the conversion in a meaningful way an {@link UnsupportedOperationException} + * would be thrown. + * + * @see ComplexOutput + * @see ArrayComplexData + * @see SetComplexData + * @see MapComplexData + * @author Tihomir Mateev + * @since 6.5 + */ +public abstract class ComplexData { + + /** + * Store a long value in the underlying data structure + * + * @param value the value to store + */ + public void store(long value) { + storeObject(value); + } + + /** + * Store a double value in the underlying data structure + * + * @param value the value to store + */ + public void store(double value) { + storeObject(value); + } + + /** + * Store a boolean value in the underlying data structure + * + * @param value the value to store + */ + public void store(boolean value) { + storeObject(value); + } + + /** + * Store an {@link Object} value in the underlying data structure. This method should be used when nesting one instance of + * {@link ComplexData} inside another + * + * @param value the value to store + */ + public abstract void storeObject(Object value); + + public void store(String value) { + storeObject(value); + } + + /** + * Returns the aggregate data in a {@link List} form. If the underlying implementation could not convert the data structure + * to a {@link List} then an {@link UnsupportedOperationException} would be thrown. + * + * @return a {@link List} of {@link Object} values + */ + public List getDynamicList() { + throw new UnsupportedOperationException("The type of data stored in this dynamic object is not a list"); + } + + /** + * Returns the aggregate data in a {@link Set} form. If the underlying implementation could not convert the data structure + * to a {@link Set} then an {@link UnsupportedOperationException} would be thrown. + * + * @return a {@link Set} of {@link Object} values + */ + public Set getDynamicSet() { + throw new UnsupportedOperationException("The type of data stored in this dynamic object is not a set"); + } + + /** + * Returns the aggregate data in a {@link Map} form. If the underlying implementation could not convert the data structure + * to a {@link Map} then an {@link UnsupportedOperationException} would be thrown. + * + * @return a {@link Map} of {@link Object} values + */ + public Map getDynamicMap() { + throw new UnsupportedOperationException("The type of data stored in this dynamic object is not a map"); + } + +} diff --git a/src/main/java/io/lettuce/core/output/ComplexDataParser.java b/src/main/java/io/lettuce/core/output/ComplexDataParser.java new file mode 100644 index 0000000000..332fb61a4b --- /dev/null +++ b/src/main/java/io/lettuce/core/output/ComplexDataParser.java @@ -0,0 +1,31 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.output; + +/** + * Any usage of the {@link ComplexOutput} comes hand in hand with a respective {@link ComplexDataParser} that is able to parse + * the data extracted from the server to a meaningful Java object. + * + * @param the type of the parsed object + * @author Tihomir Mateev + * @see ComplexData + * @see ComplexOutput + * @since 6.5 + */ +public interface ComplexDataParser { + + /** + * Parse the data extracted from the server to a specific domain object. + * + * @param data the data to parse + * @return the parsed object + * @since 6.5 + */ + T parse(ComplexData data); + +} diff --git a/src/main/java/io/lettuce/core/output/ComplexOutput.java b/src/main/java/io/lettuce/core/output/ComplexOutput.java new file mode 100644 index 0000000000..86ab339bdb --- /dev/null +++ b/src/main/java/io/lettuce/core/output/ComplexOutput.java @@ -0,0 +1,142 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.output; + +import io.lettuce.core.codec.RedisCodec; +import io.lettuce.core.codec.StringCodec; +import io.lettuce.core.internal.LettuceFactories; + +import java.nio.ByteBuffer; +import java.util.Deque; + +/** + * An implementation of the {@link CommandOutput} that is used in combination with a given {@link ComplexDataParser} to produce + * a domain object from the data extracted from the server. Since there already are implementations of the {@link CommandOutput} + * interface for most simple types, this implementation is better suited to parse complex, often nested, data structures, for + * example a map containing other maps, arrays or sets as values for one or more of its keys. + *

+ * The implementation of the {@link ComplexDataParser} is responsible for mapping the data from the result to meaningful + * properties that the user of the LEttuce driver could then use in a statically typed manner. + * + * @see ComplexDataParser + * @author Tihomir Mateev + * @since 6.5 + */ +public class ComplexOutput extends CommandOutput { + + private final Deque dataStack; + + private final ComplexDataParser parser; + + private ComplexData data; + + /** + * Constructs a new instance of the {@link ComplexOutput} + * + * @param codec the {@link RedisCodec} to be applied + */ + public ComplexOutput(RedisCodec codec, ComplexDataParser parser) { + super(codec, null); + dataStack = LettuceFactories.newSpScQueue(); + this.parser = parser; + } + + @Override + public T get() { + return parser.parse(data); + } + + @Override + public void set(long integer) { + if (data == null) { + throw new RuntimeException("Invalid output received for dynamic aggregate output." + + "Integer value should have been preceded by some sort of aggregation."); + } + + data.store(integer); + } + + @Override + public void set(double number) { + if (data == null) { + throw new RuntimeException("Invalid output received for dynamic aggregate output." + + "Double value should have been preceded by some sort of aggregation."); + } + + data.store(number); + } + + @Override + public void set(boolean value) { + if (data == null) { + throw new RuntimeException("Invalid output received for dynamic aggregate output." + + "Double value should have been preceded by some sort of aggregation."); + } + + data.store(value); + } + + @Override + public void set(ByteBuffer bytes) { + if (data == null) { + throw new RuntimeException("Invalid output received for dynamic aggregate output." + + "ByteBuffer value should have been preceded by some sort of aggregation."); + } + + data.storeObject(bytes == null ? null : codec.decodeValue(bytes)); + } + + @Override + public void setSingle(ByteBuffer bytes) { + if (data == null) { + throw new RuntimeException("Invalid output received for dynamic aggregate output." + + "String value should have been preceded by some sort of aggregation."); + } + + data.store(bytes == null ? null : StringCodec.UTF8.decodeValue(bytes)); + } + + @Override + public void complete(int depth) { + if (!dataStack.isEmpty() && depth == dataStack.size()) { + data = dataStack.pop(); + } + } + + private void multi(ComplexData newData) { + // if there is no data set, then we are at the root object + if (data == null) { + data = newData; + return; + } + + // otherwise we need to nest the provided structure + data.storeObject(newData); + dataStack.push(data); + data = newData; + } + + @Override + public void multiSet(int count) { + SetComplexData dynamicData = new SetComplexData(count); + multi(dynamicData); + } + + @Override + public void multiArray(int count) { + ArrayComplexData dynamicData = new ArrayComplexData(count); + multi(dynamicData); + } + + @Override + public void multiMap(int count) { + MapComplexData dynamicData = new MapComplexData(count); + multi(dynamicData); + } + +} diff --git a/src/main/java/io/lettuce/core/output/JsonTypeListOutput.java b/src/main/java/io/lettuce/core/output/JsonTypeListOutput.java new file mode 100644 index 0000000000..96f942f280 --- /dev/null +++ b/src/main/java/io/lettuce/core/output/JsonTypeListOutput.java @@ -0,0 +1,50 @@ +/* + * Copyright 2011-Present, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ +package io.lettuce.core.output; + +import io.lettuce.core.codec.RedisCodec; +import io.lettuce.core.json.JsonType; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; + +/** + * {@link List} of {@link JsonType} output. + * + * @param Key type. + * @param Value type. + * @author Tihomir Mateev + * @since 6.5 + */ +public class JsonTypeListOutput extends CommandOutput> { + + private boolean initialized; + + public JsonTypeListOutput(RedisCodec codec) { + super(codec, Collections.emptyList()); + } + + @Override + public void set(ByteBuffer bytes) { + if (!initialized) { + multi(1); + } + + output.add(JsonType.fromString(decodeAscii(bytes))); + } + + @Override + public void multi(int count) { + + if (!initialized) { + output = OutputFactory.newList(count); + initialized = true; + } + } + +} diff --git a/src/main/java/io/lettuce/core/output/JsonValueListOutput.java b/src/main/java/io/lettuce/core/output/JsonValueListOutput.java new file mode 100644 index 0000000000..389b696624 --- /dev/null +++ b/src/main/java/io/lettuce/core/output/JsonValueListOutput.java @@ -0,0 +1,56 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ +package io.lettuce.core.output; + +import io.lettuce.core.codec.RedisCodec; +import io.lettuce.core.json.JsonValue; +import io.lettuce.core.json.JsonParser; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; + +/** + * {@link List} of string output. + * + * @param Key type. + * @param Value type. + * @author Tihomir Mateev + * @since 6.5 + */ +public class JsonValueListOutput extends CommandOutput> { + + private boolean initialized; + + private final JsonParser parser; + + public JsonValueListOutput(RedisCodec codec, JsonParser theParser) { + super(codec, Collections.emptyList()); + parser = theParser; + } + + @Override + public void set(ByteBuffer bytes) { + if (!initialized) { + multi(1); + } + + ByteBuffer fetched = ByteBuffer.allocate(bytes.remaining()); + fetched.put(bytes); + fetched.flip(); + output.add(parser.loadJsonValue(fetched)); + } + + @Override + public void multi(int count) { + if (!initialized) { + output = OutputFactory.newList(count); + initialized = true; + } + } + +} diff --git a/src/main/java/io/lettuce/core/output/MapComplexData.java b/src/main/java/io/lettuce/core/output/MapComplexData.java new file mode 100644 index 0000000000..f2f2b29a70 --- /dev/null +++ b/src/main/java/io/lettuce/core/output/MapComplexData.java @@ -0,0 +1,48 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.output; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * An implementation of the {@link ComplexData} that handles maps. + *

+ * All data structures that the implementation returns are unmodifiable + * + * @see ComplexData + * @author Tihomir Mateev + * @since 6.5 + */ +class MapComplexData extends ComplexData { + + private final Map data; + + private Object key; + + public MapComplexData(int count) { + data = new HashMap<>(count); + } + + @Override + public void storeObject(Object value) { + if (key == null) { + key = value; + } else { + data.put(key, value); + key = null; + } + } + + @Override + public Map getDynamicMap() { + return Collections.unmodifiableMap(data); + } + +} diff --git a/src/main/java/io/lettuce/core/output/NumberListOutput.java b/src/main/java/io/lettuce/core/output/NumberListOutput.java new file mode 100644 index 0000000000..cbbb9bd9b3 --- /dev/null +++ b/src/main/java/io/lettuce/core/output/NumberListOutput.java @@ -0,0 +1,76 @@ +/* + * Copyright 2020-Present, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ +package io.lettuce.core.output; + +import io.lettuce.core.codec.RedisCodec; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.nio.ByteBuffer; +import java.text.NumberFormat; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; + +/** + * {@link List} of Number output. + * + * @param Key type. + * @param Value type. + * @author Tihomir Mateev + * @since 6.5 + */ +public class NumberListOutput extends CommandOutput> { + + private static final InternalLogger LOG = InternalLoggerFactory.getInstance(NumberListOutput.class); + + private boolean initialized; + + public NumberListOutput(RedisCodec codec) { + super(codec, new ArrayList<>()); + } + + @Override + public void set(ByteBuffer bytes) { + output.add(bytes != null ? parseNumber(bytes) : null); + } + + @Override + public void set(double number) { + output.add(number); + } + + @Override + public void set(long integer) { + output.add(integer); + } + + @Override + public void setBigNumber(ByteBuffer bytes) { + output.add(bytes != null ? parseNumber(bytes) : null); + } + + @Override + public void multi(int count) { + if (!initialized) { + output = OutputFactory.newList(count); + initialized = true; + } + } + + private Number parseNumber(ByteBuffer bytes) { + Number result = 0; + try { + result = NumberFormat.getNumberInstance().parse(decodeAscii(bytes)); + } catch (ParseException e) { + LOG.warn("Failed to parse " + bytes, e); + } + + return result; + } + +} diff --git a/src/main/java/io/lettuce/core/output/SetComplexData.java b/src/main/java/io/lettuce/core/output/SetComplexData.java new file mode 100644 index 0000000000..0d95afdd45 --- /dev/null +++ b/src/main/java/io/lettuce/core/output/SetComplexData.java @@ -0,0 +1,50 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.output; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * An implementation of the {@link ComplexData} that handles maps. + *

+ * All data structures that the implementation returns are unmodifiable + * + * @see ComplexData + * @author Tihomir Mateev + * @since 6.5 + */ +public class SetComplexData extends ComplexData { + + private final Set data; + + public SetComplexData(int count) { + data = new HashSet<>(count); + } + + @Override + public void storeObject(Object value) { + data.add(value); + } + + @Override + public Set getDynamicSet() { + return Collections.unmodifiableSet(data); + } + + @Override + public List getDynamicList() { + List list = new ArrayList<>(data.size()); + list.addAll(data); + return Collections.unmodifiableList(list); + } + +} diff --git a/src/main/java/io/lettuce/core/output/StringMatchResultOutput.java b/src/main/java/io/lettuce/core/output/StringMatchResultOutput.java index 2653f8f2cc..2217217139 100644 --- a/src/main/java/io/lettuce/core/output/StringMatchResultOutput.java +++ b/src/main/java/io/lettuce/core/output/StringMatchResultOutput.java @@ -19,15 +19,16 @@ */ package io.lettuce.core.output; -import static io.lettuce.core.StringMatchResult.MatchedPosition; -import static io.lettuce.core.StringMatchResult.Position; +import io.lettuce.core.StringMatchResult; +import io.lettuce.core.codec.RedisCodec; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; -import io.lettuce.core.StringMatchResult; -import io.lettuce.core.codec.RedisCodec; +import static io.lettuce.core.StringMatchResult.MatchedPosition; +import static io.lettuce.core.StringMatchResult.Position; /** * Command output for {@code STRALGO} returning {@link StringMatchResult}. @@ -37,7 +38,7 @@ */ public class StringMatchResultOutput extends CommandOutput { - private final boolean withIdx; + private static final ByteBuffer LEN = StandardCharsets.US_ASCII.encode("len"); private String matchString; @@ -45,30 +46,31 @@ public class StringMatchResultOutput extends CommandOutput positions; + private boolean readingLen = true; + private final List matchedPositions = new ArrayList<>(); - public StringMatchResultOutput(RedisCodec codec, boolean withIdx) { + public StringMatchResultOutput(RedisCodec codec) { super(codec, null); - this.withIdx = withIdx; } @Override public void set(ByteBuffer bytes) { - - if (!withIdx && matchString == null) { - matchString = (String) codec.decodeKey(bytes); - } + matchString = (String) codec.decodeKey(bytes); + readingLen = LEN.equals(bytes); } @Override public void set(long integer) { - - this.len = (int) integer; - - if (positions == null) { - positions = new ArrayList<>(); + if (readingLen) { + this.len = (int) integer; + } else { + if (positions == null) { + positions = new ArrayList<>(); + } + positions.add(integer); } - positions.add(integer); + matchString = null; } @Override diff --git a/src/main/java/io/lettuce/core/output/TrackingInfoParser.java b/src/main/java/io/lettuce/core/output/TrackingInfoParser.java new file mode 100644 index 0000000000..4239692dc8 --- /dev/null +++ b/src/main/java/io/lettuce/core/output/TrackingInfoParser.java @@ -0,0 +1,83 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.output; + +import io.lettuce.core.TrackingInfo; +import io.lettuce.core.protocol.CommandKeyword; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Parser for Redis CLIENT TRACKINGINFO command output. + * + * @author Tihomir Mateev + * @since 6.5 + */ +public class TrackingInfoParser implements ComplexDataParser { + + public static final TrackingInfoParser INSTANCE = new TrackingInfoParser(); + + /** + * Utility constructor. + */ + private TrackingInfoParser() { + } + + /** + * Parse the output of the Redis CLIENT TRACKINGINFO command and convert it to a {@link TrackingInfo} + * + * @param dynamicData output of CLIENT TRACKINGINFO command + * @return an {@link TrackingInfo} instance + */ + public TrackingInfo parse(ComplexData dynamicData) { + Map data = verifyStructure(dynamicData); + Set flags = ((ComplexData) data.get(CommandKeyword.FLAGS.toString().toLowerCase())).getDynamicSet(); + Long clientId = (Long) data.get(CommandKeyword.REDIRECT.toString().toLowerCase()); + List prefixes = ((ComplexData) data.get(CommandKeyword.PREFIXES.toString().toLowerCase())).getDynamicList(); + + Set parsedFlags = new HashSet<>(); + List parsedPrefixes = new ArrayList<>(); + + for (Object flag : flags) { + String toParse = (String) flag; + parsedFlags.add(TrackingInfo.TrackingFlag.from(toParse)); + } + + for (Object prefix : prefixes) { + parsedPrefixes.add((String) prefix); + } + + return new TrackingInfo(parsedFlags, clientId, parsedPrefixes); + } + + private Map verifyStructure(ComplexData trackinginfoOutput) { + + if (trackinginfoOutput == null) { + throw new IllegalArgumentException("Failed while parsing CLIENT TRACKINGINFO: trackinginfoOutput must not be null"); + } + + Map data = trackinginfoOutput.getDynamicMap(); + if (data == null || data.isEmpty()) { + throw new IllegalArgumentException("Failed while parsing CLIENT TRACKINGINFO: data must not be null or empty"); + } + + if (!data.containsKey(CommandKeyword.FLAGS.toString().toLowerCase()) + || !data.containsKey(CommandKeyword.REDIRECT.toString().toLowerCase()) + || !data.containsKey(CommandKeyword.PREFIXES.toString().toLowerCase())) { + throw new IllegalArgumentException( + "Failed while parsing CLIENT TRACKINGINFO: trackinginfoOutput has missing flags"); + } + + return data; + } + +} diff --git a/src/main/java/io/lettuce/core/protocol/BaseRedisCommandBuilder.java b/src/main/java/io/lettuce/core/protocol/BaseRedisCommandBuilder.java index fb149d4ac0..98fc293ea2 100644 --- a/src/main/java/io/lettuce/core/protocol/BaseRedisCommandBuilder.java +++ b/src/main/java/io/lettuce/core/protocol/BaseRedisCommandBuilder.java @@ -1,22 +1,36 @@ package io.lettuce.core.protocol; +import io.lettuce.core.Limit; +import io.lettuce.core.Range; import io.lettuce.core.RedisException; import io.lettuce.core.ScriptOutputType; import io.lettuce.core.codec.RedisCodec; -import io.lettuce.core.output.BooleanOutput; -import io.lettuce.core.output.CommandOutput; -import io.lettuce.core.output.IntegerOutput; -import io.lettuce.core.output.NestedMultiOutput; -import io.lettuce.core.output.ObjectOutput; -import io.lettuce.core.output.StatusOutput; -import io.lettuce.core.output.ValueOutput; +import io.lettuce.core.internal.LettuceAssert; +import io.lettuce.core.output.*; + +import java.nio.ByteBuffer; + +import static io.lettuce.core.protocol.CommandKeyword.LIMIT; /** + * Common utility methods shared by all implementations of the Redis command builder. + * * @author Mark Paluch + * @author Tihomir Mateev * @since 3.0 */ public class BaseRedisCommandBuilder { + protected static final String MUST_NOT_CONTAIN_NULL_ELEMENTS = "must not contain null elements"; + + protected static final String MUST_NOT_BE_EMPTY = "must not be empty"; + + protected static final String MUST_NOT_BE_NULL = "must not be null"; + + protected static final byte[] MINUS_BYTES = { '-' }; + + protected static final byte[] PLUS_BYTES = { '+' }; + protected final RedisCodec codec; public BaseRedisCommandBuilder(RedisCodec codec) { @@ -66,4 +80,146 @@ protected CommandOutput newScriptOutput(RedisCodec codec, Scr } } + protected boolean allElementsInstanceOf(Object[] objects, Class expectedAssignableType) { + + for (Object object : objects) { + if (!expectedAssignableType.isAssignableFrom(object.getClass())) { + return false; + } + } + + return true; + } + + protected byte[] maxValue(Range range) { + + Range.Boundary upper = range.getUpper(); + + if (upper.getValue() == null) { + return PLUS_BYTES; + } + + ByteBuffer encoded = codec.encodeValue(upper.getValue()); + ByteBuffer allocated = ByteBuffer.allocate(encoded.remaining() + 1); + allocated.put(upper.isIncluding() ? (byte) '[' : (byte) '(').put(encoded); + + return allocated.array(); + } + + protected byte[] minValue(Range range) { + + Range.Boundary lower = range.getLower(); + + if (lower.getValue() == null) { + return MINUS_BYTES; + } + + ByteBuffer encoded = codec.encodeValue(lower.getValue()); + ByteBuffer allocated = ByteBuffer.allocate(encoded.remaining() + 1); + allocated.put(lower.isIncluding() ? (byte) '[' : (byte) '(').put(encoded); + + return allocated.array(); + } + + protected static void notNull(ScoredValueStreamingChannel channel) { + LettuceAssert.notNull(channel, "ScoredValueStreamingChannel " + MUST_NOT_BE_NULL); + } + + protected static void notNull(KeyStreamingChannel channel) { + LettuceAssert.notNull(channel, "KeyValueStreamingChannel " + MUST_NOT_BE_NULL); + } + + protected static void notNull(ValueStreamingChannel channel) { + LettuceAssert.notNull(channel, "ValueStreamingChannel " + MUST_NOT_BE_NULL); + } + + protected static void notNull(KeyValueStreamingChannel channel) { + LettuceAssert.notNull(channel, "KeyValueStreamingChannel " + MUST_NOT_BE_NULL); + } + + protected static void notNullMinMax(String min, String max) { + LettuceAssert.notNull(min, "Min " + MUST_NOT_BE_NULL); + LettuceAssert.notNull(max, "Max " + MUST_NOT_BE_NULL); + } + + protected static void addLimit(CommandArgs args, Limit limit) { + + if (limit.isLimited()) { + args.add(LIMIT).add(limit.getOffset()).add(limit.getCount()); + } + } + + protected static void assertNodeId(String nodeId) { + LettuceAssert.notNull(nodeId, "NodeId " + MUST_NOT_BE_NULL); + LettuceAssert.notEmpty(nodeId, "NodeId " + MUST_NOT_BE_EMPTY); + } + + protected static String max(Range range) { + + Range.Boundary upper = range.getUpper(); + + if (upper.getValue() == null + || upper.getValue() instanceof Double && upper.getValue().doubleValue() == Double.POSITIVE_INFINITY) { + return "+inf"; + } + + if (!upper.isIncluding()) { + return "(" + upper.getValue(); + } + + return upper.getValue().toString(); + } + + protected static String min(Range range) { + + Range.Boundary lower = range.getLower(); + + if (lower.getValue() == null + || lower.getValue() instanceof Double && lower.getValue().doubleValue() == Double.NEGATIVE_INFINITY) { + return "-inf"; + } + + if (!lower.isIncluding()) { + return "(" + lower.getValue(); + } + + return lower.getValue().toString(); + } + + protected static void notEmpty(Object[] keys) { + LettuceAssert.notNull(keys, "Keys " + MUST_NOT_BE_NULL); + LettuceAssert.notEmpty(keys, "Keys " + MUST_NOT_BE_EMPTY); + } + + protected static void notEmptySlots(int[] slots) { + LettuceAssert.notNull(slots, "Slots " + MUST_NOT_BE_NULL); + LettuceAssert.notEmpty(slots, "Slots " + MUST_NOT_BE_EMPTY); + } + + protected static void notEmptyValues(Object[] values) { + LettuceAssert.notNull(values, "Values " + MUST_NOT_BE_NULL); + LettuceAssert.notEmpty(values, "Values " + MUST_NOT_BE_EMPTY); + } + + protected static void notNullKey(Object key) { + LettuceAssert.notNull(key, "Key " + MUST_NOT_BE_NULL); + } + + protected static void keyAndFieldsProvided(Object key, Object[] fields) { + LettuceAssert.notNull(key, "Key " + MUST_NOT_BE_NULL); + LettuceAssert.notEmpty(fields, "Fields " + MUST_NOT_BE_EMPTY); + } + + protected static void notNullLimit(Limit limit) { + LettuceAssert.notNull(limit, "Limit " + MUST_NOT_BE_NULL); + } + + protected static void notNullRange(Range range) { + LettuceAssert.notNull(range, "Range " + MUST_NOT_BE_NULL); + } + + protected static void notEmptyRanges(Range[] ranges) { + LettuceAssert.notEmpty(ranges, "Ranges " + MUST_NOT_BE_NULL); + } + } diff --git a/src/main/java/io/lettuce/core/protocol/CommandArgs.java b/src/main/java/io/lettuce/core/protocol/CommandArgs.java index c34ed70e1c..da5f897852 100644 --- a/src/main/java/io/lettuce/core/protocol/CommandArgs.java +++ b/src/main/java/io/lettuce/core/protocol/CommandArgs.java @@ -441,7 +441,7 @@ static BytesArgument of(ProtocolKeyword protocolKeyword) { @Override public String toString() { - return protocolKeyword.name(); + return protocolKeyword.toString(); } } diff --git a/src/main/java/io/lettuce/core/protocol/CommandHandler.java b/src/main/java/io/lettuce/core/protocol/CommandHandler.java index 83bf304020..063a0ab218 100644 --- a/src/main/java/io/lettuce/core/protocol/CommandHandler.java +++ b/src/main/java/io/lettuce/core/protocol/CommandHandler.java @@ -30,13 +30,17 @@ import java.util.Collection; import java.util.LinkedHashSet; import java.util.List; +import java.util.Optional; import java.util.Queue; import java.util.Set; import java.util.concurrent.atomic.AtomicLong; import io.lettuce.core.ClientOptions; +import io.lettuce.core.ConnectionBuilder; import io.lettuce.core.RedisConnectionException; +import io.lettuce.core.RedisCredentials; import io.lettuce.core.RedisException; +import io.lettuce.core.RedisURI; import io.lettuce.core.api.push.PushListener; import io.lettuce.core.api.push.PushMessage; import io.lettuce.core.internal.LettuceAssert; @@ -476,7 +480,16 @@ private void attachTracing(ChannelHandlerContext ctx, RedisCommand comm TraceContext context = provider.getTraceContext(); Tracer.Span span = tracer.nextSpan(context); - span.name(command.getType().name()); + span.name(command.getType().toString()); + + if (channel.hasAttr(ConnectionBuilder.REDIS_URI)) { + String redisUriStr = channel.attr(ConnectionBuilder.REDIS_URI).get(); + RedisURI redisURI = RedisURI.create(redisUriStr); + span.tag("server.address", redisURI.toString()); + span.tag("db.namespace", String.valueOf(redisURI.getDatabase())); + span.tag("user.name", Optional.ofNullable(redisURI.getCredentialsProvider().resolveCredentials().block()) + .map(RedisCredentials::getUsername).orElse("")); + } if (tracedEndpoint != null) { span.remoteEndpoint(tracedEndpoint); diff --git a/src/main/java/io/lettuce/core/protocol/CommandKeyword.java b/src/main/java/io/lettuce/core/protocol/CommandKeyword.java index 6e1af5429f..8cd1f0f9d3 100644 --- a/src/main/java/io/lettuce/core/protocol/CommandKeyword.java +++ b/src/main/java/io/lettuce/core/protocol/CommandKeyword.java @@ -37,23 +37,23 @@ public enum CommandKeyword implements ProtocolKeyword { BY, BYLEX, BYSCORE, CACHING, CAT, CH, CHANNELS, COPY, COUNT, COUNTKEYSINSLOT, CONSUMERS, CREATE, DB, DELSLOTS, DELSLOTSRANGE, DELUSER, DESC, DRYRUN, SOFT, HARD, ENCODING, - FAILOVER, FORGET, FIELDS, FLUSH, FORCE, FREQ, FLUSHSLOTS, GENPASS, GETNAME, GETUSER, GETKEYSINSLOT, GETREDIR, GROUP, GROUPS, HTSTATS, ID, IDLE, INFO, + FAILOVER, FORGET, FIELDS, FLAGS, FLUSH, FORCE, FREQ, FLUSHSLOTS, GENPASS, GETNAME, GETUSER, GETKEYSINSLOT, GETREDIR, GROUP, GROUPS, HTSTATS, ID, IDLE, INFO, - IDLETIME, JUSTID, KILL, KEYSLOT, LEFT, LEN, LIMIT, LIST, LOAD, LOG, MATCH, + IDLETIME, JUSTID, KILL, KEYSLOT, LEFT, LEN, LIMIT, LINKS, LIST, LOAD, LOG, MATCH, - MAX, MAXLEN, MEET, MIN, MINID, MOVED, NO, NOACK, NOCOMMANDS, NODE, NODES, NOMKSTREAM, NOPASS, NOSAVE, NOT, NOVALUES, NUMSUB, SHARDCHANNELS, SHARDNUMSUB, NUMPAT, NX, OFF, ON, ONE, OR, PAUSE, + MAX, MAXLEN, MEET, MIN, MINID, MOVED, NO, NOACK, NOCOMMANDS, NODE, NODES, NOMKSTREAM, NOPASS, NOSAVE, NOT, NOVALUES, NUMSUB, SHARDCHANNELS, SHARDNUMSUB, NUMPAT, NX, OFF, ON, ONE, OR, PAUSE, PREFIXES, - REFCOUNT, REMOVE, RELOAD, REPLACE, REPLICATE, REPLICAS, REV, RESET, RESETCHANNELS, RESETKEYS, RESETPASS, + REFCOUNT, REMOVE, RELOAD, REPLACE, REDIRECT, REPLICATE, REPLICAS, REV, RESET, RESETCHANNELS, RESETKEYS, RESETPASS, RESETSTAT, RESTART, RETRYCOUNT, REWRITE, RIGHT, SAVECONFIG, SDSLEN, SETINFO, SETNAME, SETSLOT, SHARDS, SLOTS, STABLE, - MIGRATING, IMPORTING, SAVE, SKIPME, SLAVES, STREAM, STORE, SUM, SEGFAULT, SETUSER, TAKEOVER, TRACKING, TYPE, UNBLOCK, USERS, USAGE, WEIGHTS, WHOAMI, + MIGRATING, IMPORTING, SAVE, SKIPME, SLAVES, STREAM, STORE, SUM, SEGFAULT, SETUSER, TAKEOVER, TRACKING, TRACKINGINFO, TYPE, UNBLOCK, USERS, USAGE, WEIGHTS, WHOAMI, - WITHSCORE, WITHSCORES, WITHVALUES, XOR, XX, YES; + WITHSCORE, WITHSCORES, WITHVALUES, XOR, XX, YES, INDENT, NEWLINE, SPACE; public final byte[] bytes; - private CommandKeyword() { + CommandKeyword() { bytes = name().getBytes(StandardCharsets.US_ASCII); } diff --git a/src/main/java/io/lettuce/core/protocol/CommandType.java b/src/main/java/io/lettuce/core/protocol/CommandType.java index 427ce80184..7eb949e5d3 100644 --- a/src/main/java/io/lettuce/core/protocol/CommandType.java +++ b/src/main/java/io/lettuce/core/protocol/CommandType.java @@ -42,7 +42,7 @@ public enum CommandType implements ProtocolKeyword { // Server - BGREWRITEAOF, BGSAVE, CLIENT, COMMAND, CONFIG, DBSIZE, DEBUG, FLUSHALL, FLUSHDB, INFO, MYID, LASTSAVE, REPLICAOF, ROLE, MONITOR, SAVE, SHUTDOWN, SLAVEOF, SLOWLOG, SYNC, MEMORY, + BGREWRITEAOF, BGSAVE, CLIENT, COMMAND, CONFIG, DBSIZE, DEBUG, FLUSHALL, FLUSHDB, INFO, MYID, MYSHARDID, LASTSAVE, REPLICAOF, ROLE, MONITOR, SAVE, SHUTDOWN, SLAVEOF, SLOWLOG, SYNC, MEMORY, // Keys @@ -103,6 +103,15 @@ public enum CommandType implements ProtocolKeyword { XACK, XADD, XAUTOCLAIM, XCLAIM, XDEL, XGROUP, XINFO, XLEN, XPENDING, XRANGE, XREVRANGE, XREAD, XREADGROUP, XTRIM, + // JSON + + JSON_ARRAPPEND("JSON.ARRAPPEND"), JSON_ARRINDEX("JSON.ARRINDEX"), JSON_ARRINSERT("JSON.ARRINSERT"), JSON_ARRLEN( + "JSON.ARRLEN"), JSON_ARRPOP("JSON.ARRPOP"), JSON_ARRTRIM("JSON.ARRTRIM"), JSON_CLEAR("JSON.CLEAR"), JSON_DEL( + "JSON.DEL"), JSON_GET("JSON.GET"), JSON_MERGE("JSON.MERGE"), JSON_MGET("JSON.MGET"), JSON_MSET( + "JSON.MSET"), JSON_NUMINCRBY("JSON.NUMINCRBY"), JSON_OBJKEYS("JSON.OBJKEYS"), JSON_OBJLEN( + "JSON.OBJLEN"), JSON_SET("JSON.SET"), JSON_STRAPPEND("JSON.STRAPPEND"), JSON_STRLEN( + "JSON.STRLEN"), JSON_TOGGLE("JSON.TOGGLE"), JSON_TYPE("JSON.TYPE"), + // Others TIME, WAIT, @@ -117,10 +126,34 @@ public enum CommandType implements ProtocolKeyword { public final byte[] bytes; + private final String command; + + /** + * Simple commands (comprised of only letters) use the name of the enum constant as command name. + */ CommandType() { + command = name(); bytes = name().getBytes(StandardCharsets.US_ASCII); } + /** + * Complex commands (comprised of other symbols besides letters) get the command name as a parameter. + * + * @param name the command name, must not be {@literal null}. + */ + CommandType(String name) { + command = name; + bytes = name.getBytes(StandardCharsets.US_ASCII); + } + + /** + * + * @return name of the command. + */ + public String toString() { + return command; + } + @Override public byte[] getBytes() { return bytes; diff --git a/src/main/java/io/lettuce/core/protocol/ProtocolKeyword.java b/src/main/java/io/lettuce/core/protocol/ProtocolKeyword.java index 88d432407a..79d96058dc 100644 --- a/src/main/java/io/lettuce/core/protocol/ProtocolKeyword.java +++ b/src/main/java/io/lettuce/core/protocol/ProtocolKeyword.java @@ -17,6 +17,6 @@ public interface ProtocolKeyword { * * @return name of the command. */ - String name(); + String toString(); } diff --git a/src/main/java/io/lettuce/core/protocol/SharedLock.java b/src/main/java/io/lettuce/core/protocol/SharedLock.java index 13a9cb8cfe..c3ad425c16 100644 --- a/src/main/java/io/lettuce/core/protocol/SharedLock.java +++ b/src/main/java/io/lettuce/core/protocol/SharedLock.java @@ -26,6 +26,8 @@ class SharedLock { private final Lock lock = new ReentrantLock(); + private final ThreadLocal threadWriters = ThreadLocal.withInitial(() -> 0); + private volatile long writers = 0; private volatile Thread exclusiveLockOwner; @@ -45,6 +47,7 @@ void incrementWriters() { if (WRITERS.get(this) >= 0) { WRITERS.incrementAndGet(this); + threadWriters.set(threadWriters.get() + 1); return; } } @@ -63,6 +66,7 @@ void decrementWriters() { } WRITERS.decrementAndGet(this); + threadWriters.set(threadWriters.get() - 1); } /** @@ -121,7 +125,8 @@ private void lockWritersExclusive() { try { for (;;) { - if (WRITERS.compareAndSet(this, 0, -1)) { + // allow reentrant exclusive lock by comparing writers count and threadWriters count + if (WRITERS.compareAndSet(this, threadWriters.get(), -1)) { exclusiveLockOwner = Thread.currentThread(); return; } @@ -137,9 +142,13 @@ private void lockWritersExclusive() { private void unlockWritersExclusive() { if (exclusiveLockOwner == Thread.currentThread()) { - if (WRITERS.incrementAndGet(this) == 0) { + // check exclusive look not reentrant first + if (WRITERS.compareAndSet(this, -1, threadWriters.get())) { exclusiveLockOwner = null; + return; } + // otherwise unlock until no more reentrant left + WRITERS.incrementAndGet(this); } } diff --git a/src/main/java/io/lettuce/core/pubsub/PubSubCommandHandler.java b/src/main/java/io/lettuce/core/pubsub/PubSubCommandHandler.java index 30d811c71f..bf7daf615a 100644 --- a/src/main/java/io/lettuce/core/pubsub/PubSubCommandHandler.java +++ b/src/main/java/io/lettuce/core/pubsub/PubSubCommandHandler.java @@ -213,7 +213,7 @@ protected void notifyPushListeners(PushMessage notification) { private boolean shouldCompleteCommand(PubSubOutput.Type type, RedisCommand command) { - String commandType = command.getType().name(); + String commandType = command.getType().toString(); switch (type) { case subscribe: return commandType.equalsIgnoreCase("SUBSCRIBE"); diff --git a/src/main/java/io/lettuce/core/pubsub/PubSubEndpoint.java b/src/main/java/io/lettuce/core/pubsub/PubSubEndpoint.java index 6da336c4d0..36c511d522 100644 --- a/src/main/java/io/lettuce/core/pubsub/PubSubEndpoint.java +++ b/src/main/java/io/lettuce/core/pubsub/PubSubEndpoint.java @@ -27,7 +27,6 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.function.Consumer; import io.lettuce.core.ClientOptions; import io.lettuce.core.ConnectionState; @@ -129,6 +128,14 @@ public Set getChannels() { return unwrap(this.channels); } + public boolean hasShardChannelSubscriptions() { + return !shardChannels.isEmpty(); + } + + public Set getShardChannels() { + return unwrap(this.shardChannels); + } + public boolean hasPatternSubscriptions() { return !patterns.isEmpty(); } @@ -151,7 +158,7 @@ public RedisCommand write(RedisCommand command return command; } - if (!subscribeWritten && SUBSCRIBE_COMMANDS.contains(command.getType().name())) { + if (!subscribeWritten && SUBSCRIBE_COMMANDS.contains(command.getType().toString())) { subscribeWritten = true; } @@ -171,7 +178,7 @@ public RedisCommand write(RedisCommand command if (!subscribeWritten) { for (RedisCommand redisCommand : redisCommands) { - if (SUBSCRIBE_COMMANDS.contains(redisCommand.getType().name())) { + if (SUBSCRIBE_COMMANDS.contains(redisCommand.getType().toString())) { subscribeWritten = true; break; } @@ -184,14 +191,14 @@ public RedisCommand write(RedisCommand command protected void rejectCommand(RedisCommand command) { command.completeExceptionally( new RedisException(String.format("Command %s not allowed while subscribed. Allowed commands are: %s", - command.getType().name(), ALLOWED_COMMANDS_SUBSCRIBED))); + command.getType().toString(), ALLOWED_COMMANDS_SUBSCRIBED))); } protected void rejectCommands(Collection> redisCommands) { for (RedisCommand command : redisCommands) { command.completeExceptionally( new RedisException(String.format("Command %s not allowed while subscribed. Allowed commands are: %s", - command.getType().name(), ALLOWED_COMMANDS_SUBSCRIBED))); + command.getType().toString(), ALLOWED_COMMANDS_SUBSCRIBED))); } } @@ -215,7 +222,7 @@ private boolean isAllowed(RedisCommand command) { protocolVersion = getProtocolVersion(); } - return protocolVersion == ProtocolVersion.RESP3 || ALLOWED_COMMANDS_SUBSCRIBED.contains(command.getType().name()); + return protocolVersion == ProtocolVersion.RESP3 || ALLOWED_COMMANDS_SUBSCRIBED.contains(command.getType().toString()); } public boolean isSubscribed() { diff --git a/src/main/java/io/lettuce/core/pubsub/RedisPubSubAsyncCommandsImpl.java b/src/main/java/io/lettuce/core/pubsub/RedisPubSubAsyncCommandsImpl.java index 3e282b5410..44108e4942 100644 --- a/src/main/java/io/lettuce/core/pubsub/RedisPubSubAsyncCommandsImpl.java +++ b/src/main/java/io/lettuce/core/pubsub/RedisPubSubAsyncCommandsImpl.java @@ -47,7 +47,7 @@ public class RedisPubSubAsyncCommandsImpl extends RedisAsyncCommandsImpl connection, RedisCodec codec) { - super(connection, codec); + super(connection, codec, null); this.commandBuilder = new PubSubCommandBuilder<>(codec); } diff --git a/src/main/java/io/lettuce/core/pubsub/RedisPubSubReactiveCommandsImpl.java b/src/main/java/io/lettuce/core/pubsub/RedisPubSubReactiveCommandsImpl.java index 19e878f3f6..54774b7269 100644 --- a/src/main/java/io/lettuce/core/pubsub/RedisPubSubReactiveCommandsImpl.java +++ b/src/main/java/io/lettuce/core/pubsub/RedisPubSubReactiveCommandsImpl.java @@ -52,7 +52,7 @@ public class RedisPubSubReactiveCommandsImpl extends RedisReactiveCommands * @param codec Codec used to encode/decode keys and values. */ public RedisPubSubReactiveCommandsImpl(StatefulRedisPubSubConnection connection, RedisCodec codec) { - super(connection, codec); + super(connection, codec, null); this.commandBuilder = new PubSubCommandBuilder<>(codec); } @@ -161,7 +161,7 @@ public Mono> pubsubShardNumsub(K... shardChannels) { @Override public Mono spublish(K shardChannel, V message) { - return createMono(() -> commandBuilder.publish(shardChannel, message)); + return createMono(() -> commandBuilder.spublish(shardChannel, message)); } @Override diff --git a/src/main/java/io/lettuce/core/pubsub/StatefulRedisPubSubConnectionImpl.java b/src/main/java/io/lettuce/core/pubsub/StatefulRedisPubSubConnectionImpl.java index 33b1f1412e..6e012f4328 100644 --- a/src/main/java/io/lettuce/core/pubsub/StatefulRedisPubSubConnectionImpl.java +++ b/src/main/java/io/lettuce/core/pubsub/StatefulRedisPubSubConnectionImpl.java @@ -62,7 +62,7 @@ public class StatefulRedisPubSubConnectionImpl extends StatefulRedisConnec public StatefulRedisPubSubConnectionImpl(PubSubEndpoint endpoint, RedisChannelWriter writer, RedisCodec codec, Duration timeout) { - super(writer, endpoint, codec, timeout); + super(writer, endpoint, codec, timeout, null); this.endpoint = endpoint; endpoint.setConnectionState(getConnectionState()); } @@ -130,6 +130,10 @@ protected List> resubscribe() { result.add(async().subscribe(toArray(endpoint.getChannels()))); } + if (endpoint.hasShardChannelSubscriptions()) { + result.add(async().ssubscribe(toArray(endpoint.getShardChannels()))); + } + if (endpoint.hasPatternSubscriptions()) { result.add(async().psubscribe(toArray(endpoint.getPatterns()))); } diff --git a/src/main/java/io/lettuce/core/resource/DefaultClientResources.java b/src/main/java/io/lettuce/core/resource/DefaultClientResources.java index 592ffff4ab..a165978df6 100644 --- a/src/main/java/io/lettuce/core/resource/DefaultClientResources.java +++ b/src/main/java/io/lettuce/core/resource/DefaultClientResources.java @@ -442,7 +442,7 @@ public Builder computationThreadPoolSize(int computationThreadPoolSize) { /** * Sets the {@link DnsResolver} that is used to resolve hostnames to {@link java.net.InetAddress}. Defaults to - * {@link DnsResolvers#JVM_DEFAULT} + * {@link DnsResolvers#UNRESOLVED} * * @param dnsResolver the DNS resolver, must not be {@code null}. * @return {@code this} {@link Builder}. diff --git a/src/main/java/io/lettuce/core/sentinel/RedisSentinelReactiveCommandsImpl.java b/src/main/java/io/lettuce/core/sentinel/RedisSentinelReactiveCommandsImpl.java index d68142cefa..e02cf4d960 100644 --- a/src/main/java/io/lettuce/core/sentinel/RedisSentinelReactiveCommandsImpl.java +++ b/src/main/java/io/lettuce/core/sentinel/RedisSentinelReactiveCommandsImpl.java @@ -28,6 +28,7 @@ import io.lettuce.core.api.StatefulConnection; import io.lettuce.core.codec.RedisCodec; import io.lettuce.core.internal.LettuceAssert; +import io.lettuce.core.json.JsonParser; import io.lettuce.core.output.CommandOutput; import io.lettuce.core.protocol.Command; import io.lettuce.core.protocol.CommandArgs; @@ -50,8 +51,9 @@ public class RedisSentinelReactiveCommandsImpl extends AbstractRedisReacti private final SentinelCommandBuilder commandBuilder; - public RedisSentinelReactiveCommandsImpl(StatefulConnection connection, RedisCodec codec) { - super(connection, codec); + public RedisSentinelReactiveCommandsImpl(StatefulConnection connection, RedisCodec codec, + Mono parser) { + super(connection, codec, parser); commandBuilder = new SentinelCommandBuilder(codec); } diff --git a/src/main/java/io/lettuce/core/sentinel/StatefulRedisSentinelConnectionImpl.java b/src/main/java/io/lettuce/core/sentinel/StatefulRedisSentinelConnectionImpl.java index 0f729d9096..a302847752 100644 --- a/src/main/java/io/lettuce/core/sentinel/StatefulRedisSentinelConnectionImpl.java +++ b/src/main/java/io/lettuce/core/sentinel/StatefulRedisSentinelConnectionImpl.java @@ -27,12 +27,14 @@ import io.lettuce.core.RedisChannelWriter; import io.lettuce.core.codec.RedisCodec; import io.lettuce.core.codec.StringCodec; +import io.lettuce.core.json.JsonParser; import io.lettuce.core.output.StatusOutput; import io.lettuce.core.protocol.*; import io.lettuce.core.sentinel.api.StatefulRedisSentinelConnection; import io.lettuce.core.sentinel.api.async.RedisSentinelAsyncCommands; import io.lettuce.core.sentinel.api.reactive.RedisSentinelReactiveCommands; import io.lettuce.core.sentinel.api.sync.RedisSentinelCommands; +import reactor.core.publisher.Mono; /** * @author Mark Paluch @@ -50,14 +52,15 @@ public class StatefulRedisSentinelConnectionImpl extends RedisChannelHandl private final SentinelConnectionState connectionState = new SentinelConnectionState(); - public StatefulRedisSentinelConnectionImpl(RedisChannelWriter writer, RedisCodec codec, Duration timeout) { + public StatefulRedisSentinelConnectionImpl(RedisChannelWriter writer, RedisCodec codec, Duration timeout, + Mono parser) { super(writer, timeout); this.codec = codec; this.async = new RedisSentinelAsyncCommandsImpl<>(this, codec); this.sync = syncHandler(async, RedisSentinelCommands.class); - this.reactive = new RedisSentinelReactiveCommandsImpl<>(this, codec); + this.reactive = new RedisSentinelReactiveCommandsImpl<>(this, codec, parser); } @Override diff --git a/src/main/java/io/lettuce/core/tracing/BraveTracing.java b/src/main/java/io/lettuce/core/tracing/BraveTracing.java index 339db4178c..05b62d4388 100644 --- a/src/main/java/io/lettuce/core/tracing/BraveTracing.java +++ b/src/main/java/io/lettuce/core/tracing/BraveTracing.java @@ -341,7 +341,7 @@ static class BraveSpan extends Tracer.Span { @Override public BraveSpan start(RedisCommand command) { - span.name(command.getType().name()); + span.name(command.getType().toString()); if (includeCommandArgsInSpanTags && command.getArgs() != null) { span.tag("redis.args", command.getArgs().toCommandString()); diff --git a/src/main/java/io/lettuce/core/tracing/DefaultLettuceObservationConvention.java b/src/main/java/io/lettuce/core/tracing/DefaultLettuceObservationConvention.java index a79dd83a09..f75293d934 100644 --- a/src/main/java/io/lettuce/core/tracing/DefaultLettuceObservationConvention.java +++ b/src/main/java/io/lettuce/core/tracing/DefaultLettuceObservationConvention.java @@ -30,7 +30,7 @@ public KeyValues getLowCardinalityKeyValues(LettuceObservationContext context) { Tracing.Endpoint ep = context.getRequiredEndpoint(); KeyValues keyValues = KeyValues.of(LowCardinalityCommandKeyNames.DATABASE_SYSTEM.withValue("redis"), // - LowCardinalityCommandKeyNames.REDIS_COMMAND.withValue(context.getRequiredCommand().getType().name())); + LowCardinalityCommandKeyNames.REDIS_COMMAND.withValue(context.getRequiredCommand().getType().toString())); if (ep instanceof SocketAddressEndpoint) { @@ -62,7 +62,7 @@ public KeyValues getHighCardinalityKeyValues(LettuceObservationContext context) if (command.getArgs() != null) { return KeyValues.of(HighCardinalityCommandKeyNames.STATEMENT - .withValue(command.getType().name() + " " + command.getArgs().toCommandString())); + .withValue(command.getType().toString() + " " + command.getArgs().toCommandString())); } } @@ -71,7 +71,7 @@ public KeyValues getHighCardinalityKeyValues(LettuceObservationContext context) @Override public String getContextualName(LettuceObservationContext context) { - return context.getRequiredCommand().getType().name().toLowerCase(Locale.ROOT); + return context.getRequiredCommand().getType().toString().toLowerCase(Locale.ROOT); } public boolean includeCommandArgsInSpanTags() { diff --git a/src/main/kotlin/io/lettuce/core/ScanFlow.kt b/src/main/kotlin/io/lettuce/core/ScanFlow.kt index 7cc560fb60..0c8853df5d 100644 --- a/src/main/kotlin/io/lettuce/core/ScanFlow.kt +++ b/src/main/kotlin/io/lettuce/core/ScanFlow.kt @@ -82,7 +82,7 @@ object ScanFlow { * @param key the key. * @param scanArgs scan arguments. * @return `Flow>` flow of key-values. - * @since 7.0 + * @since 6.4 */ fun hscanNovalues(commands: RedisHashCoroutinesCommands, key: K, scanArgs: ScanArgs? = null): Flow { val ops = when (commands) { diff --git a/src/main/kotlin/io/lettuce/core/api/coroutines/BaseRedisCoroutinesCommands.kt b/src/main/kotlin/io/lettuce/core/api/coroutines/BaseRedisCoroutinesCommands.kt index df829da7b5..475a9f81bc 100644 --- a/src/main/kotlin/io/lettuce/core/api/coroutines/BaseRedisCoroutinesCommands.kt +++ b/src/main/kotlin/io/lettuce/core/api/coroutines/BaseRedisCoroutinesCommands.kt @@ -91,7 +91,7 @@ interface BaseRedisCoroutinesCommands { * * @param shardChannels channel keys. * @return array-reply a list of channels and number of subscribers for every channel. - * @since 7.0 + * @since 6.4 */ suspend fun pubsubShardNumsub(vararg shardChannels: K): Map? @@ -108,7 +108,7 @@ interface BaseRedisCoroutinesCommands { * @param shardChannel the shard channel type: key. * @param message the message type: value. * @return Long integer-reply the number of clients that received the message. - * @since 7.0 + * @since 6.4 */ suspend fun spublish(shardChannel: K, message: V): Long? diff --git a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisCoroutinesCommands.kt b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisCoroutinesCommands.kt index 2732a4f348..f2372548fa 100644 --- a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisCoroutinesCommands.kt +++ b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisCoroutinesCommands.kt @@ -28,6 +28,7 @@ import io.lettuce.core.cluster.api.coroutines.RedisClusterCoroutinesCommands * @param Key type. * @param Value type. * @author Mikhael Sokolov + * @author Tihomir Mateev * @since 6.0 */ @ExperimentalLettuceCoroutinesApi @@ -47,7 +48,8 @@ interface RedisCoroutinesCommands : RedisStreamCoroutinesCommands, RedisStringCoroutinesCommands, RedisTransactionalCoroutinesCommands, - RedisClusterCoroutinesCommands { + RedisClusterCoroutinesCommands, + RedisJsonCoroutinesCommands{ /** * Authenticate to the server. diff --git a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisCoroutinesCommandsImpl.kt b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisCoroutinesCommandsImpl.kt index 0dc6b5e196..a262d39435 100644 --- a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisCoroutinesCommandsImpl.kt +++ b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisCoroutinesCommandsImpl.kt @@ -31,9 +31,8 @@ import kotlinx.coroutines.reactive.awaitFirstOrNull * * @param Key type. * @param Value type. - * @author Mikhael Sokolov - * @author Mark Paluch - * @since 6.0 + * @author Tihomir Mateev + * @since 6.5 */ @ExperimentalLettuceCoroutinesApi open class RedisCoroutinesCommandsImpl( @@ -53,7 +52,8 @@ open class RedisCoroutinesCommandsImpl( RedisSortedSetCoroutinesCommands by RedisSortedSetCoroutinesCommandsImpl(ops), RedisStreamCoroutinesCommands by RedisStreamCoroutinesCommandsImpl(ops), RedisStringCoroutinesCommands by RedisStringCoroutinesCommandsImpl(ops), - RedisTransactionalCoroutinesCommands by RedisTransactionalCoroutinesCommandsImpl(ops) { + RedisTransactionalCoroutinesCommands by RedisTransactionalCoroutinesCommandsImpl(ops), + RedisJsonCoroutinesCommands by RedisJsonCoroutinesCommandsImpl(ops) { /** * Authenticate to the server. diff --git a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisHashCoroutinesCommands.kt b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisHashCoroutinesCommands.kt index 99c330a35b..3bee6f59b9 100644 --- a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisHashCoroutinesCommands.kt +++ b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisHashCoroutinesCommands.kt @@ -190,7 +190,7 @@ interface RedisHashCoroutinesCommands { * * @param key the key. * @return KeyScanCursor key scan cursor. - * @since 7.0 + * @since 6.4 */ suspend fun hscanNovalues(key: K): KeyScanCursor? @@ -209,7 +209,7 @@ interface RedisHashCoroutinesCommands { * @param key the key. * @param scanArgs scan arguments. * @return KeyScanCursor key scan cursor. - * @since 7.0 + * @since 6.4 */ suspend fun hscanNovalues(key: K, scanArgs: ScanArgs): KeyScanCursor? @@ -230,7 +230,7 @@ interface RedisHashCoroutinesCommands { * @param scanCursor cursor to resume from a previous scan, must not be `null`. * @param scanArgs scan arguments. * @return KeyScanCursor key scan cursor. - * @since 7.0 + * @since 6.4 */ suspend fun hscanNovalues(key: K, scanCursor: ScanCursor, scanArgs: ScanArgs): KeyScanCursor? @@ -249,7 +249,7 @@ interface RedisHashCoroutinesCommands { * @param key the key. * @param scanCursor cursor to resume from a previous scan, must not be `null`. * @return KeyScanCursor key scan cursor. - * @since 7.0 + * @since 6.4 */ suspend fun hscanNovalues(key: K, scanCursor: ScanCursor): KeyScanCursor? @@ -317,7 +317,7 @@ interface RedisHashCoroutinesCommands { * already due to expiration, or provided expriry interval is 0; `1` indicating expiration time is * set/updated; `0` indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not * met); `-2` indicating there is no such field - * @since 7.0 + * @since 6.4 */ suspend fun hexpire(key: K, seconds: Long, vararg fields: K): List @@ -332,7 +332,7 @@ interface RedisHashCoroutinesCommands { * already due to expiration, or provided expriry interval is 0; `1` indicating expiration time is * set/updated; `0` indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not * met); `-2` indicating there is no such field - * @since 7.0 + * @since 6.4 */ suspend fun hexpire(key: K, seconds: Long, expireArgs: ExpireArgs, vararg fields: K): List @@ -346,7 +346,7 @@ interface RedisHashCoroutinesCommands { * already due to expiration, or provided expriry interval is 0; `1` indicating expiration time is * set/updated; `0` indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not * met); `-2` indicating there is no such field - * @since 7.0 + * @since 6.4 */ suspend fun hexpire(key: K, seconds: Duration, vararg fields: K): List @@ -361,7 +361,7 @@ interface RedisHashCoroutinesCommands { * already due to expiration, or provided expriry interval is 0; `1` indicating expiration time is * set/updated; `0` indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not * met); `-2` indicating there is no such field - * @since 7.0 + * @since 6.4 */ suspend fun hexpire(key: K, seconds: Duration, expireArgs: ExpireArgs, vararg fields: K): List @@ -375,7 +375,7 @@ interface RedisHashCoroutinesCommands { * already due to expiration, or provided expriry interval is in the past; `1` indicating expiration time is * set/updated; `0` indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not * met); `-2` indicating there is no such field - * @since 7.0 + * @since 6.4 */ suspend fun hexpireat(key: K, timestamp: Long, vararg fields: K): List @@ -390,7 +390,7 @@ interface RedisHashCoroutinesCommands { * already due to expiration, or provided expriry interval is in the past; `1` indicating expiration time is * set/updated; `0` indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not * met); `-2` indicating there is no such field - * @since 7.0 + * @since 6.4 */ suspend fun hexpireat(key: K, timestamp: Long, expireArgs: ExpireArgs, vararg fields: K): List @@ -404,7 +404,7 @@ interface RedisHashCoroutinesCommands { * already due to expiration, or provided expriry interval is in the past; `1` indicating expiration time is * set/updated; `0` indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not * met); `-2` indicating there is no such field - * @since 7.0 + * @since 6.4 */ suspend fun hexpireat(key: K, timestamp: Date, vararg fields: K): List @@ -419,7 +419,7 @@ interface RedisHashCoroutinesCommands { * already due to expiration, or provided expriry interval is in the past; `1` indicating expiration time is * set/updated; `0` indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not * met); `-2` indicating there is no such field - * @since 7.0 + * @since 6.4 */ suspend fun hexpireat(key: K, timestamp: Date, expireArgs: ExpireArgs, vararg fields: K): List @@ -433,7 +433,7 @@ interface RedisHashCoroutinesCommands { * already due to expiration, or provided expriry interval is in the past; `1` indicating expiration time is * set/updated; `0` indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not * met); `-2` indicating there is no such field - * @since 7.0 + * @since 6.4 */ suspend fun hexpireat(key: K, timestamp: Instant, vararg fields: K): List @@ -448,7 +448,7 @@ interface RedisHashCoroutinesCommands { * already due to expiration, or provided expriry interval is in the past; `1` indicating expiration time is * set/updated; `0` indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not * met); `-2` indicating there is no such field - * @since 7.0 + * @since 6.4 */ suspend fun hexpireat(key: K, timestamp: Instant, expireArgs: ExpireArgs, vararg fields: K): List @@ -459,7 +459,7 @@ interface RedisHashCoroutinesCommands { * @param fields one or more fields to get the TTL for. * @return a list of [Long] values for each of the fields provided: expiration time as a UNIX timestamp in seconds; * `-1` indicating the field has no expiry time set; `-2` indicating there is no such field - * @since 7.0 + * @since 6.4 */ suspend fun hexpiretime(key: K, vararg fields: K): List @@ -470,7 +470,7 @@ interface RedisHashCoroutinesCommands { * @param fields one or more fields to remove the TTL for. * @return a list of [Long] values for each of the fields provided: `1` indicating expiration time is removed; * `-1` field has no expiration time to be removed; `-2` indicating there is no such field - * @since 7.0 + * @since 6.4 */ suspend fun hpersist(key: K, vararg fields: K): List @@ -484,7 +484,7 @@ interface RedisHashCoroutinesCommands { * already due to expiration, or provided expriry interval is 0; `1` indicating expiration time is * set/updated; `0` indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not * met); `-2` indicating there is no such field - * @since 7.0 + * @since 6.4 */ suspend fun hpexpire(key: K, milliseconds: Long, vararg fields: K): List @@ -499,7 +499,7 @@ interface RedisHashCoroutinesCommands { * already due to expiration, or provided expriry interval is 0; `1` indicating expiration time is * set/updated; `0` indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not * met); `-2` indicating there is no such field - * @since 7.0 + * @since 6.4 */ suspend fun hpexpire(key: K, milliseconds: Long, expireArgs: ExpireArgs, vararg fields: K): List @@ -513,7 +513,7 @@ interface RedisHashCoroutinesCommands { * already due to expiration, or provided expriry interval is 0; `1` indicating expiration time is * set/updated; `0` indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not * met); `-2` indicating there is no such field - * @since 7.0 + * @since 6.4 */ suspend fun hpexpire(key: K, milliseconds: Duration, vararg fields: K): List @@ -528,7 +528,7 @@ interface RedisHashCoroutinesCommands { * already due to expiration, or provided expriry interval is 0; `1` indicating expiration time is * set/updated; `0` indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not * met); `-2` indicating there is no such field - * @since 7.0 + * @since 6.4 */ suspend fun hpexpire(key: K, milliseconds: Duration, expireArgs: ExpireArgs, vararg fields: K): List @@ -542,7 +542,7 @@ interface RedisHashCoroutinesCommands { * already due to expiration, or provided expriry interval is in the past; `1` indicating expiration time is * set/updated; `0` indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not * met); `-2` indicating there is no such field - * @since 7.0 + * @since 6.4 */ suspend fun hpexpireat(key: K, timestamp: Long, vararg fields: K): List @@ -557,7 +557,7 @@ interface RedisHashCoroutinesCommands { * already due to expiration, or provided expriry interval is in the past; `1` indicating expiration time is * set/updated; `0` indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not * met); `-2` indicating there is no such field - * @since 7.0 + * @since 6.4 */ suspend fun hpexpireat(key: K, timestamp: Long, expireArgs: ExpireArgs, vararg fields: K): List @@ -571,7 +571,7 @@ interface RedisHashCoroutinesCommands { * already due to expiration, or provided expriry interval is in the past; `1` indicating expiration time is * set/updated; `0` indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not * met); `-2` indicating there is no such field - * @since 7.0 + * @since 6.4 */ suspend fun hpexpireat(key: K, timestamp: Date, vararg fields: K): List @@ -586,7 +586,7 @@ interface RedisHashCoroutinesCommands { * already due to expiration, or provided expriry interval is in the past; `1` indicating expiration time is * set/updated; `0` indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not * met); `-2` indicating there is no such field - * @since 7.0 + * @since 6.4 */ suspend fun hpexpireat(key: K, timestamp: Date, expireArgs: ExpireArgs, vararg fields: K): List @@ -600,7 +600,7 @@ interface RedisHashCoroutinesCommands { * already due to expiration, or provided expriry interval is in the past; `1` indicating expiration time is * set/updated; `0` indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not * met); `-2` indicating there is no such field - * @since 7.0 + * @since 6.4 */ suspend fun hpexpireat(key: K, timestamp: Instant, vararg fields: K): List @@ -615,7 +615,7 @@ interface RedisHashCoroutinesCommands { * already due to expiration, or provided expriry interval is in the past; `1` indicating expiration time is * set/updated; `0` indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not * met); `-2` indicating there is no such field - * @since 7.0 + * @since 6.4 */ suspend fun hpexpireat(key: K, timestamp: Instant, expireArgs: ExpireArgs, vararg fields: K): List @@ -626,7 +626,7 @@ interface RedisHashCoroutinesCommands { * @param fields one or more fields to get the TTL for. * @return a list of [Long] values for each of the fields provided: expiration time as a UNIX timestamp in milliseconds; * `-1` indicating the field has no expiry time set; `-2` indicating there is no such field - * @since 7.0 + * @since 6.4 */ suspend fun hpexpiretime(key: K, vararg fields: K): List @@ -638,7 +638,7 @@ interface RedisHashCoroutinesCommands { * @return a list of [Long] values for each of the fields provided: the time to live in seconds; or a negative value in * order to signal an error. The command returns `-1` if the key exists but has no associated expiration time. * The command returns `-2` if the key does not exist. - * @since 7.0 + * @since 6.4 */ suspend fun httl(key: K, vararg fields: K): List @@ -650,7 +650,7 @@ interface RedisHashCoroutinesCommands { * @return a list of [Long] values for each of the fields provided: the time to live in milliseconds; or a negative * value in order to signal an error. The command returns `-1` if the key exists but has no associated * expiration time. The command returns `-2` if the key does not exist. - * @since 7.0 + * @since 6.4 */ suspend fun hpttl(key: K, vararg fields: K): List diff --git a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisJsonCoroutinesCommands.kt b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisJsonCoroutinesCommands.kt new file mode 100644 index 0000000000..589cad946c --- /dev/null +++ b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisJsonCoroutinesCommands.kt @@ -0,0 +1,444 @@ +/* + * Copyright 2020-2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.api.coroutines + +import io.lettuce.core.ExperimentalLettuceCoroutinesApi +import kotlinx.coroutines.flow.Flow +import io.lettuce.core.json.JsonType +import io.lettuce.core.json.JsonValue +import io.lettuce.core.json.arguments.JsonGetArgs +import io.lettuce.core.json.arguments.JsonMsetArgs +import io.lettuce.core.json.JsonPath +import io.lettuce.core.json.arguments.JsonRangeArgs +import io.lettuce.core.json.arguments.JsonSetArgs + +/** + * Coroutine executed commands for JSON documents + * + * @param Key type. + * @param Value type. + * @author Tihomir Mateev + * @see Redis JSON + * @since 6.5 + * @generated by io.lettuce.apigenerator.CreateKotlinCoroutinesApi + */ +@ExperimentalLettuceCoroutinesApi +interface RedisJsonCoroutinesCommands { + + /** + * Append the JSON values into the array at a given [JsonPath] after the last element in a said array. + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the array inside the document. + * @param values one or more [JsonValue] to be appended. + * @return Long the resulting size of the arrays after the new data was appended, or null if the path does not exist. + * @since 6.5 + */ + suspend fun jsonArrappend(key: K, jsonPath: JsonPath, vararg values: JsonValue): List + + /** + * Append the JSON values into the array at the {@link JsonPath#ROOT_PATH} after the last element in a said array. + * + * @param key the key holding the JSON document. + * @param values one or more [JsonValue] to be appended. + * @return Long the resulting size of the arrays after the new data was appended, or null if the path does not exist. + * @since 6.5 + */ + suspend fun jsonArrappend(key: K, vararg values: JsonValue): List + + /** + * Search for the first occurrence of a [JsonValue] in an array at a given [JsonPath] and return its index. + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the array inside the document. + * @param value the [JsonValue] to search for. + * @param range the [JsonRangeArgs] to search within. + * @return Long the index hosting the searched element, -1 if not found or null if the specified path is not an array. + * @since 6.5 + */ + suspend fun jsonArrindex(key: K, jsonPath: JsonPath, value: JsonValue, range: JsonRangeArgs): List + + /** + * Search for the first occurrence of a [JsonValue] in an array at a given [JsonPath] and return its index. This + * method uses defaults for the start and end indexes, see {@link JsonRangeArgs#DEFAULT_START_INDEX} and + * {@link JsonRangeArgs#DEFAULT_END_INDEX}. + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the array inside the document. + * @param value the [JsonValue] to search for. + * @return Long the index hosting the searched element, -1 if not found or null if the specified path is not an array. + * @since 6.5 + */ + suspend fun jsonArrindex(key: K, jsonPath: JsonPath, value: JsonValue): List + + /** + * Insert the [JsonValue]s into the array at a given [JsonPath] before the provided index, shifting the existing + * elements to the right + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the array inside the document. + * @param index the index before which the new elements will be inserted. + * @param values one or more [JsonValue]s to be inserted. + * @return Long the resulting size of the arrays after the new data was inserted, or null if the path does not exist. + * @since 6.5 + */ + suspend fun jsonArrinsert(key: K, jsonPath: JsonPath, index: Int, vararg values: JsonValue): List + + /** + * Report the length of the JSON array at a given [JsonPath] + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the array inside the document. + * @return the size of the arrays, or null if the path does not exist. + * @since 6.5 + */ + suspend fun jsonArrlen(key: K, jsonPath: JsonPath): List + + /** + * Report the length of the JSON array at a the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return the size of the arrays, or null if the path does not exist. + * @since 6.5 + */ + suspend fun jsonArrlen(key: K): List + + /** + * Remove and return [JsonValue] at a given index in the array at a given [JsonPath] + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the array inside the document. + * @param index the index of the element to be removed. Default is -1, meaning the last element. Out-of-range indexes round + * to their respective array ends. Popping an empty array returns null. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + suspend fun jsonArrpop(key: K, jsonPath: JsonPath, index: Int): List + + /** + * Remove and return [JsonValue] at index -1 (last element) in the array at a given [JsonPath] + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the array inside the document. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + suspend fun jsonArrpop(key: K, jsonPath: JsonPath): List + + /** + * Remove and return [JsonValue] at index -1 (last element) in the array at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + suspend fun jsonArrpop(key: K): List + + /** + * Trim an array at a given [JsonPath] so that it contains only the specified inclusive range of elements. All + * elements with indexes smaller than the start range and all elements with indexes bigger than the end range are trimmed. + *

+ * Behavior as of RedisJSON v2.0: + *

    + *
  • If start is larger than the array's size or start > stop, returns 0 and an empty array.
  • + *
  • If start is < 0, then start from the end of the array.
  • + *
  • If stop is larger than the end of the array, it is treated like the last element.
  • + *
+ * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the array inside the document. + * @param range the [JsonRangeArgs] to trim by. + * @return Long the resulting size of the arrays after the trimming, or null if the path does not exist. + * @since 6.5 + */ + suspend fun jsonArrtrim(key: K, jsonPath: JsonPath, range: JsonRangeArgs): List + + /** + * Clear container values (arrays/objects) and set numeric values to 0 + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the value to clear. + * @return Long the number of values removed plus all the matching JSON numerical values that are zeroed. + * @since 6.5 + */ + suspend fun jsonClear(key: K, jsonPath: JsonPath): Long? + + /** + * Clear container values (arrays/objects) and set numeric values to 0 at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return Long the number of values removed plus all the matching JSON numerical values that are zeroed. + * @since 6.5 + */ + suspend fun jsonClear(key: K): Long? + + /** + * Deletes a value inside the JSON document at a given [JsonPath] + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the value to clear. + * @return Long the number of values removed (0 or more). + * @since 6.5 + */ + suspend fun jsonDel(key: K, jsonPath: JsonPath): Long? + + /** + * Deletes a value inside the JSON document at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return Long the number of values removed (0 or more). + * @since 6.5 + */ + suspend fun jsonDel(key: K): Long? + + /** + * Return the value at the specified path in JSON serialized form. + *

+ * When using a single JSONPath, the root of the matching values is a JSON string with a top-level array of serialized JSON + * value. In contrast, a legacy path returns a single value. + *

+ * When using multiple JSONPath arguments, the root of the matching values is a JSON string with a top-level object, with + * each object value being a top-level array of serialized JSON value. In contrast, if all paths are legacy paths, each + * object value is a single serialized JSON value. If there are multiple paths that include both legacy path and JSONPath, + * the returned value conforms to the JSONPath version (an array of values). + * + * @param key the key holding the JSON document. + * @param options the [JsonGetArgs] to use. + * @param jsonPaths the [JsonPath]s to use to identify the values to get. + * @return JsonValue the value at path in JSON serialized form, or null if the path does not exist. + * @since 6.5 + */ + suspend fun jsonGet(key: K, options: JsonGetArgs, vararg jsonPaths: JsonPath): List + + /** + * Return the value at the specified path in JSON serialized form. Uses defaults for the [JsonGetArgs]. + *

+ * When using a single JSONPath, the root of the matching values is a JSON string with a top-level array of serialized JSON + * value. In contrast, a legacy path returns a single value. + *

+ * When using multiple JSONPath arguments, the root of the matching values is a JSON string with a top-level object, with + * each object value being a top-level array of serialized JSON value. In contrast, if all paths are legacy paths, each + * object value is a single serialized JSON value. If there are multiple paths that include both legacy path and JSONPath, + * the returned value conforms to the JSONPath version (an array of values). + * + * @param key the key holding the JSON document. + * @param jsonPaths the [JsonPath]s to use to identify the values to get. + * @return JsonValue the value at path in JSON serialized form, or null if the path does not exist. + * @since 6.5 + */ + suspend fun jsonGet(key: K, vararg jsonPaths: JsonPath): List + + /** + * Merge a given [JsonValue] with the value matching [JsonPath]. Consequently, JSON values at matching paths are + * updated, deleted, or expanded with new children. + *

+ * Merging is done according to the following rules per JSON value in the value argument while considering the corresponding + * original value if it exists: + *

    + *
  • merging an existing object key with a null value deletes the key
  • + *
  • merging an existing object key with non-null value updates the value
  • + *
  • merging a non-existing object key adds the key and value
  • + *
  • merging an existing array with any merged value, replaces the entire array with the value
  • + *
+ *

+ * This command complies with RFC7396 "Json Merge Patch" + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the value to merge. + * @param value the [JsonValue] to merge. + * @return String "OK" if the set was successful, error if the operation failed. + * @since 6.5 + * @see RFC7396 + */ + suspend fun jsonMerge(key: K, jsonPath: JsonPath, value: JsonValue): String? + + /** + * Return the values at the specified path from multiple key arguments. + * + * @param jsonPath the [JsonPath] pointing to the value to fetch. + * @param keys the keys holding the [JsonValue]s to fetch. + * @return List the values at path, or null if the path does not exist. + * @since 6.5 + */ + suspend fun jsonMGet(jsonPath: JsonPath, vararg keys: K): List + + /** + * Set or update one or more JSON values according to the specified [JsonMsetArgs] + *

+ * JSON.MSET is atomic, hence, all given additions or updates are either applied or not. It is not possible for clients to + * see that some keys were updated while others are unchanged. + *

+ * A JSON value is a hierarchical structure. If you change a value in a specific path - nested values are affected. + * + * @param arguments the [JsonMsetArgs] specifying the values to change. + * @return "OK" if the operation was successful, error otherwise + * @since 6.5 + */ + suspend fun jsonMSet(arguments: List>): String? + + /** + * Increment the number value stored at the specified [JsonPath] in the JSON document by the provided increment. + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the value to increment. + * @param number the increment value. + * @return a [List] of the new values after the increment. + * @since 6.5 + */ + suspend fun jsonNumincrby(key: K, jsonPath: JsonPath, number: Number): List + + /** + * Return the keys in the JSON document that are referenced by the given [JsonPath] + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the value(s) whose key(s) we want. + * @return List the keys in the JSON document that are referenced by the given [JsonPath]. + * @since 6.5 + */ + suspend fun jsonObjkeys(key: K, jsonPath: JsonPath): List + + /** + * Return the keys in the JSON document that are referenced by the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return List the keys in the JSON document that are referenced by the given [JsonPath]. + * @since 6.5 + */ + suspend fun jsonObjkeys(key: K): List + + /** + * Report the number of keys in the JSON object at the specified [JsonPath] and for the provided key + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the value(s) whose key(s) we want to count + * @return Long the number of keys in the JSON object at the specified path, or null if the path does not exist. + * @since 6.5 + */ + suspend fun jsonObjlen(key: K, jsonPath: JsonPath): List + + /** + * Report the number of keys in the JSON object at the {@link JsonPath#ROOT_PATH} and for the provided key + * + * @param key the key holding the JSON document. + * @return Long the number of keys in the JSON object at the specified path, or null if the path does not exist. + * @since 6.5 + */ + suspend fun jsonObjlen(key: K): List + + /** + * Sets the JSON value at a given [JsonPath] in the JSON document. + *

+ * For new Redis keys, the path must be the root. For existing keys, when the entire path exists, the value that it contains + * is replaced with the JSON value. For existing keys, when the path exists, except for the last element, a new child is + * added with the JSON value. + *

+ * Adds a key (with its respective value) to a JSON Any (in a RedisJSON data type key) only if it is the last child in + * the path, or it is the parent of a new child being added in the path. Optional arguments NX and XX modify this behavior + * for both new RedisJSON data type keys and the JSON Any keys in them. + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the value(s) where we want to set the value. + * @param value the [JsonValue] to set. + * @param options the [JsonSetArgs] the options for setting the value. + * @return String "OK" if the set was successful, null if the [JsonSetArgs] conditions are not met. + * @since 6.5 + */ + suspend fun jsonSet(key: K, jsonPath: JsonPath, value: JsonValue, options: JsonSetArgs): String? + + /** + * Sets the JSON value at a given [JsonPath] in the JSON document using defaults for the [JsonSetArgs]. + *

+ * For new Redis keys the path must be the root. For existing keys, when the entire path exists, the value that it contains + * is replaced with the JSON value. For existing keys, when the path exists, except for the last element, a new child is + * added with the JSON value. + *

+ * Adds a key (with its respective value) to a JSON Any (in a RedisJSON data type key) only if it is the last child in + * the path, or it is the parent of a new child being added in the path. Optional arguments NX and XX modify this behavior + * for both new RedisJSON data type keys and the JSON Any keys in them. + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the value(s) where we want to set the value. + * @param value the [JsonValue] to set. + * @return String "OK" if the set was successful, null if the [JsonSetArgs] conditions are not met. + * @since 6.5 + */ + suspend fun jsonSet(key: K, jsonPath: JsonPath, value: JsonValue): String? + + /** + * Append the json-string values to the string at the provided [JsonPath] in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the value(s) where we want to append the value. + * @param value the [JsonValue] to append. + * @return Long the new length of the string, or null if the matching JSON value is not a string. + * @since 6.5 + */ + suspend fun jsonStrappend(key: K, jsonPath: JsonPath, value: JsonValue): List + + /** + * Append the json-string values to the string at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @param value the [JsonValue] to append. + * @return Long the new length of the string, or null if the matching JSON value is not a string. + * @since 6.5 + */ + suspend fun jsonStrappend(key: K, value: JsonValue): List + + /** + * Report the length of the JSON String at the provided [JsonPath] in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the value(s). + * @return Long (in recursive descent) the length of the JSON String at the provided [JsonPath], or null if the value + * ath the desired path is not a string. + * @since 6.5 + */ + suspend fun jsonStrlen(key: K, jsonPath: JsonPath): List + + /** + * Report the length of the JSON String at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @return Long (in recursive descent) the length of the JSON String at the provided [JsonPath], or null if the value + * ath the desired path is not a string. + * @since 6.5 + */ + suspend fun jsonStrlen(key: K): List + + /** + * Toggle a Boolean value stored at the provided [JsonPath] in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the value(s). + * @return List the new value after the toggle, 0 for false, 1 for true or null if the path does not exist. + * @since 6.5 + */ + suspend fun jsonToggle(key: K, jsonPath: JsonPath): List + + /** + * Report the type of JSON value at the provided [JsonPath] in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the [JsonPath] pointing to the value(s). + * @return List the type of JSON value at the provided [JsonPath] + * @since 6.5 + */ + suspend fun jsonType(key: K, jsonPath: JsonPath): List + + /** + * Report the type of JSON value at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @return List the type of JSON value at the provided [JsonPath] + * @since 6.5 + */ + suspend fun jsonType(key: K): List + +} + diff --git a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisJsonCoroutinesCommandsImpl.kt b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisJsonCoroutinesCommandsImpl.kt new file mode 100644 index 0000000000..8941f21106 --- /dev/null +++ b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisJsonCoroutinesCommandsImpl.kt @@ -0,0 +1,136 @@ +/* + * Copyright 2020-Present, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.api.coroutines + +import io.lettuce.core.* +import io.lettuce.core.api.reactive.RedisJsonReactiveCommands +import io.lettuce.core.json.JsonPath +import io.lettuce.core.json.JsonType +import io.lettuce.core.json.JsonValue +import io.lettuce.core.json.arguments.JsonGetArgs +import io.lettuce.core.json.arguments.JsonMsetArgs +import io.lettuce.core.json.arguments.JsonRangeArgs +import io.lettuce.core.json.arguments.JsonSetArgs +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.reactive.asFlow +import kotlinx.coroutines.reactive.awaitFirstOrNull + +/** + * Coroutine executed commands (based on reactive commands) for Keys (Key manipulation/querying). + * + * @param Key type. + * @param Value type. + * @author Tihomir Mateev + * @since 6.5 + */ +@ExperimentalLettuceCoroutinesApi +internal class RedisJsonCoroutinesCommandsImpl(internal val ops: RedisJsonReactiveCommands) : + RedisJsonCoroutinesCommands { + override suspend fun jsonArrappend(key: K, jsonPath: JsonPath, vararg values: JsonValue): List = + ops.jsonArrappend(key, jsonPath, *values).asFlow().toList() + + override suspend fun jsonArrappend(key: K, vararg values: JsonValue): List = + ops.jsonArrappend(key, *values).asFlow().toList() + + override suspend fun jsonArrindex( + key: K, + jsonPath: JsonPath, + value: JsonValue, + range: JsonRangeArgs + ): List = ops.jsonArrindex(key, jsonPath, value, range).asFlow().toList() + + override suspend fun jsonArrindex(key: K, jsonPath: JsonPath, value: JsonValue): List = + ops.jsonArrindex(key, jsonPath, value).asFlow().toList() + + override suspend fun jsonArrinsert( + key: K, + jsonPath: JsonPath, + index: Int, + vararg values: JsonValue + ): List = ops.jsonArrinsert(key, jsonPath, index, *values).asFlow().toList() + + override suspend fun jsonArrlen(key: K, jsonPath: JsonPath): List = + ops.jsonArrlen(key, jsonPath).asFlow().toList() + + override suspend fun jsonArrlen(key: K): List = ops.jsonArrlen(key).asFlow().toList() + + override suspend fun jsonArrpop(key: K, jsonPath: JsonPath, index: Int): List = + ops.jsonArrpop(key, jsonPath, index).asFlow().toList() + + override suspend fun jsonArrpop(key: K, jsonPath: JsonPath): List = + ops.jsonArrpop(key, jsonPath).asFlow().toList() + + override suspend fun jsonArrpop(key: K): List = ops.jsonArrpop(key).asFlow().toList() + + override suspend fun jsonArrtrim(key: K, jsonPath: JsonPath, range: JsonRangeArgs): List = + ops.jsonArrtrim(key, jsonPath, range).asFlow().toList() + + override suspend fun jsonClear(key: K, jsonPath: JsonPath): Long? = + ops.jsonClear(key, jsonPath).awaitFirstOrNull() + + override suspend fun jsonClear(key: K): Long? = ops.jsonClear(key).awaitFirstOrNull() + + override suspend fun jsonDel(key: K, jsonPath: JsonPath): Long? = + ops.jsonDel(key, jsonPath).awaitFirstOrNull() + + override suspend fun jsonDel(key: K): Long? = ops.jsonDel(key).awaitFirstOrNull() + + override suspend fun jsonGet(key: K, options: JsonGetArgs, vararg jsonPaths: JsonPath): List = + ops.jsonGet(key, options, *jsonPaths).asFlow().toList() + + override suspend fun jsonGet(key: K, vararg jsonPaths: JsonPath): List = + ops.jsonGet(key, *jsonPaths).asFlow().toList() + + override suspend fun jsonMerge(key: K, jsonPath: JsonPath, value: JsonValue): String? = + ops.jsonMerge(key, jsonPath, value).awaitFirstOrNull() + + override suspend fun jsonMGet(jsonPath: JsonPath, vararg keys: K): List = + ops.jsonMGet(jsonPath, *keys).asFlow().toList() + + override suspend fun jsonMSet(arguments: List>): String? = + ops.jsonMSet(arguments).awaitFirstOrNull() + + override suspend fun jsonType(key: K, jsonPath: JsonPath): List = + ops.jsonType(key, jsonPath).asFlow().toList() + + override suspend fun jsonType(key: K): List = ops.jsonType(key).asFlow().toList() + + override suspend fun jsonToggle(key: K, jsonPath: JsonPath): List = + ops.jsonToggle(key, jsonPath).asFlow().toList() + + override suspend fun jsonStrlen(key: K, jsonPath: JsonPath): List = + ops.jsonStrlen(key, jsonPath).asFlow().toList() + + override suspend fun jsonStrlen(key: K): List = ops.jsonStrlen(key).asFlow().toList() + + override suspend fun jsonStrappend(key: K, jsonPath: JsonPath, value: JsonValue): List = + ops.jsonStrappend(key, jsonPath, value).asFlow().toList() + + override suspend fun jsonStrappend(key: K, value: JsonValue): List = + ops.jsonStrappend(key, value).asFlow().toList() + + override suspend fun jsonSet(key: K, jsonPath: JsonPath, value: JsonValue, options: JsonSetArgs): String? = + ops.jsonSet(key, jsonPath, value, options).awaitFirstOrNull() + + override suspend fun jsonSet(key: K, jsonPath: JsonPath, value: JsonValue): String? = + ops.jsonSet(key, jsonPath, value).awaitFirstOrNull() + + override suspend fun jsonObjlen(key: K, jsonPath: JsonPath): List = + ops.jsonObjlen(key, jsonPath).asFlow().toList() + + override suspend fun jsonObjlen(key: K): List = ops.jsonObjlen(key).asFlow().toList() + + override suspend fun jsonObjkeys(key: K, jsonPath: JsonPath): List = + ops.jsonObjkeys(key, jsonPath).asFlow().toList() + + override suspend fun jsonObjkeys(key: K): List = ops.jsonObjkeys(key).asFlow().toList() + + override suspend fun jsonNumincrby(key: K, jsonPath: JsonPath, number: Number): List = + ops.jsonNumincrby(key, jsonPath, number).asFlow().toList() +} + diff --git a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisScriptingCoroutinesCommands.kt b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisScriptingCoroutinesCommands.kt index 711e7569b8..8b0d4a0aa3 100644 --- a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisScriptingCoroutinesCommands.kt +++ b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisScriptingCoroutinesCommands.kt @@ -94,7 +94,7 @@ interface RedisScriptingCoroutinesCommands { * @param values the values. * @param expected return type. * @return script result. - * @since 7.0 + * @since 6.4 */ suspend fun evalReadOnly( script: String, diff --git a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisServerCoroutinesCommands.kt b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisServerCoroutinesCommands.kt index a0524be3d6..b5a92e53c4 100644 --- a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisServerCoroutinesCommands.kt +++ b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisServerCoroutinesCommands.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-Present, Redis Ltd. and Contributors + * Copyright 2017-Present, Redis Ltd. and Contributors * All rights reserved. * * Licensed under the MIT License. @@ -21,6 +21,7 @@ package io.lettuce.core.api.coroutines import io.lettuce.core.* +import io.lettuce.core.TrackingInfo import io.lettuce.core.protocol.CommandType import java.util.* @@ -169,6 +170,14 @@ interface RedisServerCoroutinesCommands { */ suspend fun clientTracking(args: TrackingArgs): String? + /** + * Returns information about the current client connection's use of the server assisted client side caching feature. + * + * @return @link TrackingInfo}, for more information check the documentation + * @since 6.5 + */ + suspend fun clientTrackinginfo(): TrackingInfo? + /** * Unblock the specified blocked client. * diff --git a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisServerCoroutinesCommandsImpl.kt b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisServerCoroutinesCommandsImpl.kt index 3427baa780..ae0ef25481 100644 --- a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisServerCoroutinesCommandsImpl.kt +++ b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisServerCoroutinesCommandsImpl.kt @@ -21,6 +21,7 @@ package io.lettuce.core.api.coroutines import io.lettuce.core.* +import io.lettuce.core.TrackingInfo import io.lettuce.core.api.reactive.RedisServerReactiveCommands import io.lettuce.core.protocol.CommandType import kotlinx.coroutines.flow.toList @@ -75,6 +76,8 @@ internal class RedisServerCoroutinesCommandsImpl(internal val override suspend fun clientTracking(args: TrackingArgs): String? = ops.clientTracking(args).awaitFirstOrNull() + override suspend fun clientTrackinginfo(): TrackingInfo? = ops.clientTrackinginfo().awaitFirstOrNull() + override suspend fun clientUnblock(id: Long, type: UnblockType): Long? = ops.clientUnblock(id, type).awaitFirstOrNull() override suspend fun command(): List = ops.command().asFlow().toList() diff --git a/src/main/resources/META-INF/LICENSE b/src/main/resources/META-INF/LICENSE index 62589edd12..7e01d712b2 100644 --- a/src/main/resources/META-INF/LICENSE +++ b/src/main/resources/META-INF/LICENSE @@ -1,202 +1,21 @@ - - Apache License - Version 2.0, January 2004 - https://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +MIT License + +Copyright (c) 2023-Present, Redis Ltd. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/src/main/resources/META-INF/NOTICE b/src/main/resources/META-INF/NOTICE deleted file mode 100644 index ffffbcdf64..0000000000 --- a/src/main/resources/META-INF/NOTICE +++ /dev/null @@ -1,11 +0,0 @@ -Lettuce Java Redis Client ${version} -Copyright (c) 2011-2020 Mark Paluch - -This product is licensed to you under the Apache License, Version 2.0 -(the "License"). You may not use this product except in compliance with -the License. - -This product may include a number of subcomponents with separate -copyright notices and license terms. Your use of the source code for -these subcomponents is subject to the terms and conditions of the -subcomponent's license, as noted in the license file. diff --git a/src/main/resources/META-INF/services/io.lettuce.core.json.JsonParser b/src/main/resources/META-INF/services/io.lettuce.core.json.JsonParser new file mode 100644 index 0000000000..f53cf85425 --- /dev/null +++ b/src/main/resources/META-INF/services/io.lettuce.core.json.JsonParser @@ -0,0 +1 @@ +io.lettuce.core.json.DefaultJsonParser \ No newline at end of file diff --git a/src/main/templates/io/lettuce/core/api/RedisHashCommands.java b/src/main/templates/io/lettuce/core/api/RedisHashCommands.java index ac05591741..461563ad6a 100644 --- a/src/main/templates/io/lettuce/core/api/RedisHashCommands.java +++ b/src/main/templates/io/lettuce/core/api/RedisHashCommands.java @@ -19,9 +19,13 @@ */ package io.lettuce.core.api; +import java.time.Duration; +import java.time.Instant; +import java.util.Date; import java.util.List; import java.util.Map; +import io.lettuce.core.ExpireArgs; import io.lettuce.core.KeyScanCursor; import io.lettuce.core.KeyValue; import io.lettuce.core.MapScanCursor; diff --git a/src/main/templates/io/lettuce/core/api/RedisJsonCommands.java b/src/main/templates/io/lettuce/core/api/RedisJsonCommands.java new file mode 100644 index 0000000000..fdc9dc2040 --- /dev/null +++ b/src/main/templates/io/lettuce/core/api/RedisJsonCommands.java @@ -0,0 +1,440 @@ +/* + * Copyright 2017-2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ +package io.lettuce.core.api; + +import io.lettuce.core.json.JsonType; +import io.lettuce.core.json.JsonValue; +import io.lettuce.core.json.arguments.JsonGetArgs; +import io.lettuce.core.json.arguments.JsonMsetArgs; +import io.lettuce.core.json.JsonPath; +import io.lettuce.core.json.arguments.JsonRangeArgs; +import io.lettuce.core.json.arguments.JsonSetArgs; + +import java.util.List; + +/** + * ${intent} for JSON documents + * + * @param Key type. + * @param Value type. + * @author Tihomir Mateev + * @see Redis JSON + * @since 6.5 + */ +public interface RedisJsonCommands { + + /** + * Append the JSON values into the array at a given {@link JsonPath} after the last element in a said array. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param values one or more {@link JsonValue} to be appended. + * @return Long the resulting size of the arrays after the new data was appended, or null if the path does not exist. + * @since 6.5 + */ + List jsonArrappend(K key, JsonPath jsonPath, JsonValue... values); + + /** + * Append the JSON values into the array at the {@link JsonPath#ROOT_PATH} after the last element in a said array. + * + * @param key the key holding the JSON document. + * @param values one or more {@link JsonValue} to be appended. + * @return Long the resulting size of the arrays after the new data was appended, or null if the path does not exist. + * @since 6.5 + */ + List jsonArrappend(K key, JsonValue... values); + + /** + * Search for the first occurrence of a {@link JsonValue} in an array at a given {@link JsonPath} and return its index. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param value the {@link JsonValue} to search for. + * @param range the {@link JsonRangeArgs} to search within. + * @return Long the index hosting the searched element, -1 if not found or null if the specified path is not an array. + * @since 6.5 + */ + List jsonArrindex(K key, JsonPath jsonPath, JsonValue value, JsonRangeArgs range); + + /** + * Search for the first occurrence of a {@link JsonValue} in an array at a given {@link JsonPath} and return its index. This + * method uses defaults for the start and end indexes, see {@link JsonRangeArgs#DEFAULT_START_INDEX} and + * {@link JsonRangeArgs#DEFAULT_END_INDEX}. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param value the {@link JsonValue} to search for. + * @return Long the index hosting the searched element, -1 if not found or null if the specified path is not an array. + * @since 6.5 + */ + List jsonArrindex(K key, JsonPath jsonPath, JsonValue value); + + /** + * Insert the {@link JsonValue}s into the array at a given {@link JsonPath} before the provided index, shifting the existing + * elements to the right + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param index the index before which the new elements will be inserted. + * @param values one or more {@link JsonValue}s to be inserted. + * @return Long the resulting size of the arrays after the new data was inserted, or null if the path does not exist. + * @since 6.5 + */ + List jsonArrinsert(K key, JsonPath jsonPath, int index, JsonValue... values); + + /** + * Report the length of the JSON array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @return the size of the arrays, or null if the path does not exist. + * @since 6.5 + */ + List jsonArrlen(K key, JsonPath jsonPath); + + /** + * Report the length of the JSON array at a the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return the size of the arrays, or null if the path does not exist. + * @since 6.5 + */ + List jsonArrlen(K key); + + /** + * Remove and return {@link JsonValue} at a given index in the array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param index the index of the element to be removed. Default is -1, meaning the last element. Out-of-range indexes round + * to their respective array ends. Popping an empty array returns null. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + List jsonArrpop(K key, JsonPath jsonPath, int index); + + /** + * Remove and return {@link JsonValue} at index -1 (last element) in the array at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + List jsonArrpop(K key, JsonPath jsonPath); + + /** + * Remove and return {@link JsonValue} at index -1 (last element) in the array at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return List the removed element, or null if the specified path is not an array. + * @since 6.5 + */ + List jsonArrpop(K key); + + /** + * Trim an array at a given {@link JsonPath} so that it contains only the specified inclusive range of elements. All + * elements with indexes smaller than the start range and all elements with indexes bigger than the end range are trimmed. + *

+ * Behavior as of RedisJSON v2.0: + *

    + *
  • If start is larger than the array's size or start > stop, returns 0 and an empty array.
  • + *
  • If start is < 0, then start from the end of the array.
  • + *
  • If stop is larger than the end of the array, it is treated like the last element.
  • + *
+ * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the array inside the document. + * @param range the {@link JsonRangeArgs} to trim by. + * @return Long the resulting size of the arrays after the trimming, or null if the path does not exist. + * @since 6.5 + */ + List jsonArrtrim(K key, JsonPath jsonPath, JsonRangeArgs range); + + /** + * Clear container values (arrays/objects) and set numeric values to 0 + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to clear. + * @return Long the number of values removed plus all the matching JSON numerical values that are zeroed. + * @since 6.5 + */ + Long jsonClear(K key, JsonPath jsonPath); + + /** + * Clear container values (arrays/objects) and set numeric values to 0 at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return Long the number of values removed plus all the matching JSON numerical values that are zeroed. + * @since 6.5 + */ + Long jsonClear(K key); + + /** + * Deletes a value inside the JSON document at a given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to clear. + * @return Long the number of values removed (0 or more). + * @since 6.5 + */ + Long jsonDel(K key, JsonPath jsonPath); + + /** + * Deletes a value inside the JSON document at the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return Long the number of values removed (0 or more). + * @since 6.5 + */ + Long jsonDel(K key); + + /** + * Return the value at the specified path in JSON serialized form. + *

+ * When using a single JSONPath, the root of the matching values is a JSON string with a top-level array of serialized JSON + * value. In contrast, a legacy path returns a single value. + *

+ * When using multiple JSONPath arguments, the root of the matching values is a JSON string with a top-level object, with + * each object value being a top-level array of serialized JSON value. In contrast, if all paths are legacy paths, each + * object value is a single serialized JSON value. If there are multiple paths that include both legacy path and JSONPath, + * the returned value conforms to the JSONPath version (an array of values). + * + * @param key the key holding the JSON document. + * @param options the {@link JsonGetArgs} to use. + * @param jsonPaths the {@link JsonPath}s to use to identify the values to get. + * @return JsonValue the value at path in JSON serialized form, or null if the path does not exist. + * @since 6.5 + */ + List jsonGet(K key, JsonGetArgs options, JsonPath... jsonPaths); + + /** + * Return the value at the specified path in JSON serialized form. Uses defaults for the {@link JsonGetArgs}. + *

+ * When using a single JSONPath, the root of the matching values is a JSON string with a top-level array of serialized JSON + * value. In contrast, a legacy path returns a single value. + *

+ * When using multiple JSONPath arguments, the root of the matching values is a JSON string with a top-level object, with + * each object value being a top-level array of serialized JSON value. In contrast, if all paths are legacy paths, each + * object value is a single serialized JSON value. If there are multiple paths that include both legacy path and JSONPath, + * the returned value conforms to the JSONPath version (an array of values). + * + * @param key the key holding the JSON document. + * @param jsonPaths the {@link JsonPath}s to use to identify the values to get. + * @return JsonValue the value at path in JSON serialized form, or null if the path does not exist. + * @since 6.5 + */ + List jsonGet(K key, JsonPath... jsonPaths); + + /** + * Merge a given {@link JsonValue} with the value matching {@link JsonPath}. Consequently, JSON values at matching paths are + * updated, deleted, or expanded with new children. + *

+ * Merging is done according to the following rules per JSON value in the value argument while considering the corresponding + * original value if it exists: + *

    + *
  • merging an existing object key with a null value deletes the key
  • + *
  • merging an existing object key with non-null value updates the value
  • + *
  • merging a non-existing object key adds the key and value
  • + *
  • merging an existing array with any merged value, replaces the entire array with the value
  • + *
+ *

+ * This command complies with RFC7396 "Json Merge Patch" + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to merge. + * @param value the {@link JsonValue} to merge. + * @return String "OK" if the set was successful, error if the operation failed. + * @since 6.5 + * @see RFC7396 + */ + String jsonMerge(K key, JsonPath jsonPath, JsonValue value); + + /** + * Return the values at the specified path from multiple key arguments. + * + * @param jsonPath the {@link JsonPath} pointing to the value to fetch. + * @param keys the keys holding the {@link JsonValue}s to fetch. + * @return List the values at path, or null if the path does not exist. + * @since 6.5 + */ + List jsonMGet(JsonPath jsonPath, K... keys); + + /** + * Set or update one or more JSON values according to the specified {@link JsonMsetArgs} + *

+ * JSON.MSET is atomic, hence, all given additions or updates are either applied or not. It is not possible for clients to + * see that some keys were updated while others are unchanged. + *

+ * A JSON value is a hierarchical structure. If you change a value in a specific path - nested values are affected. + * + * @param arguments the {@link JsonMsetArgs} specifying the values to change. + * @return "OK" if the operation was successful, error otherwise + * @since 6.5 + */ + String jsonMSet(List> arguments); + + /** + * Increment the number value stored at the specified {@link JsonPath} in the JSON document by the provided increment. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value to increment. + * @param number the increment value. + * @return a {@link List} of the new values after the increment. + * @since 6.5 + */ + List jsonNumincrby(K key, JsonPath jsonPath, Number number); + + /** + * Return the keys in the JSON document that are referenced by the given {@link JsonPath} + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) whose key(s) we want. + * @return List the keys in the JSON document that are referenced by the given {@link JsonPath}. + * @since 6.5 + */ + List jsonObjkeys(K key, JsonPath jsonPath); + + /** + * Return the keys in the JSON document that are referenced by the {@link JsonPath#ROOT_PATH} + * + * @param key the key holding the JSON document. + * @return List the keys in the JSON document that are referenced by the given {@link JsonPath}. + * @since 6.5 + */ + List jsonObjkeys(K key); + + /** + * Report the number of keys in the JSON object at the specified {@link JsonPath} and for the provided key + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) whose key(s) we want to count + * @return Long the number of keys in the JSON object at the specified path, or null if the path does not exist. + * @since 6.5 + */ + List jsonObjlen(K key, JsonPath jsonPath); + + /** + * Report the number of keys in the JSON object at the {@link JsonPath#ROOT_PATH} and for the provided key + * + * @param key the key holding the JSON document. + * @return Long the number of keys in the JSON object at the specified path, or null if the path does not exist. + * @since 6.5 + */ + List jsonObjlen(K key); + + /** + * Sets the JSON value at a given {@link JsonPath} in the JSON document. + *

+ * For new Redis keys, the path must be the root. For existing keys, when the entire path exists, the value that it contains + * is replaced with the JSON value. For existing keys, when the path exists, except for the last element, a new child is + * added with the JSON value. + *

+ * Adds a key (with its respective value) to a JSON Object (in a RedisJSON data type key) only if it is the last child in + * the path, or it is the parent of a new child being added in the path. Optional arguments NX and XX modify this behavior + * for both new RedisJSON data type keys and the JSON Object keys in them. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to set the value. + * @param value the {@link JsonValue} to set. + * @param options the {@link JsonSetArgs} the options for setting the value. + * @return String "OK" if the set was successful, null if the {@link JsonSetArgs} conditions are not met. + * @since 6.5 + */ + String jsonSet(K key, JsonPath jsonPath, JsonValue value, JsonSetArgs options); + + /** + * Sets the JSON value at a given {@link JsonPath} in the JSON document using defaults for the {@link JsonSetArgs}. + *

+ * For new Redis keys the path must be the root. For existing keys, when the entire path exists, the value that it contains + * is replaced with the JSON value. For existing keys, when the path exists, except for the last element, a new child is + * added with the JSON value. + *

+ * Adds a key (with its respective value) to a JSON Object (in a RedisJSON data type key) only if it is the last child in + * the path, or it is the parent of a new child being added in the path. Optional arguments NX and XX modify this behavior + * for both new RedisJSON data type keys and the JSON Object keys in them. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to set the value. + * @param value the {@link JsonValue} to set. + * @return String "OK" if the set was successful, null if the {@link JsonSetArgs} conditions are not met. + * @since 6.5 + */ + String jsonSet(K key, JsonPath jsonPath, JsonValue value); + + /** + * Append the json-string values to the string at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s) where we want to append the value. + * @param value the {@link JsonValue} to append. + * @return Long the new length of the string, or null if the matching JSON value is not a string. + * @since 6.5 + */ + List jsonStrappend(K key, JsonPath jsonPath, JsonValue value); + + /** + * Append the json-string values to the string at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @param value the {@link JsonValue} to append. + * @return Long the new length of the string, or null if the matching JSON value is not a string. + * @since 6.5 + */ + List jsonStrappend(K key, JsonValue value); + + /** + * Report the length of the JSON String at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return Long (in recursive descent) the length of the JSON String at the provided {@link JsonPath}, or null if the value + * ath the desired path is not a string. + * @since 6.5 + */ + List jsonStrlen(K key, JsonPath jsonPath); + + /** + * Report the length of the JSON String at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @return Long (in recursive descent) the length of the JSON String at the provided {@link JsonPath}, or null if the value + * ath the desired path is not a string. + * @since 6.5 + */ + List jsonStrlen(K key); + + /** + * Toggle a Boolean value stored at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return List the new value after the toggle, 0 for false, 1 for true or null if the path does not exist. + * @since 6.5 + */ + List jsonToggle(K key, JsonPath jsonPath); + + /** + * Report the type of JSON value at the provided {@link JsonPath} in the JSON document. + * + * @param key the key holding the JSON document. + * @param jsonPath the {@link JsonPath} pointing to the value(s). + * @return List the type of JSON value at the provided {@link JsonPath} + * @since 6.5 + */ + List jsonType(K key, JsonPath jsonPath); + + /** + * Report the type of JSON value at the {@link JsonPath#ROOT_PATH} in the JSON document. + * + * @param key the key holding the JSON document. + * @return List the type of JSON value at the provided {@link JsonPath} + * @since 6.5 + */ + List jsonType(K key); + +} diff --git a/src/main/templates/io/lettuce/core/api/RedisKeyCommands.java b/src/main/templates/io/lettuce/core/api/RedisKeyCommands.java index 602b0442ad..0357998dbb 100644 --- a/src/main/templates/io/lettuce/core/api/RedisKeyCommands.java +++ b/src/main/templates/io/lettuce/core/api/RedisKeyCommands.java @@ -24,9 +24,7 @@ import java.util.Date; import java.util.List; -import io.lettuce.core.ExpireArgs; -import io.lettuce.core.KeyScanArgs; -import io.lettuce.core.RestoreArgs; +import io.lettuce.core.*; import io.lettuce.core.output.KeyStreamingChannel; import io.lettuce.core.output.ValueStreamingChannel; diff --git a/src/main/templates/io/lettuce/core/api/RedisServerCommands.java b/src/main/templates/io/lettuce/core/api/RedisServerCommands.java index 6775494a61..486443173a 100644 --- a/src/main/templates/io/lettuce/core/api/RedisServerCommands.java +++ b/src/main/templates/io/lettuce/core/api/RedisServerCommands.java @@ -29,6 +29,7 @@ import io.lettuce.core.ShutdownArgs; import io.lettuce.core.TrackingArgs; import io.lettuce.core.UnblockType; +import io.lettuce.core.TrackingInfo; import io.lettuce.core.protocol.CommandType; /** @@ -175,6 +176,14 @@ public interface RedisServerCommands { */ String clientTracking(TrackingArgs args); + /** + * Returns information about the current client connection's use of the server assisted client side caching feature. + * + * @return {@link TrackingInfo}, for more information check the documentation + * @since 6.5 + */ + TrackingInfo clientTrackinginfo(); + /** * Unblock the specified blocked client. * diff --git a/src/main/templates/io/lettuce/core/api/RedisStreamCommands.java b/src/main/templates/io/lettuce/core/api/RedisStreamCommands.java index 232f1f618c..954216a544 100644 --- a/src/main/templates/io/lettuce/core/api/RedisStreamCommands.java +++ b/src/main/templates/io/lettuce/core/api/RedisStreamCommands.java @@ -22,15 +22,9 @@ import java.util.List; import java.util.Map; -import io.lettuce.core.Consumer; -import io.lettuce.core.Limit; -import io.lettuce.core.Range; -import io.lettuce.core.StreamMessage; -import io.lettuce.core.XAddArgs; -import io.lettuce.core.XGroupCreateArgs; -import io.lettuce.core.XReadArgs; -import io.lettuce.core.XClaimArgs; +import io.lettuce.core.*; import io.lettuce.core.XReadArgs.StreamOffset; +import io.lettuce.core.models.stream.ClaimedMessages; import io.lettuce.core.models.stream.PendingMessage; import io.lettuce.core.models.stream.PendingMessages; diff --git a/src/test/java/biz/paluch/redis/extensibility/MyExtendedRedisClusterClient.java b/src/test/java/biz/paluch/redis/extensibility/MyExtendedRedisClusterClient.java index e97e5d823e..1ac2848077 100644 --- a/src/test/java/biz/paluch/redis/extensibility/MyExtendedRedisClusterClient.java +++ b/src/test/java/biz/paluch/redis/extensibility/MyExtendedRedisClusterClient.java @@ -29,7 +29,9 @@ import io.lettuce.core.cluster.RedisClusterClient; import io.lettuce.core.cluster.StatefulRedisClusterConnectionImpl; import io.lettuce.core.codec.RedisCodec; +import io.lettuce.core.json.JsonParser; import io.lettuce.core.resource.ClientResources; +import reactor.core.publisher.Mono; /** * Demo code for extending a RedisClusterClient. @@ -48,8 +50,9 @@ public MyExtendedRedisClusterClient() { @Override protected StatefulRedisClusterConnectionImpl newStatefulRedisClusterConnection( - RedisChannelWriter channelWriter, ClusterPushHandler pushHandler, RedisCodec codec, Duration timeout) { - return new MyRedisClusterConnection<>(channelWriter, pushHandler, codec, timeout); + RedisChannelWriter channelWriter, ClusterPushHandler pushHandler, RedisCodec codec, Duration timeout, + Mono parser) { + return new MyRedisClusterConnection<>(channelWriter, pushHandler, codec, timeout, parser); } } diff --git a/src/test/java/biz/paluch/redis/extensibility/MyRedisClusterConnection.java b/src/test/java/biz/paluch/redis/extensibility/MyRedisClusterConnection.java index 18e8feacf8..04632e463c 100644 --- a/src/test/java/biz/paluch/redis/extensibility/MyRedisClusterConnection.java +++ b/src/test/java/biz/paluch/redis/extensibility/MyRedisClusterConnection.java @@ -25,6 +25,8 @@ import io.lettuce.core.cluster.ClusterPushHandler; import io.lettuce.core.cluster.StatefulRedisClusterConnectionImpl; import io.lettuce.core.codec.RedisCodec; +import io.lettuce.core.json.JsonParser; +import reactor.core.publisher.Mono; /** * Demo code for extending a @{@link StatefulRedisClusterConnectionImpl} @@ -35,8 +37,8 @@ class MyRedisClusterConnection extends StatefulRedisClusterConnectionImpl { public MyRedisClusterConnection(RedisChannelWriter writer, ClusterPushHandler pushHandler, RedisCodec codec, - Duration timeout) { - super(writer, pushHandler, codec, timeout); + Duration timeout, Mono parser) { + super(writer, pushHandler, codec, timeout, parser); } } diff --git a/src/test/java/io/lettuce/TestTags.java b/src/test/java/io/lettuce/TestTags.java new file mode 100644 index 0000000000..68a3434e02 --- /dev/null +++ b/src/test/java/io/lettuce/TestTags.java @@ -0,0 +1,32 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce; + +/** + * Test tags for the different types of tests. + */ +public class TestTags { + + /** + * Tag for unit tests (run in isolation) + */ + public static final String UNIT_TEST = "unit"; + + /** + * Tag for integration tests (require a running environment) + */ + public static final String INTEGRATION_TEST = "integration"; + + /** + * Tag for tests that generate the different types of APIs. + * + * @see io.lettuce.apigenerator + */ + public static final String API_GENERATOR = "api_generator"; + +} diff --git a/src/test/java/io/lettuce/apigenerator/Constants.java b/src/test/java/io/lettuce/apigenerator/Constants.java index 2779ed17aa..896b939951 100644 --- a/src/test/java/io/lettuce/apigenerator/Constants.java +++ b/src/test/java/io/lettuce/apigenerator/Constants.java @@ -29,7 +29,8 @@ class Constants { public static final String[] TEMPLATE_NAMES = { "BaseRedisCommands", "RedisAclCommands", "RedisFunctionCommands", "RedisGeoCommands", "RedisHashCommands", "RedisHLLCommands", "RedisKeyCommands", "RedisListCommands", "RedisScriptingCommands", "RedisSentinelCommands", "RedisServerCommands", "RedisSetCommands", - "RedisSortedSetCommands", "RedisStreamCommands", "RedisStringCommands", "RedisTransactionalCommands" }; + "RedisSortedSetCommands", "RedisStreamCommands", "RedisStringCommands", "RedisTransactionalCommands", + "RedisJsonCommands" }; public static final File TEMPLATES = new File("src/main/templates"); diff --git a/src/test/java/io/lettuce/apigenerator/CreateAsyncApi.java b/src/test/java/io/lettuce/apigenerator/CreateAsyncApi.java index 8006578bd7..a6bd40f8d2 100644 --- a/src/test/java/io/lettuce/apigenerator/CreateAsyncApi.java +++ b/src/test/java/io/lettuce/apigenerator/CreateAsyncApi.java @@ -27,6 +27,7 @@ import java.util.function.Function; import java.util.function.Supplier; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -35,6 +36,8 @@ import io.lettuce.core.internal.LettuceSets; +import static io.lettuce.TestTags.API_GENERATOR; + /** * Create async API based on the templates. * @@ -76,6 +79,7 @@ Supplier> importSupplier() { @ParameterizedTest @MethodSource("arguments") + @Tag(API_GENERATOR) void createInterface(String argument) throws Exception { createFactory(argument).createInterface(); } diff --git a/src/test/java/io/lettuce/apigenerator/CreateAsyncNodeSelectionClusterApi.java b/src/test/java/io/lettuce/apigenerator/CreateAsyncNodeSelectionClusterApi.java index 7fc1a4f516..577c1b8f59 100644 --- a/src/test/java/io/lettuce/apigenerator/CreateAsyncNodeSelectionClusterApi.java +++ b/src/test/java/io/lettuce/apigenerator/CreateAsyncNodeSelectionClusterApi.java @@ -29,6 +29,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -37,6 +38,8 @@ import io.lettuce.core.internal.LettuceSets; +import static io.lettuce.TestTags.API_GENERATOR; + /** * Create async API based on the templates. * @@ -91,6 +94,7 @@ Supplier> importSupplier() { @ParameterizedTest @MethodSource("arguments") + @Tag(API_GENERATOR) void createInterface(String argument) throws Exception { createFactory(argument).createInterface(); } diff --git a/src/test/java/io/lettuce/apigenerator/CreateKotlinCoroutinesApi.java b/src/test/java/io/lettuce/apigenerator/CreateKotlinCoroutinesApi.java index ce38436e25..8007474b64 100644 --- a/src/test/java/io/lettuce/apigenerator/CreateKotlinCoroutinesApi.java +++ b/src/test/java/io/lettuce/apigenerator/CreateKotlinCoroutinesApi.java @@ -19,6 +19,7 @@ */ package io.lettuce.apigenerator; +import static io.lettuce.TestTags.API_GENERATOR; import static io.lettuce.apigenerator.Constants.KOTLIN_SOURCES; import static io.lettuce.apigenerator.Constants.TEMPLATES; @@ -28,6 +29,7 @@ import java.util.function.Function; import java.util.function.Supplier; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -59,6 +61,7 @@ Function commentInjector() { @ParameterizedTest @MethodSource("arguments") + @Tag(API_GENERATOR) void createInterface(String argument) throws Exception { createFactory(argument).create(); } diff --git a/src/test/java/io/lettuce/apigenerator/CreateReactiveApi.java b/src/test/java/io/lettuce/apigenerator/CreateReactiveApi.java index d66872e4bc..21097102be 100644 --- a/src/test/java/io/lettuce/apigenerator/CreateReactiveApi.java +++ b/src/test/java/io/lettuce/apigenerator/CreateReactiveApi.java @@ -30,6 +30,7 @@ import java.util.function.Function; import java.util.function.Supplier; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -44,6 +45,8 @@ import io.lettuce.core.internal.LettuceSets; +import static io.lettuce.TestTags.API_GENERATOR; + /** * Create reactive API based on the templates. * @@ -193,6 +196,7 @@ Supplier> importSupplier() { @ParameterizedTest @MethodSource("arguments") + @Tag(API_GENERATOR) void createInterface(String argument) throws Exception { createFactory(argument).createInterface(); } diff --git a/src/test/java/io/lettuce/apigenerator/CreateSyncApi.java b/src/test/java/io/lettuce/apigenerator/CreateSyncApi.java index 697bf5dce7..94210bd71b 100644 --- a/src/test/java/io/lettuce/apigenerator/CreateSyncApi.java +++ b/src/test/java/io/lettuce/apigenerator/CreateSyncApi.java @@ -28,6 +28,7 @@ import java.util.function.Predicate; import java.util.function.Supplier; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -36,6 +37,8 @@ import io.lettuce.core.internal.LettuceSets; +import static io.lettuce.TestTags.API_GENERATOR; + /** * Create sync API based on the templates. * @@ -84,6 +87,7 @@ Supplier> importSupplier() { @ParameterizedTest @MethodSource("arguments") + @Tag(API_GENERATOR) void createInterface(String argument) throws Exception { createFactory(argument).createInterface(); } diff --git a/src/test/java/io/lettuce/apigenerator/CreateSyncNodeSelectionClusterApi.java b/src/test/java/io/lettuce/apigenerator/CreateSyncNodeSelectionClusterApi.java index 9813912cc2..6f051d2fc3 100644 --- a/src/test/java/io/lettuce/apigenerator/CreateSyncNodeSelectionClusterApi.java +++ b/src/test/java/io/lettuce/apigenerator/CreateSyncNodeSelectionClusterApi.java @@ -29,6 +29,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -38,6 +39,9 @@ import io.lettuce.core.internal.LettuceSets; +import static io.lettuce.TestTags.API_GENERATOR; +import static io.lettuce.TestTags.UNIT_TEST; + /** * Create sync API based on the templates. * @@ -97,6 +101,7 @@ Supplier> importSupplier() { @ParameterizedTest @MethodSource("arguments") + @Tag(API_GENERATOR) void createInterface(String argument) throws Exception { createFactory(argument).createInterface(); } diff --git a/src/test/java/io/lettuce/codec/CRC16UnitTests.java b/src/test/java/io/lettuce/codec/CRC16UnitTests.java index 5f8ae13c22..693ef585db 100644 --- a/src/test/java/io/lettuce/codec/CRC16UnitTests.java +++ b/src/test/java/io/lettuce/codec/CRC16UnitTests.java @@ -1,10 +1,12 @@ package io.lettuce.codec; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.util.ArrayList; import java.util.List; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -13,6 +15,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class CRC16UnitTests { static List parameters() { diff --git a/src/test/java/io/lettuce/core/AclSetuserArgsUnitTests.java b/src/test/java/io/lettuce/core/AclSetuserArgsUnitTests.java index aed35cd717..79f5685050 100644 --- a/src/test/java/io/lettuce/core/AclSetuserArgsUnitTests.java +++ b/src/test/java/io/lettuce/core/AclSetuserArgsUnitTests.java @@ -19,8 +19,10 @@ */ package io.lettuce.core; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.codec.StringCodec; @@ -33,6 +35,7 @@ * @author Mark Paluch * @author Rohan Nagar */ +@Tag(UNIT_TEST) class AclSetuserArgsUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/AsyncConnectionIntegrationTests.java b/src/test/java/io/lettuce/core/AsyncConnectionIntegrationTests.java index 35e23a7b52..3be2c9200b 100644 --- a/src/test/java/io/lettuce/core/AsyncConnectionIntegrationTests.java +++ b/src/test/java/io/lettuce/core/AsyncConnectionIntegrationTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.time.Duration; @@ -29,6 +30,7 @@ import javax.inject.Inject; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -43,6 +45,7 @@ * @author Will Glozer * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class AsyncConnectionIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/AuthenticationIntegrationTests.java b/src/test/java/io/lettuce/core/AuthenticationIntegrationTests.java index 8543dbeb9f..864a2103b0 100644 --- a/src/test/java/io/lettuce/core/AuthenticationIntegrationTests.java +++ b/src/test/java/io/lettuce/core/AuthenticationIntegrationTests.java @@ -1,10 +1,12 @@ package io.lettuce.core; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import javax.inject.Inject; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -24,6 +26,7 @@ * * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) @EnabledOnCommand("ACL") class AuthenticationIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/ClientIntegrationTests.java b/src/test/java/io/lettuce/core/ClientIntegrationTests.java index 9309c17e65..d2694a1fba 100644 --- a/src/test/java/io/lettuce/core/ClientIntegrationTests.java +++ b/src/test/java/io/lettuce/core/ClientIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import java.net.SocketAddress; @@ -10,6 +11,7 @@ import javax.enterprise.inject.New; import javax.inject.Inject; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -28,6 +30,7 @@ * @author Will Glozer * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class ClientIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/ClientMetricsIntegrationTests.java b/src/test/java/io/lettuce/core/ClientMetricsIntegrationTests.java index 55a9fd26f1..ab5b849cd3 100644 --- a/src/test/java/io/lettuce/core/ClientMetricsIntegrationTests.java +++ b/src/test/java/io/lettuce/core/ClientMetricsIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.util.Collection; @@ -7,6 +8,7 @@ import javax.inject.Inject; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import io.lettuce.test.ReflectionTestUtils; @@ -23,6 +25,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class ClientMetricsIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/ClientOptionsIntegrationTests.java b/src/test/java/io/lettuce/core/ClientOptionsIntegrationTests.java index 347ec18e92..4e960caeea 100644 --- a/src/test/java/io/lettuce/core/ClientOptionsIntegrationTests.java +++ b/src/test/java/io/lettuce/core/ClientOptionsIntegrationTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static io.lettuce.test.ConnectionTestUtil.getChannel; import static io.lettuce.test.ConnectionTestUtil.getConnectionWatchdog; import static io.lettuce.test.ConnectionTestUtil.getStack; @@ -36,6 +37,7 @@ import javax.inject.Inject; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -60,6 +62,7 @@ * * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class ClientOptionsIntegrationTests extends TestSupport { @@ -92,7 +95,8 @@ void variousClientOptions() { @Test void requestQueueSize() { - client.setOptions(ClientOptions.builder().requestQueueSize(10).build()); + client.setOptions(ClientOptions.builder().requestQueueSize(10) + .timeoutOptions(TimeoutOptions.builder().timeoutCommands(false).build()).build()); StatefulRedisConnection connection = client.connect(); getConnectionWatchdog(connection).setListenOnChannelInactive(false); @@ -115,7 +119,8 @@ void requestQueueSize() { @Test void requestQueueSizeAppliedForReconnect() { - client.setOptions(ClientOptions.builder().requestQueueSize(10).build()); + client.setOptions(ClientOptions.builder().requestQueueSize(10) + .timeoutOptions(TimeoutOptions.builder().timeoutCommands(false).build()).build()); RedisAsyncCommands connection = client.connect().async(); testHitRequestQueueLimit(connection); @@ -127,7 +132,7 @@ void testHitRequestQueueLimitReconnectWithAuthCommand() { WithPassword.run(client, () -> { client.setOptions(ClientOptions.builder().protocolVersion(ProtocolVersion.RESP2).pingBeforeActivateConnection(false) - .requestQueueSize(10).build()); + .requestQueueSize(10).timeoutOptions(TimeoutOptions.builder().timeoutCommands(false).build()).build()); RedisAsyncCommands connection = client.connect().async(); connection.auth(passwd); @@ -142,7 +147,7 @@ void testHitRequestQueueLimitReconnectWithAuthUsernamePasswordCommand() { WithPassword.run(client, () -> { client.setOptions(ClientOptions.builder().protocolVersion(ProtocolVersion.RESP2).pingBeforeActivateConnection(false) - .requestQueueSize(10).build()); + .requestQueueSize(10).timeoutOptions(TimeoutOptions.builder().timeoutCommands(false).build()).build()); RedisAsyncCommands connection = client.connect().async(); connection.auth(username, passwd); @@ -154,7 +159,9 @@ void testHitRequestQueueLimitReconnectWithAuthUsernamePasswordCommand() { void testHitRequestQueueLimitReconnectWithUriAuth() { WithPassword.run(client, () -> { - client.setOptions(ClientOptions.builder().requestQueueSize(10).build()); + client.setOptions(ClientOptions.builder().requestQueueSize(10) + .timeoutOptions(TimeoutOptions.builder().timeoutCommands(false).build()).build()); + ; RedisURI redisURI = RedisURI.create(host, port); redisURI.setPassword(passwd); @@ -169,7 +176,8 @@ void testHitRequestQueueLimitReconnectWithUriAuthPingCommand() { WithPassword.run(client, () -> { - client.setOptions(ClientOptions.builder().requestQueueSize(10).build()); + client.setOptions(ClientOptions.builder().requestQueueSize(10) + .timeoutOptions(TimeoutOptions.builder().timeoutCommands(false).build()).build()); RedisURI redisURI = RedisURI.create(host, port); redisURI.setPassword(passwd); @@ -207,7 +215,8 @@ private void testHitRequestQueueLimit(RedisAsyncCommands connect @Test void requestQueueSizeOvercommittedReconnect() { - client.setOptions(ClientOptions.builder().requestQueueSize(10).build()); + client.setOptions(ClientOptions.builder().requestQueueSize(10) + .timeoutOptions(TimeoutOptions.builder().timeoutCommands(false).build()).build()); StatefulRedisConnection connection = client.connect(); ConnectionWatchdog watchdog = getConnectionWatchdog(connection); @@ -262,8 +271,8 @@ void disconnectedWithoutReconnect() { @Test void disconnectedRejectCommands() { - client.setOptions( - ClientOptions.builder().disconnectedBehavior(ClientOptions.DisconnectedBehavior.REJECT_COMMANDS).build()); + client.setOptions(ClientOptions.builder().disconnectedBehavior(ClientOptions.DisconnectedBehavior.REJECT_COMMANDS) + .timeoutOptions(TimeoutOptions.builder().timeoutCommands(false).build()).build()); RedisAsyncCommands connection = client.connect().async(); @@ -471,7 +480,8 @@ void timeoutExpiresBatchedCommands() { @Test void pingBeforeConnectWithQueuedCommandsAndReconnect() throws Exception { - + client.setOptions( + ClientOptions.builder().timeoutOptions(TimeoutOptions.builder().timeoutCommands(false).build()).build()); StatefulRedisConnection controlConnection = client.connect(); StatefulRedisConnection redisConnection = client.connect(RedisURI.create("redis://localhost:6479/5")); @@ -513,6 +523,8 @@ void authenticatedPingBeforeConnectWithQueuedCommandsAndReconnect() { WithPassword.run(client, () -> { RedisURI redisURI = RedisURI.Builder.redis(host, port).withPassword(passwd).withDatabase(5).build(); + client.setOptions( + ClientOptions.builder().timeoutOptions(TimeoutOptions.builder().timeoutCommands(false).build()).build()); StatefulRedisConnection controlConnection = client.connect(redisURI); StatefulRedisConnection redisConnection = client.connect(redisURI); diff --git a/src/test/java/io/lettuce/core/ClientOptionsUnitTests.java b/src/test/java/io/lettuce/core/ClientOptionsUnitTests.java index e7a29c6bb9..bb2d0f91a0 100644 --- a/src/test/java/io/lettuce/core/ClientOptionsUnitTests.java +++ b/src/test/java/io/lettuce/core/ClientOptionsUnitTests.java @@ -1,20 +1,30 @@ package io.lettuce.core; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; +import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import io.lettuce.core.json.DefaultJsonParser; +import io.lettuce.core.json.JsonArray; +import io.lettuce.core.json.JsonObject; +import io.lettuce.core.json.JsonParser; +import io.lettuce.core.json.JsonValue; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.protocol.Command; import io.lettuce.core.protocol.CommandType; import io.lettuce.core.protocol.ProtocolVersion; +import reactor.core.publisher.Mono; /** * Unit tests for {@link ClientOptions}. * * @author Mark Paluch */ +@Tag(UNIT_TEST) class ClientOptionsUnitTests { @Test @@ -30,6 +40,7 @@ void testDefault() { assertThat(options.getReadOnlyCommands().isReadOnly(new Command<>(CommandType.SET, null))).isFalse(); assertThat(options.getReadOnlyCommands().isReadOnly(new Command<>(CommandType.PUBLISH, null))).isFalse(); assertThat(options.getReadOnlyCommands().isReadOnly(new Command<>(CommandType.GET, null))).isTrue(); + assertThat(options.getJsonParser().block()).isInstanceOf(DefaultJsonParser.class); } @Test @@ -52,6 +63,47 @@ void testCopy() { assertThat(original.mutate()).isNotSameAs(copy.mutate()); } + @Test + void jsonParser() { + JsonParser parser = new CustomJsonParser(); + ClientOptions options = ClientOptions.builder().jsonParser(Mono.justOrEmpty(parser)).build(); + assertThat(options.getJsonParser().block()).isInstanceOf(CustomJsonParser.class); + } + + static class CustomJsonParser implements JsonParser { + + @Override + public JsonValue loadJsonValue(ByteBuffer buffer) { + return null; + } + + @Override + public JsonValue createJsonValue(ByteBuffer bytes) { + return null; + } + + @Override + public JsonValue createJsonValue(String value) { + return null; + } + + @Override + public JsonObject createJsonObject() { + return null; + } + + @Override + public JsonArray createJsonArray() { + return null; + } + + @Override + public JsonValue fromObject(Object object) { + return null; + } + + } + void checkAssertions(ClientOptions sut) { assertThat(sut.isAutoReconnect()).isTrue(); assertThat(sut.isCancelCommandsOnReconnectFailure()).isFalse(); diff --git a/src/test/java/io/lettuce/core/CommandListenerIntegrationTests.java b/src/test/java/io/lettuce/core/CommandListenerIntegrationTests.java index 93037b91c4..34cfd3a39e 100644 --- a/src/test/java/io/lettuce/core/CommandListenerIntegrationTests.java +++ b/src/test/java/io/lettuce/core/CommandListenerIntegrationTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import java.util.ArrayList; @@ -27,6 +28,7 @@ import javax.inject.Inject; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -43,6 +45,7 @@ * @author Mikhael Sokolov * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) @SuppressWarnings({ "rawtypes", "unchecked" }) public class CommandListenerIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/ConnectMethodsIntegrationTests.java b/src/test/java/io/lettuce/core/ConnectMethodsIntegrationTests.java index a5879a4758..5eab339557 100644 --- a/src/test/java/io/lettuce/core/ConnectMethodsIntegrationTests.java +++ b/src/test/java/io/lettuce/core/ConnectMethodsIntegrationTests.java @@ -2,6 +2,7 @@ import javax.inject.Inject; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -10,9 +11,12 @@ import io.lettuce.core.cluster.api.async.AsyncNodeSelection; import io.lettuce.test.LettuceExtension; +import static io.lettuce.TestTags.INTEGRATION_TEST; + /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class ConnectMethodsIntegrationTests { diff --git a/src/test/java/io/lettuce/core/ConnectionCommandIntegrationTests.java b/src/test/java/io/lettuce/core/ConnectionCommandIntegrationTests.java index 65e9ec1a6f..8f8b906fce 100644 --- a/src/test/java/io/lettuce/core/ConnectionCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/ConnectionCommandIntegrationTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import java.time.Duration; @@ -30,6 +31,7 @@ import javax.inject.Inject; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -53,6 +55,7 @@ * @author Mark Paluch * @author Tugdual Grall */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class ConnectionCommandIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/ConnectionFutureUnitTests.java b/src/test/java/io/lettuce/core/ConnectionFutureUnitTests.java index 68f434ac77..562f6fc51b 100644 --- a/src/test/java/io/lettuce/core/ConnectionFutureUnitTests.java +++ b/src/test/java/io/lettuce/core/ConnectionFutureUnitTests.java @@ -1,11 +1,13 @@ package io.lettuce.core; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.internal.Futures; @@ -13,6 +15,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class ConnectionFutureUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/CopyArgsUnitTests.java b/src/test/java/io/lettuce/core/CopyArgsUnitTests.java index 4b5170b903..f4fd2fab87 100644 --- a/src/test/java/io/lettuce/core/CopyArgsUnitTests.java +++ b/src/test/java/io/lettuce/core/CopyArgsUnitTests.java @@ -2,8 +2,10 @@ import io.lettuce.core.codec.StringCodec; import io.lettuce.core.protocol.CommandArgs; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; /** @@ -11,6 +13,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class CopyArgsUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/CustomCodecIntegrationTests.java b/src/test/java/io/lettuce/core/CustomCodecIntegrationTests.java index 94fa64205d..831328d215 100644 --- a/src/test/java/io/lettuce/core/CustomCodecIntegrationTests.java +++ b/src/test/java/io/lettuce/core/CustomCodecIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.io.*; @@ -13,6 +14,7 @@ import javax.crypto.spec.SecretKeySpec; import javax.inject.Inject; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -26,6 +28,7 @@ * @author Will Glozer * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class CustomCodecIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/ExceptionFactoryUnitTests.java b/src/test/java/io/lettuce/core/ExceptionFactoryUnitTests.java index 41af0392f5..43b31198b5 100644 --- a/src/test/java/io/lettuce/core/ExceptionFactoryUnitTests.java +++ b/src/test/java/io/lettuce/core/ExceptionFactoryUnitTests.java @@ -19,10 +19,12 @@ */ package io.lettuce.core; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import java.time.Duration; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.internal.ExceptionFactory; @@ -33,6 +35,7 @@ * @author Mark Paluch * @author Tobias Nehrlich */ +@Tag(UNIT_TEST) class ExceptionFactoryUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/GeoModelUnitTests.java b/src/test/java/io/lettuce/core/GeoModelUnitTests.java index 35e75bd2cf..b4c6437b7c 100644 --- a/src/test/java/io/lettuce/core/GeoModelUnitTests.java +++ b/src/test/java/io/lettuce/core/GeoModelUnitTests.java @@ -1,15 +1,18 @@ package io.lettuce.core; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.util.Collections; import java.util.Map; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class GeoModelUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/GeoValueUnitTests.java b/src/test/java/io/lettuce/core/GeoValueUnitTests.java index 6688b11b08..d3813afa14 100644 --- a/src/test/java/io/lettuce/core/GeoValueUnitTests.java +++ b/src/test/java/io/lettuce/core/GeoValueUnitTests.java @@ -1,9 +1,11 @@ package io.lettuce.core; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import java.util.Optional; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; /** @@ -11,6 +13,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class GeoValueUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/KeepAliveOptionsUnitTests.java b/src/test/java/io/lettuce/core/KeepAliveOptionsUnitTests.java index dfde78d0d5..a80aed6c0f 100644 --- a/src/test/java/io/lettuce/core/KeepAliveOptionsUnitTests.java +++ b/src/test/java/io/lettuce/core/KeepAliveOptionsUnitTests.java @@ -19,10 +19,12 @@ */ package io.lettuce.core; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import java.time.Duration; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.SocketOptions.KeepAliveOptions; @@ -32,6 +34,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class KeepAliveOptionsUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/KeyValueUnitTests.java b/src/test/java/io/lettuce/core/KeyValueUnitTests.java index 08191831d7..4dd357477a 100644 --- a/src/test/java/io/lettuce/core/KeyValueUnitTests.java +++ b/src/test/java/io/lettuce/core/KeyValueUnitTests.java @@ -16,18 +16,21 @@ */ package io.lettuce.core; +import static io.lettuce.TestTags.UNIT_TEST; import static io.lettuce.core.Value.just; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.Optional; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; /** * @author Will Glozer * @author Mark Paluch */ +@Tag(UNIT_TEST) class KeyValueUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/LimitUnitTests.java b/src/test/java/io/lettuce/core/LimitUnitTests.java index d4d6490e3f..223ef5464b 100644 --- a/src/test/java/io/lettuce/core/LimitUnitTests.java +++ b/src/test/java/io/lettuce/core/LimitUnitTests.java @@ -1,12 +1,15 @@ package io.lettuce.core; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class LimitUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/PipeliningIntegrationTests.java b/src/test/java/io/lettuce/core/PipeliningIntegrationTests.java index b0ba4a3544..90aa5f1182 100644 --- a/src/test/java/io/lettuce/core/PipeliningIntegrationTests.java +++ b/src/test/java/io/lettuce/core/PipeliningIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.util.ArrayList; @@ -9,6 +10,7 @@ import javax.inject.Inject; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; @@ -24,6 +26,7 @@ @SuppressWarnings("rawtypes") @ExtendWith(LettuceExtension.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) +@Tag(INTEGRATION_TEST) class PipeliningIntegrationTests extends TestSupport { private final RedisClient client; diff --git a/src/test/java/io/lettuce/core/ProtectedModeTests.java b/src/test/java/io/lettuce/core/ProtectedModeIntegrationTests.java similarity index 97% rename from src/test/java/io/lettuce/core/ProtectedModeTests.java rename to src/test/java/io/lettuce/core/ProtectedModeIntegrationTests.java index 490e6d99d7..671ad6047c 100644 --- a/src/test/java/io/lettuce/core/ProtectedModeTests.java +++ b/src/test/java/io/lettuce/core/ProtectedModeIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -9,6 +10,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.api.StatefulRedisConnection; @@ -24,7 +26,8 @@ /** * @author Mark Paluch */ -class ProtectedModeTests { +@Tag(INTEGRATION_TEST) +class ProtectedModeIntegrationTests { private static MockTcpServer server; diff --git a/src/test/java/io/lettuce/core/RangeUnitTests.java b/src/test/java/io/lettuce/core/RangeUnitTests.java index 5341c65ba4..35854b6a14 100644 --- a/src/test/java/io/lettuce/core/RangeUnitTests.java +++ b/src/test/java/io/lettuce/core/RangeUnitTests.java @@ -1,9 +1,11 @@ package io.lettuce.core; +import static io.lettuce.TestTags.UNIT_TEST; import static io.lettuce.core.Range.Boundary.excluding; import static io.lettuce.core.Range.Boundary.including; import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; /** @@ -11,6 +13,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class RangeUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/ReactiveBackpressurePropagationUnitTests.java b/src/test/java/io/lettuce/core/ReactiveBackpressurePropagationUnitTests.java index 2a1e701fb1..96a531f921 100644 --- a/src/test/java/io/lettuce/core/ReactiveBackpressurePropagationUnitTests.java +++ b/src/test/java/io/lettuce/core/ReactiveBackpressurePropagationUnitTests.java @@ -1,5 +1,6 @@ package io.lettuce.core; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @@ -8,6 +9,7 @@ import java.util.concurrent.CountDownLatch; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -35,6 +37,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) @ExtendWith(MockitoExtension.class) class ReactiveBackpressurePropagationUnitTests { diff --git a/src/test/java/io/lettuce/core/ReactiveConnectionIntegrationTests.java b/src/test/java/io/lettuce/core/ReactiveConnectionIntegrationTests.java index 6b26c79295..940a181732 100644 --- a/src/test/java/io/lettuce/core/ReactiveConnectionIntegrationTests.java +++ b/src/test/java/io/lettuce/core/ReactiveConnectionIntegrationTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static io.lettuce.core.ClientOptions.DisconnectedBehavior.*; import static io.lettuce.core.ScriptOutputType.INTEGER; import static org.assertj.core.api.Assertions.*; @@ -31,6 +32,7 @@ import javax.inject.Inject; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; @@ -54,6 +56,7 @@ * @author Nikolai Perevozchikov * @author Tugdual Grall */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) class ReactiveConnectionIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/ReactiveStreamingOutputIntegrationTests.java b/src/test/java/io/lettuce/core/ReactiveStreamingOutputIntegrationTests.java index 98549b93ae..0d649e5993 100644 --- a/src/test/java/io/lettuce/core/ReactiveStreamingOutputIntegrationTests.java +++ b/src/test/java/io/lettuce/core/ReactiveStreamingOutputIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.util.ArrayList; @@ -8,6 +9,7 @@ import javax.inject.Inject; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; @@ -24,6 +26,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) class ReactiveStreamingOutputIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/RedisClientConnectIntegrationTests.java b/src/test/java/io/lettuce/core/RedisClientConnectIntegrationTests.java index 83e2c351e3..4e7c281e40 100644 --- a/src/test/java/io/lettuce/core/RedisClientConnectIntegrationTests.java +++ b/src/test/java/io/lettuce/core/RedisClientConnectIntegrationTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static io.lettuce.core.RedisURI.Builder.redis; import static io.lettuce.core.codec.StringCodec.UTF8; import static java.util.concurrent.TimeUnit.SECONDS; @@ -33,6 +34,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -47,6 +49,7 @@ * @author Jongyeol Choi */ @ExtendWith(LettuceExtension.class) +@Tag(INTEGRATION_TEST) class RedisClientConnectIntegrationTests extends TestSupport { private static final Duration EXPECTED_TIMEOUT = Duration.ofMillis(500); diff --git a/src/test/java/io/lettuce/core/RedisClientFactoryUnitTests.java b/src/test/java/io/lettuce/core/RedisClientFactoryUnitTests.java index 20351373d5..45c3d2288e 100644 --- a/src/test/java/io/lettuce/core/RedisClientFactoryUnitTests.java +++ b/src/test/java/io/lettuce/core/RedisClientFactoryUnitTests.java @@ -1,7 +1,9 @@ package io.lettuce.core; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.resource.ClientResources; @@ -12,6 +14,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class RedisClientFactoryUnitTests { private static final String URI = "redis://" + TestSettings.host() + ":" + TestSettings.port(); diff --git a/src/test/java/io/lettuce/core/RedisClientIntegrationTests.java b/src/test/java/io/lettuce/core/RedisClientIntegrationTests.java index 42c411f7a9..991286b906 100644 --- a/src/test/java/io/lettuce/core/RedisClientIntegrationTests.java +++ b/src/test/java/io/lettuce/core/RedisClientIntegrationTests.java @@ -15,6 +15,7 @@ import io.lettuce.test.resource.TestClientResources; import io.lettuce.test.settings.TestSettings; import io.netty.util.concurrent.EventExecutorGroup; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import java.lang.reflect.Field; @@ -25,6 +26,7 @@ import java.util.Map; import java.util.concurrent.TimeUnit; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; /** @@ -32,6 +34,7 @@ * * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class RedisClientIntegrationTests extends TestSupport { private final ClientResources clientResources = TestClientResources.get(); diff --git a/src/test/java/io/lettuce/core/RedisClientListenerIntegrationTests.java b/src/test/java/io/lettuce/core/RedisClientListenerIntegrationTests.java index 478d5a1700..86062989fa 100644 --- a/src/test/java/io/lettuce/core/RedisClientListenerIntegrationTests.java +++ b/src/test/java/io/lettuce/core/RedisClientListenerIntegrationTests.java @@ -1,14 +1,18 @@ package io.lettuce.core; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.extension.ExtendWith; import io.lettuce.test.LettuceExtension; +import static io.lettuce.TestTags.INTEGRATION_TEST; + /** * Integration tests for {@link RedisConnectionStateListener} via {@link RedisClient}. * * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class RedisClientListenerIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/RedisClientUnitTests.java b/src/test/java/io/lettuce/core/RedisClientUnitTests.java index 608035e43c..ce978ddb61 100644 --- a/src/test/java/io/lettuce/core/RedisClientUnitTests.java +++ b/src/test/java/io/lettuce/core/RedisClientUnitTests.java @@ -1,5 +1,6 @@ package io.lettuce.core; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; @@ -12,6 +13,7 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -29,6 +31,7 @@ */ @SuppressWarnings("unchecked") @ExtendWith(MockitoExtension.class) +@Tag(UNIT_TEST) class RedisClientUnitTests { @Mock diff --git a/src/test/java/io/lettuce/core/RedisCommandBuilderUnitTests.java b/src/test/java/io/lettuce/core/RedisCommandBuilderUnitTests.java index 5e6eba1939..d8e71cffb3 100644 --- a/src/test/java/io/lettuce/core/RedisCommandBuilderUnitTests.java +++ b/src/test/java/io/lettuce/core/RedisCommandBuilderUnitTests.java @@ -4,10 +4,14 @@ import io.lettuce.core.protocol.Command; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; /** @@ -15,6 +19,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class RedisCommandBuilderUnitTests { public static final String MY_KEY = "hKey"; @@ -146,4 +151,36 @@ void shouldCorrectlyConstructHpttl() { + "3\r\n" + "$7\r\n" + "hField1\r\n" + "$7\r\n" + "hField2\r\n" + "$7\r\n" + "hField3\r\n"); } + @Test + void shouldCorrectlyConstructClientTrackinginfo() { + + Command command = sut.clientTrackinginfo(); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)) + .isEqualTo("*2\r\n" + "$6\r\n" + "CLIENT\r\n" + "$12\r\n" + "TRACKINGINFO\r\n"); + } + + @Test + void shouldCorrectlyConstructClusterMyshardid() { + + Command command = sut.clusterMyShardId(); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)) + .isEqualTo("*2\r\n" + "$7\r\n" + "CLUSTER\r\n" + "$9\r\n" + "MYSHARDID\r\n"); + } + + @Test + void shouldCorrectlyConstructClusterLinks() { + + Command>> command = sut.clusterLinks(); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*2\r\n$7\r\nCLUSTER\r\n$5\r\nLINKS\r\n"); + } + } diff --git a/src/test/java/io/lettuce/core/RedisContainerIntegrationTests.java b/src/test/java/io/lettuce/core/RedisContainerIntegrationTests.java new file mode 100644 index 0000000000..5dda7a1166 --- /dev/null +++ b/src/test/java/io/lettuce/core/RedisContainerIntegrationTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.junit.jupiter.api.BeforeAll; +import org.testcontainers.containers.ComposeContainer; +import org.testcontainers.containers.output.OutputFrame; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.io.File; +import java.io.IOException; + +@Testcontainers +public class RedisContainerIntegrationTests { + + private static final Logger LOGGER = LogManager.getLogger(RedisContainerIntegrationTests.class); + + private static final String REDIS_STACK_STANDALONE = "standalone-stack"; + + private static final String REDIS_STACK_CLUSTER = "clustered-stack"; + + public static ComposeContainer CLUSTERED_STACK = new ComposeContainer( + new File("src/test/resources/docker/docker-compose.yml")).withExposedService(REDIS_STACK_CLUSTER, 36379) + .withExposedService(REDIS_STACK_CLUSTER, 36380).withExposedService(REDIS_STACK_CLUSTER, 36381) + .withExposedService(REDIS_STACK_CLUSTER, 36382).withExposedService(REDIS_STACK_CLUSTER, 36383) + .withExposedService(REDIS_STACK_CLUSTER, 36384).withExposedService(REDIS_STACK_STANDALONE, 6379); + + @BeforeAll + public static void setup() throws IOException, InterruptedException { + // In case you need to debug the container uncomment these lines to redirect the output + CLUSTERED_STACK.withLogConsumer(REDIS_STACK_CLUSTER, (OutputFrame frame) -> LOGGER.debug(frame.getUtf8String())); + CLUSTERED_STACK.withLogConsumer(REDIS_STACK_STANDALONE, (OutputFrame frame) -> LOGGER.debug(frame.getUtf8String())); + + CLUSTERED_STACK.waitingFor(REDIS_STACK_CLUSTER, + Wait.forLogMessage(".*Background RDB transfer terminated with success.*", 1)); + CLUSTERED_STACK.start(); + } + +} diff --git a/src/test/java/io/lettuce/core/RedisHandshakeUnitTests.java b/src/test/java/io/lettuce/core/RedisHandshakeUnitTests.java index ed96e8be7f..d4b968c4f3 100644 --- a/src/test/java/io/lettuce/core/RedisHandshakeUnitTests.java +++ b/src/test/java/io/lettuce/core/RedisHandshakeUnitTests.java @@ -1,5 +1,6 @@ package io.lettuce.core; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import java.nio.ByteBuffer; @@ -7,6 +8,7 @@ import java.util.Map; import java.util.concurrent.CompletionStage; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.output.CommandOutput; @@ -19,6 +21,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class RedisHandshakeUnitTests { public static final String ERR_UNKNOWN_COMMAND = "ERR unknown command 'CLIENT', with args beginning with: 'SETINFO' 'lib-name' 'Lettuce'"; diff --git a/src/test/java/io/lettuce/core/RedisJsonCommandBuilderUnitTests.java b/src/test/java/io/lettuce/core/RedisJsonCommandBuilderUnitTests.java new file mode 100644 index 0000000000..4106a711c2 --- /dev/null +++ b/src/test/java/io/lettuce/core/RedisJsonCommandBuilderUnitTests.java @@ -0,0 +1,274 @@ +package io.lettuce.core; + +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ +import io.lettuce.core.codec.StringCodec; +import io.lettuce.core.json.DefaultJsonParser; +import io.lettuce.core.json.JsonParser; +import io.lettuce.core.json.JsonValue; +import io.lettuce.core.json.JsonPath; +import io.lettuce.core.json.arguments.JsonGetArgs; +import io.lettuce.core.json.arguments.JsonMsetArgs; +import io.lettuce.core.json.arguments.JsonRangeArgs; +import io.lettuce.core.json.arguments.JsonSetArgs; +import io.lettuce.core.protocol.Command; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; + +import static io.lettuce.TestTags.UNIT_TEST; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link RedisJsonCommandBuilder}. + * + * @author Tihomir Mateev + */ +@Tag(UNIT_TEST) +class RedisJsonCommandBuilderUnitTests { + + public static final String MY_KEY = "bikes:inventory"; + + public static final String MY_KEY2 = "bikes:repairLog"; + + public static final String ID_BIKE_6 = "{\"id\":\"bike6\"}"; + + public static final JsonParser PARSER = new DefaultJsonParser(); + + public static final JsonValue ELEMENT = PARSER.createJsonValue(ID_BIKE_6); + + public static final JsonPath MY_PATH = JsonPath.of("$..commuter_bikes"); + + RedisJsonCommandBuilder builder = new RedisJsonCommandBuilder<>(StringCodec.UTF8, Mono.just(PARSER)); + + @Test + void shouldCorrectlyConstructJsonArrappend() { + Command> command = builder.jsonArrappend(MY_KEY, MY_PATH, ELEMENT); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*4\r\n" + "$14\r\n" + "JSON.ARRAPPEND\r\n" + "$15\r\n" + + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n" + "$14\r\n" + ID_BIKE_6 + "\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonArrindex() { + JsonRangeArgs range = JsonRangeArgs.Builder.start(0).stop(1); + Command> command = builder.jsonArrindex(MY_KEY, MY_PATH, ELEMENT, range); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*6\r\n" + "$13\r\n" + "JSON.ARRINDEX\r\n" + "$15\r\n" + + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n" + "$14\r\n" + ID_BIKE_6 + "\r\n" + "$1" + "\r\n" + + "0" + "\r\n" + "$1" + "\r\n" + "1" + "\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonArrinsert() { + Command> command = builder.jsonArrinsert(MY_KEY, MY_PATH, 1, ELEMENT); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)) + .isEqualTo("*5\r\n" + "$14\r\n" + "JSON.ARRINSERT\r\n" + "$15\r\n" + "bikes:inventory\r\n" + "$17\r\n" + + "$..commuter_bikes\r\n" + "$1" + "\r\n" + "1" + "\r\n" + "$14\r\n" + ID_BIKE_6 + "\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonArrlen() { + Command> command = builder.jsonArrlen(MY_KEY, MY_PATH); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*3\r\n" + "$11\r\n" + "JSON.ARRLEN\r\n" + "$15\r\n" + + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonArrpop() { + Command> command = builder.jsonArrpop(MY_KEY, MY_PATH, 3); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*4\r\n" + "$11\r\n" + "JSON.ARRPOP\r\n" + "$15\r\n" + + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n" + "$1" + "\r\n" + "3" + "\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonArrtrim() { + JsonRangeArgs range = JsonRangeArgs.Builder.start(0).stop(1); + Command> command = builder.jsonArrtrim(MY_KEY, MY_PATH, range); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)) + .isEqualTo("*5\r\n" + "$12\r\n" + "JSON.ARRTRIM\r\n" + "$15\r\n" + "bikes:inventory\r\n" + "$17\r\n" + + "$..commuter_bikes\r\n" + "$1" + "\r\n" + "0" + "\r\n" + "$1" + "\r\n" + "1" + "\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonClear() { + Command command = builder.jsonClear(MY_KEY, MY_PATH); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*3\r\n" + "$10\r\n" + "JSON.CLEAR\r\n" + "$15\r\n" + + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonGet() { + JsonGetArgs args = JsonGetArgs.Builder.indent(" ").newline("\n").space("/"); + Command> command = builder.jsonGet(MY_KEY, args, MY_PATH); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*9\r\n" + "$8\r\n" + "JSON.GET\r\n" + "$15\r\n" + + "bikes:inventory\r\n" + "$6\r\n" + "INDENT\r\n" + "$3\r\n" + " \r\n" + "$7\r\n" + "NEWLINE\r\n" + "$1\r\n" + + "\n\r\n" + "$5\r\n" + "SPACE\r\n" + "$1\r\n" + "/\r\n" + "$17\r\n" + "$..commuter_bikes\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonMerge() { + Command command = builder.jsonMerge(MY_KEY, MY_PATH, ELEMENT); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*4\r\n" + "$10\r\n" + "JSON.MERGE\r\n" + "$15\r\n" + + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n" + "$14\r\n" + ID_BIKE_6 + "\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonMget() { + Command> command = builder.jsonMGet(MY_PATH, MY_KEY, MY_KEY2); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*4\r\n" + "$9\r\n" + "JSON.MGET\r\n" + "$15\r\n" + + "bikes:inventory\r\n" + "$15\r\n" + "bikes:repairLog\r\n" + "$17\r\n" + "$..commuter_bikes\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonMset() { + JsonMsetArgs args1 = new JsonMsetArgs<>(MY_KEY, MY_PATH, ELEMENT); + Command command = builder.jsonMSet(Collections.singletonList(args1)); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*4\r\n" + "$9\r\n" + "JSON.MSET\r\n" + "$15\r\n" + + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n" + "$14\r\n" + ID_BIKE_6 + "\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonNumincrby() { + Command> command = builder.jsonNumincrby(MY_KEY, MY_PATH, 3); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*4\r\n" + "$14\r\n" + "JSON.NUMINCRBY\r\n" + "$15\r\n" + + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n" + "$1" + "\r\n" + "3" + "\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonObjkeys() { + Command> command = builder.jsonObjkeys(MY_KEY, MY_PATH); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*3\r\n" + "$12\r\n" + "JSON.OBJKEYS\r\n" + "$15\r\n" + + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonObjlen() { + Command> command = builder.jsonObjlen(MY_KEY, MY_PATH); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*3\r\n" + "$11\r\n" + "JSON.OBJLEN\r\n" + "$15\r\n" + + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonSet() { + JsonSetArgs args = JsonSetArgs.Builder.nx(); + Command command = builder.jsonSet(MY_KEY, MY_PATH, ELEMENT, args); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)) + .isEqualTo("*5\r\n" + "$8\r\n" + "JSON.SET\r\n" + "$15\r\n" + "bikes:inventory\r\n" + "$17\r\n" + + "$..commuter_bikes\r\n" + "$14\r\n" + ID_BIKE_6 + "\r\n" + "$2\r\n" + "NX\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonStrappend() { + Command> command = builder.jsonStrappend(MY_KEY, MY_PATH, ELEMENT); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*4\r\n" + "$14\r\n" + "JSON.STRAPPEND\r\n" + "$15\r\n" + + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n" + "$14\r\n" + ID_BIKE_6 + "\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonStrlen() { + Command> command = builder.jsonStrlen(MY_KEY, MY_PATH); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*3\r\n" + "$11\r\n" + "JSON.STRLEN\r\n" + "$15\r\n" + + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonToggle() { + Command> command = builder.jsonToggle(MY_KEY, MY_PATH); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*3\r\n" + "$11\r\n" + "JSON.TOGGLE\r\n" + "$15\r\n" + + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonDel() { + Command command = builder.jsonDel(MY_KEY, MY_PATH); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo( + "*3\r\n" + "$8\r\n" + "JSON.DEL\r\n" + "$15\r\n" + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonType() { + Command command = builder.jsonType(MY_KEY, MY_PATH); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)).isEqualTo("*3\r\n" + "$9\r\n" + "JSON.TYPE\r\n" + "$15\r\n" + + "bikes:inventory\r\n" + "$17\r\n" + "$..commuter_bikes\r\n"); + } + + @Test + void shouldCorrectlyConstructJsonTypeRootPath() { + Command command = builder.jsonType(MY_KEY, JsonPath.ROOT_PATH); + ByteBuf buf = Unpooled.directBuffer(); + command.encode(buf); + + assertThat(buf.toString(StandardCharsets.UTF_8)) + .isEqualTo("*2\r\n" + "$9\r\n" + "JSON.TYPE\r\n" + "$15\r\n" + "bikes:inventory\r\n"); + } + +} diff --git a/src/test/java/io/lettuce/core/RedisURIBuilderUnitTests.java b/src/test/java/io/lettuce/core/RedisURIBuilderUnitTests.java index e368b84e99..5fa22762b4 100644 --- a/src/test/java/io/lettuce/core/RedisURIBuilderUnitTests.java +++ b/src/test/java/io/lettuce/core/RedisURIBuilderUnitTests.java @@ -19,12 +19,14 @@ */ package io.lettuce.core; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import java.io.File; import java.io.IOException; import java.time.Duration; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledOnOs; import org.junit.jupiter.api.condition.OS; @@ -35,6 +37,7 @@ * @author Mark Paluch * @author Guy Korland */ +@Tag(UNIT_TEST) class RedisURIBuilderUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/RedisURIUnitTests.java b/src/test/java/io/lettuce/core/RedisURIUnitTests.java index 999be45c29..077a3df076 100644 --- a/src/test/java/io/lettuce/core/RedisURIUnitTests.java +++ b/src/test/java/io/lettuce/core/RedisURIUnitTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import java.io.UnsupportedEncodingException; @@ -30,6 +31,7 @@ import java.util.Set; import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.internal.LettuceSets; @@ -42,6 +44,7 @@ * @author Lei Zhang * @author Jacob Halsey */ +@Tag(UNIT_TEST) class RedisURIUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/ScanArgsUnitTests.java b/src/test/java/io/lettuce/core/ScanArgsUnitTests.java index 2d655c3637..0d080df520 100644 --- a/src/test/java/io/lettuce/core/ScanArgsUnitTests.java +++ b/src/test/java/io/lettuce/core/ScanArgsUnitTests.java @@ -1,7 +1,9 @@ package io.lettuce.core; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.codec.StringCodec; @@ -10,6 +12,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class ScanArgsUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/ScanCursorUnitTests.java b/src/test/java/io/lettuce/core/ScanCursorUnitTests.java index 8cdeec4334..0f6345abe8 100644 --- a/src/test/java/io/lettuce/core/ScanCursorUnitTests.java +++ b/src/test/java/io/lettuce/core/ScanCursorUnitTests.java @@ -1,13 +1,16 @@ package io.lettuce.core; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class ScanCursorUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/ScanIteratorIntegrationTests.java b/src/test/java/io/lettuce/core/ScanIteratorIntegrationTests.java index c3b5a8e203..da3e121450 100644 --- a/src/test/java/io/lettuce/core/ScanIteratorIntegrationTests.java +++ b/src/test/java/io/lettuce/core/ScanIteratorIntegrationTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.AssertionsForClassTypes.*; import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; @@ -30,6 +31,7 @@ import javax.inject.Inject; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; @@ -42,6 +44,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) class ScanIteratorIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/ScanStreamIntegrationTests.java b/src/test/java/io/lettuce/core/ScanStreamIntegrationTests.java index f527eafcb7..dfa4078db9 100644 --- a/src/test/java/io/lettuce/core/ScanStreamIntegrationTests.java +++ b/src/test/java/io/lettuce/core/ScanStreamIntegrationTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import java.util.List; @@ -27,6 +28,7 @@ import javax.inject.Inject; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; @@ -41,6 +43,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) class ScanStreamIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/ScoredValueUnitTests.java b/src/test/java/io/lettuce/core/ScoredValueUnitTests.java index 3a786971e3..fe101050dc 100644 --- a/src/test/java/io/lettuce/core/ScoredValueUnitTests.java +++ b/src/test/java/io/lettuce/core/ScoredValueUnitTests.java @@ -19,10 +19,12 @@ */ package io.lettuce.core; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import java.util.Optional; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; /** @@ -31,6 +33,7 @@ * @author Will Glozer * @author Mark Paluch */ +@Tag(UNIT_TEST) class ScoredValueUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/SocketOptionsIntegrationTests.java b/src/test/java/io/lettuce/core/SocketOptionsIntegrationTests.java index a7f7e2a548..65e01a93cf 100644 --- a/src/test/java/io/lettuce/core/SocketOptionsIntegrationTests.java +++ b/src/test/java/io/lettuce/core/SocketOptionsIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; @@ -8,6 +9,7 @@ import javax.inject.Inject; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -17,6 +19,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class SocketOptionsIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/SocketOptionsUnitTests.java b/src/test/java/io/lettuce/core/SocketOptionsUnitTests.java index 57aee0a22f..6160e261bd 100644 --- a/src/test/java/io/lettuce/core/SocketOptionsUnitTests.java +++ b/src/test/java/io/lettuce/core/SocketOptionsUnitTests.java @@ -19,12 +19,14 @@ */ package io.lettuce.core; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import java.time.Duration; import java.util.concurrent.TimeUnit; import io.lettuce.core.SocketOptions.TcpUserTimeoutOptions; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; /** @@ -32,6 +34,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class SocketOptionsUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/SslIntegrationTests.java b/src/test/java/io/lettuce/core/SslIntegrationTests.java index e9d6c1cd67..bbad87472a 100644 --- a/src/test/java/io/lettuce/core/SslIntegrationTests.java +++ b/src/test/java/io/lettuce/core/SslIntegrationTests.java @@ -34,6 +34,7 @@ import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -47,6 +48,7 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static io.lettuce.test.settings.TestSettings.sslPort; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -58,6 +60,7 @@ * @author Mark Paluch * @author Adam McElwee */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class SslIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/SslOptionsUnitTests.java b/src/test/java/io/lettuce/core/SslOptionsUnitTests.java index c134da39a8..64ef28b28a 100644 --- a/src/test/java/io/lettuce/core/SslOptionsUnitTests.java +++ b/src/test/java/io/lettuce/core/SslOptionsUnitTests.java @@ -1,5 +1,6 @@ package io.lettuce.core; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.time.Duration; @@ -7,6 +8,7 @@ import javax.net.ssl.SSLParameters; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.netty.handler.ssl.SslContext; @@ -16,6 +18,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class SslOptionsUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/SubscriptionCommandUnitTests.java b/src/test/java/io/lettuce/core/SubscriptionCommandUnitTests.java index b0598788ae..6fa76b81ca 100644 --- a/src/test/java/io/lettuce/core/SubscriptionCommandUnitTests.java +++ b/src/test/java/io/lettuce/core/SubscriptionCommandUnitTests.java @@ -1,11 +1,13 @@ package io.lettuce.core; +import static io.lettuce.TestTags.UNIT_TEST; import static org.mockito.Mockito.*; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.reactivestreams.Subscriber; @@ -23,6 +25,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class SubscriptionCommandUnitTests { private RedisCodec codec = StringCodec.UTF8; diff --git a/src/test/java/io/lettuce/core/SyncAsyncApiConvergenceUnitTests.java b/src/test/java/io/lettuce/core/SyncAsyncApiConvergenceUnitTests.java index e033b7cf7b..facad48792 100644 --- a/src/test/java/io/lettuce/core/SyncAsyncApiConvergenceUnitTests.java +++ b/src/test/java/io/lettuce/core/SyncAsyncApiConvergenceUnitTests.java @@ -1,11 +1,13 @@ package io.lettuce.core; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.lang.reflect.*; import java.util.Arrays; import java.util.stream.Stream; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -17,6 +19,7 @@ * @author Mark Paluch * @since 3.0 */ +@Tag(UNIT_TEST) class SyncAsyncApiConvergenceUnitTests { @SuppressWarnings("rawtypes") diff --git a/src/test/java/io/lettuce/core/TimeoutOptionsUnitTests.java b/src/test/java/io/lettuce/core/TimeoutOptionsUnitTests.java index dd0ae8f4a1..1ceeced9b1 100644 --- a/src/test/java/io/lettuce/core/TimeoutOptionsUnitTests.java +++ b/src/test/java/io/lettuce/core/TimeoutOptionsUnitTests.java @@ -1,16 +1,19 @@ package io.lettuce.core; +import static io.lettuce.TestTags.UNIT_TEST; import static io.lettuce.core.TimeoutOptions.TimeoutSource; import static org.assertj.core.api.Assertions.assertThat; import java.time.Duration; import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class TimeoutOptionsUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/UnixDomainSocketIntegrationTests.java b/src/test/java/io/lettuce/core/UnixDomainSocketIntegrationTests.java index ba440b03c6..f6b7d334f4 100644 --- a/src/test/java/io/lettuce/core/UnixDomainSocketIntegrationTests.java +++ b/src/test/java/io/lettuce/core/UnixDomainSocketIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assumptions.*; @@ -11,6 +12,7 @@ import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.api.StatefulRedisConnection; @@ -25,6 +27,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class UnixDomainSocketIntegrationTests { private static final String MASTER_ID = "mymaster"; diff --git a/src/test/java/io/lettuce/core/Utf8StringCodecIntegrationTests.java b/src/test/java/io/lettuce/core/Utf8StringCodecIntegrationTests.java index 2feb8d11b5..a75940e5a8 100644 --- a/src/test/java/io/lettuce/core/Utf8StringCodecIntegrationTests.java +++ b/src/test/java/io/lettuce/core/Utf8StringCodecIntegrationTests.java @@ -19,12 +19,14 @@ */ package io.lettuce.core; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.util.Arrays; import javax.inject.Inject; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -36,6 +38,7 @@ * @author Will Glozer * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class Utf8StringCodecIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/ValueUnitTests.java b/src/test/java/io/lettuce/core/ValueUnitTests.java index f78b355091..e4b42e19df 100644 --- a/src/test/java/io/lettuce/core/ValueUnitTests.java +++ b/src/test/java/io/lettuce/core/ValueUnitTests.java @@ -1,11 +1,13 @@ package io.lettuce.core; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import java.util.NoSuchElementException; import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; /** @@ -13,6 +15,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class ValueUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/ZAggregateArgsUnitTests.java b/src/test/java/io/lettuce/core/ZAggregateArgsUnitTests.java index 1973c6393b..3de3d4f2b0 100644 --- a/src/test/java/io/lettuce/core/ZAggregateArgsUnitTests.java +++ b/src/test/java/io/lettuce/core/ZAggregateArgsUnitTests.java @@ -19,8 +19,10 @@ */ package io.lettuce.core; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.codec.StringCodec; @@ -29,6 +31,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class ZAggregateArgsUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/cluster/AdvancedClusterClientIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/AdvancedClusterClientIntegrationTests.java index 64c5a04fab..5c1c6bd906 100644 --- a/src/test/java/io/lettuce/core/cluster/AdvancedClusterClientIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/AdvancedClusterClientIntegrationTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static io.lettuce.test.LettuceExtension.*; import static org.assertj.core.api.Assertions.*; @@ -36,6 +37,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -65,6 +67,7 @@ * @author Mark Paluch * @author Jon Chambers */ +@Tag(INTEGRATION_TEST) @SuppressWarnings("rawtypes") @ExtendWith(LettuceExtension.class) class AdvancedClusterClientIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/cluster/AdvancedClusterReactiveIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/AdvancedClusterReactiveIntegrationTests.java index cfa4cc8519..e9e7eb3461 100644 --- a/src/test/java/io/lettuce/core/cluster/AdvancedClusterReactiveIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/AdvancedClusterReactiveIntegrationTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import java.util.ArrayList; @@ -36,6 +37,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -71,6 +73,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class AdvancedClusterReactiveIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/cluster/AsyncConnectionProviderIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/AsyncConnectionProviderIntegrationTests.java index c2352a7898..712d5af9e5 100644 --- a/src/test/java/io/lettuce/core/cluster/AsyncConnectionProviderIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/AsyncConnectionProviderIntegrationTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import java.io.IOException; @@ -34,6 +35,7 @@ import org.apache.commons.lang3.time.StopWatch; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -56,6 +58,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class AsyncConnectionProviderIntegrationTests { diff --git a/src/test/java/io/lettuce/core/cluster/ByteCodecClusterIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/ByteCodecClusterIntegrationTests.java index 17f5ddd57c..95a38a785c 100644 --- a/src/test/java/io/lettuce/core/cluster/ByteCodecClusterIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/ByteCodecClusterIntegrationTests.java @@ -1,9 +1,11 @@ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import javax.inject.Inject; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -15,6 +17,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class ByteCodecClusterIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/cluster/ClusterClientOptionsIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/ClusterClientOptionsIntegrationTests.java index d83b7ee0cf..6eddfa2e0d 100644 --- a/src/test/java/io/lettuce/core/cluster/ClusterClientOptionsIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/ClusterClientOptionsIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import java.time.Duration; @@ -8,6 +9,7 @@ import javax.inject.Inject; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -22,6 +24,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class ClusterClientOptionsIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/cluster/ClusterClientOptionsUnitTests.java b/src/test/java/io/lettuce/core/cluster/ClusterClientOptionsUnitTests.java index abd9b270c2..bf45314864 100644 --- a/src/test/java/io/lettuce/core/cluster/ClusterClientOptionsUnitTests.java +++ b/src/test/java/io/lettuce/core/cluster/ClusterClientOptionsUnitTests.java @@ -1,10 +1,12 @@ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import java.nio.charset.StandardCharsets; import java.util.function.Predicate; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.ClientOptions; @@ -18,6 +20,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class ClusterClientOptionsUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/cluster/ClusterCommandIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/ClusterCommandIntegrationTests.java index 2cdff16990..78eace6029 100644 --- a/src/test/java/io/lettuce/core/cluster/ClusterCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/ClusterCommandIntegrationTests.java @@ -1,14 +1,17 @@ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static io.lettuce.core.cluster.ClusterTestUtil.*; import static org.assertj.core.api.Assertions.*; import java.time.Duration; import java.util.List; +import java.util.Map; import javax.inject.Inject; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -35,6 +38,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class ClusterCommandIntegrationTests extends TestSupport { @@ -271,6 +275,26 @@ void clusterReplicas() { assertThat(result.size()).isGreaterThan(0); } + @Test + void testClusterLinks() { + List> values = sync.clusterLinks(); + assertThat(values).isNotEmpty(); + for (Map value : values) { + assertThat(value).containsKeys("direction", "node", "create-time", "events", "send-buffer-allocated", + "send-buffer-used"); + } + } + + @Test + void testClusterLinksAsync() throws Exception { + RedisFuture>> futureLinks = async.clusterLinks(); + List> values = futureLinks.get(); + for (Map value : values) { + assertThat(value).containsKeys("direction", "node", "create-time", "events", "send-buffer-allocated", + "send-buffer-used"); + } + } + private void prepareReadonlyTest(String key) { async.set(key, value); diff --git a/src/test/java/io/lettuce/core/cluster/ClusterCommandUnitTests.java b/src/test/java/io/lettuce/core/cluster/ClusterCommandUnitTests.java index 2c028f0b39..60205d324f 100644 --- a/src/test/java/io/lettuce/core/cluster/ClusterCommandUnitTests.java +++ b/src/test/java/io/lettuce/core/cluster/ClusterCommandUnitTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.verify; @@ -8,6 +9,7 @@ import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -23,6 +25,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) @ExtendWith(MockitoExtension.class) class ClusterCommandUnitTests { diff --git a/src/test/java/io/lettuce/core/cluster/ClusterDistributionChannelWriterUnitTests.java b/src/test/java/io/lettuce/core/cluster/ClusterDistributionChannelWriterUnitTests.java index f76d481c00..9d7d96c870 100644 --- a/src/test/java/io/lettuce/core/cluster/ClusterDistributionChannelWriterUnitTests.java +++ b/src/test/java/io/lettuce/core/cluster/ClusterDistributionChannelWriterUnitTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @@ -28,6 +29,7 @@ import java.util.concurrent.CompletableFuture; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentMatchers; @@ -65,6 +67,7 @@ * @author koisyu * @author Jim Brunner */ +@Tag(UNIT_TEST) @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class ClusterDistributionChannelWriterUnitTests { diff --git a/src/test/java/io/lettuce/core/cluster/ClusterNodeEndpointUnitTests.java b/src/test/java/io/lettuce/core/cluster/ClusterNodeEndpointUnitTests.java index 524526457d..12b2cd66c5 100644 --- a/src/test/java/io/lettuce/core/cluster/ClusterNodeEndpointUnitTests.java +++ b/src/test/java/io/lettuce/core/cluster/ClusterNodeEndpointUnitTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.mockito.ArgumentMatchers.*; @@ -8,6 +9,7 @@ import java.util.Queue; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -31,6 +33,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) @ExtendWith(MockitoExtension.class) class ClusterNodeEndpointUnitTests { diff --git a/src/test/java/io/lettuce/core/cluster/ClusterPartiallyDownIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/ClusterPartiallyDownIntegrationTests.java index 8f103c0861..4f30b3e2cb 100644 --- a/src/test/java/io/lettuce/core/cluster/ClusterPartiallyDownIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/ClusterPartiallyDownIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Fail.fail; @@ -10,6 +11,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.RedisConnectionException; @@ -27,6 +29,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class ClusterPartiallyDownIntegrationTests extends TestSupport { private static ClientResources clientResources; diff --git a/src/test/java/io/lettuce/core/cluster/ClusterReactiveCommandIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/ClusterReactiveCommandIntegrationTests.java index 74ddc18d0d..0d95d926dc 100644 --- a/src/test/java/io/lettuce/core/cluster/ClusterReactiveCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/ClusterReactiveCommandIntegrationTests.java @@ -1,11 +1,14 @@ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.util.List; +import java.util.Map; import javax.inject.Inject; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -21,6 +24,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class ClusterReactiveCommandIntegrationTests { @@ -97,4 +101,13 @@ void clusterSlaves() { assertThat(result.size()).isGreaterThan(0); } + @Test + void testClusterLinks() { + List> values = reactive.clusterLinks().block(); + for (Map value : values) { + assertThat(value).containsKeys("direction", "node", "create-time", "events", "send-buffer-allocated", + "send-buffer-used"); + } + } + } diff --git a/src/test/java/io/lettuce/core/cluster/ClusterReadOnlyCommandsUnitTests.java b/src/test/java/io/lettuce/core/cluster/ClusterReadOnlyCommandsUnitTests.java index 502198b7ae..f5dfa8308e 100644 --- a/src/test/java/io/lettuce/core/cluster/ClusterReadOnlyCommandsUnitTests.java +++ b/src/test/java/io/lettuce/core/cluster/ClusterReadOnlyCommandsUnitTests.java @@ -1,7 +1,9 @@ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.protocol.CommandType; @@ -12,6 +14,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class ClusterReadOnlyCommandsUnitTests { @Test @@ -23,7 +26,7 @@ void testCount() { void testResolvableCommandNames() { for (ProtocolKeyword readOnlyCommand : ClusterReadOnlyCommands.getReadOnlyCommands()) { - assertThat(readOnlyCommand.name()).isEqualTo(CommandType.valueOf(readOnlyCommand.name()).name()); + assertThat(readOnlyCommand.toString()).isEqualTo(CommandType.valueOf(readOnlyCommand.toString()).name()); } } diff --git a/src/test/java/io/lettuce/core/cluster/ClusterTopologyRefreshOptionsUnitTests.java b/src/test/java/io/lettuce/core/cluster/ClusterTopologyRefreshOptionsUnitTests.java index 6932e034be..2844fd7fa4 100644 --- a/src/test/java/io/lettuce/core/cluster/ClusterTopologyRefreshOptionsUnitTests.java +++ b/src/test/java/io/lettuce/core/cluster/ClusterTopologyRefreshOptionsUnitTests.java @@ -1,10 +1,12 @@ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import java.time.Duration; import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.cluster.ClusterTopologyRefreshOptions.RefreshTrigger; @@ -14,6 +16,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class ClusterTopologyRefreshOptionsUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/cluster/ClusterTopologyRefreshSchedulerUnitTests.java b/src/test/java/io/lettuce/core/cluster/ClusterTopologyRefreshSchedulerUnitTests.java index 6872005898..327f49c97d 100644 --- a/src/test/java/io/lettuce/core/cluster/ClusterTopologyRefreshSchedulerUnitTests.java +++ b/src/test/java/io/lettuce/core/cluster/ClusterTopologyRefreshSchedulerUnitTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @@ -8,6 +9,7 @@ import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; @@ -28,6 +30,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class ClusterTopologyRefreshSchedulerUnitTests { diff --git a/src/test/java/io/lettuce/core/cluster/CommandSetIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/CommandSetIntegrationTests.java index 05c2c32263..d5eca13c5e 100644 --- a/src/test/java/io/lettuce/core/cluster/CommandSetIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/CommandSetIntegrationTests.java @@ -1,11 +1,13 @@ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.util.List; import javax.inject.Inject; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -20,6 +22,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) public class CommandSetIntegrationTests { diff --git a/src/test/java/io/lettuce/core/cluster/HealthyMajorityPartitionsConsensusUnitTests.java b/src/test/java/io/lettuce/core/cluster/HealthyMajorityPartitionsConsensusUnitTests.java index 2fa3597cd0..32980bfc91 100644 --- a/src/test/java/io/lettuce/core/cluster/HealthyMajorityPartitionsConsensusUnitTests.java +++ b/src/test/java/io/lettuce/core/cluster/HealthyMajorityPartitionsConsensusUnitTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.UNIT_TEST; import static io.lettuce.core.cluster.PartitionsConsensusTestSupport.createMap; import static io.lettuce.core.cluster.PartitionsConsensusTestSupport.createNode; import static io.lettuce.core.cluster.PartitionsConsensusTestSupport.createPartitions; @@ -9,6 +10,7 @@ import java.util.Collections; import java.util.Map; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.RedisURI; @@ -18,6 +20,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class HealthyMajorityPartitionsConsensusUnitTests { private RedisClusterNode node1 = createNode(1); diff --git a/src/test/java/io/lettuce/core/cluster/KnownMajorityPartitionsConsensusUnitTests.java b/src/test/java/io/lettuce/core/cluster/KnownMajorityPartitionsConsensusUnitTests.java index 95af7f54f6..ede83f4fca 100644 --- a/src/test/java/io/lettuce/core/cluster/KnownMajorityPartitionsConsensusUnitTests.java +++ b/src/test/java/io/lettuce/core/cluster/KnownMajorityPartitionsConsensusUnitTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.UNIT_TEST; import static io.lettuce.core.cluster.PartitionsConsensusTestSupport.createMap; import static io.lettuce.core.cluster.PartitionsConsensusTestSupport.createNode; import static io.lettuce.core.cluster.PartitionsConsensusTestSupport.createPartitions; @@ -8,6 +9,7 @@ import java.util.Arrays; import java.util.Map; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.RedisURI; @@ -17,6 +19,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class KnownMajorityPartitionsConsensusUnitTests { private RedisClusterNode node1 = createNode(1); diff --git a/src/test/java/io/lettuce/core/cluster/NodeSelectionAsyncIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/NodeSelectionAsyncIntegrationTests.java index ec2faf90d8..e4200834b4 100644 --- a/src/test/java/io/lettuce/core/cluster/NodeSelectionAsyncIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/NodeSelectionAsyncIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import java.util.ArrayList; @@ -15,6 +16,7 @@ import javax.inject.Inject; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -39,6 +41,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class NodeSelectionAsyncIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/cluster/NodeSelectionSyncIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/NodeSelectionSyncIntegrationTests.java index 5d7059dd8d..dc913dae85 100644 --- a/src/test/java/io/lettuce/core/cluster/NodeSelectionSyncIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/NodeSelectionSyncIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.Fail.fail; @@ -12,6 +13,7 @@ import javax.inject.Inject; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -31,6 +33,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class NodeSelectionSyncIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/cluster/PartitionsConsensusTestSupport.java b/src/test/java/io/lettuce/core/cluster/PartitionsConsensusTestSupport.java index a8187aa8fc..cdcf22437d 100644 --- a/src/test/java/io/lettuce/core/cluster/PartitionsConsensusTestSupport.java +++ b/src/test/java/io/lettuce/core/cluster/PartitionsConsensusTestSupport.java @@ -5,10 +5,14 @@ import io.lettuce.core.RedisURI; import io.lettuce.core.cluster.models.partitions.Partitions; import io.lettuce.core.cluster.models.partitions.RedisClusterNode; +import org.junit.jupiter.api.Tag; + +import static io.lettuce.TestTags.INTEGRATION_TEST; /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class PartitionsConsensusTestSupport { static RedisClusterNode createNode(int nodeId) { diff --git a/src/test/java/io/lettuce/core/cluster/PipelinedRedisFutureUnitTests.java b/src/test/java/io/lettuce/core/cluster/PipelinedRedisFutureUnitTests.java index 578201423b..2831d57ec3 100644 --- a/src/test/java/io/lettuce/core/cluster/PipelinedRedisFutureUnitTests.java +++ b/src/test/java/io/lettuce/core/cluster/PipelinedRedisFutureUnitTests.java @@ -1,9 +1,11 @@ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.util.HashMap; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.test.TestFutures; @@ -11,6 +13,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class PipelinedRedisFutureUnitTests { private PipelinedRedisFuture sut; diff --git a/src/test/java/io/lettuce/core/cluster/PooledClusterConnectionProviderUnitTests.java b/src/test/java/io/lettuce/core/cluster/PooledClusterConnectionProviderUnitTests.java index 52ae370cc9..8a00a2cdad 100644 --- a/src/test/java/io/lettuce/core/cluster/PooledClusterConnectionProviderUnitTests.java +++ b/src/test/java/io/lettuce/core/cluster/PooledClusterConnectionProviderUnitTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @@ -32,6 +33,7 @@ import java.util.stream.IntStream; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -65,6 +67,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class PooledClusterConnectionProviderUnitTests { diff --git a/src/test/java/io/lettuce/core/cluster/ReadFromUnitTests.java b/src/test/java/io/lettuce/core/cluster/ReadFromUnitTests.java index 3ee0b59450..c86bddd0cc 100644 --- a/src/test/java/io/lettuce/core/cluster/ReadFromUnitTests.java +++ b/src/test/java/io/lettuce/core/cluster/ReadFromUnitTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; @@ -30,7 +31,10 @@ import java.util.regex.Pattern; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import io.lettuce.core.ReadFrom; import io.lettuce.core.RedisURI; @@ -44,6 +48,7 @@ * @author Omer Cilingir * @author Yohei Ueki */ +@Tag(UNIT_TEST) class ReadFromUnitTests { private Partitions sut = new Partitions(); @@ -220,44 +225,133 @@ void valueOfUnknown() { assertThatThrownBy(() -> ReadFrom.valueOf("unknown")).isInstanceOf(IllegalArgumentException.class); } - @Test - void valueOfNearest() { - assertThat(ReadFrom.valueOf("nearest")).isEqualTo(ReadFrom.NEAREST); + @ParameterizedTest + @ValueSource(strings = { "NEAREST", "nearest", "Nearest" }) + void valueOfNearest(String name) { + assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.NEAREST); } - @Test - void valueOfMaster() { - assertThat(ReadFrom.valueOf("master")).isEqualTo(ReadFrom.UPSTREAM); + @ParameterizedTest + @ValueSource(strings = { "lowestLatency", "lowestlatency", "LOWESTLATENCY" }) + void valueOfLowestLatency(String name) { + assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.LOWEST_LATENCY); } - @Test - void valueOfMasterPreferred() { - assertThat(ReadFrom.valueOf("masterPreferred")).isEqualTo(ReadFrom.UPSTREAM_PREFERRED); + @ParameterizedTest + @ValueSource(strings = { "MASTER", "master", "Master" }) + void valueOfMaster(String name) { + assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.UPSTREAM); + } + + @ParameterizedTest + @ValueSource(strings = { "masterPreferred", "masterpreferred", "MASTERPREFERRED" }) + void valueOfMasterPreferred(String name) { + assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.UPSTREAM_PREFERRED); + } + + @ParameterizedTest + @ValueSource(strings = { "slave", "SLAVE", "Slave" }) + void valueOfSlave(String name) { + assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.REPLICA); + } + + @ParameterizedTest + @ValueSource(strings = { "slavePreferred", "slavepreferred", "SLAVEPREFERRED" }) + void valueOfSlavePreferred(String name) { + assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.REPLICA_PREFERRED); + } + + @ParameterizedTest + @ValueSource(strings = { "replicaPreferred", "replicapreferred", "REPLICAPREFERRED" }) + void valueOfReplicaPreferred(String name) { + assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.REPLICA_PREFERRED); + } + + @ParameterizedTest + @ValueSource(strings = { "anyReplica", "anyreplica", "ANYREPLICA" }) + void valueOfAnyReplica(String name) { + assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.ANY_REPLICA); } @Test - void valueOfSlave() { - assertThat(ReadFrom.valueOf("slave")).isEqualTo(ReadFrom.REPLICA); + void valueOfSubnetWithEmptyCidrNotations() { + assertThatThrownBy(() -> ReadFrom.valueOf("subnet")).isInstanceOf(IllegalArgumentException.class); + } + + @ParameterizedTest + @ValueSource(strings = { "subnet:192.0.2.0/24,2001:db8:abcd:0000::/52", "SUBNET:192.0.2.0/24,2001:db8:abcd:0000::/52" }) + void valueOfSubnet(String name) { + RedisClusterNode nodeInSubnetIpv4 = createNodeWithHost("192.0.2.1"); + RedisClusterNode nodeNotInSubnetIpv4 = createNodeWithHost("198.51.100.1"); + RedisClusterNode nodeInSubnetIpv6 = createNodeWithHost("2001:db8:abcd:0000::1"); + RedisClusterNode nodeNotInSubnetIpv6 = createNodeWithHost("2001:db8:abcd:1000::"); + ReadFrom sut = ReadFrom.valueOf(name); + List result = sut + .select(getNodes(nodeInSubnetIpv4, nodeNotInSubnetIpv4, nodeInSubnetIpv6, nodeNotInSubnetIpv6)); + assertThat(result).hasSize(2).containsExactly(nodeInSubnetIpv4, nodeInSubnetIpv6); } @Test - void valueOfSlavePreferred() { - assertThat(ReadFrom.valueOf("slavePreferred")).isEqualTo(ReadFrom.REPLICA_PREFERRED); + void valueOfRegexWithEmptyRegexValue() { + assertThatThrownBy(() -> ReadFrom.valueOf("regex")).isInstanceOf(IllegalArgumentException.class); + } + + @ParameterizedTest + @ValueSource(strings = { "regex:.*region-1.*", "REGEX:.*region-1.*" }) + void valueOfRegex(String name) { + ReadFrom sut = ReadFrom.valueOf(name); + + RedisClusterNode node1 = createNodeWithHost("redis-node-1.region-1.example.com"); + RedisClusterNode node2 = createNodeWithHost("redis-node-2.region-1.example.com"); + RedisClusterNode node3 = createNodeWithHost("redis-node-1.region-2.example.com"); + RedisClusterNode node4 = createNodeWithHost("redis-node-2.region-2.example.com"); + + List result = sut.select(getNodes(node1, node2, node3, node4)); + + assertThat(sut).hasFieldOrPropertyWithValue("orderSensitive", false); + assertThat(result).hasSize(2).containsExactly(node1, node2); + } + + @ParameterizedTest + @ValueSource(strings = { "REPLICA", "replica", "Replica" }) + void valueOfReplica(String name) { + assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.REPLICA); + } + + @ParameterizedTest + @ValueSource(strings = { "UPSTREAM", "upstream", "Upstream" }) + void valueOfUpstream(String name) { + assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.UPSTREAM); + } + + @ParameterizedTest + @ValueSource(strings = { "upstreamPreferred", "UPSTREAMPREFERRED", "UpstreamPreferred" }) + void valueOfUpstreamPreferred(String name) { + assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.UPSTREAM_PREFERRED); } @Test - void valueOfAnyReplica() { - assertThat(ReadFrom.valueOf("anyReplica")).isEqualTo(ReadFrom.ANY_REPLICA); + void valueOfWhenNameIsPresentButValueIsAbsent() { + assertThatThrownBy(() -> ReadFrom.valueOf("subnet:")).isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Value must not be empty for the type 'subnet'"); } @Test - void valueOfSubnet() { - assertThatThrownBy(() -> ReadFrom.valueOf("subnet")).isInstanceOf(IllegalArgumentException.class); + void valueOfWhenNameIsEmptyButValueIsPresent() { + assertThatThrownBy(() -> ReadFrom.valueOf(":192.0.2.0/24")).isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("ReadFrom :192.0.2.0/24 not supported"); } @Test - void valueOfRegex() { - assertThatThrownBy(() -> ReadFrom.valueOf("regex")).isInstanceOf(IllegalArgumentException.class); + void valueOfRegexWithInvalidPatternShouldThrownIllegalArgumentException() { + assertThatThrownBy(() -> ReadFrom.valueOf("regex:\\")).isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("is not a valid regular expression"); + } + + @ParameterizedTest + @ValueSource(strings = { "ANY", "any", "Any" }) + void valueOfAny(String name) { + assertThat(ReadFrom.valueOf(name)).isEqualTo(ReadFrom.ANY); } private ReadFrom.Nodes getNodes() { diff --git a/src/test/java/io/lettuce/core/cluster/RedisClusterClientFactoryTests.java b/src/test/java/io/lettuce/core/cluster/RedisClusterClientFactoryUnitTests.java similarity index 96% rename from src/test/java/io/lettuce/core/cluster/RedisClusterClientFactoryTests.java rename to src/test/java/io/lettuce/core/cluster/RedisClusterClientFactoryUnitTests.java index f8741e0a78..5f552eca69 100644 --- a/src/test/java/io/lettuce/core/cluster/RedisClusterClientFactoryTests.java +++ b/src/test/java/io/lettuce/core/cluster/RedisClusterClientFactoryUnitTests.java @@ -1,10 +1,12 @@ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.Arrays; import java.util.List; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.RedisURI; @@ -16,7 +18,8 @@ /** * @author Mark Paluch */ -class RedisClusterClientFactoryTests { +@Tag(UNIT_TEST) +class RedisClusterClientFactoryUnitTests { private static final String URI = "redis://" + TestSettings.host() + ":" + TestSettings.port(); diff --git a/src/test/java/io/lettuce/core/cluster/RedisClusterClientIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/RedisClusterClientIntegrationTests.java index c48b6e8eab..00b87784f6 100644 --- a/src/test/java/io/lettuce/core/cluster/RedisClusterClientIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/RedisClusterClientIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static io.lettuce.core.cluster.ClusterTestUtil.*; import static org.assertj.core.api.Assertions.*; @@ -17,6 +18,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -56,6 +58,7 @@ */ @SuppressWarnings("unchecked") @ExtendWith(LettuceExtension.class) +@Tag(INTEGRATION_TEST) class RedisClusterClientIntegrationTests extends TestSupport { private final RedisClient client; diff --git a/src/test/java/io/lettuce/core/cluster/RedisClusterPasswordSecuredSslIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/RedisClusterPasswordSecuredSslIntegrationTests.java index 4d02a8fa5c..a276130532 100644 --- a/src/test/java/io/lettuce/core/cluster/RedisClusterPasswordSecuredSslIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/RedisClusterPasswordSecuredSslIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static io.lettuce.test.settings.TestSettings.*; import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assumptions.*; @@ -9,6 +10,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.RedisCommandExecutionException; @@ -27,6 +29,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class RedisClusterPasswordSecuredSslIntegrationTests extends TestSupport { private static final int CLUSTER_PORT_SSL_1 = 7442; diff --git a/src/test/java/io/lettuce/core/cluster/RedisClusterReadFromIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/RedisClusterReadFromIntegrationTests.java index d3543b7c40..54af0098c5 100644 --- a/src/test/java/io/lettuce/core/cluster/RedisClusterReadFromIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/RedisClusterReadFromIntegrationTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.util.regex.Pattern; @@ -27,6 +28,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -40,6 +42,7 @@ * @author Mark Paluch * @author Yohei Ueki */ +@Tag(INTEGRATION_TEST) @SuppressWarnings("unchecked") @ExtendWith(LettuceExtension.class) class RedisClusterReadFromIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/cluster/RedisClusterSetupTest.java b/src/test/java/io/lettuce/core/cluster/RedisClusterSetupIntegrationTests.java similarity index 96% rename from src/test/java/io/lettuce/core/cluster/RedisClusterSetupTest.java rename to src/test/java/io/lettuce/core/cluster/RedisClusterSetupIntegrationTests.java index ce956e0dea..62ac7848a2 100644 --- a/src/test/java/io/lettuce/core/cluster/RedisClusterSetupTest.java +++ b/src/test/java/io/lettuce/core/cluster/RedisClusterSetupIntegrationTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static io.lettuce.core.cluster.ClusterTestSettings.*; import static io.lettuce.core.cluster.ClusterTestUtil.*; import static org.assertj.core.api.Assertions.*; @@ -31,10 +32,12 @@ import java.util.function.Supplier; import java.util.stream.Collectors; +import io.lettuce.core.TimeoutOptions; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.category.SlowTests; @@ -73,9 +76,10 @@ * @author dengliming * @since 3.0 */ +@Tag(INTEGRATION_TEST) @SuppressWarnings({ "unchecked" }) @SlowTests -public class RedisClusterSetupTest extends TestSupport { +public class RedisClusterSetupIntegrationTests extends TestSupport { private static final String host = TestSettings.hostAddr(); @@ -303,10 +307,12 @@ public void slotMigrationShouldUseAsking() { public void disconnectedConnectionRejectTest() throws Exception { clusterClient.setOptions(ClusterClientOptions.builder().topologyRefreshOptions(PERIODIC_REFRESH_ENABLED) - .disconnectedBehavior(ClientOptions.DisconnectedBehavior.REJECT_COMMANDS).build()); + .disconnectedBehavior(ClientOptions.DisconnectedBehavior.REJECT_COMMANDS) + .timeoutOptions(TimeoutOptions.builder().timeoutCommands(false).build()).build()); RedisAdvancedClusterAsyncCommands clusterConnection = clusterClient.connect().async(); - clusterClient.setOptions(ClusterClientOptions.builder() - .disconnectedBehavior(ClientOptions.DisconnectedBehavior.REJECT_COMMANDS).build()); + clusterClient.setOptions( + ClusterClientOptions.builder().disconnectedBehavior(ClientOptions.DisconnectedBehavior.REJECT_COMMANDS) + .timeoutOptions(TimeoutOptions.builder().timeoutCommands(false).build()).build()); ClusterSetup.setup2Masters(clusterHelper); assertRoutedExecution(clusterConnection); @@ -330,9 +336,11 @@ public void disconnectedConnectionRejectTest() throws Exception { @Test public void atLeastOnceForgetNodeFailover() throws Exception { - clusterClient.setOptions(ClusterClientOptions.builder().topologyRefreshOptions(PERIODIC_REFRESH_ENABLED).build()); + clusterClient.setOptions(ClusterClientOptions.builder().topologyRefreshOptions(PERIODIC_REFRESH_ENABLED) + .timeoutOptions(TimeoutOptions.builder().timeoutCommands(false).build()).build()); RedisAdvancedClusterAsyncCommands clusterConnection = clusterClient.connect().async(); - clusterClient.setOptions(ClusterClientOptions.create()); + clusterClient.setOptions( + ClusterClientOptions.builder().timeoutOptions(TimeoutOptions.builder().timeoutCommands(false).build()).build()); ClusterSetup.setup2Masters(clusterHelper); assertRoutedExecution(clusterConnection); diff --git a/src/test/java/io/lettuce/core/cluster/RedisClusterStressScenariosTest.java b/src/test/java/io/lettuce/core/cluster/RedisClusterStressScenariosIntegrationTests.java similarity index 97% rename from src/test/java/io/lettuce/core/cluster/RedisClusterStressScenariosTest.java rename to src/test/java/io/lettuce/core/cluster/RedisClusterStressScenariosIntegrationTests.java index 5887a7edc8..65aad4071b 100644 --- a/src/test/java/io/lettuce/core/cluster/RedisClusterStressScenariosTest.java +++ b/src/test/java/io/lettuce/core/cluster/RedisClusterStressScenariosIntegrationTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static io.lettuce.core.cluster.ClusterTestUtil.*; import static org.assertj.core.api.Assertions.*; @@ -31,6 +32,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; @@ -51,10 +53,11 @@ import io.lettuce.test.resource.TestClientResources; import io.lettuce.test.settings.TestSettings; +@Tag(INTEGRATION_TEST) @TestMethodOrder(MethodOrderer.MethodName.class) @SuppressWarnings("unchecked") @SlowTests -public class RedisClusterStressScenariosTest extends TestSupport { +public class RedisClusterStressScenariosIntegrationTests extends TestSupport { private static final String host = TestSettings.hostAddr(); diff --git a/src/test/java/io/lettuce/core/cluster/RedisClusterURIUtilUnitTests.java b/src/test/java/io/lettuce/core/cluster/RedisClusterURIUtilUnitTests.java index e0e707eecc..766c8b8cfe 100644 --- a/src/test/java/io/lettuce/core/cluster/RedisClusterURIUtilUnitTests.java +++ b/src/test/java/io/lettuce/core/cluster/RedisClusterURIUtilUnitTests.java @@ -1,10 +1,12 @@ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.net.URI; import java.util.List; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.RedisURI; @@ -12,6 +14,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class RedisClusterURIUtilUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/cluster/RedisReactiveClusterClientIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/RedisReactiveClusterClientIntegrationTests.java index 0ab13d2b0c..e99fbd1ebb 100644 --- a/src/test/java/io/lettuce/core/cluster/RedisReactiveClusterClientIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/RedisReactiveClusterClientIntegrationTests.java @@ -1,10 +1,12 @@ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static io.lettuce.core.cluster.ClusterTestUtil.getOwnPartition; import static org.assertj.core.api.Assertions.assertThat; import javax.inject.Inject; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -20,6 +22,7 @@ * @author Mark Paluch */ @SuppressWarnings("unchecked") +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class RedisReactiveClusterClientIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/cluster/RoundRobinSocketAddressSupplierUnitTests.java b/src/test/java/io/lettuce/core/cluster/RoundRobinSocketAddressSupplierUnitTests.java index f19a3ec4ff..3bd9ee3087 100644 --- a/src/test/java/io/lettuce/core/cluster/RoundRobinSocketAddressSupplierUnitTests.java +++ b/src/test/java/io/lettuce/core/cluster/RoundRobinSocketAddressSupplierUnitTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; @@ -28,6 +29,7 @@ import java.util.HashSet; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -46,6 +48,7 @@ * @author Mark Paluch * @author Christian Lang */ +@Tag(UNIT_TEST) @ExtendWith(MockitoExtension.class) class RoundRobinSocketAddressSupplierUnitTests { diff --git a/src/test/java/io/lettuce/core/cluster/RoundRobinUnitTests.java b/src/test/java/io/lettuce/core/cluster/RoundRobinUnitTests.java index 86f8857120..db323925f7 100644 --- a/src/test/java/io/lettuce/core/cluster/RoundRobinUnitTests.java +++ b/src/test/java/io/lettuce/core/cluster/RoundRobinUnitTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import java.util.ArrayList; @@ -7,6 +8,7 @@ import java.util.Collections; import java.util.HashSet; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.RedisURI; @@ -17,6 +19,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class RoundRobinUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/cluster/ScanIteratorIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/ScanIteratorIntegrationTests.java index 67524a46f8..0683535170 100644 --- a/src/test/java/io/lettuce/core/cluster/ScanIteratorIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/ScanIteratorIntegrationTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.AssertionsForClassTypes.*; import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; @@ -30,6 +31,7 @@ import javax.inject.Inject; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; @@ -48,6 +50,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) class ScanIteratorIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/cluster/ScanStreamIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/ScanStreamIntegrationTests.java index 621ddb1b19..9b9974dd0e 100644 --- a/src/test/java/io/lettuce/core/cluster/ScanStreamIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/ScanStreamIntegrationTests.java @@ -2,6 +2,7 @@ import javax.inject.Inject; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -14,9 +15,12 @@ import io.lettuce.core.cluster.api.sync.RedisClusterCommands; import io.lettuce.test.LettuceExtension; +import static io.lettuce.TestTags.INTEGRATION_TEST; + /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class ScanStreamIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/cluster/SingleThreadedReactiveClusterClientIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/SingleThreadedReactiveClusterClientIntegrationTests.java index fcf4fdbbd7..703ac44a7e 100644 --- a/src/test/java/io/lettuce/core/cluster/SingleThreadedReactiveClusterClientIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/SingleThreadedReactiveClusterClientIntegrationTests.java @@ -1,11 +1,13 @@ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.util.List; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.RedisURI; @@ -19,6 +21,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class SingleThreadedReactiveClusterClientIntegrationTests { private RedisClusterClient client; diff --git a/src/test/java/io/lettuce/core/cluster/SlotHashUnitTests.java b/src/test/java/io/lettuce/core/cluster/SlotHashUnitTests.java index b7b26c029a..7ef2d5032f 100644 --- a/src/test/java/io/lettuce/core/cluster/SlotHashUnitTests.java +++ b/src/test/java/io/lettuce/core/cluster/SlotHashUnitTests.java @@ -1,15 +1,18 @@ package io.lettuce.core.cluster; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.nio.ByteBuffer; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; /** * @author Mark Paluch * @since 3.0 */ +@Tag(UNIT_TEST) class SlotHashUnitTests { private static final byte[] BYTES = "123456789".getBytes(); diff --git a/src/test/java/io/lettuce/core/cluster/commands/CustomClusterCommandIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/commands/CustomClusterCommandIntegrationTests.java index 84f1d48a49..5f335c311a 100644 --- a/src/test/java/io/lettuce/core/cluster/commands/CustomClusterCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/commands/CustomClusterCommandIntegrationTests.java @@ -1,11 +1,13 @@ package io.lettuce.core.cluster.commands; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import java.util.Arrays; import javax.inject.Inject; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -30,6 +32,7 @@ * * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class CustomClusterCommandIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/cluster/commands/GeoClusterCommandIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/commands/GeoClusterCommandIntegrationTests.java index e162c52f22..2a91248963 100644 --- a/src/test/java/io/lettuce/core/cluster/commands/GeoClusterCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/commands/GeoClusterCommandIntegrationTests.java @@ -7,12 +7,16 @@ import io.lettuce.core.cluster.ClusterTestUtil; import io.lettuce.core.cluster.api.StatefulRedisClusterConnection; import io.lettuce.core.commands.GeoCommandIntegrationTests; +import org.junit.jupiter.api.Tag; + +import static io.lettuce.TestTags.INTEGRATION_TEST; /** * Integration tests for {@link io.lettuce.core.api.sync.RedisGeoCommands} using Redis Cluster. * * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class GeoClusterCommandIntegrationTests extends GeoCommandIntegrationTests { @Inject diff --git a/src/test/java/io/lettuce/core/cluster/commands/HashClusterCommandIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/commands/HashClusterCommandIntegrationTests.java index 6342a50874..5eb6920c18 100644 --- a/src/test/java/io/lettuce/core/cluster/commands/HashClusterCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/commands/HashClusterCommandIntegrationTests.java @@ -5,12 +5,16 @@ import io.lettuce.core.cluster.ClusterTestUtil; import io.lettuce.core.cluster.api.StatefulRedisClusterConnection; import io.lettuce.core.commands.HashCommandIntegrationTests; +import org.junit.jupiter.api.Tag; + +import static io.lettuce.TestTags.INTEGRATION_TEST; /** * Integration tests for {@link io.lettuce.core.api.sync.RedisHashCommands} using Redis Cluster. * * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) public class HashClusterCommandIntegrationTests extends HashCommandIntegrationTests { @Inject diff --git a/src/test/java/io/lettuce/core/cluster/commands/KeyClusterCommandIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/commands/KeyClusterCommandIntegrationTests.java index 9ad153da8c..757df6ea79 100644 --- a/src/test/java/io/lettuce/core/cluster/commands/KeyClusterCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/commands/KeyClusterCommandIntegrationTests.java @@ -1,10 +1,12 @@ package io.lettuce.core.cluster.commands; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import javax.inject.Inject; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; @@ -21,6 +23,7 @@ * * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) class KeyClusterCommandIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/cluster/commands/ListClusterCommandIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/commands/ListClusterCommandIntegrationTests.java index 145278a491..035bcd421d 100644 --- a/src/test/java/io/lettuce/core/cluster/commands/ListClusterCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/commands/ListClusterCommandIntegrationTests.java @@ -1,9 +1,11 @@ package io.lettuce.core.cluster.commands; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import javax.inject.Inject; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.cluster.ClusterTestUtil; @@ -17,6 +19,7 @@ * * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class ListClusterCommandIntegrationTests extends ListCommandIntegrationTests { private final RedisClusterCommands redis; diff --git a/src/test/java/io/lettuce/core/cluster/commands/StreamClusterCommandIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/commands/StreamClusterCommandIntegrationTests.java index 18619d924d..7bbf667577 100644 --- a/src/test/java/io/lettuce/core/cluster/commands/StreamClusterCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/commands/StreamClusterCommandIntegrationTests.java @@ -7,13 +7,17 @@ import io.lettuce.core.cluster.ClusterTestUtil; import io.lettuce.core.cluster.api.StatefulRedisClusterConnection; import io.lettuce.core.commands.StreamCommandIntegrationTests; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; +import static io.lettuce.TestTags.INTEGRATION_TEST; + /** * Integration tests for {@link io.lettuce.core.api.sync.RedisStreamCommands} using Redis Cluster. * * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class StreamClusterCommandIntegrationTests extends StreamCommandIntegrationTests { @Inject diff --git a/src/test/java/io/lettuce/core/cluster/commands/StringClusterCommandIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/commands/StringClusterCommandIntegrationTests.java index 1b539abfc5..245faf490b 100644 --- a/src/test/java/io/lettuce/core/cluster/commands/StringClusterCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/commands/StringClusterCommandIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.cluster.commands; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import java.util.LinkedHashMap; @@ -7,6 +8,7 @@ import javax.inject.Inject; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.cluster.ClusterTestUtil; @@ -20,6 +22,7 @@ * * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class StringClusterCommandIntegrationTests extends StringCommandIntegrationTests { private final RedisClusterCommands redis; diff --git a/src/test/java/io/lettuce/core/cluster/commands/reactive/HashClusterReactiveCommandIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/commands/reactive/HashClusterReactiveCommandIntegrationTests.java index 6da2e608d0..582cb7c4b4 100644 --- a/src/test/java/io/lettuce/core/cluster/commands/reactive/HashClusterReactiveCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/commands/reactive/HashClusterReactiveCommandIntegrationTests.java @@ -3,15 +3,19 @@ import javax.inject.Inject; import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.cluster.api.StatefulRedisClusterConnection; import io.lettuce.core.commands.HashCommandIntegrationTests; import io.lettuce.test.ReactiveSyncInvocationHandler; +import static io.lettuce.TestTags.INTEGRATION_TEST; + /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class HashClusterReactiveCommandIntegrationTests extends HashCommandIntegrationTests { @Inject diff --git a/src/test/java/io/lettuce/core/cluster/commands/reactive/ListClusterReactiveCommandIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/commands/reactive/ListClusterReactiveCommandIntegrationTests.java index f02e6901cb..689c4a1825 100644 --- a/src/test/java/io/lettuce/core/cluster/commands/reactive/ListClusterReactiveCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/commands/reactive/ListClusterReactiveCommandIntegrationTests.java @@ -1,9 +1,11 @@ package io.lettuce.core.cluster.commands.reactive; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import javax.inject.Inject; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.cluster.api.StatefulRedisClusterConnection; @@ -15,6 +17,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class ListClusterReactiveCommandIntegrationTests extends ListCommandIntegrationTests { private final RedisClusterCommands redis; diff --git a/src/test/java/io/lettuce/core/cluster/commands/reactive/StringClusterReactiveCommandIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/commands/reactive/StringClusterReactiveCommandIntegrationTests.java index 651bd529b2..1be2b44ee5 100644 --- a/src/test/java/io/lettuce/core/cluster/commands/reactive/StringClusterReactiveCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/commands/reactive/StringClusterReactiveCommandIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.cluster.commands.reactive; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.util.LinkedHashMap; @@ -7,6 +8,7 @@ import javax.inject.Inject; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; @@ -21,6 +23,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class StringClusterReactiveCommandIntegrationTests extends StringCommandIntegrationTests { private final StatefulRedisClusterConnection connection; diff --git a/src/test/java/io/lettuce/core/cluster/models/partitions/ClusterPartitionParserUnitTests.java b/src/test/java/io/lettuce/core/cluster/models/partitions/ClusterPartitionParserUnitTests.java index 8d028f00d4..4cf98596a2 100644 --- a/src/test/java/io/lettuce/core/cluster/models/partitions/ClusterPartitionParserUnitTests.java +++ b/src/test/java/io/lettuce/core/cluster/models/partitions/ClusterPartitionParserUnitTests.java @@ -1,11 +1,13 @@ package io.lettuce.core.cluster.models.partitions; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import java.time.Duration; import java.util.Collections; import java.util.HashSet; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.RedisURI; @@ -14,6 +16,7 @@ /** * Unit tests for {@link ClusterPartitionParser}. */ +@Tag(UNIT_TEST) class ClusterPartitionParserUnitTests { private static String nodes = "c37ab8396be428403d4e55c0d317348be27ed973 127.0.0.1:7381 master - 111 1401258245007 222 connected 7000 12000 12002-16383\n" diff --git a/src/test/java/io/lettuce/core/cluster/models/partitions/PartitionsUnitTests.java b/src/test/java/io/lettuce/core/cluster/models/partitions/PartitionsUnitTests.java index bad3da7e00..b6c38c37ce 100644 --- a/src/test/java/io/lettuce/core/cluster/models/partitions/PartitionsUnitTests.java +++ b/src/test/java/io/lettuce/core/cluster/models/partitions/PartitionsUnitTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.cluster.models.partitions; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.AssertionsForInterfaceTypes.*; import java.util.Arrays; @@ -7,6 +8,7 @@ import java.util.HashSet; import java.util.Iterator; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.RedisURI; @@ -16,6 +18,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class PartitionsUnitTests { private RedisClusterNode node1 = new RedisClusterNode(RedisURI.create("localhost", 6379), "a", true, "", 0, 0, 0, diff --git a/src/test/java/io/lettuce/core/cluster/models/partitions/RedisClusterNodeUnitTests.java b/src/test/java/io/lettuce/core/cluster/models/partitions/RedisClusterNodeUnitTests.java index 4c0d02fc53..39d9415be8 100644 --- a/src/test/java/io/lettuce/core/cluster/models/partitions/RedisClusterNodeUnitTests.java +++ b/src/test/java/io/lettuce/core/cluster/models/partitions/RedisClusterNodeUnitTests.java @@ -1,9 +1,14 @@ package io.lettuce.core.cluster.models.partitions; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import java.util.Arrays; +import java.util.BitSet; +import java.util.Collections; +import java.util.HashSet; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.RedisURI; @@ -14,8 +19,91 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class RedisClusterNodeUnitTests { + @Test + void shouldCreateNodeWithEmptySlots() { + + BitSet slots = new BitSet(); + RedisClusterNode node = new RedisClusterNode(RedisURI.create("localhost", 6379), "1", true, null, 0, 0, 0, slots, + Collections.emptySet()); + + assertThat(node.getSlots()).isEmpty(); + assertThat(node.getSlots()).isNotNull(); + } + + @Test + void shouldCreateNodeWithNonEmptySlots() { + + BitSet slots = new BitSet(); + slots.set(1); + slots.set(2); + RedisClusterNode node = new RedisClusterNode(RedisURI.create("localhost", 6379), "1", true, null, 0, 0, 0, slots, + Collections.emptySet()); + + assertThat(node.getSlots()).containsExactly(1, 2); + } + + @Test + void shouldCopyNodeWithEmptySlots() { + + BitSet slots = new BitSet(); + RedisClusterNode originalNode = new RedisClusterNode(RedisURI.create("localhost", 6379), "1", true, null, 0, 0, 0, + slots, Collections.emptySet()); + + RedisClusterNode copiedNode = new RedisClusterNode(originalNode); + + assertThat(copiedNode.getSlots()).isEmpty(); + assertThat(copiedNode.getSlots()).isNotNull(); + } + + @Test + void shouldCopyNodeWithNonEmptySlots() { + + BitSet slots = new BitSet(); + slots.set(1); + slots.set(2); + RedisClusterNode originalNode = new RedisClusterNode(RedisURI.create("localhost", 6379), "1", true, null, 0, 0, 0, + slots, Collections.emptySet()); + + RedisClusterNode copiedNode = new RedisClusterNode(originalNode); + + assertThat(copiedNode.getSlots()).containsExactly(1, 2); + } + + @Test + public void testHasSameSlotsAs() { + + BitSet emptySlots = new BitSet(SlotHash.SLOT_COUNT); + emptySlots.set(1); + emptySlots.set(2); + + RedisClusterNode node1 = new RedisClusterNode(RedisURI.create("localhost", 6379), "nodeId1", true, "slaveOf", 0L, 0L, + 0L, emptySlots, new HashSet<>()); + + RedisClusterNode node2 = new RedisClusterNode(node1); + + assertThat(node1.hasSameSlotsAs(node2)).isTrue(); + } + + @Test + public void testHasDifferentSlotsAs() { + + BitSet slots1 = new BitSet(SlotHash.SLOT_COUNT); + slots1.set(1); + + BitSet slots2 = new BitSet(SlotHash.SLOT_COUNT); + slots2.set(2); + + RedisClusterNode node1 = new RedisClusterNode(RedisURI.create("localhost", 6379), "nodeId1", true, "slaveOf", 0L, 0L, + 0L, slots1, new HashSet<>()); + RedisClusterNode node2 = new RedisClusterNode(RedisURI.create("localhost", 6379), "nodeId2", true, "slaveOf", 0L, 0L, + 0L, slots2, new HashSet<>()); + + assertThat(node1.hasSameSlotsAs(node2)).isFalse(); + } + @Test void shouldCopyNode() { @@ -55,4 +143,35 @@ void testToString() { assertThat(node.toString()).contains(RedisClusterNode.class.getSimpleName()); } + @Test + void shouldReturnTrueWhenSlotsAreNull() { + + BitSet emptySlots = null; + RedisClusterNode node = new RedisClusterNode(RedisURI.create("localhost", 6379), "1", true, null, 0, 0, 0, emptySlots, + Collections.emptySet()); + + assertThat(node.hasNoSlots()).isTrue(); + } + + @Test + void shouldReturnTrueWhenSlotsAreEmpty() { + + BitSet emptySlots = new BitSet(); // Empty BitSet + RedisClusterNode node = new RedisClusterNode(RedisURI.create("localhost", 6379), "1", true, null, 0, 0, 0, emptySlots, + Collections.emptySet()); + + assertThat(node.hasNoSlots()).isTrue(); + } + + @Test + void shouldReturnFalseWhenSlotsAreAssigned() { + + BitSet slots = new BitSet(); + slots.set(1); // Assign a slot + RedisClusterNode node = new RedisClusterNode(RedisURI.create("localhost", 6379), "1", true, null, 0, 0, 0, slots, + Collections.emptySet()); + + assertThat(node.hasNoSlots()).isFalse(); + } + } diff --git a/src/test/java/io/lettuce/core/cluster/models/slots/ClusterSlotsParserUnitTests.java b/src/test/java/io/lettuce/core/cluster/models/slots/ClusterSlotsParserUnitTests.java index 9a47ad0894..28b4086909 100644 --- a/src/test/java/io/lettuce/core/cluster/models/slots/ClusterSlotsParserUnitTests.java +++ b/src/test/java/io/lettuce/core/cluster/models/slots/ClusterSlotsParserUnitTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.cluster.models.slots; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -7,12 +8,14 @@ import java.util.Arrays; import java.util.List; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.cluster.models.partitions.RedisClusterNode; import io.lettuce.core.internal.LettuceLists; @SuppressWarnings("unchecked") +@Tag(UNIT_TEST) class ClusterSlotsParserUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/cluster/pubsub/RedisClusterPubSubConnectionIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/pubsub/RedisClusterPubSubConnectionIntegrationTests.java index cd08f520e6..7cba0dd163 100644 --- a/src/test/java/io/lettuce/core/cluster/pubsub/RedisClusterPubSubConnectionIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/pubsub/RedisClusterPubSubConnectionIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.cluster.pubsub; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import java.util.List; @@ -11,6 +12,7 @@ import io.lettuce.core.pubsub.api.async.RedisPubSubAsyncCommands; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -42,6 +44,7 @@ * * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class RedisClusterPubSubConnectionIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/cluster/topology/ClusterTopologyRefreshUnitTests.java b/src/test/java/io/lettuce/core/cluster/topology/ClusterTopologyRefreshUnitTests.java index 6106fa571c..48cc3e8acc 100644 --- a/src/test/java/io/lettuce/core/cluster/topology/ClusterTopologyRefreshUnitTests.java +++ b/src/test/java/io/lettuce/core/cluster/topology/ClusterTopologyRefreshUnitTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core.cluster.topology; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @@ -40,6 +41,7 @@ import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -71,6 +73,7 @@ * @author Mark Paluch * @author Christian Weitendorf */ +@Tag(UNIT_TEST) @SuppressWarnings("unchecked") @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) diff --git a/src/test/java/io/lettuce/core/cluster/topology/NodeTopologyViewsUnitTests.java b/src/test/java/io/lettuce/core/cluster/topology/NodeTopologyViewsUnitTests.java index cc98c276ff..d83381e0b6 100644 --- a/src/test/java/io/lettuce/core/cluster/topology/NodeTopologyViewsUnitTests.java +++ b/src/test/java/io/lettuce/core/cluster/topology/NodeTopologyViewsUnitTests.java @@ -19,11 +19,13 @@ */ package io.lettuce.core.cluster.topology; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import java.util.Arrays; import java.util.Set; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.RedisURI; @@ -33,6 +35,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class NodeTopologyViewsUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/cluster/topology/RequestsUnitTests.java b/src/test/java/io/lettuce/core/cluster/topology/RequestsUnitTests.java index fcf3f0e8dd..f376dec702 100644 --- a/src/test/java/io/lettuce/core/cluster/topology/RequestsUnitTests.java +++ b/src/test/java/io/lettuce/core/cluster/topology/RequestsUnitTests.java @@ -19,10 +19,12 @@ */ package io.lettuce.core.cluster.topology; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import java.nio.ByteBuffer; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.RedisURI; @@ -35,6 +37,7 @@ * @author Mark Paluch * @author Xujs */ +@Tag(UNIT_TEST) class RequestsUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/cluster/topology/TopologyComparatorsUnitTests.java b/src/test/java/io/lettuce/core/cluster/topology/TopologyComparatorsUnitTests.java index 5e027077d7..416c6c3527 100644 --- a/src/test/java/io/lettuce/core/cluster/topology/TopologyComparatorsUnitTests.java +++ b/src/test/java/io/lettuce/core/cluster/topology/TopologyComparatorsUnitTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core.cluster.topology; +import static io.lettuce.TestTags.UNIT_TEST; import static io.lettuce.core.cluster.topology.TopologyComparators.isChanged; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -26,6 +27,7 @@ import java.util.*; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.RedisURI; @@ -40,6 +42,7 @@ * @author Mark Paluch * @author Alessandro Simi */ +@Tag(UNIT_TEST) class TopologyComparatorsUnitTests { private RedisClusterNodeSnapshot node1 = createNode("1"); diff --git a/src/test/java/io/lettuce/core/cluster/topology/TopologyRefreshIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/topology/TopologyRefreshIntegrationTests.java index c11212c228..77c0f3073b 100644 --- a/src/test/java/io/lettuce/core/cluster/topology/TopologyRefreshIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/topology/TopologyRefreshIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.cluster.topology; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import java.time.Duration; @@ -12,6 +13,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -42,6 +44,7 @@ * * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @SuppressWarnings({ "unchecked" }) @SlowTests @ExtendWith(LettuceExtension.class) diff --git a/src/test/java/io/lettuce/core/codec/ByteArrayCodecUnitTests.java b/src/test/java/io/lettuce/core/codec/ByteArrayCodecUnitTests.java index c8f2394dac..9442517fe0 100644 --- a/src/test/java/io/lettuce/core/codec/ByteArrayCodecUnitTests.java +++ b/src/test/java/io/lettuce/core/codec/ByteArrayCodecUnitTests.java @@ -1,11 +1,14 @@ package io.lettuce.core.codec; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import java.nio.ByteBuffer; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; +@Tag(UNIT_TEST) class ByteArrayCodecUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/codec/CipherCodecUnitTests.java b/src/test/java/io/lettuce/core/codec/CipherCodecUnitTests.java index c6f8c1e654..82b680fc69 100644 --- a/src/test/java/io/lettuce/core/codec/CipherCodecUnitTests.java +++ b/src/test/java/io/lettuce/core/codec/CipherCodecUnitTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.codec; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -14,6 +15,7 @@ import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -27,6 +29,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class CipherCodecUnitTests { private final SecretKeySpec key = new SecretKeySpec("1234567890123456".getBytes(), "AES"); diff --git a/src/test/java/io/lettuce/core/codec/CompressionCodecUnitTests.java b/src/test/java/io/lettuce/core/codec/CompressionCodecUnitTests.java index 0351d9a51a..6c22a4fdf4 100644 --- a/src/test/java/io/lettuce/core/codec/CompressionCodecUnitTests.java +++ b/src/test/java/io/lettuce/core/codec/CompressionCodecUnitTests.java @@ -1,10 +1,12 @@ package io.lettuce.core.codec; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; /** @@ -12,6 +14,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class CompressionCodecUnitTests { private String key = "key"; diff --git a/src/test/java/io/lettuce/core/codec/StringCodecUnitTests.java b/src/test/java/io/lettuce/core/codec/StringCodecUnitTests.java index 8dfdd1ca56..03961d9188 100644 --- a/src/test/java/io/lettuce/core/codec/StringCodecUnitTests.java +++ b/src/test/java/io/lettuce/core/codec/StringCodecUnitTests.java @@ -19,11 +19,13 @@ */ package io.lettuce.core.codec; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.netty.buffer.ByteBuf; @@ -36,6 +38,7 @@ * @author Mark Paluch * @author Dimitris Mandalidis */ +@Tag(UNIT_TEST) class StringCodecUnitTests { private String teststring = "hello üäü~∑†®†ª€∂‚¶¢ Wørld"; diff --git a/src/test/java/io/lettuce/core/commands/AclCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/AclCommandIntegrationTests.java index 9ed326d999..7fbeb7cf9f 100644 --- a/src/test/java/io/lettuce/core/commands/AclCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/AclCommandIntegrationTests.java @@ -19,11 +19,13 @@ */ package io.lettuce.core.commands; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import javax.inject.Inject; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; @@ -47,6 +49,7 @@ * @author Mikhael Sokolov * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) @EnabledOnCommand("ACL") diff --git a/src/test/java/io/lettuce/core/commands/BitCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/BitCommandIntegrationTests.java index 2752bc813c..6bc667e9b4 100644 --- a/src/test/java/io/lettuce/core/commands/BitCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/BitCommandIntegrationTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core.commands; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static io.lettuce.core.BitFieldArgs.offset; import static io.lettuce.core.BitFieldArgs.signed; import static io.lettuce.core.BitFieldArgs.typeWidthBasedOffset; @@ -33,6 +34,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; @@ -48,6 +50,7 @@ * @author Will Glozer * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class BitCommandIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/commands/CustomCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/CustomCommandIntegrationTests.java index ceb49722e2..cadfa08f3e 100644 --- a/src/test/java/io/lettuce/core/commands/CustomCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/CustomCommandIntegrationTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core.commands; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assumptions.*; @@ -27,6 +28,7 @@ import javax.inject.Inject; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; @@ -53,6 +55,7 @@ * * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class CustomCommandIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/commands/FunctionCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/FunctionCommandIntegrationTests.java index 5340b07c0b..e303080bef 100644 --- a/src/test/java/io/lettuce/core/commands/FunctionCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/FunctionCommandIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.commands; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static io.lettuce.core.ScriptOutputType.*; import static org.assertj.core.api.Assertions.*; @@ -10,6 +11,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; @@ -29,6 +31,7 @@ * * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) @EnabledOnCommand("FUNCTION") diff --git a/src/test/java/io/lettuce/core/commands/GeoCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/GeoCommandIntegrationTests.java index 478fb117ed..849b3cdeb6 100644 --- a/src/test/java/io/lettuce/core/commands/GeoCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/GeoCommandIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.commands; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assumptions.*; @@ -9,6 +10,7 @@ import javax.inject.Inject; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; @@ -22,6 +24,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) @EnabledOnCommand("GEOADD") @TestInstance(TestInstance.Lifecycle.PER_CLASS) diff --git a/src/test/java/io/lettuce/core/commands/HLLCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/HLLCommandIntegrationTests.java index 7749a2fd50..1c7e26a6bf 100644 --- a/src/test/java/io/lettuce/core/commands/HLLCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/HLLCommandIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.commands; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Fail.fail; @@ -7,6 +8,7 @@ import javax.inject.Inject; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; @@ -18,6 +20,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class HLLCommandIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/commands/HashCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/HashCommandIntegrationTests.java index 2cd1608e4a..70a2a263e2 100644 --- a/src/test/java/io/lettuce/core/commands/HashCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/HashCommandIntegrationTests.java @@ -33,6 +33,7 @@ import io.lettuce.test.ListStreamingAdapter; import io.lettuce.test.condition.EnabledOnCommand; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; @@ -48,6 +49,7 @@ import java.util.Map; import java.util.Set; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.offset; import static org.awaitility.Awaitility.await; @@ -59,6 +61,7 @@ * @author Mark Paluch * @author Hodur Heidarsson */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class HashCommandIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/commands/KeyCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/KeyCommandIntegrationTests.java index 45d7e3840d..c24e231f17 100644 --- a/src/test/java/io/lettuce/core/commands/KeyCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/KeyCommandIntegrationTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core.commands; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import java.time.Duration; @@ -33,6 +34,7 @@ import javax.inject.Inject; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; @@ -58,6 +60,7 @@ * @author Mark Paluch * @author dengliming */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class KeyCommandIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/commands/ListCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/ListCommandIntegrationTests.java index 4f26a86804..d5ea4c4673 100644 --- a/src/test/java/io/lettuce/core/commands/ListCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/ListCommandIntegrationTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core.commands; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assumptions.*; @@ -30,6 +31,7 @@ import javax.inject.Inject; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; @@ -52,6 +54,7 @@ * @author Mikhael Sokolov * @author M Sazzadul Hoque */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class ListCommandIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/commands/NumericCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/NumericCommandIntegrationTests.java index 6a2a7c1000..963a4cbc99 100644 --- a/src/test/java/io/lettuce/core/commands/NumericCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/NumericCommandIntegrationTests.java @@ -19,12 +19,14 @@ */ package io.lettuce.core.commands; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.offset; import javax.inject.Inject; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; @@ -37,6 +39,7 @@ * @author Will Glozer * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class NumericCommandIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/commands/RunOnlyOnceServerCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/RunOnlyOnceServerCommandIntegrationTests.java index 25d75291de..2e94eb035c 100644 --- a/src/test/java/io/lettuce/core/commands/RunOnlyOnceServerCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/RunOnlyOnceServerCommandIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.commands; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static io.lettuce.test.settings.TestSettings.*; import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assumptions.*; @@ -9,6 +10,7 @@ import javax.inject.Inject; import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -28,6 +30,7 @@ * @author Will Glozer * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class RunOnlyOnceServerCommandIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/commands/ScriptingCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/ScriptingCommandIntegrationTests.java index 1c7afd30e2..bc2b83eb53 100644 --- a/src/test/java/io/lettuce/core/commands/ScriptingCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/ScriptingCommandIntegrationTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core.commands; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static io.lettuce.core.ScriptOutputType.*; import static io.lettuce.core.ScriptOutputType.BOOLEAN; import static io.lettuce.core.ScriptOutputType.INTEGER; @@ -31,6 +32,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; @@ -52,6 +54,7 @@ * @author Mark Paluch * @author dengliming */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class ScriptingCommandIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/commands/ServerCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/ServerCommandIntegrationTests.java index a64414edf4..d523a88555 100644 --- a/src/test/java/io/lettuce/core/commands/ServerCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/ServerCommandIntegrationTests.java @@ -19,9 +19,6 @@ */ package io.lettuce.core.commands; -import static org.assertj.core.api.Assertions.*; -import static org.junit.jupiter.api.Assumptions.*; - import java.util.Date; import java.util.HashMap; import java.util.List; @@ -38,6 +35,7 @@ import io.lettuce.test.condition.RedisConditions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; @@ -57,6 +55,12 @@ import io.lettuce.test.condition.EnabledOnCommand; import io.lettuce.test.settings.TestSettings; +import static io.lettuce.TestTags.INTEGRATION_TEST; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assumptions.assumeFalse; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + /** * Integration tests for {@link io.lettuce.core.api.sync.RedisServerCommands}. * @@ -65,6 +69,7 @@ * @author Zhang Jessey * @author dengliming */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class ServerCommandIntegrationTests extends TestSupport { @@ -118,6 +123,30 @@ void clientCaching() { } } + @Test + void clientTrackinginfoDefaults() { + TrackingInfo info = redis.clientTrackinginfo(); + + assertThat(info.getFlags()).contains(TrackingInfo.TrackingFlag.OFF); + assertThat(info.getRedirect()).isEqualTo(-1L); + assertThat(info.getPrefixes()).isEmpty(); + } + + @Test + void clientTrackinginfo() { + try { + redis.clientTracking(TrackingArgs.Builder.enabled(true).bcast().prefixes("usr:", "grp:")); + TrackingInfo info = redis.clientTrackinginfo(); + + assertThat(info.getFlags()).contains(TrackingInfo.TrackingFlag.ON); + assertThat(info.getFlags()).contains(TrackingInfo.TrackingFlag.BCAST); + assertThat(info.getRedirect()).isEqualTo(0L); + assertThat(info.getPrefixes()).contains("usr:", "grp:"); + } finally { + redis.clientTracking(TrackingArgs.Builder.enabled(false)); + } + } + @Test void clientGetSetname() { assertThat(redis.clientGetname()).isNull(); @@ -180,7 +209,8 @@ void clientKillExtended() { } @Test - @EnabledOnCommand("XAUTOCLAIM") // Redis 6.2 + @EnabledOnCommand("XAUTOCLAIM") + // Redis 6.2 void clientKillUser() { RedisCommands connection2 = client.connect().sync(); redis.aclSetuser("test_kill", AclSetuserArgs.Builder.addPassword("password1").on().addCommand(CommandType.ACL)); @@ -218,7 +248,8 @@ void clientList() { } @Test - @EnabledOnCommand("WAITAOF") // Redis 7.2 + @EnabledOnCommand("WAITAOF") + // Redis 7.2 void clientListExtended() { Long clientId = redis.clientId(); @@ -229,7 +260,8 @@ void clientListExtended() { } @Test - @EnabledOnCommand("EVAL_RO") // Redis 7.0 + @EnabledOnCommand("EVAL_RO") + // Redis 7.0 void clientNoEvict() { assertThat(redis.clientNoEvict(true)).isEqualTo("OK"); assertThat(redis.clientNoEvict(false)).isEqualTo("OK"); @@ -359,7 +391,8 @@ void configGet() { } @Test - @EnabledOnCommand("EVAL_RO") // Redis 7.0 + @EnabledOnCommand("EVAL_RO") + // Redis 7.0 void configGetMultipleParameters() { assertThat(redis.configGet("maxmemory", "*max-*-entries*")).containsEntry("maxmemory", "0") .containsEntry("hash-max-listpack-entries", "512"); @@ -382,7 +415,8 @@ void configSet() { } @Test - @EnabledOnCommand("EVAL_RO") // Redis 7.0 + @EnabledOnCommand("EVAL_RO") + // Redis 7.0 void configSetMultipleParameters() { Map original = redis.configGet("maxmemory", "hash-max-listpack-entries"); Map config = new HashMap<>(); @@ -416,7 +450,8 @@ void flushall() { } @Test - @EnabledOnCommand("MEMORY") // Redis 4.0 + @EnabledOnCommand("MEMORY") + // Redis 4.0 void flushallAsync() { redis.set(key, value); assertThat(redis.flushallAsync()).isEqualTo("OK"); @@ -424,7 +459,8 @@ void flushallAsync() { } @Test - @EnabledOnCommand("XAUTOCLAIM") // Redis 6.2 + @EnabledOnCommand("XAUTOCLAIM") + // Redis 6.2 void flushallSync() { redis.set(key, value); assertThat(redis.flushall(FlushMode.SYNC)).isEqualTo("OK"); @@ -439,7 +475,8 @@ void flushdb() { } @Test - @EnabledOnCommand("MEMORY") // Redis 4.0 + @EnabledOnCommand("MEMORY") + // Redis 4.0 void flushdbAsync() { redis.set(key, value); redis.select(1); @@ -451,7 +488,8 @@ void flushdbAsync() { } @Test - @EnabledOnCommand("XAUTOCLAIM") // Redis 6.2 + @EnabledOnCommand("XAUTOCLAIM") + // Redis 6.2 void flushdbSync() { redis.set(key, value); assertThat(redis.flushdb(FlushMode.SYNC)).isEqualTo("OK"); @@ -575,19 +613,22 @@ void swapdb() { } @Test - @Disabled("Run me manually") // Redis 7.0 + @Disabled("Run me manually") + // Redis 7.0 void shutdown() { redis.shutdown(new ShutdownArgs().save(true).now()); } @Test - @EnabledOnCommand("WAITAOF") // Redis 7.2 + @EnabledOnCommand("WAITAOF") + // Redis 7.2 void clientInfo() { assertThat(redis.clientInfo().contains("addr=")).isTrue(); } @Test - @EnabledOnCommand("WAITAOF") // Redis 7.2 + @EnabledOnCommand("WAITAOF") + // Redis 7.2 void clientSetinfo() { redis.clientSetinfo("lib-name", "lettuce"); @@ -597,7 +638,7 @@ void clientSetinfo() { @Test void testReadOnlyCommands() { for (ProtocolKeyword readOnlyCommand : ClusterReadOnlyCommands.getReadOnlyCommands()) { - assertThat(isCommandReadOnly(readOnlyCommand.name())).isTrue(); + assertThat(isCommandReadOnly(readOnlyCommand.toString())).isTrue(); } } diff --git a/src/test/java/io/lettuce/core/commands/SetCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/SetCommandIntegrationTests.java index 9f0d579eac..4775635663 100644 --- a/src/test/java/io/lettuce/core/commands/SetCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/SetCommandIntegrationTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core.commands; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assumptions.assumeTrue; @@ -31,6 +32,7 @@ import javax.inject.Inject; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; @@ -47,6 +49,7 @@ * @author Mark Paluch * @author dengliming */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class SetCommandIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/commands/SortCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/SortCommandIntegrationTests.java index a408016469..356dd41c51 100644 --- a/src/test/java/io/lettuce/core/commands/SortCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/SortCommandIntegrationTests.java @@ -19,12 +19,14 @@ */ package io.lettuce.core.commands; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static io.lettuce.core.SortArgs.Builder.*; import static org.assertj.core.api.Assertions.*; import javax.inject.Inject; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; @@ -41,6 +43,7 @@ * @author Will Glozer * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class SortCommandIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/commands/SortedSetCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/SortedSetCommandIntegrationTests.java index 1e37c5850d..d53e5c16e8 100644 --- a/src/test/java/io/lettuce/core/commands/SortedSetCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/SortedSetCommandIntegrationTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core.commands; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static io.lettuce.core.Range.Boundary.*; import static io.lettuce.core.ZStoreArgs.Builder.*; import static io.lettuce.core.ZStoreArgs.Builder.max; @@ -35,6 +36,7 @@ import javax.inject.Inject; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; @@ -54,6 +56,7 @@ * @author dengliming * @author Mikhael Sokolov */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class SortedSetCommandIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/commands/StreamCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/StreamCommandIntegrationTests.java index 0a4b233a9c..1816d3f480 100644 --- a/src/test/java/io/lettuce/core/commands/StreamCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/StreamCommandIntegrationTests.java @@ -31,6 +31,7 @@ import io.lettuce.test.LettuceExtension; import io.lettuce.test.condition.EnabledOnCommand; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; @@ -45,6 +46,7 @@ import java.util.List; import java.util.Map; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static io.lettuce.core.protocol.CommandType.XINFO; import static org.assertj.core.api.Assertions.assertThat; @@ -57,6 +59,7 @@ @ExtendWith(LettuceExtension.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) @EnabledOnCommand("XADD") +@Tag(INTEGRATION_TEST) public class StreamCommandIntegrationTests extends TestSupport { private final RedisCommands redis; diff --git a/src/test/java/io/lettuce/core/commands/StringCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/StringCommandIntegrationTests.java index 0bee9425fd..b32a9beb86 100644 --- a/src/test/java/io/lettuce/core/commands/StringCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/StringCommandIntegrationTests.java @@ -19,10 +19,13 @@ */ package io.lettuce.core.commands; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static io.lettuce.core.SetArgs.Builder.*; -import static io.lettuce.core.StringMatchResult.*; -import static org.assertj.core.api.Assertions.*; +import static io.lettuce.core.StringMatchResult.Position; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import java.lang.reflect.Proxy; import java.time.Duration; import java.time.Instant; import java.util.LinkedHashMap; @@ -31,19 +34,21 @@ import javax.inject.Inject; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; -import io.lettuce.core.GetExArgs; -import io.lettuce.core.KeyValue; -import io.lettuce.core.RedisException; -import io.lettuce.core.SetArgs; -import io.lettuce.core.StrAlgoArgs; -import io.lettuce.core.StringMatchResult; -import io.lettuce.core.TestSupport; +import io.lettuce.core.*; +import io.lettuce.core.api.StatefulConnection; +import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.api.sync.RedisCommands; +import io.lettuce.core.dynamic.Commands; +import io.lettuce.core.dynamic.RedisCommandFactory; +import io.lettuce.core.dynamic.annotation.Command; +import io.lettuce.core.dynamic.annotation.Param; import io.lettuce.test.KeyValueStreamingAdapter; import io.lettuce.test.LettuceExtension; import io.lettuce.test.condition.EnabledOnCommand; @@ -56,6 +61,7 @@ * @author dengliming * @author Andrey Shlykov */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class StringCommandIntegrationTests extends TestSupport { @@ -373,4 +379,117 @@ void strAlgoWithIdx() { assertThat(matchResult.getLen()).isEqualTo(6); } + @Test + @EnabledOnCommand("LCS") + void lcs() { + redis.set("key1", "ohmytext"); + redis.set("key2", "mynewtext"); + + // LCS key1 key2 + CustomStringCommands commands = CustomStringCommands.instance(getConnection()); + StringMatchResult matchResult = commands.lcs("key1", "key2"); + assertThat(matchResult.getMatchString()).isEqualTo("mytext"); + + // LCS a b IDX MINMATCHLEN 4 WITHMATCHLEN + // Keys don't exist. + matchResult = commands.lcsMinMatchLenWithMatchLen("a", "b", 4); + assertThat(matchResult.getMatchString()).isNullOrEmpty(); + assertThat(matchResult.getLen()).isEqualTo(0); + } + + @Test + @EnabledOnCommand("LCS") + void lcsUsingKeys() { + + redis.set("key1{k}", "ohmytext"); + redis.set("key2{k}", "mynewtext"); + + CustomStringCommands commands = CustomStringCommands.instance(getConnection()); + + StringMatchResult matchResult = commands.lcs("key1{k}", "key2{k}"); + assertThat(matchResult.getMatchString()).isEqualTo("mytext"); + + // STRALGO LCS STRINGS a b + matchResult = commands.lcsMinMatchLenWithMatchLen("a", "b", 4); + assertThat(matchResult.getMatchString()).isNullOrEmpty(); + assertThat(matchResult.getLen()).isEqualTo(0); + } + + @Test + @EnabledOnCommand("LCS") + void lcsJustLen() { + redis.set("one", "ohmytext"); + redis.set("two", "mynewtext"); + + CustomStringCommands commands = CustomStringCommands.instance(getConnection()); + + StringMatchResult matchResult = commands.lcsLen("one", "two"); + + assertThat(matchResult.getLen()).isEqualTo(6); + } + + @Test + @EnabledOnCommand("LCS") + void lcsWithMinMatchLen() { + redis.set("key1", "ohmytext"); + redis.set("key2", "mynewtext"); + + CustomStringCommands commands = CustomStringCommands.instance(getConnection()); + + StringMatchResult matchResult = commands.lcsMinMatchLen("key1", "key2", 4); + + assertThat(matchResult.getMatchString()).isEqualTo("mytext"); + } + + @Test + @EnabledOnCommand("LCS") + void lcsMinMatchLenIdxMatchLen() { + redis.set("key1", "ohmytext"); + redis.set("key2", "mynewtext"); + + CustomStringCommands commands = CustomStringCommands.instance(getConnection()); + + // LCS key1 key2 IDX MINMATCHLEN 4 WITHMATCHLEN + StringMatchResult matchResult = commands.lcsMinMatchLenWithMatchLen("key1", "key2", 4); + + assertThat(matchResult.getMatches()).hasSize(1); + assertThat(matchResult.getMatches().get(0).getMatchLen()).isEqualTo(4); + + Position a = matchResult.getMatches().get(0).getA(); + Position b = matchResult.getMatches().get(0).getB(); + + assertThat(a.getStart()).isEqualTo(4); + assertThat(a.getEnd()).isEqualTo(7); + assertThat(b.getStart()).isEqualTo(5); + assertThat(b.getEnd()).isEqualTo(8); + assertThat(matchResult.getLen()).isEqualTo(6); + } + + protected StatefulConnection getConnection() { + StatefulRedisConnection src = redis.getStatefulConnection(); + Assumptions.assumeFalse(Proxy.isProxyClass(src.getClass()), "Redis connection is proxy, skipping."); + return src; + } + + private interface CustomStringCommands extends Commands { + + @Command("LCS :k1 :k2") + StringMatchResult lcs(@Param("k1") String k1, @Param("k2") String k2); + + @Command("LCS :k1 :k2 LEN") + StringMatchResult lcsLen(@Param("k1") String k1, @Param("k2") String k2); + + @Command("LCS :k1 :k2 MINMATCHLEN :mml") + StringMatchResult lcsMinMatchLen(@Param("k1") String k1, @Param("k2") String k2, @Param("mml") int mml); + + @Command("LCS :k1 :k2 IDX MINMATCHLEN :mml WITHMATCHLEN") + StringMatchResult lcsMinMatchLenWithMatchLen(@Param("k1") String k1, @Param("k2") String k2, @Param("mml") int mml); + + static CustomStringCommands instance(StatefulConnection conn) { + RedisCommandFactory factory = new RedisCommandFactory(conn); + return factory.getCommands(CustomStringCommands.class); + } + + } + } diff --git a/src/test/java/io/lettuce/core/commands/TransactionCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/TransactionCommandIntegrationTests.java index df8d6adf86..c0f4b5f92d 100644 --- a/src/test/java/io/lettuce/core/commands/TransactionCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/TransactionCommandIntegrationTests.java @@ -19,12 +19,14 @@ */ package io.lettuce.core.commands; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import javax.inject.Inject; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; @@ -37,6 +39,7 @@ * @author Will Glozer * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class TransactionCommandIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/commands/reactive/AclReactiveCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/reactive/AclReactiveCommandIntegrationTests.java index e3fba38f39..ebc7b3ef06 100644 --- a/src/test/java/io/lettuce/core/commands/reactive/AclReactiveCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/reactive/AclReactiveCommandIntegrationTests.java @@ -5,12 +5,16 @@ import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.commands.AclCommandIntegrationTests; import io.lettuce.test.ReactiveSyncInvocationHandler; +import org.junit.jupiter.api.Tag; + +import static io.lettuce.TestTags.INTEGRATION_TEST; /** * Integration tests though the reactive facade for {@link AclCommandIntegrationTests}. * * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class AclReactiveCommandIntegrationTests extends AclCommandIntegrationTests { @Inject diff --git a/src/test/java/io/lettuce/core/commands/reactive/BitReactiveCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/reactive/BitReactiveCommandIntegrationTests.java index ed5c30f36d..648a074885 100644 --- a/src/test/java/io/lettuce/core/commands/reactive/BitReactiveCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/reactive/BitReactiveCommandIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.commands.reactive; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static io.lettuce.core.BitFieldArgs.offset; import static io.lettuce.core.BitFieldArgs.signed; import static io.lettuce.core.BitFieldArgs.typeWidthBasedOffset; @@ -8,6 +9,7 @@ import javax.inject.Inject; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import reactor.test.StepVerifier; @@ -22,6 +24,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class BitReactiveCommandIntegrationTests extends BitCommandIntegrationTests { private RedisStringReactiveCommands reactive; diff --git a/src/test/java/io/lettuce/core/commands/reactive/CustomReactiveCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/reactive/CustomReactiveCommandIntegrationTests.java index 6e82d20391..20c4a63381 100644 --- a/src/test/java/io/lettuce/core/commands/reactive/CustomReactiveCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/reactive/CustomReactiveCommandIntegrationTests.java @@ -2,6 +2,7 @@ import javax.inject.Inject; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -18,9 +19,12 @@ import io.lettuce.core.protocol.CommandType; import io.lettuce.test.LettuceExtension; +import static io.lettuce.TestTags.INTEGRATION_TEST; + /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class CustomReactiveCommandIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/commands/reactive/FunctionReactiveCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/reactive/FunctionReactiveCommandIntegrationTests.java index f9bf8958d4..dddbb1ca3e 100644 --- a/src/test/java/io/lettuce/core/commands/reactive/FunctionReactiveCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/reactive/FunctionReactiveCommandIntegrationTests.java @@ -6,10 +6,14 @@ import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.commands.FunctionCommandIntegrationTests; import io.lettuce.test.ReactiveSyncInvocationHandler; +import org.junit.jupiter.api.Tag; + +import static io.lettuce.TestTags.INTEGRATION_TEST; /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class FunctionReactiveCommandIntegrationTests extends FunctionCommandIntegrationTests { @Inject diff --git a/src/test/java/io/lettuce/core/commands/reactive/GeoReactiveCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/reactive/GeoReactiveCommandIntegrationTests.java index da0975c0b2..9617c695a8 100644 --- a/src/test/java/io/lettuce/core/commands/reactive/GeoReactiveCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/reactive/GeoReactiveCommandIntegrationTests.java @@ -1,11 +1,13 @@ package io.lettuce.core.commands.reactive; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.offset; import javax.inject.Inject; import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import reactor.test.StepVerifier; @@ -17,6 +19,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class GeoReactiveCommandIntegrationTests extends GeoCommandIntegrationTests { private final StatefulRedisConnection connection; diff --git a/src/test/java/io/lettuce/core/commands/reactive/HLLReactiveCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/reactive/HLLReactiveCommandIntegrationTests.java index fe4d90d746..f0e4737c95 100644 --- a/src/test/java/io/lettuce/core/commands/reactive/HLLReactiveCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/reactive/HLLReactiveCommandIntegrationTests.java @@ -5,10 +5,14 @@ import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.commands.HLLCommandIntegrationTests; import io.lettuce.test.ReactiveSyncInvocationHandler; +import org.junit.jupiter.api.Tag; + +import static io.lettuce.TestTags.INTEGRATION_TEST; /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class HLLReactiveCommandIntegrationTests extends HLLCommandIntegrationTests { @Inject diff --git a/src/test/java/io/lettuce/core/commands/reactive/HashReactiveCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/reactive/HashReactiveCommandIntegrationTests.java index cca4947dca..57f566f389 100644 --- a/src/test/java/io/lettuce/core/commands/reactive/HashReactiveCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/reactive/HashReactiveCommandIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.commands.reactive; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.util.stream.Collectors; @@ -7,6 +8,7 @@ import javax.inject.Inject; import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import reactor.test.StepVerifier; @@ -19,6 +21,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class HashReactiveCommandIntegrationTests extends HashCommandIntegrationTests { private final StatefulRedisConnection connection; diff --git a/src/test/java/io/lettuce/core/commands/reactive/KeyReactiveCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/reactive/KeyReactiveCommandIntegrationTests.java index 134f4ce1a6..5dd50109c7 100644 --- a/src/test/java/io/lettuce/core/commands/reactive/KeyReactiveCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/reactive/KeyReactiveCommandIntegrationTests.java @@ -5,10 +5,14 @@ import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.commands.KeyCommandIntegrationTests; import io.lettuce.test.ReactiveSyncInvocationHandler; +import org.junit.jupiter.api.Tag; + +import static io.lettuce.TestTags.INTEGRATION_TEST; /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class KeyReactiveCommandIntegrationTests extends KeyCommandIntegrationTests { @Inject diff --git a/src/test/java/io/lettuce/core/commands/reactive/ListReactiveCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/reactive/ListReactiveCommandIntegrationTests.java index 58e4f43d35..bec74c7ac4 100644 --- a/src/test/java/io/lettuce/core/commands/reactive/ListReactiveCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/reactive/ListReactiveCommandIntegrationTests.java @@ -5,10 +5,14 @@ import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.commands.ListCommandIntegrationTests; import io.lettuce.test.ReactiveSyncInvocationHandler; +import org.junit.jupiter.api.Tag; + +import static io.lettuce.TestTags.INTEGRATION_TEST; /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class ListReactiveCommandIntegrationTests extends ListCommandIntegrationTests { @Inject diff --git a/src/test/java/io/lettuce/core/commands/reactive/NumericReactiveCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/reactive/NumericReactiveCommandIntegrationTests.java index 39a0fb78e1..40761770cd 100644 --- a/src/test/java/io/lettuce/core/commands/reactive/NumericReactiveCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/reactive/NumericReactiveCommandIntegrationTests.java @@ -5,10 +5,14 @@ import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.commands.NumericCommandIntegrationTests; import io.lettuce.test.ReactiveSyncInvocationHandler; +import org.junit.jupiter.api.Tag; + +import static io.lettuce.TestTags.INTEGRATION_TEST; /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class NumericReactiveCommandIntegrationTests extends NumericCommandIntegrationTests { @Inject diff --git a/src/test/java/io/lettuce/core/commands/reactive/ScriptingReactiveCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/reactive/ScriptingReactiveCommandIntegrationTests.java index fcd5a574d4..824fdbf988 100644 --- a/src/test/java/io/lettuce/core/commands/reactive/ScriptingReactiveCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/reactive/ScriptingReactiveCommandIntegrationTests.java @@ -6,10 +6,14 @@ import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.commands.ScriptingCommandIntegrationTests; import io.lettuce.test.ReactiveSyncInvocationHandler; +import org.junit.jupiter.api.Tag; + +import static io.lettuce.TestTags.INTEGRATION_TEST; /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class ScriptingReactiveCommandIntegrationTests extends ScriptingCommandIntegrationTests { @Inject diff --git a/src/test/java/io/lettuce/core/commands/reactive/ServerReactiveCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/reactive/ServerReactiveCommandIntegrationTests.java index b811298065..e8d60e951c 100644 --- a/src/test/java/io/lettuce/core/commands/reactive/ServerReactiveCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/reactive/ServerReactiveCommandIntegrationTests.java @@ -1,11 +1,13 @@ package io.lettuce.core.commands.reactive; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.util.List; import javax.inject.Inject; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.RedisClient; @@ -22,6 +24,7 @@ * * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class ServerReactiveCommandIntegrationTests extends ServerCommandIntegrationTests { private RedisReactiveCommands reactive; diff --git a/src/test/java/io/lettuce/core/commands/reactive/SetReactiveCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/reactive/SetReactiveCommandIntegrationTests.java index 8bef010797..759bdf2702 100644 --- a/src/test/java/io/lettuce/core/commands/reactive/SetReactiveCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/reactive/SetReactiveCommandIntegrationTests.java @@ -5,10 +5,14 @@ import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.commands.SetCommandIntegrationTests; import io.lettuce.test.ReactiveSyncInvocationHandler; +import org.junit.jupiter.api.Tag; + +import static io.lettuce.TestTags.INTEGRATION_TEST; /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class SetReactiveCommandIntegrationTests extends SetCommandIntegrationTests { @Inject diff --git a/src/test/java/io/lettuce/core/commands/reactive/SortReactiveCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/reactive/SortReactiveCommandIntegrationTests.java index 6e7bcc95e6..b8f7e3dfc5 100644 --- a/src/test/java/io/lettuce/core/commands/reactive/SortReactiveCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/reactive/SortReactiveCommandIntegrationTests.java @@ -5,10 +5,14 @@ import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.commands.SortCommandIntegrationTests; import io.lettuce.test.ReactiveSyncInvocationHandler; +import org.junit.jupiter.api.Tag; + +import static io.lettuce.TestTags.INTEGRATION_TEST; /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class SortReactiveCommandIntegrationTests extends SortCommandIntegrationTests { @Inject diff --git a/src/test/java/io/lettuce/core/commands/reactive/SortedSetReactiveCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/reactive/SortedSetReactiveCommandIntegrationTests.java index 71ab51524a..9f2a1511a1 100644 --- a/src/test/java/io/lettuce/core/commands/reactive/SortedSetReactiveCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/reactive/SortedSetReactiveCommandIntegrationTests.java @@ -1,9 +1,11 @@ package io.lettuce.core.commands.reactive; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import javax.inject.Inject; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import reactor.test.StepVerifier; @@ -18,6 +20,7 @@ * * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class SortedSetReactiveCommandIntegrationTests extends SortedSetCommandIntegrationTests { private final RedisReactiveCommands reactive; diff --git a/src/test/java/io/lettuce/core/commands/reactive/StreamReactiveCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/reactive/StreamReactiveCommandIntegrationTests.java index 218bd4e412..b1d17175e0 100644 --- a/src/test/java/io/lettuce/core/commands/reactive/StreamReactiveCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/reactive/StreamReactiveCommandIntegrationTests.java @@ -5,10 +5,14 @@ import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.commands.StreamCommandIntegrationTests; import io.lettuce.test.ReactiveSyncInvocationHandler; +import org.junit.jupiter.api.Tag; + +import static io.lettuce.TestTags.INTEGRATION_TEST; /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class StreamReactiveCommandIntegrationTests extends StreamCommandIntegrationTests { @Inject diff --git a/src/test/java/io/lettuce/core/commands/reactive/StringReactiveCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/reactive/StringReactiveCommandIntegrationTests.java index ea81cebd85..7a878799af 100644 --- a/src/test/java/io/lettuce/core/commands/reactive/StringReactiveCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/reactive/StringReactiveCommandIntegrationTests.java @@ -2,6 +2,7 @@ import javax.inject.Inject; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; @@ -13,9 +14,12 @@ import io.lettuce.core.commands.StringCommandIntegrationTests; import io.lettuce.test.ReactiveSyncInvocationHandler; +import static io.lettuce.TestTags.INTEGRATION_TEST; + /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class StringReactiveCommandIntegrationTests extends StringCommandIntegrationTests { private final RedisCommands redis; diff --git a/src/test/java/io/lettuce/core/commands/reactive/TransactionReactiveCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/reactive/TransactionReactiveCommandIntegrationTests.java index 771da89434..bba86a02ed 100644 --- a/src/test/java/io/lettuce/core/commands/reactive/TransactionReactiveCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/reactive/TransactionReactiveCommandIntegrationTests.java @@ -1,9 +1,11 @@ package io.lettuce.core.commands.reactive; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import javax.inject.Inject; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.KeyValue; @@ -18,6 +20,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) public class TransactionReactiveCommandIntegrationTests extends TransactionCommandIntegrationTests { private final RedisClient client; diff --git a/src/test/java/io/lettuce/core/commands/transactional/BitTxCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/transactional/BitTxCommandIntegrationTests.java index 9cdbb9f1e7..680ccc63f6 100644 --- a/src/test/java/io/lettuce/core/commands/transactional/BitTxCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/transactional/BitTxCommandIntegrationTests.java @@ -5,10 +5,14 @@ import io.lettuce.core.RedisClient; import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.commands.BitCommandIntegrationTests; +import org.junit.jupiter.api.Tag; + +import static io.lettuce.TestTags.INTEGRATION_TEST; /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class BitTxCommandIntegrationTests extends BitCommandIntegrationTests { @Inject diff --git a/src/test/java/io/lettuce/core/commands/transactional/GeoTxCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/transactional/GeoTxCommandIntegrationTests.java index 8d2f19c08d..5ff47979cd 100644 --- a/src/test/java/io/lettuce/core/commands/transactional/GeoTxCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/transactional/GeoTxCommandIntegrationTests.java @@ -6,10 +6,14 @@ import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.commands.GeoCommandIntegrationTests; +import org.junit.jupiter.api.Tag; + +import static io.lettuce.TestTags.INTEGRATION_TEST; /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class GeoTxCommandIntegrationTests extends GeoCommandIntegrationTests { @Inject diff --git a/src/test/java/io/lettuce/core/commands/transactional/HLLTxCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/transactional/HLLTxCommandIntegrationTests.java index c3707fe685..89d1d50e48 100644 --- a/src/test/java/io/lettuce/core/commands/transactional/HLLTxCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/transactional/HLLTxCommandIntegrationTests.java @@ -4,10 +4,14 @@ import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.commands.HLLCommandIntegrationTests; +import org.junit.jupiter.api.Tag; + +import static io.lettuce.TestTags.INTEGRATION_TEST; /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class HLLTxCommandIntegrationTests extends HLLCommandIntegrationTests { @Inject diff --git a/src/test/java/io/lettuce/core/commands/transactional/HashTxCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/transactional/HashTxCommandIntegrationTests.java index 12965fd2c3..20fe65db72 100644 --- a/src/test/java/io/lettuce/core/commands/transactional/HashTxCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/transactional/HashTxCommandIntegrationTests.java @@ -4,10 +4,14 @@ import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.commands.HashCommandIntegrationTests; +import org.junit.jupiter.api.Tag; + +import static io.lettuce.TestTags.INTEGRATION_TEST; /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class HashTxCommandIntegrationTests extends HashCommandIntegrationTests { @Inject diff --git a/src/test/java/io/lettuce/core/commands/transactional/KeyTxCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/transactional/KeyTxCommandIntegrationTests.java index cc9e298302..eb7b6db497 100644 --- a/src/test/java/io/lettuce/core/commands/transactional/KeyTxCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/transactional/KeyTxCommandIntegrationTests.java @@ -6,10 +6,14 @@ import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.commands.KeyCommandIntegrationTests; +import org.junit.jupiter.api.Tag; + +import static io.lettuce.TestTags.INTEGRATION_TEST; /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) public class KeyTxCommandIntegrationTests extends KeyCommandIntegrationTests { @Inject diff --git a/src/test/java/io/lettuce/core/commands/transactional/ListTxCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/transactional/ListTxCommandIntegrationTests.java index b86291b0f6..281168af8f 100644 --- a/src/test/java/io/lettuce/core/commands/transactional/ListTxCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/transactional/ListTxCommandIntegrationTests.java @@ -4,10 +4,14 @@ import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.commands.ListCommandIntegrationTests; +import org.junit.jupiter.api.Tag; + +import static io.lettuce.TestTags.INTEGRATION_TEST; /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class ListTxCommandIntegrationTests extends ListCommandIntegrationTests { @Inject diff --git a/src/test/java/io/lettuce/core/commands/transactional/SetTxCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/transactional/SetTxCommandIntegrationTests.java index 47714e0b87..2372f58f86 100644 --- a/src/test/java/io/lettuce/core/commands/transactional/SetTxCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/transactional/SetTxCommandIntegrationTests.java @@ -4,10 +4,14 @@ import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.commands.SetCommandIntegrationTests; +import org.junit.jupiter.api.Tag; + +import static io.lettuce.TestTags.INTEGRATION_TEST; /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class SetTxCommandIntegrationTests extends SetCommandIntegrationTests { @Inject diff --git a/src/test/java/io/lettuce/core/commands/transactional/SortTxCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/transactional/SortTxCommandIntegrationTests.java index 8813b5e34f..01e23a594d 100644 --- a/src/test/java/io/lettuce/core/commands/transactional/SortTxCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/transactional/SortTxCommandIntegrationTests.java @@ -4,10 +4,14 @@ import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.commands.SortCommandIntegrationTests; +import org.junit.jupiter.api.Tag; + +import static io.lettuce.TestTags.INTEGRATION_TEST; /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class SortTxCommandIntegrationTests extends SortCommandIntegrationTests { @Inject diff --git a/src/test/java/io/lettuce/core/commands/transactional/SortedSetTxCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/transactional/SortedSetTxCommandIntegrationTests.java index 3d443f6a1e..283976ad09 100644 --- a/src/test/java/io/lettuce/core/commands/transactional/SortedSetTxCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/transactional/SortedSetTxCommandIntegrationTests.java @@ -4,10 +4,14 @@ import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.commands.SortedSetCommandIntegrationTests; +import org.junit.jupiter.api.Tag; + +import static io.lettuce.TestTags.INTEGRATION_TEST; /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class SortedSetTxCommandIntegrationTests extends SortedSetCommandIntegrationTests { @Inject diff --git a/src/test/java/io/lettuce/core/commands/transactional/StreamTxCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/transactional/StreamTxCommandIntegrationTests.java index 489be34ef9..814baad9a3 100644 --- a/src/test/java/io/lettuce/core/commands/transactional/StreamTxCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/transactional/StreamTxCommandIntegrationTests.java @@ -3,14 +3,18 @@ import javax.inject.Inject; import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.commands.StreamCommandIntegrationTests; +import static io.lettuce.TestTags.INTEGRATION_TEST; + /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class StreamTxCommandIntegrationTests extends StreamCommandIntegrationTests { @Inject diff --git a/src/test/java/io/lettuce/core/commands/transactional/StringTxCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/transactional/StringTxCommandIntegrationTests.java index 46e388460e..fe83617f6f 100644 --- a/src/test/java/io/lettuce/core/commands/transactional/StringTxCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/transactional/StringTxCommandIntegrationTests.java @@ -4,10 +4,14 @@ import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.commands.StringCommandIntegrationTests; +import org.junit.jupiter.api.Tag; + +import static io.lettuce.TestTags.INTEGRATION_TEST; /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class StringTxCommandIntegrationTests extends StringCommandIntegrationTests { @Inject diff --git a/src/test/java/io/lettuce/core/dynamic/BatchExecutableCommandLookupStrategyUnitTests.java b/src/test/java/io/lettuce/core/dynamic/BatchExecutableCommandLookupStrategyUnitTests.java index 7163785b42..105eda036c 100644 --- a/src/test/java/io/lettuce/core/dynamic/BatchExecutableCommandLookupStrategyUnitTests.java +++ b/src/test/java/io/lettuce/core/dynamic/BatchExecutableCommandLookupStrategyUnitTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.dynamic; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @@ -8,6 +9,7 @@ import java.util.concurrent.Future; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -26,6 +28,7 @@ */ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) +@Tag(UNIT_TEST) class BatchExecutableCommandLookupStrategyUnitTests { @Mock diff --git a/src/test/java/io/lettuce/core/dynamic/CommandSegmentCommandFactoryUnitTests.java b/src/test/java/io/lettuce/core/dynamic/CommandSegmentCommandFactoryUnitTests.java index 1b962d1f7b..f7560d1131 100644 --- a/src/test/java/io/lettuce/core/dynamic/CommandSegmentCommandFactoryUnitTests.java +++ b/src/test/java/io/lettuce/core/dynamic/CommandSegmentCommandFactoryUnitTests.java @@ -1,10 +1,12 @@ package io.lettuce.core.dynamic; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; import java.util.concurrent.Future; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.ScanArgs; @@ -28,6 +30,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class CommandSegmentCommandFactoryUnitTests { @Test @@ -155,7 +158,7 @@ private String toString(RedisCommand command) { StringBuilder builder = new StringBuilder(); - builder.append(command.getType().name()); + builder.append(command.getType().toString()); String commandString = command.getArgs().toCommandString(); diff --git a/src/test/java/io/lettuce/core/dynamic/ConversionServiceUnitTests.java b/src/test/java/io/lettuce/core/dynamic/ConversionServiceUnitTests.java index 3a684a5882..28e01b4ff3 100644 --- a/src/test/java/io/lettuce/core/dynamic/ConversionServiceUnitTests.java +++ b/src/test/java/io/lettuce/core/dynamic/ConversionServiceUnitTests.java @@ -1,10 +1,12 @@ package io.lettuce.core.dynamic; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; import java.util.function.Function; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; @@ -14,6 +16,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class ConversionServiceUnitTests { private ConversionService sut = new ConversionService(); diff --git a/src/test/java/io/lettuce/core/dynamic/DeclaredCommandMethodUnitTests.java b/src/test/java/io/lettuce/core/dynamic/DeclaredCommandMethodUnitTests.java index c60efd0973..b4f1c5a5a9 100644 --- a/src/test/java/io/lettuce/core/dynamic/DeclaredCommandMethodUnitTests.java +++ b/src/test/java/io/lettuce/core/dynamic/DeclaredCommandMethodUnitTests.java @@ -1,10 +1,12 @@ package io.lettuce.core.dynamic; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.lang.reflect.Method; import java.util.concurrent.Future; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; @@ -12,6 +14,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class DeclaredCommandMethodUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/dynamic/DefaultCommandMethodVerifierUnitTests.java b/src/test/java/io/lettuce/core/dynamic/DefaultCommandMethodVerifierUnitTests.java index 8ad6f2e84d..84b7227ff7 100644 --- a/src/test/java/io/lettuce/core/dynamic/DefaultCommandMethodVerifierUnitTests.java +++ b/src/test/java/io/lettuce/core/dynamic/DefaultCommandMethodVerifierUnitTests.java @@ -1,11 +1,13 @@ package io.lettuce.core.dynamic; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.Fail.fail; import java.lang.reflect.Method; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.GeoCoordinates; @@ -24,6 +26,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class DefaultCommandMethodVerifierUnitTests { private DefaultCommandMethodVerifier sut; diff --git a/src/test/java/io/lettuce/core/dynamic/ParameterBinderUnitTests.java b/src/test/java/io/lettuce/core/dynamic/ParameterBinderUnitTests.java index bb065be03e..1808904da6 100644 --- a/src/test/java/io/lettuce/core/dynamic/ParameterBinderUnitTests.java +++ b/src/test/java/io/lettuce/core/dynamic/ParameterBinderUnitTests.java @@ -1,11 +1,13 @@ package io.lettuce.core.dynamic; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.Base64; import java.util.Collections; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; @@ -21,6 +23,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) @ExtendWith(MockitoExtension.class) class ParameterBinderUnitTests { diff --git a/src/test/java/io/lettuce/core/dynamic/ReactiveCommandSegmentCommandFactoryUnitTests.java b/src/test/java/io/lettuce/core/dynamic/ReactiveCommandSegmentCommandFactoryUnitTests.java index 11bd196489..8a9114f8ba 100644 --- a/src/test/java/io/lettuce/core/dynamic/ReactiveCommandSegmentCommandFactoryUnitTests.java +++ b/src/test/java/io/lettuce/core/dynamic/ReactiveCommandSegmentCommandFactoryUnitTests.java @@ -1,10 +1,12 @@ package io.lettuce.core.dynamic; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; import java.lang.reflect.Method; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; @@ -26,6 +28,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) @ExtendWith(MockitoExtension.class) class ReactiveCommandSegmentCommandFactoryUnitTests { diff --git a/src/test/java/io/lettuce/core/dynamic/ReactiveTypeAdaptersUnitTests.java b/src/test/java/io/lettuce/core/dynamic/ReactiveTypeAdaptersUnitTests.java index 151d8ddbe4..f75fbe989f 100644 --- a/src/test/java/io/lettuce/core/dynamic/ReactiveTypeAdaptersUnitTests.java +++ b/src/test/java/io/lettuce/core/dynamic/ReactiveTypeAdaptersUnitTests.java @@ -1,8 +1,10 @@ package io.lettuce.core.dynamic; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; @@ -16,6 +18,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class ReactiveTypeAdaptersUnitTests { private ConversionService conversionService = new ConversionService(); diff --git a/src/test/java/io/lettuce/core/dynamic/ReactiveTypeAdaptionIntegrationTests.java b/src/test/java/io/lettuce/core/dynamic/ReactiveTypeAdaptionIntegrationTests.java index 3bcb712b50..069ca7f2de 100644 --- a/src/test/java/io/lettuce/core/dynamic/ReactiveTypeAdaptionIntegrationTests.java +++ b/src/test/java/io/lettuce/core/dynamic/ReactiveTypeAdaptionIntegrationTests.java @@ -1,10 +1,12 @@ package io.lettuce.core.dynamic; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import javax.inject.Inject; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -19,6 +21,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class ReactiveTypeAdaptionIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/dynamic/RedisCommandsAsyncIntegrationTests.java b/src/test/java/io/lettuce/core/dynamic/RedisCommandsAsyncIntegrationTests.java index df94daa623..b98cd8154b 100644 --- a/src/test/java/io/lettuce/core/dynamic/RedisCommandsAsyncIntegrationTests.java +++ b/src/test/java/io/lettuce/core/dynamic/RedisCommandsAsyncIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.dynamic; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.util.concurrent.CompletableFuture; @@ -7,6 +8,7 @@ import javax.inject.Inject; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -18,6 +20,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class RedisCommandsAsyncIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/dynamic/RedisCommandsBatchingIntegrationTests.java b/src/test/java/io/lettuce/core/dynamic/RedisCommandsBatchingIntegrationTests.java index b786eb62c5..54466d87d9 100644 --- a/src/test/java/io/lettuce/core/dynamic/RedisCommandsBatchingIntegrationTests.java +++ b/src/test/java/io/lettuce/core/dynamic/RedisCommandsBatchingIntegrationTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core.dynamic; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Fail.fail; @@ -27,6 +28,7 @@ import javax.inject.Inject; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -48,6 +50,7 @@ * @author Mark Paluch * @author Lucio Paiva */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class RedisCommandsBatchingIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/dynamic/RedisCommandsClusterIntegrationTests.java b/src/test/java/io/lettuce/core/dynamic/RedisCommandsClusterIntegrationTests.java index d430f8b485..a561ec0063 100644 --- a/src/test/java/io/lettuce/core/dynamic/RedisCommandsClusterIntegrationTests.java +++ b/src/test/java/io/lettuce/core/dynamic/RedisCommandsClusterIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.dynamic; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.time.Duration; @@ -8,6 +9,7 @@ import javax.inject.Inject; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -22,6 +24,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class RedisCommandsClusterIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/dynamic/RedisCommandsIntegrationTests.java b/src/test/java/io/lettuce/core/dynamic/RedisCommandsIntegrationTests.java index 351989f03f..21c2b1e987 100644 --- a/src/test/java/io/lettuce/core/dynamic/RedisCommandsIntegrationTests.java +++ b/src/test/java/io/lettuce/core/dynamic/RedisCommandsIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.dynamic; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; import static org.mockito.Mockito.doThrow; @@ -9,6 +10,7 @@ import org.apache.commons.pool2.impl.GenericObjectPool; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito; @@ -30,6 +32,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class RedisCommandsIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/dynamic/RedisCommandsReactiveIntegrationTests.java b/src/test/java/io/lettuce/core/dynamic/RedisCommandsReactiveIntegrationTests.java index d394bb10bb..baa0f65d20 100644 --- a/src/test/java/io/lettuce/core/dynamic/RedisCommandsReactiveIntegrationTests.java +++ b/src/test/java/io/lettuce/core/dynamic/RedisCommandsReactiveIntegrationTests.java @@ -3,6 +3,7 @@ import javax.inject.Inject; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -15,9 +16,12 @@ import io.lettuce.test.LettuceExtension; import io.reactivex.Maybe; +import static io.lettuce.TestTags.INTEGRATION_TEST; + /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class RedisCommandsReactiveIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/dynamic/RedisCommandsSyncIntegrationTests.java b/src/test/java/io/lettuce/core/dynamic/RedisCommandsSyncIntegrationTests.java index 37181b0460..d9b04db9ff 100644 --- a/src/test/java/io/lettuce/core/dynamic/RedisCommandsSyncIntegrationTests.java +++ b/src/test/java/io/lettuce/core/dynamic/RedisCommandsSyncIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.dynamic; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import java.util.Collections; @@ -8,6 +9,7 @@ import javax.inject.Inject; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -26,6 +28,7 @@ * * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class RedisCommandsSyncIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/dynamic/SimpleBatcherUnitTests.java b/src/test/java/io/lettuce/core/dynamic/SimpleBatcherUnitTests.java index 11f425b2d1..592bfe29ba 100644 --- a/src/test/java/io/lettuce/core/dynamic/SimpleBatcherUnitTests.java +++ b/src/test/java/io/lettuce/core/dynamic/SimpleBatcherUnitTests.java @@ -1,10 +1,12 @@ package io.lettuce.core.dynamic; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; import java.util.Arrays; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -20,6 +22,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) @ExtendWith(MockitoExtension.class) class SimpleBatcherUnitTests { diff --git a/src/test/java/io/lettuce/core/dynamic/codec/AnnotationRedisCodecResolverUnitTests.java b/src/test/java/io/lettuce/core/dynamic/codec/AnnotationRedisCodecResolverUnitTests.java index 7c86802307..9aced2ff7a 100644 --- a/src/test/java/io/lettuce/core/dynamic/codec/AnnotationRedisCodecResolverUnitTests.java +++ b/src/test/java/io/lettuce/core/dynamic/codec/AnnotationRedisCodecResolverUnitTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core.dynamic.codec; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -28,6 +29,7 @@ import java.util.Map; import java.util.Set; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.Range; @@ -46,6 +48,7 @@ * @author Mark Paluch * @author Manyanda Chitimbo */ +@Tag(UNIT_TEST) class AnnotationRedisCodecResolverUnitTests { private List> codecs = Arrays.asList(new StringCodec(), new ByteArrayCodec()); diff --git a/src/test/java/io/lettuce/core/dynamic/codec/ParameterWrappersUnitTests.java b/src/test/java/io/lettuce/core/dynamic/codec/ParameterWrappersUnitTests.java index 3eadb15d3d..35f690f766 100644 --- a/src/test/java/io/lettuce/core/dynamic/codec/ParameterWrappersUnitTests.java +++ b/src/test/java/io/lettuce/core/dynamic/codec/ParameterWrappersUnitTests.java @@ -1,11 +1,13 @@ package io.lettuce.core.dynamic.codec; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.lang.reflect.Method; import java.util.List; import java.util.Map; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.KeyValue; @@ -19,6 +21,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class ParameterWrappersUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/dynamic/intercept/InvocationProxyFactoryUnitTests.java b/src/test/java/io/lettuce/core/dynamic/intercept/InvocationProxyFactoryUnitTests.java index afaabf21fb..5f26fbf68f 100644 --- a/src/test/java/io/lettuce/core/dynamic/intercept/InvocationProxyFactoryUnitTests.java +++ b/src/test/java/io/lettuce/core/dynamic/intercept/InvocationProxyFactoryUnitTests.java @@ -1,12 +1,15 @@ package io.lettuce.core.dynamic.intercept; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class InvocationProxyFactoryUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/dynamic/output/CodecAwareOutputResolverUnitTests.java b/src/test/java/io/lettuce/core/dynamic/output/CodecAwareOutputResolverUnitTests.java index f20e53badf..0ad4183bed 100644 --- a/src/test/java/io/lettuce/core/dynamic/output/CodecAwareOutputResolverUnitTests.java +++ b/src/test/java/io/lettuce/core/dynamic/output/CodecAwareOutputResolverUnitTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.dynamic.output; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.lang.reflect.Method; @@ -7,6 +8,7 @@ import java.util.List; import java.util.Map; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.codec.RedisCodec; @@ -18,6 +20,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class CodecAwareOutputResolverUnitTests { private CodecAwareOutputFactoryResolver resolver = new CodecAwareOutputFactoryResolver( diff --git a/src/test/java/io/lettuce/core/dynamic/output/OutputRegistryCommandOutputFactoryResolverUnitTests.java b/src/test/java/io/lettuce/core/dynamic/output/OutputRegistryCommandOutputFactoryResolverUnitTests.java index c6486cabb5..a5d77a455f 100644 --- a/src/test/java/io/lettuce/core/dynamic/output/OutputRegistryCommandOutputFactoryResolverUnitTests.java +++ b/src/test/java/io/lettuce/core/dynamic/output/OutputRegistryCommandOutputFactoryResolverUnitTests.java @@ -1,11 +1,13 @@ package io.lettuce.core.dynamic.output; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.lang.reflect.Method; import java.util.Collection; import java.util.List; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; @@ -22,6 +24,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class OutputRegistryCommandOutputFactoryResolverUnitTests { private OutputRegistryCommandOutputFactoryResolver resolver = new OutputRegistryCommandOutputFactoryResolver( diff --git a/src/test/java/io/lettuce/core/dynamic/output/OutputRegistryUnitTests.java b/src/test/java/io/lettuce/core/dynamic/output/OutputRegistryUnitTests.java index 0d20f3401c..f0a2d557ba 100644 --- a/src/test/java/io/lettuce/core/dynamic/output/OutputRegistryUnitTests.java +++ b/src/test/java/io/lettuce/core/dynamic/output/OutputRegistryUnitTests.java @@ -1,10 +1,12 @@ package io.lettuce.core.dynamic.output; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.nio.ByteBuffer; import java.util.List; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.ScoredValue; @@ -18,6 +20,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class OutputRegistryUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/dynamic/segment/AnnotationCommandSegmentFactoryUnitTests.java b/src/test/java/io/lettuce/core/dynamic/segment/AnnotationCommandSegmentFactoryUnitTests.java index 7363bb45b3..741f110b36 100644 --- a/src/test/java/io/lettuce/core/dynamic/segment/AnnotationCommandSegmentFactoryUnitTests.java +++ b/src/test/java/io/lettuce/core/dynamic/segment/AnnotationCommandSegmentFactoryUnitTests.java @@ -1,7 +1,9 @@ package io.lettuce.core.dynamic.segment; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.dynamic.CommandMethod; @@ -15,6 +17,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class AnnotationCommandSegmentFactoryUnitTests { private AnnotationCommandSegmentFactory factory = new AnnotationCommandSegmentFactory(); @@ -28,7 +31,7 @@ void notAnnotatedDotAsIs() { CommandSegments commandSegments = factory.createCommandSegments(commandMethod); assertThat(commandSegments).isEmpty(); - assertThat(commandSegments.getCommandType().name()).isEqualTo("not.Annotated"); + assertThat(commandSegments.getCommandType().toString()).isEqualTo("not.Annotated"); } @Test @@ -40,7 +43,7 @@ void uppercaseDot() { CommandSegments commandSegments = factory.createCommandSegments(commandMethod); assertThat(commandSegments).isEmpty(); - assertThat(commandSegments.getCommandType().name()).isEqualTo("UPPER.CASE"); + assertThat(commandSegments.getCommandType().toString()).isEqualTo("UPPER.CASE"); } @Test @@ -52,7 +55,7 @@ void methodNameAsIs() { CommandSegments commandSegments = factory.createCommandSegments(commandMethod); assertThat(commandSegments).isEmpty(); - assertThat(commandSegments.getCommandType().name()).isEqualTo("methodName"); + assertThat(commandSegments.getCommandType().toString()).isEqualTo("methodName"); } @Test @@ -64,7 +67,7 @@ void splitAsIs() { CommandSegments commandSegments = factory.createCommandSegments(commandMethod); assertThat(commandSegments).hasSize(1).extracting(CommandSegment::asString).contains("Setname"); - assertThat(commandSegments.getCommandType().name()).isEqualTo("client"); + assertThat(commandSegments.getCommandType().toString()).isEqualTo("client"); } @Test @@ -76,7 +79,7 @@ void commandAnnotation() { CommandSegments commandSegments = factory.createCommandSegments(commandMethod); assertThat(commandSegments).hasSize(1).extracting(CommandSegment::asString).contains("WORLD"); - assertThat(commandSegments.getCommandType().name()).isEqualTo("HELLO"); + assertThat(commandSegments.getCommandType().toString()).isEqualTo("HELLO"); } @Test @@ -88,7 +91,7 @@ void splitDefault() { CommandSegments commandSegments = factory.createCommandSegments(commandMethod); assertThat(commandSegments).hasSize(1).extracting(CommandSegment::asString).contains("SETNAME"); - assertThat(commandSegments.getCommandType().name()).isEqualTo("CLIENT"); + assertThat(commandSegments.getCommandType().toString()).isEqualTo("CLIENT"); } @CommandNaming(strategy = Strategy.DOT, letterCase = LetterCase.AS_IS) diff --git a/src/test/java/io/lettuce/core/dynamic/support/ParametrizedTypeInformationUnitTests.java b/src/test/java/io/lettuce/core/dynamic/support/ParametrizedTypeInformationUnitTests.java index 7b98b1dcbf..d2620eaabf 100644 --- a/src/test/java/io/lettuce/core/dynamic/support/ParametrizedTypeInformationUnitTests.java +++ b/src/test/java/io/lettuce/core/dynamic/support/ParametrizedTypeInformationUnitTests.java @@ -1,16 +1,19 @@ package io.lettuce.core.dynamic.support; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.util.Collection; import java.util.List; import java.util.Set; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class ParametrizedTypeInformationUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/dynamic/support/WildcardTypeInformationUnitTests.java b/src/test/java/io/lettuce/core/dynamic/support/WildcardTypeInformationUnitTests.java index fe20304f66..354fa85f66 100644 --- a/src/test/java/io/lettuce/core/dynamic/support/WildcardTypeInformationUnitTests.java +++ b/src/test/java/io/lettuce/core/dynamic/support/WildcardTypeInformationUnitTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.dynamic.support; +import static io.lettuce.TestTags.UNIT_TEST; import static io.lettuce.core.dynamic.support.ClassTypeInformation.from; import static org.assertj.core.api.Assertions.assertThat; @@ -7,11 +8,13 @@ import java.util.Collection; import java.util.List; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class WildcardTypeInformationUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/event/ConnectionEventsTriggeredIntegrationTests.java b/src/test/java/io/lettuce/core/event/ConnectionEventsTriggeredIntegrationTests.java index ab91f9bd7f..21d9eb5e83 100644 --- a/src/test/java/io/lettuce/core/event/ConnectionEventsTriggeredIntegrationTests.java +++ b/src/test/java/io/lettuce/core/event/ConnectionEventsTriggeredIntegrationTests.java @@ -1,10 +1,12 @@ package io.lettuce.core.event; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.time.Duration; import java.time.temporal.ChronoUnit; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; @@ -19,6 +21,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class ConnectionEventsTriggeredIntegrationTests extends TestSupport { @Test diff --git a/src/test/java/io/lettuce/core/event/DefaultEventBusUnitTests.java b/src/test/java/io/lettuce/core/event/DefaultEventBusUnitTests.java index 5d39762010..da0e5f4412 100644 --- a/src/test/java/io/lettuce/core/event/DefaultEventBusUnitTests.java +++ b/src/test/java/io/lettuce/core/event/DefaultEventBusUnitTests.java @@ -1,9 +1,11 @@ package io.lettuce.core.event; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.util.concurrent.ArrayBlockingQueue; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -16,6 +18,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) @ExtendWith(MockitoExtension.class) class DefaultEventBusUnitTests { diff --git a/src/test/java/io/lettuce/core/event/DefaultEventPublisherOptionsUnitTests.java b/src/test/java/io/lettuce/core/event/DefaultEventPublisherOptionsUnitTests.java index cdc9790fad..11c915c441 100644 --- a/src/test/java/io/lettuce/core/event/DefaultEventPublisherOptionsUnitTests.java +++ b/src/test/java/io/lettuce/core/event/DefaultEventPublisherOptionsUnitTests.java @@ -1,15 +1,18 @@ package io.lettuce.core.event; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.time.Duration; import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class DefaultEventPublisherOptionsUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/internal/AbstractInvocationHandlerUnitTests.java b/src/test/java/io/lettuce/core/internal/AbstractInvocationHandlerUnitTests.java index 6b1f2c17f7..fcb2d33a93 100644 --- a/src/test/java/io/lettuce/core/internal/AbstractInvocationHandlerUnitTests.java +++ b/src/test/java/io/lettuce/core/internal/AbstractInvocationHandlerUnitTests.java @@ -1,16 +1,19 @@ package io.lettuce.core.internal; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Collection; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class AbstractInvocationHandlerUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/internal/FuturesUnitTests.java b/src/test/java/io/lettuce/core/internal/FuturesUnitTests.java index 1e3ec41789..4931fa2f67 100644 --- a/src/test/java/io/lettuce/core/internal/FuturesUnitTests.java +++ b/src/test/java/io/lettuce/core/internal/FuturesUnitTests.java @@ -1,12 +1,20 @@ package io.lettuce.core.internal; +import static io.lettuce.TestTags.UNIT_TEST; import static java.util.concurrent.TimeUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.RedisCommandExecutionException; @@ -16,7 +24,9 @@ * Unit tests for {@link Futures}. * * @author Mark Paluch + * @author Tihomir Mateev */ +@Tag(UNIT_TEST) class FuturesUnitTests { @BeforeEach @@ -56,4 +66,33 @@ void awaitAllShouldSetInterruptedBit() { assertThat(Thread.currentThread().isInterrupted()).isTrue(); } + @Test + void allOfShouldNotThrow() throws InterruptedException { + int threadCount = 100; + ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + List issues = new ArrayList<>(); + List> futures = Collections.synchronizedList(new ArrayList<>()); + // Submit multiple threads to perform concurrent operations + CountDownLatch latch = new CountDownLatch(threadCount); + for (int i = 0; i < threadCount; i++) { + executorService.submit(() -> { + try { + for (int y = 0; y < 1000; y++) { + futures.add(new CompletableFuture<>()); + } + + Futures.allOf(futures); + } catch (Exception e) { + issues.add(e); + } finally { + latch.countDown(); + } + }); + } + + // wait for all threads to complete + latch.await(); + assertThat(issues).doesNotHaveAnyElementsOfTypes(ArrayIndexOutOfBoundsException.class); + } + } diff --git a/src/test/java/io/lettuce/core/internal/HostAndPortUnitTests.java b/src/test/java/io/lettuce/core/internal/HostAndPortUnitTests.java index 273e8cae67..0e931d1351 100644 --- a/src/test/java/io/lettuce/core/internal/HostAndPortUnitTests.java +++ b/src/test/java/io/lettuce/core/internal/HostAndPortUnitTests.java @@ -1,13 +1,16 @@ package io.lettuce.core.internal; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class HostAndPortUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/internal/LettuceStringsTests.java b/src/test/java/io/lettuce/core/internal/LettuceStringsTests.java index fb3b65aaf7..b236dc4a91 100644 --- a/src/test/java/io/lettuce/core/internal/LettuceStringsTests.java +++ b/src/test/java/io/lettuce/core/internal/LettuceStringsTests.java @@ -19,13 +19,16 @@ */ package io.lettuce.core.internal; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; /** * @author Kevin McLaughlin */ +@Tag(UNIT_TEST) class LettuceStringsTests { @Test diff --git a/src/test/java/io/lettuce/core/internal/TimeoutProviderUnitTests.java b/src/test/java/io/lettuce/core/internal/TimeoutProviderUnitTests.java index f14ae94ffc..ef354e9f98 100644 --- a/src/test/java/io/lettuce/core/internal/TimeoutProviderUnitTests.java +++ b/src/test/java/io/lettuce/core/internal/TimeoutProviderUnitTests.java @@ -1,11 +1,13 @@ package io.lettuce.core.internal; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import java.time.Duration; import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.TimeoutOptions; @@ -16,6 +18,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class TimeoutProviderUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/json/DefaultJsonParserUnitTests.java b/src/test/java/io/lettuce/core/json/DefaultJsonParserUnitTests.java new file mode 100644 index 0000000000..101ec78ded --- /dev/null +++ b/src/test/java/io/lettuce/core/json/DefaultJsonParserUnitTests.java @@ -0,0 +1,107 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static io.lettuce.TestTags.UNIT_TEST; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +/** + * Unit tests for {@link DefaultJsonParser}. + */ +@Tag(UNIT_TEST) +class DefaultJsonParserUnitTests { + + @Test + void loadJsonValue() { + final String unprocessed = "{\"a\":1,\"b\":2}"; + + DefaultJsonParser parser = new DefaultJsonParser(); + JsonValue jsonValue = parser.loadJsonValue(ByteBuffer.wrap(unprocessed.getBytes())); + + assertThat(jsonValue).isNotNull(); + assertThat(jsonValue).isInstanceOf(UnproccessedJsonValue.class); + assertThat(((UnproccessedJsonValue) jsonValue).isDeserialized()).isFalse(); + } + + @Test + void createJsonValue() { + final String unprocessed = "\"someValue\""; + + DefaultJsonParser parser = new DefaultJsonParser(); + JsonValue jsonValue = parser.createJsonValue(ByteBuffer.wrap(unprocessed.getBytes())); + + assertThat(jsonValue).isNotNull(); + assertThat(jsonValue.isString()).isTrue(); + assertThat(jsonValue.asString()).isEqualTo("someValue"); + } + + @Test + void createJsonObject() { + final String unprocessed = "{\"a\":1,\"b\":2}"; + + DefaultJsonParser parser = new DefaultJsonParser(); + JsonValue jsonValue = parser.createJsonObject(); + + assertThat(jsonValue).isNotNull(); + assertThat(jsonValue.isJsonObject()).isTrue(); + assertThat(jsonValue.asJsonObject().size()).isZero(); + + parser = new DefaultJsonParser(); + jsonValue = parser.createJsonValue(ByteBuffer.wrap(unprocessed.getBytes())); + + assertThat(jsonValue).isNotNull(); + assertThat(jsonValue.isJsonObject()).isTrue(); + assertThat(jsonValue.asJsonObject().get("a").asNumber()).isEqualTo(1); + assertThat(jsonValue.asJsonObject().get("b").asNumber()).isEqualTo(2); + + jsonValue = parser.createJsonValue(unprocessed); + + assertThat(jsonValue).isNotNull(); + assertThat(jsonValue.isJsonObject()).isTrue(); + assertThat(jsonValue.asJsonObject().get("a").asNumber()).isEqualTo(1); + assertThat(jsonValue.asJsonObject().get("b").asNumber()).isEqualTo(2); + } + + @Test + void createJsonArray() { + DefaultJsonParser parser = new DefaultJsonParser(); + JsonValue jsonValue = parser.createJsonArray(); + + assertThat(jsonValue).isNotNull(); + assertThat(jsonValue.isJsonArray()).isTrue(); + assertThat(jsonValue.asJsonArray().size()).isZero(); + + final String unprocessed = "[1,2]"; + + jsonValue = parser.createJsonValue(ByteBuffer.wrap(unprocessed.getBytes())); + + assertThat(jsonValue).isNotNull(); + assertThat(jsonValue.isJsonArray()).isTrue(); + assertThat(jsonValue.asJsonArray().get(0).asNumber()).isEqualTo(1); + assertThat(jsonValue.asJsonArray().get(1).asNumber()).isEqualTo(2); + } + + @Test + void parsingIssues() { + final String unprocessed = "{a\":1,\"b\":2}"; + + DefaultJsonParser parser = new DefaultJsonParser(); + + assertThatThrownBy(() -> parser.createJsonValue(unprocessed)).isInstanceOf(RedisJsonException.class); + assertThatThrownBy(() -> parser.createJsonValue(ByteBuffer.wrap(unprocessed.getBytes()))) + .isInstanceOf(RedisJsonException.class); + + } + +} diff --git a/src/test/java/io/lettuce/core/json/DelegateJsonArrayUnitTests.java b/src/test/java/io/lettuce/core/json/DelegateJsonArrayUnitTests.java new file mode 100644 index 0000000000..8921968023 --- /dev/null +++ b/src/test/java/io/lettuce/core/json/DelegateJsonArrayUnitTests.java @@ -0,0 +1,176 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.util.Iterator; + +import static io.lettuce.TestTags.UNIT_TEST; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Unit tests for {@link DelegateJsonArray}. + */ +@Tag(UNIT_TEST) +class DelegateJsonArrayUnitTests { + + @Test + void add() { + DefaultJsonParser parser = new DefaultJsonParser(); + DelegateJsonArray underTest = new DelegateJsonArray(); + underTest.add(parser.createJsonValue("\"test\"")).add(parser.createJsonValue("\"test2\"")) + .add(parser.createJsonValue("\"test3\"")); + + assertThat(underTest.size()).isEqualTo(3); + assertThat(underTest.get(0).isString()).isTrue(); + assertThat(underTest.get(0).asString()).isEqualTo("test"); + assertThat(underTest.get(1).isString()).isTrue(); + assertThat(underTest.get(1).asString()).isEqualTo("test2"); + assertThat(underTest.get(2).isString()).isTrue(); + assertThat(underTest.get(2).asString()).isEqualTo("test3"); + } + + @Test + void addCornerCases() { + DefaultJsonParser parser = new DefaultJsonParser(); + DelegateJsonArray underTest = new DelegateJsonArray(); + underTest.add(null).add(parser.createJsonValue("null")).add(parser.createJsonValue("\"test3\"")); + + assertThatThrownBy(() -> underTest.addAll(null)).isInstanceOf(IllegalArgumentException.class); + + assertThat(underTest.size()).isEqualTo(3); + assertThat(underTest.get(0).isNull()).isTrue(); + assertThat(underTest.get(1).isNull()).isTrue(); + assertThat(underTest.get(2).isString()).isTrue(); + assertThat(underTest.get(2).asString()).isEqualTo("test3"); + } + + @Test + void getCornerCases() { + DefaultJsonParser parser = new DefaultJsonParser(); + DelegateJsonArray underTest = new DelegateJsonArray(); + underTest.add(parser.createJsonValue("\"test\"")).add(parser.createJsonValue("\"test2\"")) + .add(parser.createJsonValue("\"test3\"")); + + assertThat(underTest.get(3)).isNull(); + assertThat(underTest.get(-1)).isNull(); + } + + @Test + void addAll() { + DefaultJsonParser parser = new DefaultJsonParser(); + DelegateJsonArray array = new DelegateJsonArray(); + array.add(parser.createJsonValue("\"test\"")).add(parser.createJsonValue("\"test2\"")) + .add(parser.createJsonValue("\"test3\"")); + + DelegateJsonArray underTest = new DelegateJsonArray(); + underTest.addAll(array); + array.remove(1); // verify source array modifications not propagated + + assertThat(underTest.size()).isEqualTo(3); + assertThat(underTest.get(0).isString()).isTrue(); + assertThat(underTest.get(0).asString()).isEqualTo("test"); + assertThat(underTest.get(1).isString()).isTrue(); + assertThat(underTest.get(1).asString()).isEqualTo("test2"); + assertThat(underTest.get(2).isString()).isTrue(); + assertThat(underTest.get(2).asString()).isEqualTo("test3"); + } + + @Test + void asList() { + DefaultJsonParser parser = new DefaultJsonParser(); + DelegateJsonArray underTest = new DelegateJsonArray(); + underTest.add(parser.createJsonValue("1")).add(parser.createJsonValue("2")).add(parser.createJsonValue("3")); + + assertThat(underTest.size()).isEqualTo(3); + assertThat(underTest.asList()).hasSize(3); + assertThat(underTest.asList().get(0).isNumber()).isTrue(); + assertThat(underTest.asList().get(0).asNumber()).isEqualTo(1); + } + + @Test + void getFirst() { + DefaultJsonParser parser = new DefaultJsonParser(); + DelegateJsonArray underTest = new DelegateJsonArray(); + underTest.add(parser.createJsonValue("\"test\"")).add(parser.createJsonValue("\"test2\"")) + .add(parser.createJsonValue("\"test3\"")); + + assertThat(underTest.size()).isEqualTo(3); + assertThat(underTest.getFirst().isString()).isTrue(); + assertThat(underTest.getFirst().asString()).isEqualTo("test"); + } + + @Test + void iterator() { + DefaultJsonParser parser = new DefaultJsonParser(); + DelegateJsonArray underTest = new DelegateJsonArray(); + underTest.add(parser.createJsonValue("1")).add(parser.createJsonValue("2")).add(parser.createJsonValue("3")); + + Iterator iterator = underTest.iterator(); + assertThat(iterator.hasNext()).isTrue(); + while (iterator.hasNext()) { + assertThat(iterator.next().isNumber()).isTrue(); + } + } + + @Test + void remove() { + DefaultJsonParser parser = new DefaultJsonParser(); + DelegateJsonArray underTest = new DelegateJsonArray(); + underTest.add(parser.createJsonValue("1")).add(parser.createJsonValue("2")).add(parser.createJsonValue("3")); + + assertThat(underTest.remove(1).asNumber()).isEqualTo(2); + assertThat(underTest.size()).isEqualTo(2); + assertThat(underTest.get(0).asNumber()).isEqualTo(1); + assertThat(underTest.get(1).asNumber()).isEqualTo(3); + } + + @Test + void replace() { + DefaultJsonParser parser = new DefaultJsonParser(); + DelegateJsonArray underTest = new DelegateJsonArray(); + underTest.add(parser.createJsonValue("1")).add(parser.createJsonValue("2")).add(parser.createJsonValue("3")); + underTest.replace(1, parser.createJsonValue("4")); + + assertThat(underTest.size()).isEqualTo(3); + assertThat(underTest.get(0).asNumber()).isEqualTo(1); + assertThat(underTest.get(1).asNumber()).isEqualTo(4); + assertThat(underTest.get(2).asNumber()).isEqualTo(3); + } + + @Test + void isJsonArray() { + DelegateJsonArray underTest = new DelegateJsonArray(); + assertThat(underTest.isJsonArray()).isTrue(); + + assertThat(underTest.isJsonObject()).isFalse(); + assertThat(underTest.isNull()).isFalse(); + assertThat(underTest.isNumber()).isFalse(); + assertThat(underTest.isString()).isFalse(); + } + + @Test + void asJsonArray() { + DelegateJsonArray underTest = new DelegateJsonArray(); + assertThat(underTest.asJsonArray()).isSameAs(underTest); + } + + @Test + void asAnythingElse() { + DelegateJsonArray underTest = new DelegateJsonArray(); + + assertThat(underTest.asBoolean()).isNull(); + assertThat(underTest.asJsonObject()).isNull(); + assertThat(underTest.asString()).isNull(); + assertThat(underTest.asNumber()).isNull(); + } + +} diff --git a/src/test/java/io/lettuce/core/json/DelegateJsonObjectUnitTests.java b/src/test/java/io/lettuce/core/json/DelegateJsonObjectUnitTests.java new file mode 100644 index 0000000000..fe14469d47 --- /dev/null +++ b/src/test/java/io/lettuce/core/json/DelegateJsonObjectUnitTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static io.lettuce.TestTags.UNIT_TEST; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link DelegateJsonObject}. + */ +@Tag(UNIT_TEST) +class DelegateJsonObjectUnitTests { + + @Test + void put() { + DefaultJsonParser parser = new DefaultJsonParser(); + DelegateJsonObject underTest = new DelegateJsonObject(); + + underTest.put("test", parser.createJsonValue("\"test\"")).put("test2", parser.createJsonValue("1")).put("test2", + parser.createJsonValue("true")); + + assertThat(underTest.size()).isEqualTo(2); + assertThat(underTest.get("test").asString()).isEqualTo("test"); + assertThat(underTest.get("test2").asBoolean()).isTrue(); + } + + @Test + void remove() { + DefaultJsonParser parser = new DefaultJsonParser(); + DelegateJsonObject underTest = new DelegateJsonObject(); + + underTest.put("test", parser.createJsonValue("\"test\"")).put("test2", parser.createJsonValue("1")).remove("test"); + + assertThat(underTest.size()).isEqualTo(1); + assertThat(underTest.get("test")).isNull(); + assertThat(underTest.get("test2").asNumber()).isEqualTo(1); + } + + @Test + void isAnythingElse() { + DelegateJsonObject underTest = new DelegateJsonObject(); + + assertThat(underTest.isJsonObject()).isTrue(); + + assertThat(underTest.isNull()).isFalse(); + assertThat(underTest.isBoolean()).isFalse(); + assertThat(underTest.isNumber()).isFalse(); + assertThat(underTest.isString()).isFalse(); + assertThat(underTest.isJsonArray()).isFalse(); + } + + @Test + void asAnythingElse() { + DelegateJsonObject underTest = new DelegateJsonObject(); + + assertThat(underTest.asJsonObject()).isNotNull(); + + assertThat(underTest.asBoolean()).isNull(); + assertThat(underTest.asJsonArray()).isNull(); + assertThat(underTest.asString()).isNull(); + assertThat(underTest.asNumber()).isNull(); + } + +} diff --git a/src/test/java/io/lettuce/core/json/DelegateJsonValueUnitTests.java b/src/test/java/io/lettuce/core/json/DelegateJsonValueUnitTests.java new file mode 100644 index 0000000000..cbefa3ffb3 --- /dev/null +++ b/src/test/java/io/lettuce/core/json/DelegateJsonValueUnitTests.java @@ -0,0 +1,148 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static io.lettuce.TestTags.UNIT_TEST; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link DelegateJsonValue}. + */ +@Tag(UNIT_TEST) +class DelegateJsonValueUnitTests { + + @Test + void testString() { + DefaultJsonParser parser = new DefaultJsonParser(); + JsonValue underTest = parser.createJsonValue("\"test\""); + + assertThat(underTest.toString()).isEqualTo("\"test\""); + assertThat(underTest.asByteBuffer().array()).isEqualTo("\"test\"".getBytes()); + + assertThat(underTest.isJsonArray()).isFalse(); + assertThat(underTest.asJsonArray()).isNull(); + + assertThat(underTest.isJsonObject()).isFalse(); + assertThat(underTest.asJsonObject()).isNull(); + + assertThat(underTest.isString()).isTrue(); + assertThat(underTest.asString()).isEqualTo("test"); + + assertThat(underTest.isNumber()).isFalse(); + assertThat(underTest.asNumber()).isNull(); + + assertThat(underTest.isBoolean()).isFalse(); + assertThat(underTest.asBoolean()).isNull(); + + assertThat(underTest.isNull()).isFalse(); + } + + @Test + void testNumber() { + DefaultJsonParser parser = new DefaultJsonParser(); + JsonValue underTest = parser.createJsonValue("1"); + + assertThat(underTest.toString()).isEqualTo("1"); + assertThat(underTest.asByteBuffer().array()).isEqualTo("1".getBytes()); + + assertThat(underTest.isJsonArray()).isFalse(); + assertThat(underTest.asJsonArray()).isNull(); + + assertThat(underTest.isJsonObject()).isFalse(); + assertThat(underTest.asJsonObject()).isNull(); + + assertThat(underTest.isNumber()).isTrue(); + assertThat(underTest.asNumber()).isEqualTo(1); + + assertThat(underTest.isString()).isFalse(); + assertThat(underTest.asString()).isNull(); + + assertThat(underTest.isBoolean()).isFalse(); + assertThat(underTest.asBoolean()).isNull(); + + assertThat(underTest.isNull()).isFalse(); + } + + @Test + void testNumberExtended() { + DefaultJsonParser parser = new DefaultJsonParser(); + JsonValue underTest = parser.createJsonValue("1"); + + assertThat(underTest.isNumber()).isTrue(); + assertThat(underTest.asNumber()).isEqualTo(1); + assertThat(underTest.asNumber()).isInstanceOf(Integer.class); + + underTest = parser.createJsonValue(String.valueOf(Long.MAX_VALUE)); + + assertThat(underTest.isNumber()).isTrue(); + assertThat(underTest.asNumber()).isEqualTo(Long.MAX_VALUE); + assertThat(underTest.asNumber()).isInstanceOf(Long.class); + + underTest = parser.createJsonValue(String.valueOf(Double.MAX_VALUE)); + + assertThat(underTest.isNumber()).isTrue(); + assertThat(underTest.asNumber()).isEqualTo(Double.MAX_VALUE); + assertThat(underTest.asNumber()).isInstanceOf(Double.class); + } + + @Test + void testBoolean() { + DefaultJsonParser parser = new DefaultJsonParser(); + JsonValue underTest = parser.createJsonValue("true"); + + assertThat(underTest.toString()).isEqualTo("true"); + assertThat(underTest.asByteBuffer().array()).isEqualTo("true".getBytes()); + + assertThat(underTest.isJsonArray()).isFalse(); + assertThat(underTest.asJsonArray()).isNull(); + + assertThat(underTest.isJsonObject()).isFalse(); + assertThat(underTest.asJsonObject()).isNull(); + + assertThat(underTest.isBoolean()).isTrue(); + assertThat(underTest.asBoolean()).isTrue(); + + assertThat(underTest.isString()).isFalse(); + assertThat(underTest.asString()).isNull(); + + assertThat(underTest.isNumber()).isFalse(); + assertThat(underTest.asNumber()).isNull(); + + assertThat(underTest.isNull()).isFalse(); + } + + @Test + void testNull() { + DefaultJsonParser parser = new DefaultJsonParser(); + JsonValue underTest = parser.createJsonValue("null"); + + assertThat(underTest.toString()).isEqualTo("null"); + assertThat(underTest.asByteBuffer().array()).isEqualTo("null".getBytes()); + + assertThat(underTest.isJsonArray()).isFalse(); + assertThat(underTest.asJsonArray()).isNull(); + + assertThat(underTest.isJsonObject()).isFalse(); + assertThat(underTest.asJsonObject()).isNull(); + + assertThat(underTest.isNumber()).isFalse(); + assertThat(underTest.asNumber()).isNull(); + + assertThat(underTest.isString()).isFalse(); + assertThat(underTest.asString()).isNull(); + + assertThat(underTest.isBoolean()).isFalse(); + assertThat(underTest.asBoolean()).isNull(); + + assertThat(underTest.isNull()).isTrue(); + } + +} diff --git a/src/test/java/io/lettuce/core/json/RedisJsonClusterIntegrationTests.java b/src/test/java/io/lettuce/core/json/RedisJsonClusterIntegrationTests.java new file mode 100644 index 0000000000..5bcbfd5376 --- /dev/null +++ b/src/test/java/io/lettuce/core/json/RedisJsonClusterIntegrationTests.java @@ -0,0 +1,436 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +import io.lettuce.core.RedisContainerIntegrationTests; +import io.lettuce.core.RedisURI; +import io.lettuce.core.cluster.RedisClusterClient; +import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands; +import io.lettuce.core.json.arguments.JsonGetArgs; +import io.lettuce.core.json.arguments.JsonMsetArgs; +import io.lettuce.core.json.arguments.JsonRangeArgs; +import io.lettuce.core.json.arguments.JsonSetArgs; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.List; + +import static io.lettuce.TestTags.INTEGRATION_TEST; +import static org.assertj.core.api.Assertions.assertThat; + +@Tag(INTEGRATION_TEST) +public class RedisJsonClusterIntegrationTests extends RedisContainerIntegrationTests { + + protected static RedisClusterClient client; + + protected static RedisAdvancedClusterCommands redis; + + public RedisJsonClusterIntegrationTests() { + RedisURI redisURI = RedisURI.Builder.redis("127.0.0.1").withPort(36379).build(); + + client = RedisClusterClient.create(redisURI); + redis = client.connect().sync(); + } + + private static final String BIKES_INVENTORY = "bikes:inventory"; + + private static final String BIKE_COLORS_V1 = "..mountain_bikes[1].colors"; + + private static final String BIKE_COLORS_V2 = "$..mountain_bikes[1].colors"; + + private static final String MOUNTAIN_BIKES_V1 = "..mountain_bikes"; + + private static final String MOUNTAIN_BIKES_V2 = "$..mountain_bikes"; + + @BeforeEach + public void prepare() throws IOException { + redis.flushall(); + + Path path = Paths.get("src/test/resources/bike-inventory.json"); + String read = String.join("", Files.readAllLines(path)); + JsonValue value = redis.getJsonParser().createJsonValue(read); + + redis.jsonSet("bikes:inventory", JsonPath.ROOT_PATH, value, JsonSetArgs.Builder.defaults()); + } + + @AfterAll + static void teardown() { + if (client != null) { + client.shutdown(); + } + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1, MOUNTAIN_BIKES_V2 }) + void jsonArrappend(String path) { + JsonParser parser = redis.getJsonParser(); + JsonPath myPath = JsonPath.of(path); + + JsonValue element = parser.createJsonValue("\"{id:bike6}\""); + List appendedElements = redis.jsonArrappend(BIKES_INVENTORY, myPath, element); + assertThat(appendedElements).hasSize(1); + assertThat(appendedElements.get(0)).isEqualTo(4); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { BIKE_COLORS_V1, BIKE_COLORS_V2 }) + void jsonArrindex(String path) { + JsonParser parser = redis.getJsonParser(); + JsonPath myPath = JsonPath.of(path); + JsonValue element = parser.createJsonValue("\"white\""); + + List arrayIndex = redis.jsonArrindex(BIKES_INVENTORY, myPath, element, null); + assertThat(arrayIndex).isNotNull(); + assertThat(arrayIndex).hasSize(1); + assertThat(arrayIndex.get(0).longValue()).isEqualTo(1L); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { BIKE_COLORS_V1, BIKE_COLORS_V2 }) + void jsonArrinsert(String path) { + JsonParser parser = redis.getJsonParser(); + JsonPath myPath = JsonPath.of(path); + JsonValue element = parser.createJsonValue("\"ultramarine\""); + + List arrayIndex = redis.jsonArrinsert(BIKES_INVENTORY, myPath, 1, element); + assertThat(arrayIndex).isNotNull(); + assertThat(arrayIndex).hasSize(1); + assertThat(arrayIndex.get(0).longValue()).isEqualTo(3L); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1, MOUNTAIN_BIKES_V2 }) + void jsonArrlen(String path) { + JsonPath myPath = JsonPath.of(path); + + List poppedJson = redis.jsonArrlen(BIKES_INVENTORY, myPath); + assertThat(poppedJson).hasSize(1); + assertThat(poppedJson.get(0).longValue()).isEqualTo(3); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1, MOUNTAIN_BIKES_V2 }) + void jsonArrpop(String path) { + JsonPath myPath = JsonPath.of(path); + + List poppedJson = redis.jsonArrpop(BIKES_INVENTORY, myPath, -1); + assertThat(poppedJson).hasSize(1); + assertThat(poppedJson.get(0).toString()).contains( + "{\"id\":\"bike:3\",\"model\":\"Weywot\",\"description\":\"This bike gives kids aged six years and old"); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { BIKE_COLORS_V1, BIKE_COLORS_V2 }) + void jsonArrtrim(String path) { + JsonPath myPath = JsonPath.of(path); + JsonRangeArgs range = JsonRangeArgs.Builder.start(1).stop(2); + + List arrayIndex = redis.jsonArrtrim(BIKES_INVENTORY, myPath, range); + assertThat(arrayIndex).isNotNull(); + assertThat(arrayIndex).hasSize(1); + assertThat(arrayIndex.get(0).longValue()).isEqualTo(1L); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { BIKE_COLORS_V1, BIKE_COLORS_V2 }) + void jsonClear(String path) { + JsonPath myPath = JsonPath.of(path); + + Long result = redis.jsonClear(BIKES_INVENTORY, myPath); + assertThat(result).isNotNull(); + assertThat(result).isEqualTo(1L); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { "..mountain_bikes[0:2].model", "$..mountain_bikes[0:2].model" }) + void jsonGet(String path) { + JsonPath myPath = JsonPath.of(path); + + // Verify codec parsing + List value = redis.jsonGet(BIKES_INVENTORY, JsonGetArgs.Builder.defaults(), myPath); + assertThat(value).hasSize(1); + + if (path.startsWith("$")) { + assertThat(value.get(0).toString()).isEqualTo("[\"Phoebe\",\"Quaoar\"]"); + + // Verify array parsing + assertThat(value.get(0).isJsonArray()).isTrue(); + assertThat(value.get(0).asJsonArray().size()).isEqualTo(2); + assertThat(value.get(0).asJsonArray().asList().get(0).toString()).isEqualTo("\"Phoebe\""); + assertThat(value.get(0).asJsonArray().asList().get(1).toString()).isEqualTo("\"Quaoar\""); + + // Verify String parsing + assertThat(value.get(0).asJsonArray().asList().get(0).isString()).isTrue(); + assertThat(value.get(0).asJsonArray().asList().get(0).asString()).isEqualTo("Phoebe"); + assertThat(value.get(0).asJsonArray().asList().get(1).isString()).isTrue(); + assertThat(value.get(0).asJsonArray().asList().get(1).asString()).isEqualTo("Quaoar"); + } else { + assertThat(value.get(0).toString()).isEqualTo("\"Phoebe\""); + + // Verify array parsing + assertThat(value.get(0).isString()).isTrue(); + assertThat(value.get(0).asString()).isEqualTo("Phoebe"); + } + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1 + "[1]", MOUNTAIN_BIKES_V2 + "[1]" }) + void jsonMerge(String path) { + JsonParser parser = redis.getJsonParser(); + JsonPath myPath = JsonPath.of(path); + JsonValue element = parser.createJsonValue("\"ultramarine\""); + + String result = redis.jsonMerge(BIKES_INVENTORY, myPath, element); + assertThat(result).isNotNull(); + assertThat(result).isEqualTo("OK"); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { "..model", "$..model" }) + void jsonMGet(String path) { + JsonPath myPath = JsonPath.of(path); + + List value = redis.jsonMGet(myPath, BIKES_INVENTORY); + assertThat(value).hasSize(1); + if (path.startsWith("$")) { + assertThat(value.get(0).toString()).isEqualTo("[\"Phoebe\",\"Quaoar\",\"Weywot\"]"); + } else { + assertThat(value.get(0).toString()).isEqualTo("\"Phoebe\""); + } + } + + @Test + void jsonCrossSlot() { + JsonParser parser = redis.getJsonParser(); + + JsonObject bikeRecord = parser.createJsonObject(); + JsonObject bikeSpecs = parser.createJsonObject(); + JsonArray bikeColors = parser.createJsonArray(); + + bikeSpecs.put("material", parser.createJsonValue("\"composite\"")); + bikeSpecs.put("weight", parser.createJsonValue("11")); + + bikeColors.add(parser.createJsonValue("\"yellow\"")); + bikeColors.add(parser.createJsonValue("\"orange\"")); + + bikeRecord.put("id", parser.createJsonValue("\"bike:43\"")); + bikeRecord.put("model", parser.createJsonValue("\"DesertFox\"")); + bikeRecord.put("description", parser.createJsonValue("\"The DesertFox is a versatile bike for all terrains\"")); + bikeRecord.put("price", parser.createJsonValue("\"1299\"")); + bikeRecord.put("specs", bikeSpecs); + bikeRecord.put("colors", bikeColors); + + JsonObject bikeServiceRecord = parser.createJsonObject(); + String today = "\"" + DateTimeFormatter.ISO_LOCAL_DATE.format(LocalDateTime.now()) + "\""; + String lastWeek = "\"" + DateTimeFormatter.ISO_LOCAL_DATE.format(LocalDateTime.now().minusDays(7)) + "\""; + + JsonArray serviceHistory = parser.createJsonArray(); + + serviceHistory.add(parser.createJsonValue(today)); + serviceHistory.add(parser.createJsonValue(lastWeek)); + + bikeServiceRecord.put("id", parser.createJsonValue("\"bike:43\"")); + bikeServiceRecord.put("serviceHistory", serviceHistory); + bikeServiceRecord.put("purchaseDate", parser.createJsonValue(lastWeek)); + bikeServiceRecord.put("guarantee", parser.createJsonValue("\"2 years\"")); + + // set value on a different slot + + JsonMsetArgs args1 = new JsonMsetArgs<>(BIKES_INVENTORY, JsonPath.ROOT_PATH, bikeRecord); + JsonMsetArgs args2 = new JsonMsetArgs<>("bikes:service", JsonPath.ROOT_PATH, bikeServiceRecord); + String result = redis.jsonMSet(Arrays.asList(args1, args2)); + assertThat(result).isEqualTo("OK"); + + // get values from two different slots + List value = redis.jsonMGet(JsonPath.ROOT_PATH, BIKES_INVENTORY, "bikes:service"); + assertThat(value).hasSize(2); + JsonValue slot1 = value.get(0); + JsonValue slot2 = value.get(1); + assertThat(slot1.toString()).contains("bike:43"); + assertThat(slot2.toString()).contains("bike:43"); + assertThat(slot1.isJsonArray()).isTrue(); + assertThat(slot2.isJsonArray()).isTrue(); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1 + "[1]", MOUNTAIN_BIKES_V2 + "[1]" }) + void jsonMset(String path) { + JsonParser parser = redis.getJsonParser(); + JsonPath myPath = JsonPath.of(path); + + JsonObject bikeRecord = parser.createJsonObject(); + JsonObject bikeSpecs = parser.createJsonObject(); + JsonArray bikeColors = parser.createJsonArray(); + bikeSpecs.put("material", parser.createJsonValue("\"composite\"")); + bikeSpecs.put("weight", parser.createJsonValue("11")); + bikeColors.add(parser.createJsonValue("\"yellow\"")); + bikeColors.add(parser.createJsonValue("\"orange\"")); + bikeRecord.put("id", parser.createJsonValue("\"bike:43\"")); + bikeRecord.put("model", parser.createJsonValue("\"DesertFox\"")); + bikeRecord.put("description", parser.createJsonValue("\"The DesertFox is a versatile bike for all terrains\"")); + bikeRecord.put("price", parser.createJsonValue("\"1299\"")); + bikeRecord.put("specs", bikeSpecs); + bikeRecord.put("colors", bikeColors); + + JsonMsetArgs args1 = new JsonMsetArgs<>(BIKES_INVENTORY, myPath, bikeRecord); + + bikeRecord = parser.createJsonObject(); + bikeSpecs = parser.createJsonObject(); + bikeColors = parser.createJsonArray(); + bikeSpecs.put("material", parser.createJsonValue("\"wood\"")); + bikeSpecs.put("weight", parser.createJsonValue("19")); + bikeColors.add(parser.createJsonValue("\"walnut\"")); + bikeColors.add(parser.createJsonValue("\"chestnut\"")); + bikeRecord.put("id", parser.createJsonValue("\"bike:13\"")); + bikeRecord.put("model", parser.createJsonValue("\"Woody\"")); + bikeRecord.put("description", parser.createJsonValue("\"The Woody is an environmentally-friendly wooden bike\"")); + bikeRecord.put("price", parser.createJsonValue("\"1112\"")); + bikeRecord.put("specs", bikeSpecs); + bikeRecord.put("colors", bikeColors); + + JsonMsetArgs args2 = new JsonMsetArgs<>(BIKES_INVENTORY, myPath, bikeRecord); + + List> args = Arrays.asList(args1, args2); + String result = redis.jsonMSet(args); + + assertThat(result).isNotNull(); + assertThat(result).isEqualTo("OK"); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { "$..mountain_bikes[0:1].price", "..mountain_bikes[0:1].price" }) + void jsonNumincrby(String path) { + JsonPath myPath = JsonPath.of(path); + + List value = redis.jsonNumincrby(BIKES_INVENTORY, myPath, 5L); + assertThat(value).hasSize(1); + assertThat(value.get(0).longValue()).isEqualTo(1933L); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1 + "[1]", MOUNTAIN_BIKES_V2 + "[1]" }) + void jsonObjkeys(String path) { + JsonPath myPath = JsonPath.of(path); + + List result = redis.jsonObjkeys(BIKES_INVENTORY, myPath); + assertThat(result).isNotNull(); + assertThat(result).hasSize(6); + assertThat(result).contains("id", "model", "description", "price", "specs", "colors"); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1 + "[1]", MOUNTAIN_BIKES_V2 + "[1]" }) + void jsonObjlen(String path) { + JsonPath myPath = JsonPath.of(path); + + List result = redis.jsonObjlen(BIKES_INVENTORY, myPath); + assertThat(result).isNotNull(); + assertThat(result).hasSize(1); + assertThat(result.get(0)).isEqualTo(6L); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1, MOUNTAIN_BIKES_V2 }) + void jsonSet(String path) { + JsonPath myPath = JsonPath.of(path); + + JsonParser parser = redis.getJsonParser(); + JsonObject bikeRecord = parser.createJsonObject(); + JsonObject bikeSpecs = parser.createJsonObject(); + JsonArray bikeColors = parser.createJsonArray(); + + bikeSpecs.put("material", parser.createJsonValue("\"composite\"")); + bikeSpecs.put("weight", parser.createJsonValue("11")); + + bikeColors.add(parser.createJsonValue("\"yellow\"")); + bikeColors.add(parser.createJsonValue("\"orange\"")); + + bikeRecord.put("id", parser.createJsonValue("\"bike:43\"")); + bikeRecord.put("model", parser.createJsonValue("\"DesertFox\"")); + bikeRecord.put("description", parser.createJsonValue("\"The DesertFox is a versatile bike for all terrains\"")); + bikeRecord.put("price", parser.createJsonValue("\"1299\"")); + bikeRecord.put("specs", bikeSpecs); + bikeRecord.put("colors", bikeColors); + + JsonSetArgs args = JsonSetArgs.Builder.defaults(); + + String result = redis.jsonSet(BIKES_INVENTORY, myPath, bikeRecord, args); + assertThat(result).isEqualTo("OK"); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { "..mountain_bikes[1].colors[1]", "$..mountain_bikes[1].colors[1]" }) + void jsonStrappend(String path) { + JsonParser parser = redis.getJsonParser(); + JsonPath myPath = JsonPath.of(path); + JsonValue element = parser.createJsonValue("\"-light\""); + + List result = redis.jsonStrappend(BIKES_INVENTORY, myPath, element); + assertThat(result).isNotNull(); + assertThat(result).hasSize(1); + assertThat(result.get(0)).isEqualTo(11L); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { BIKE_COLORS_V1 + "[1]", BIKE_COLORS_V2 + "[1]" }) + void jsonStrlen(String path) { + JsonPath myPath = JsonPath.of(path); + + List result = redis.jsonStrlen(BIKES_INVENTORY, myPath); + assertThat(result).isNotNull(); + assertThat(result).hasSize(1); + assertThat(result.get(0)).isEqualTo(5L); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { "$..complete", "..complete" }) + void jsonToggle(String path) { + JsonPath myPath = JsonPath.of(path); + + List result = redis.jsonToggle(BIKES_INVENTORY, myPath); + assertThat(result).isNotNull(); + assertThat(result).hasSize(1); + if (path.startsWith("$")) { + assertThat(result.get(0)).isEqualTo(1L); + } else { + // seems that for JSON.TOGGLE when we use a V1 path the resulting value is a list of string values and not a + // list of integer values as per the documentation + assertThat(result).isNotEmpty(); + } + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1 + "[2:3]", MOUNTAIN_BIKES_V2 + "[2:3]" }) + void jsonDel(String path) { + JsonPath myPath = JsonPath.of(path); + + Long value = redis.jsonDel(BIKES_INVENTORY, myPath); + assertThat(value).isEqualTo(1); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1, MOUNTAIN_BIKES_V2 }) + void jsonType(String path) { + JsonPath myPath = JsonPath.of(path); + + JsonType jsonType = redis.jsonType(BIKES_INVENTORY, myPath).get(0); + assertThat(jsonType).isEqualTo(JsonType.ARRAY); + } + +} diff --git a/src/test/java/io/lettuce/core/json/RedisJsonIntegrationTests.java b/src/test/java/io/lettuce/core/json/RedisJsonIntegrationTests.java new file mode 100644 index 0000000000..d8473ee964 --- /dev/null +++ b/src/test/java/io/lettuce/core/json/RedisJsonIntegrationTests.java @@ -0,0 +1,627 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.json; + +import io.lettuce.core.ClientOptions; +import io.lettuce.core.RedisClient; +import io.lettuce.core.RedisContainerIntegrationTests; +import io.lettuce.core.RedisFuture; +import io.lettuce.core.RedisURI; +import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.api.async.RedisAsyncCommands; +import io.lettuce.core.api.sync.RedisCommands; +import io.lettuce.core.codec.ByteArrayCodec; +import io.lettuce.core.codec.StringCodec; +import io.lettuce.core.json.arguments.JsonMsetArgs; +import io.lettuce.core.json.arguments.JsonRangeArgs; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import reactor.core.publisher.Mono; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; + +import static io.lettuce.TestTags.INTEGRATION_TEST; +import static org.assertj.core.api.Assertions.assertThat; + +@Tag(INTEGRATION_TEST) +public class RedisJsonIntegrationTests extends RedisContainerIntegrationTests { + + private static final String BIKES_INVENTORY = "bikes:inventory"; + + private static final String BIKE_COLORS_V1 = "..mountain_bikes[1].colors"; + + private static final String BIKE_COLORS_V2 = "$..mountain_bikes[1].colors"; + + private static final String MOUNTAIN_BIKES_V1 = "..mountain_bikes"; + + private static final String MOUNTAIN_BIKES_V2 = "$..mountain_bikes"; + + protected static RedisClient client; + + protected static RedisCommands redis; + + public RedisJsonIntegrationTests() { + RedisURI redisURI = RedisURI.Builder.redis("127.0.0.1").withPort(16379).build(); + + client = RedisClient.create(redisURI); + redis = client.connect().sync(); + } + + @BeforeEach + public void prepare() throws IOException { + redis.flushall(); + + Path path = Paths.get("src/test/resources/bike-inventory.json"); + String read = String.join("", Files.readAllLines(path)); + JsonValue value = redis.getJsonParser().createJsonValue(read); + + redis.jsonSet("bikes:inventory", JsonPath.ROOT_PATH, value); + } + + @AfterAll + static void teardown() { + if (client != null) { + client.shutdown(); + } + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1, MOUNTAIN_BIKES_V2 }) + void jsonArrappend(String path) { + JsonParser parser = redis.getJsonParser(); + JsonPath myPath = JsonPath.of(path); + + JsonValue element = parser.createJsonValue("\"{id:bike6}\""); + List appendedElements = redis.jsonArrappend(BIKES_INVENTORY, myPath, element); + assertThat(appendedElements).hasSize(1); + assertThat(appendedElements.get(0)).isEqualTo(4); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { BIKE_COLORS_V1, BIKE_COLORS_V2 }) + void jsonArrindex(String path) { + JsonParser parser = redis.getJsonParser(); + JsonPath myPath = JsonPath.of(path); + JsonValue element = parser.createJsonValue("\"white\""); + + List arrayIndex = redis.jsonArrindex(BIKES_INVENTORY, myPath, element); + assertThat(arrayIndex).isNotNull(); + assertThat(arrayIndex).hasSize(1); + assertThat(arrayIndex.get(0).longValue()).isEqualTo(1L); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { BIKE_COLORS_V1, BIKE_COLORS_V2 }) + void jsonArrinsert(String path) { + JsonParser parser = redis.getJsonParser(); + JsonPath myPath = JsonPath.of(path); + JsonValue element = parser.createJsonValue("\"ultramarine\""); + + List arrayIndex = redis.jsonArrinsert(BIKES_INVENTORY, myPath, 1, element); + assertThat(arrayIndex).isNotNull(); + assertThat(arrayIndex).hasSize(1); + assertThat(arrayIndex.get(0).longValue()).isEqualTo(3L); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1, MOUNTAIN_BIKES_V2 }) + void jsonArrlen(String path) { + JsonPath myPath = JsonPath.of(path); + + List poppedJson = redis.jsonArrlen(BIKES_INVENTORY, myPath); + assertThat(poppedJson).hasSize(1); + assertThat(poppedJson.get(0).longValue()).isEqualTo(3); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1, MOUNTAIN_BIKES_V2 }) + void jsonArrpop(String path) { + JsonPath myPath = JsonPath.of(path); + + List poppedJson = redis.jsonArrpop(BIKES_INVENTORY, myPath); + assertThat(poppedJson).hasSize(1); + assertThat(poppedJson.get(0).toString()).contains( + "{\"id\":\"bike:3\",\"model\":\"Weywot\",\"description\":\"This bike gives kids aged six years and old"); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { BIKE_COLORS_V1, BIKE_COLORS_V2 }) + void jsonArrtrim(String path) { + JsonPath myPath = JsonPath.of(path); + JsonRangeArgs range = JsonRangeArgs.Builder.start(1).stop(2); + + List arrayIndex = redis.jsonArrtrim(BIKES_INVENTORY, myPath, range); + assertThat(arrayIndex).isNotNull(); + assertThat(arrayIndex).hasSize(1); + assertThat(arrayIndex.get(0).longValue()).isEqualTo(1L); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { BIKE_COLORS_V1, BIKE_COLORS_V2 }) + void jsonClear(String path) { + JsonPath myPath = JsonPath.of(path); + + Long result = redis.jsonClear(BIKES_INVENTORY, myPath); + assertThat(result).isNotNull(); + assertThat(result).isEqualTo(1L); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { "..mountain_bikes[0:2].model", "$..mountain_bikes[0:2].model" }) + void jsonGet(String path) { + JsonPath myPath = JsonPath.of(path); + + // Verify codec parsing + List value = redis.jsonGet(BIKES_INVENTORY, myPath); + assertThat(value).hasSize(1); + + if (path.startsWith("$")) { + assertThat(value.get(0).toString()).isEqualTo("[\"Phoebe\",\"Quaoar\"]"); + + // Verify array parsing + assertThat(value.get(0).isJsonArray()).isTrue(); + assertThat(value.get(0).asJsonArray().size()).isEqualTo(2); + assertThat(value.get(0).asJsonArray().asList().get(0).toString()).isEqualTo("\"Phoebe\""); + assertThat(value.get(0).asJsonArray().asList().get(1).toString()).isEqualTo("\"Quaoar\""); + + // Verify String parsing + assertThat(value.get(0).asJsonArray().asList().get(0).isString()).isTrue(); + assertThat(value.get(0).asJsonArray().asList().get(0).asString()).isEqualTo("Phoebe"); + assertThat(value.get(0).asJsonArray().asList().get(1).isString()).isTrue(); + assertThat(value.get(0).asJsonArray().asList().get(1).isNull()).isFalse(); + assertThat(value.get(0).asJsonArray().asList().get(1).asString()).isEqualTo("Quaoar"); + } else { + assertThat(value.get(0).toString()).isEqualTo("\"Phoebe\""); + + // Verify array parsing + assertThat(value.get(0).isString()).isTrue(); + assertThat(value.get(0).asString()).isEqualTo("Phoebe"); + } + } + + @Test + void jsonGetNull() { + JsonPath myPath = JsonPath.of("$..inventory.owner"); + + // Verify codec parsing + List value = redis.jsonGet(BIKES_INVENTORY, myPath); + assertThat(value).hasSize(1); + + assertThat(value.get(0).toString()).isEqualTo("[null]"); + + // Verify array parsing + assertThat(value.get(0).isJsonArray()).isTrue(); + assertThat(value.get(0).asJsonArray().size()).isEqualTo(1); + assertThat(value.get(0).asJsonArray().asList().get(0).toString()).isEqualTo("null"); + assertThat(value.get(0).asJsonArray().asList().get(0).isNull()).isTrue(); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1 + "[1]", MOUNTAIN_BIKES_V2 + "[1]" }) + void jsonMerge(String path) { + JsonParser parser = redis.getJsonParser(); + JsonPath myPath = JsonPath.of(path); + JsonValue element = parser.createJsonValue("\"ultramarine\""); + + String result = redis.jsonMerge(BIKES_INVENTORY, myPath, element); + assertThat(result).isNotNull(); + assertThat(result).isEqualTo("OK"); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { "..model", "$..model" }) + void jsonMGet(String path) { + JsonPath myPath = JsonPath.of(path); + + List value = redis.jsonMGet(myPath, BIKES_INVENTORY); + assertThat(value).hasSize(1); + if (path.startsWith("$")) { + assertThat(value.get(0).toString()).isEqualTo("[\"Phoebe\",\"Quaoar\",\"Weywot\"]"); + } else { + assertThat(value.get(0).toString()).isEqualTo("\"Phoebe\""); + } + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1 + "[1]", MOUNTAIN_BIKES_V2 + "[1]" }) + void jsonMset(String path) { + JsonParser parser = redis.getJsonParser(); + JsonPath myPath = JsonPath.of(path); + + JsonObject bikeRecord = parser.createJsonObject(); + JsonObject bikeSpecs = parser.createJsonObject(); + JsonArray bikeColors = parser.createJsonArray(); + bikeSpecs.put("material", parser.createJsonValue("\"composite\"")); + bikeSpecs.put("weight", parser.createJsonValue("11")); + bikeColors.add(parser.createJsonValue("\"yellow\"")); + bikeColors.add(parser.createJsonValue("\"orange\"")); + bikeRecord.put("id", parser.createJsonValue("\"bike:43\"")); + bikeRecord.put("model", parser.createJsonValue("\"DesertFox\"")); + bikeRecord.put("description", parser.createJsonValue("\"The DesertFox is a versatile bike for all terrains\"")); + bikeRecord.put("price", parser.createJsonValue("\"1299\"")); + bikeRecord.put("specs", bikeSpecs); + bikeRecord.put("colors", bikeColors); + + JsonMsetArgs args1 = new JsonMsetArgs<>(BIKES_INVENTORY, myPath, bikeRecord); + + bikeRecord = parser.createJsonObject(); + bikeSpecs = parser.createJsonObject(); + bikeColors = parser.createJsonArray(); + bikeSpecs.put("material", parser.createJsonValue("\"wood\"")); + bikeSpecs.put("weight", parser.createJsonValue("19")); + bikeColors.add(parser.createJsonValue("\"walnut\"")); + bikeColors.add(parser.createJsonValue("\"chestnut\"")); + bikeRecord.put("id", parser.createJsonValue("\"bike:13\"")); + bikeRecord.put("model", parser.createJsonValue("\"Woody\"")); + bikeRecord.put("description", parser.createJsonValue("\"The Woody is an environmentally-friendly wooden bike\"")); + bikeRecord.put("price", parser.createJsonValue("\"1112\"")); + bikeRecord.put("specs", bikeSpecs); + bikeRecord.put("colors", bikeColors); + + JsonMsetArgs args2 = new JsonMsetArgs<>(BIKES_INVENTORY, myPath, bikeRecord); + + List> args = Arrays.asList(args1, args2); + String result = redis.jsonMSet(args); + + assertThat(result).isNotNull(); + assertThat(result).isEqualTo("OK"); + + JsonValue value = redis.jsonGet(BIKES_INVENTORY, JsonPath.ROOT_PATH).get(0); + assertThat(value).isNotNull(); + assertThat(value.isJsonArray()).isTrue(); + assertThat(value.asJsonArray().size()).isEqualTo(1); + assertThat(value.asJsonArray().asList().get(0).toString()).contains( + "{\"id\":\"bike:13\",\"model\":\"Woody\",\"description\":\"The Woody is an environmentally-friendly wooden bike\""); + } + + @Test + void jsonMsetCrossslot() { + JsonParser parser = redis.getJsonParser(); + JsonPath myPath = JsonPath.of(BIKES_INVENTORY); + + JsonObject bikeRecord = parser.createJsonObject(); + JsonObject bikeSpecs = parser.createJsonObject(); + JsonArray bikeColors = parser.createJsonArray(); + bikeSpecs.put("material", parser.createJsonValue("\"composite\"")); + bikeSpecs.put("weight", parser.createJsonValue("11")); + bikeColors.add(parser.createJsonValue("\"yellow\"")); + bikeColors.add(parser.createJsonValue("\"orange\"")); + bikeRecord.put("id", parser.createJsonValue("\"bike:43\"")); + bikeRecord.put("model", parser.createJsonValue("\"DesertFox\"")); + bikeRecord.put("description", parser.createJsonValue("\"The DesertFox is a versatile bike for all terrains\"")); + bikeRecord.put("price", parser.createJsonValue("\"1299\"")); + bikeRecord.put("specs", bikeSpecs); + bikeRecord.put("colors", bikeColors); + + JsonMsetArgs args1 = new JsonMsetArgs<>(BIKES_INVENTORY, myPath, bikeRecord); + + bikeRecord = parser.createJsonObject(); + bikeSpecs = parser.createJsonObject(); + bikeColors = parser.createJsonArray(); + bikeSpecs.put("material", parser.createJsonValue("\"wood\"")); + bikeSpecs.put("weight", parser.createJsonValue("19")); + bikeColors.add(parser.createJsonValue("\"walnut\"")); + bikeColors.add(parser.createJsonValue("\"chestnut\"")); + bikeRecord.put("id", parser.createJsonValue("\"bike:13\"")); + bikeRecord.put("model", parser.createJsonValue("\"Woody\"")); + bikeRecord.put("description", parser.createJsonValue("\"The Woody is an environmentally-friendly wooden bike\"")); + bikeRecord.put("price", parser.createJsonValue("\"1112\"")); + bikeRecord.put("specs", bikeSpecs); + bikeRecord.put("colors", bikeColors); + + JsonMsetArgs args2 = new JsonMsetArgs<>("bikes:service", JsonPath.ROOT_PATH, bikeRecord); + + List> args = Arrays.asList(args1, args2); + String result = redis.jsonMSet(args); + + assertThat(result).isNotNull(); + assertThat(result).isEqualTo("OK"); + + JsonValue value = redis.jsonGet("bikes:service", JsonPath.ROOT_PATH).get(0); + assertThat(value).isNotNull(); + assertThat(value.isJsonArray()).isTrue(); + assertThat(value.asJsonArray().size()).isEqualTo(1); + assertThat(value.asJsonArray().asList().get(0).toString()).contains( + "{\"id\":\"bike:13\",\"model\":\"Woody\",\"description\":\"The Woody is an environmentally-friendly wooden bike\""); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { "$..mountain_bikes[0:1].price", "..mountain_bikes[0:1].price" }) + void jsonNumincrby(String path) { + JsonPath myPath = JsonPath.of(path); + + List value = redis.jsonNumincrby(BIKES_INVENTORY, myPath, 5L); + assertThat(value).hasSize(1); + assertThat(value.get(0).longValue()).isEqualTo(1933L); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1 + "[1]", MOUNTAIN_BIKES_V2 + "[1]" }) + void jsonObjkeys(String path) { + JsonPath myPath = JsonPath.of(path); + + List result = redis.jsonObjkeys(BIKES_INVENTORY, myPath); + assertThat(result).isNotNull(); + assertThat(result).hasSize(6); + assertThat(result).contains("id", "model", "description", "price", "specs", "colors"); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1 + "[1]", MOUNTAIN_BIKES_V2 + "[1]" }) + void jsonObjlen(String path) { + JsonPath myPath = JsonPath.of(path); + + List result = redis.jsonObjlen(BIKES_INVENTORY, myPath); + assertThat(result).isNotNull(); + assertThat(result).hasSize(1); + assertThat(result.get(0)).isEqualTo(6L); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1, MOUNTAIN_BIKES_V2 }) + void jsonSet(String path) { + JsonPath myPath = JsonPath.of(path); + + JsonParser parser = redis.getJsonParser(); + JsonObject bikeRecord = parser.createJsonObject(); + JsonObject bikeSpecs = parser.createJsonObject(); + JsonArray bikeColors = parser.createJsonArray(); + + bikeSpecs.put("material", parser.createJsonValue("null")); + bikeSpecs.put("weight", parser.createJsonValue("11")); + + bikeColors.add(parser.createJsonValue("\"yellow\"")); + bikeColors.add(parser.createJsonValue("\"orange\"")); + + bikeRecord.put("id", parser.createJsonValue("\"bike:43\"")); + bikeRecord.put("model", parser.createJsonValue("\"DesertFox\"")); + bikeRecord.put("description", parser.createJsonValue("\"The DesertFox is a versatile bike for all terrains\"")); + bikeRecord.put("price", parser.createJsonValue("\"1299\"")); + bikeRecord.put("specs", bikeSpecs); + bikeRecord.put("colors", bikeColors); + + String result = redis.jsonSet(BIKES_INVENTORY, myPath, bikeRecord); + assertThat(result).isEqualTo("OK"); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { "..mountain_bikes[1].colors[1]", "$..mountain_bikes[1].colors[1]" }) + void jsonStrappend(String path) { + JsonParser parser = redis.getJsonParser(); + JsonPath myPath = JsonPath.of(path); + JsonValue element = parser.createJsonValue("\"-light\""); + + List result = redis.jsonStrappend(BIKES_INVENTORY, myPath, element); + assertThat(result).isNotNull(); + assertThat(result).hasSize(1); + assertThat(result.get(0)).isEqualTo(11L); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { BIKE_COLORS_V1 + "[1]", BIKE_COLORS_V2 + "[1]" }) + void jsonStrlen(String path) { + JsonPath myPath = JsonPath.of(path); + + List result = redis.jsonStrlen(BIKES_INVENTORY, myPath); + assertThat(result).isNotNull(); + assertThat(result).hasSize(1); + assertThat(result.get(0)).isEqualTo(5L); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { "$..complete", "..complete" }) + void jsonToggle(String path) { + JsonPath myPath = JsonPath.of(path); + + List result = redis.jsonToggle(BIKES_INVENTORY, myPath); + assertThat(result).isNotNull(); + assertThat(result).hasSize(1); + if (path.startsWith("$")) { + assertThat(result.get(0)).isEqualTo(1L); + } else { + // seems that for JSON.TOGGLE when we use a V1 path the resulting value is a list of string values and not a + // list of integer values as per the documentation + assertThat(result).isNotEmpty(); + } + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1 + "[2:3]", MOUNTAIN_BIKES_V2 + "[2:3]" }) + void jsonDel(String path) { + JsonPath myPath = JsonPath.of(path); + + Long value = redis.jsonDel(BIKES_INVENTORY, myPath); + assertThat(value).isEqualTo(1); + } + + @ParameterizedTest(name = "With {0} as path") + @ValueSource(strings = { MOUNTAIN_BIKES_V1, MOUNTAIN_BIKES_V2 }) + void jsonType(String path) { + JsonPath myPath = JsonPath.of(path); + + JsonType jsonType = redis.jsonType(BIKES_INVENTORY, myPath).get(0); + assertThat(jsonType).isEqualTo(JsonType.ARRAY); + } + + @Test + void jsonAllTypes() { + JsonPath myPath = JsonPath.of("$..mountain_bikes[1]"); + + JsonType jsonType = redis.jsonType(BIKES_INVENTORY, myPath).get(0); + assertThat(jsonType).isEqualTo(JsonType.OBJECT); + + myPath = JsonPath.of("$..mountain_bikes[0:1].price"); + jsonType = redis.jsonType(BIKES_INVENTORY, myPath).get(0); + assertThat(jsonType).isEqualTo(JsonType.INTEGER); + + myPath = JsonPath.of("$..weight"); + jsonType = redis.jsonType(BIKES_INVENTORY, myPath).get(0); + assertThat(jsonType).isEqualTo(JsonType.NUMBER); + + myPath = JsonPath.of("$..complete"); + jsonType = redis.jsonType(BIKES_INVENTORY, myPath).get(0); + assertThat(jsonType).isEqualTo(JsonType.BOOLEAN); + + myPath = JsonPath.of("$..inventory.owner"); + jsonType = redis.jsonType(BIKES_INVENTORY, myPath).get(0); + assertThat(jsonType).isEqualTo(JsonType.UNKNOWN); + } + + @Test + void jsonGetToObject() { + JsonPath myPath = JsonPath.of("$..mountain_bikes[1]"); + JsonValue value = redis.jsonGet(BIKES_INVENTORY, myPath).get(0); + assertThat(value).isNotNull(); + assertThat(value.isNull()).isFalse(); + assertThat(value.asJsonArray().get(0).isJsonObject()).isTrue(); + + MountainBike bike = value.asJsonArray().get(0).asJsonObject().toObject(MountainBike.class); + + assertThat(bike).isNotNull(); + assertThat(bike).isInstanceOf(MountainBike.class); + + assertThat(bike.id).isEqualTo("bike:2"); + assertThat(bike.model).isEqualTo("Quaoar"); + assertThat(bike.description).contains("Redesigned for the 2020 model year, this bike impressed"); + } + + static class MountainBike { + + public String id; + + public String model; + + public String description; + + public String price; + + public Specs specs; + + public List colors; + + } + + static class Specs { + + public String material; + + public String weight; + + } + + @Test + void jsonSetFromObject() { + JsonPath myPath = JsonPath.of("$..mountain_bikes[1]"); + JsonValue value = redis.jsonGet(BIKES_INVENTORY, myPath).get(0); + JsonParser parser = redis.getJsonParser(); + + MountainBike desertFox = new MountainBike(); + desertFox.specs = new Specs(); + desertFox.id = "bike:43"; + desertFox.model = "DesertFox"; + desertFox.description = "The DesertFox is a versatile bike for all terrains"; + desertFox.price = "1299"; + desertFox.specs.material = "composite"; + desertFox.specs.weight = "11"; + desertFox.colors = Arrays.asList("yellow", "orange"); + + JsonValue newValue = parser.fromObject(desertFox); + + assertThat(newValue).isNotNull(); + assertThat(newValue.isNull()).isFalse(); + assertThat(newValue.isJsonObject()).isTrue(); + assertThat(newValue.asJsonObject().size()).isEqualTo(6); + assertThat(newValue.asJsonObject().get("id").toString()).isEqualTo("\"bike:43\""); + assertThat(newValue.asJsonObject().get("model").toString()).isEqualTo("\"DesertFox\""); + assertThat(newValue.asJsonObject().get("description").toString()) + .isEqualTo("\"The DesertFox is a versatile bike for all terrains\""); + assertThat(newValue.asJsonObject().get("price").toString()).isEqualTo("\"1299\""); + assertThat(newValue.asJsonObject().get("specs").toString()).isEqualTo("{\"material\":\"composite\",\"weight\":\"11\"}"); + assertThat(newValue.asJsonObject().get("colors").toString()).isEqualTo("[\"yellow\",\"orange\"]"); + + String result = redis.jsonSet(BIKES_INVENTORY, myPath, newValue); + + assertThat(result).isEqualTo("OK"); + } + + @Test + void byteArrayCodec() throws ExecutionException, InterruptedException { + JsonPath myPath = JsonPath.of("$..mountain_bikes"); + byte[] myMountainBikesKey = BIKES_INVENTORY.getBytes(); + byte[] myServiceBikesKey = "service_bikes".getBytes(); + + RedisAsyncCommands redis = client.connect(ByteArrayCodec.INSTANCE).async(); + RedisFuture> bikes = redis.jsonGet(myMountainBikesKey, myPath); + + CompletionStage> stage = bikes + .thenApply(fetchedBikes -> redis.jsonSet(myServiceBikesKey, JsonPath.ROOT_PATH, fetchedBikes.get(0))); + + String result = stage.toCompletableFuture().get().get(); + + assertThat(result).isEqualTo("OK"); + } + + @Test + void withCustomParser() { + RedisURI redisURI = RedisURI.Builder.redis("127.0.0.1").withPort(16379).build(); + + try (RedisClient client = RedisClient.create(redisURI)) { + client.setOptions(ClientOptions.builder().jsonParser(Mono.just(new CustomParser())).build()); + StatefulRedisConnection connection = client.connect(StringCodec.UTF8); + RedisCommands redis = connection.sync(); + assertThat(redis.getJsonParser()).isInstanceOf(CustomParser.class); + } + } + + static class CustomParser implements JsonParser { + + @Override + public JsonValue loadJsonValue(ByteBuffer bytes) { + return null; + } + + @Override + public JsonValue createJsonValue(ByteBuffer bytes) { + return null; + } + + @Override + public JsonValue createJsonValue(String value) { + return null; + } + + @Override + public JsonObject createJsonObject() { + return null; + } + + @Override + public JsonArray createJsonArray() { + return null; + } + + @Override + public JsonValue fromObject(Object object) { + return null; + } + + } + +} diff --git a/src/test/java/io/lettuce/core/json/UnproccessedJsonValueUnitTests.java b/src/test/java/io/lettuce/core/json/UnproccessedJsonValueUnitTests.java new file mode 100644 index 0000000000..2123bfb7e1 --- /dev/null +++ b/src/test/java/io/lettuce/core/json/UnproccessedJsonValueUnitTests.java @@ -0,0 +1,142 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + * + * This file contains contributions from third-party contributors + * licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.lettuce.core.json; + +import io.lettuce.core.codec.StringCodec; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static io.lettuce.TestTags.UNIT_TEST; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +/** + * Unit tests for {@link UnproccessedJsonValue}. + */ +@Tag(UNIT_TEST) +class UnproccessedJsonValueUnitTests { + + @Test + void asString() { + final String unprocessed = "{\"a\":1,\"b\":2}"; + final String modified = "{\"a\":1}"; + + DefaultJsonParser parser = new DefaultJsonParser(); + ByteBuffer buffer = ByteBuffer.wrap(unprocessed.getBytes()); + UnproccessedJsonValue underTest = new UnproccessedJsonValue(buffer, parser); + + String value = StringCodec.UTF8.decodeValue(buffer); + assertThat(underTest.toString()).isEqualTo(value); + assertThat(underTest.asByteBuffer()).isEqualTo(ByteBuffer.wrap(unprocessed.getBytes())); + + assertThat(underTest.isJsonObject()).isTrue(); + assertThat(underTest.asJsonObject().remove("b")).isNotNull(); + + assertThat(underTest.toString()).isEqualTo(modified); + assertThat(underTest.asByteBuffer()).isEqualTo(ByteBuffer.wrap(modified.getBytes())); + } + + @Test + void asTextual() { + DefaultJsonParser parser = new DefaultJsonParser(); + ByteBuffer buffer = ByteBuffer.wrap("\"textual\"".getBytes()); + UnproccessedJsonValue underTest = new UnproccessedJsonValue(buffer, parser); + + assertThat(underTest.isString()).isTrue(); + assertThat(underTest.asString()).isEqualTo("textual"); + + Assertions.assertThat(underTest.isBoolean()).isFalse(); + Assertions.assertThat(underTest.isNull()).isFalse(); + Assertions.assertThat(underTest.isNumber()).isFalse(); + Assertions.assertThat(underTest.isJsonObject()).isFalse(); + Assertions.assertThat(underTest.isJsonArray()).isFalse(); + } + + @Test + void asNull() { + + DefaultJsonParser parser = new DefaultJsonParser(); + ByteBuffer buffer = ByteBuffer.wrap("null".getBytes()); + UnproccessedJsonValue underTest = new UnproccessedJsonValue(buffer, parser); + + assertThat(underTest.isNull()).isTrue(); + + Assertions.assertThat(underTest.isBoolean()).isFalse(); + Assertions.assertThat(underTest.isString()).isFalse(); + Assertions.assertThat(underTest.isNumber()).isFalse(); + Assertions.assertThat(underTest.isJsonObject()).isFalse(); + Assertions.assertThat(underTest.isJsonArray()).isFalse(); + } + + @Test + void asNumber() { + + DefaultJsonParser parser = new DefaultJsonParser(); + ByteBuffer buffer = ByteBuffer.wrap("1".getBytes()); + UnproccessedJsonValue underTest = new UnproccessedJsonValue(buffer, parser); + + assertThat(underTest.isNumber()).isTrue(); + assertThat(underTest.asNumber()).isEqualTo(1); + + Assertions.assertThat(underTest.isBoolean()).isFalse(); + Assertions.assertThat(underTest.isString()).isFalse(); + Assertions.assertThat(underTest.isNull()).isFalse(); + Assertions.assertThat(underTest.isJsonObject()).isFalse(); + Assertions.assertThat(underTest.isJsonArray()).isFalse(); + } + + @Test + void asBoolean() { + + DefaultJsonParser parser = new DefaultJsonParser(); + ByteBuffer buffer = ByteBuffer.wrap("true".getBytes()); + UnproccessedJsonValue underTest = new UnproccessedJsonValue(buffer, parser); + + assertThat(underTest.isBoolean()).isTrue(); + assertThat(underTest.asBoolean()).isEqualTo(true); + + Assertions.assertThat(underTest.isNumber()).isFalse(); + Assertions.assertThat(underTest.isString()).isFalse(); + Assertions.assertThat(underTest.isNull()).isFalse(); + Assertions.assertThat(underTest.isJsonObject()).isFalse(); + Assertions.assertThat(underTest.isJsonArray()).isFalse(); + } + + @Test + void asArray() { + + DefaultJsonParser parser = new DefaultJsonParser(); + ByteBuffer buffer = ByteBuffer.wrap("[1,2,3,4]".getBytes()); + UnproccessedJsonValue underTest = new UnproccessedJsonValue(buffer, parser); + + assertThat(underTest.isJsonArray()).isTrue(); + assertThat(underTest.asJsonArray().toString()).isEqualTo("[1,2,3,4]"); + + Assertions.assertThat(underTest.isNumber()).isFalse(); + Assertions.assertThat(underTest.isString()).isFalse(); + Assertions.assertThat(underTest.isNull()).isFalse(); + Assertions.assertThat(underTest.isJsonObject()).isFalse(); + Assertions.assertThat(underTest.isBoolean()).isFalse(); + } + +} diff --git a/src/test/java/io/lettuce/core/masterreplica/ConnectionsUnitTests.java b/src/test/java/io/lettuce/core/masterreplica/ConnectionsUnitTests.java index 2c32c0c019..7bbfeeaf83 100644 --- a/src/test/java/io/lettuce/core/masterreplica/ConnectionsUnitTests.java +++ b/src/test/java/io/lettuce/core/masterreplica/ConnectionsUnitTests.java @@ -1,11 +1,13 @@ package io.lettuce.core.masterreplica; +import static io.lettuce.TestTags.UNIT_TEST; import static org.mockito.Mockito.*; import java.util.Collections; import java.util.concurrent.CompletableFuture; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -20,6 +22,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class ConnectionsUnitTests { diff --git a/src/test/java/io/lettuce/core/masterreplica/CustomCommandIntegrationTests.java b/src/test/java/io/lettuce/core/masterreplica/CustomCommandIntegrationTests.java index 5d0702d15d..bab2535fba 100644 --- a/src/test/java/io/lettuce/core/masterreplica/CustomCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/masterreplica/CustomCommandIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.masterreplica; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assumptions.assumeTrue; @@ -10,6 +11,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -25,6 +27,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class CustomCommandIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/masterreplica/MasterReplicaChannelWriterUnitTests.java b/src/test/java/io/lettuce/core/masterreplica/MasterReplicaChannelWriterUnitTests.java index fead0e6a8f..7acce94210 100644 --- a/src/test/java/io/lettuce/core/masterreplica/MasterReplicaChannelWriterUnitTests.java +++ b/src/test/java/io/lettuce/core/masterreplica/MasterReplicaChannelWriterUnitTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core.masterreplica; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @@ -28,6 +29,7 @@ import java.util.List; import java.util.concurrent.CompletableFuture; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -49,6 +51,7 @@ * @author Mark Paluch * @author Jim Brunner */ +@Tag(UNIT_TEST) @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class MasterReplicaChannelWriterUnitTests { diff --git a/src/test/java/io/lettuce/core/masterreplica/MasterReplicaConnectionProviderUnitTests.java b/src/test/java/io/lettuce/core/masterreplica/MasterReplicaConnectionProviderUnitTests.java index 81b0e4bda2..5ebe878d4b 100644 --- a/src/test/java/io/lettuce/core/masterreplica/MasterReplicaConnectionProviderUnitTests.java +++ b/src/test/java/io/lettuce/core/masterreplica/MasterReplicaConnectionProviderUnitTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.masterreplica; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @@ -9,6 +10,7 @@ import java.util.concurrent.CompletableFuture; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -29,6 +31,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class MasterReplicaConnectionProviderUnitTests { diff --git a/src/test/java/io/lettuce/core/masterreplica/MasterReplicaTest.java b/src/test/java/io/lettuce/core/masterreplica/MasterReplicaIntegrationTests.java similarity index 97% rename from src/test/java/io/lettuce/core/masterreplica/MasterReplicaTest.java rename to src/test/java/io/lettuce/core/masterreplica/MasterReplicaIntegrationTests.java index 97c3d27a8c..d5aa5b82bb 100644 --- a/src/test/java/io/lettuce/core/masterreplica/MasterReplicaTest.java +++ b/src/test/java/io/lettuce/core/masterreplica/MasterReplicaIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.masterreplica; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assumptions.*; @@ -10,6 +11,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.AbstractRedisClientTest; @@ -30,7 +32,8 @@ * * @author Mark Paluch */ -class MasterReplicaTest extends AbstractRedisClientTest { +@Tag(INTEGRATION_TEST) +class MasterReplicaIntegrationTests extends AbstractRedisClientTest { private RedisURI masterURI = RedisURI.Builder.redis(host, TestSettings.port(3)).withPassword(passwd) .withClientName("my-client").withDatabase(5).build(); diff --git a/src/test/java/io/lettuce/core/masterreplica/MasterReplicaSentinelSslIntegrationTests.java b/src/test/java/io/lettuce/core/masterreplica/MasterReplicaSentinelSslIntegrationTests.java index 5e19c1a679..1697835398 100644 --- a/src/test/java/io/lettuce/core/masterreplica/MasterReplicaSentinelSslIntegrationTests.java +++ b/src/test/java/io/lettuce/core/masterreplica/MasterReplicaSentinelSslIntegrationTests.java @@ -2,6 +2,7 @@ import javax.inject.Inject; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -18,11 +19,14 @@ import io.lettuce.test.resource.FastShutdown; import io.lettuce.test.settings.TestSettings; +import static io.lettuce.TestTags.INTEGRATION_TEST; + /** * Integration test for Master/Replica using Redis Sentinel over SSL. * * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class MasterReplicaSentinelSslIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/masterreplica/MasterReplicaTopologyProviderUnitTests.java b/src/test/java/io/lettuce/core/masterreplica/MasterReplicaTopologyProviderUnitTests.java index dc354e4175..f772e5e84c 100644 --- a/src/test/java/io/lettuce/core/masterreplica/MasterReplicaTopologyProviderUnitTests.java +++ b/src/test/java/io/lettuce/core/masterreplica/MasterReplicaTopologyProviderUnitTests.java @@ -19,11 +19,13 @@ */ package io.lettuce.core.masterreplica; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; import java.util.List; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.RedisURI; @@ -37,6 +39,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class MasterReplicaTopologyProviderUnitTests { private StatefulRedisConnection connectionMock = mock(StatefulRedisConnection.class); diff --git a/src/test/java/io/lettuce/core/masterreplica/MasterReplicaTopologyRefreshUnitTests.java b/src/test/java/io/lettuce/core/masterreplica/MasterReplicaTopologyRefreshUnitTests.java index ee305db84c..4fd2b6aeee 100644 --- a/src/test/java/io/lettuce/core/masterreplica/MasterReplicaTopologyRefreshUnitTests.java +++ b/src/test/java/io/lettuce/core/masterreplica/MasterReplicaTopologyRefreshUnitTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.masterreplica; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @@ -12,6 +13,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -30,6 +32,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class MasterReplicaTopologyRefreshUnitTests { diff --git a/src/test/java/io/lettuce/core/masterreplica/MasterReplicaUtilsUnitTests.java b/src/test/java/io/lettuce/core/masterreplica/MasterReplicaUtilsUnitTests.java index 107bd5f6bd..1eef489024 100644 --- a/src/test/java/io/lettuce/core/masterreplica/MasterReplicaUtilsUnitTests.java +++ b/src/test/java/io/lettuce/core/masterreplica/MasterReplicaUtilsUnitTests.java @@ -1,9 +1,11 @@ package io.lettuce.core.masterreplica; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.AssertionsForInterfaceTypes.*; import java.util.Arrays; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.RedisURI; @@ -14,6 +16,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class MasterReplicaUtilsUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/masterreplica/SentinelTopologyRefreshUnitTests.java b/src/test/java/io/lettuce/core/masterreplica/SentinelTopologyRefreshUnitTests.java index ba1b95148c..68421063d4 100644 --- a/src/test/java/io/lettuce/core/masterreplica/SentinelTopologyRefreshUnitTests.java +++ b/src/test/java/io/lettuce/core/masterreplica/SentinelTopologyRefreshUnitTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.masterreplica; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @@ -12,6 +13,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; @@ -42,6 +44,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) @SuppressWarnings("unchecked") @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) diff --git a/src/test/java/io/lettuce/core/masterreplica/StaticMasterReplicaTest.java b/src/test/java/io/lettuce/core/masterreplica/StaticMasterReplicaIntegrationTests.java similarity index 97% rename from src/test/java/io/lettuce/core/masterreplica/StaticMasterReplicaTest.java rename to src/test/java/io/lettuce/core/masterreplica/StaticMasterReplicaIntegrationTests.java index 0828ddfab2..40c2f720d6 100644 --- a/src/test/java/io/lettuce/core/masterreplica/StaticMasterReplicaTest.java +++ b/src/test/java/io/lettuce/core/masterreplica/StaticMasterReplicaIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.masterreplica; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assumptions.*; @@ -10,6 +11,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.AbstractRedisClientTest; @@ -28,7 +30,8 @@ * * @author Mark Paluch */ -class StaticMasterReplicaTest extends AbstractRedisClientTest { +@Tag(INTEGRATION_TEST) +class StaticMasterReplicaIntegrationTests extends AbstractRedisClientTest { private StatefulRedisMasterReplicaConnection connection; diff --git a/src/test/java/io/lettuce/core/masterslave/MasterSlaveTest.java b/src/test/java/io/lettuce/core/masterslave/MasterSlaveIntegrationTests.java similarity index 97% rename from src/test/java/io/lettuce/core/masterslave/MasterSlaveTest.java rename to src/test/java/io/lettuce/core/masterslave/MasterSlaveIntegrationTests.java index 009bc24536..eb36a731a8 100644 --- a/src/test/java/io/lettuce/core/masterslave/MasterSlaveTest.java +++ b/src/test/java/io/lettuce/core/masterslave/MasterSlaveIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.masterslave; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assumptions.*; @@ -10,6 +11,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.AbstractRedisClientTest; @@ -28,7 +30,8 @@ /** * @author Mark Paluch */ -class MasterSlaveTest extends AbstractRedisClientTest { +@Tag(INTEGRATION_TEST) +class MasterSlaveIntegrationTests extends AbstractRedisClientTest { private RedisURI upstreamURI = RedisURI.Builder.redis(host, TestSettings.port(3)).withPassword(passwd) .withClientName("my-client").withDatabase(5).build(); diff --git a/src/test/java/io/lettuce/core/masterslave/MasterSlaveSentinelIntegrationTests.java b/src/test/java/io/lettuce/core/masterslave/MasterSlaveSentinelIntegrationTests.java index 9a0a5b603a..b7ef1ee4b2 100644 --- a/src/test/java/io/lettuce/core/masterslave/MasterSlaveSentinelIntegrationTests.java +++ b/src/test/java/io/lettuce/core/masterslave/MasterSlaveSentinelIntegrationTests.java @@ -1,6 +1,7 @@ package io.lettuce.core.masterslave; -import static io.lettuce.core.masterslave.MasterSlaveTest.slaveCall; +import static io.lettuce.TestTags.INTEGRATION_TEST; +import static io.lettuce.core.masterslave.MasterSlaveIntegrationTests.slaveCall; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; @@ -10,6 +11,7 @@ import javax.inject.Inject; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -24,6 +26,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class MasterSlaveSentinelIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/masterslave/StaticMasterSlaveTest.java b/src/test/java/io/lettuce/core/masterslave/StaticMasterSlaveIntegrationTests.java similarity index 96% rename from src/test/java/io/lettuce/core/masterslave/StaticMasterSlaveTest.java rename to src/test/java/io/lettuce/core/masterslave/StaticMasterSlaveIntegrationTests.java index 91d0ea4f4f..d9e6186f95 100644 --- a/src/test/java/io/lettuce/core/masterslave/StaticMasterSlaveTest.java +++ b/src/test/java/io/lettuce/core/masterslave/StaticMasterSlaveIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.masterslave; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assumptions.*; @@ -9,6 +10,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.AbstractRedisClientTest; @@ -25,7 +27,8 @@ /** * @author Mark Paluch */ -class StaticMasterSlaveTest extends AbstractRedisClientTest { +@Tag(INTEGRATION_TEST) +class StaticMasterSlaveIntegrationTests extends AbstractRedisClientTest { private StatefulRedisMasterSlaveConnection connection; diff --git a/src/test/java/io/lettuce/core/metrics/CommandLatencyCollectorOptionsUnitTests.java b/src/test/java/io/lettuce/core/metrics/CommandLatencyCollectorOptionsUnitTests.java index 91734cd678..80c7bc33da 100644 --- a/src/test/java/io/lettuce/core/metrics/CommandLatencyCollectorOptionsUnitTests.java +++ b/src/test/java/io/lettuce/core/metrics/CommandLatencyCollectorOptionsUnitTests.java @@ -19,15 +19,18 @@ */ package io.lettuce.core.metrics; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; /** * @author Larry Battle */ +@Tag(UNIT_TEST) class CommandLatencyCollectorOptionsUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/metrics/CommandLatencyIdUnitTests.java b/src/test/java/io/lettuce/core/metrics/CommandLatencyIdUnitTests.java index f6f32247b3..b141e5e3ea 100644 --- a/src/test/java/io/lettuce/core/metrics/CommandLatencyIdUnitTests.java +++ b/src/test/java/io/lettuce/core/metrics/CommandLatencyIdUnitTests.java @@ -1,7 +1,9 @@ package io.lettuce.core.metrics; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.protocol.CommandKeyword; @@ -13,6 +15,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class CommandLatencyIdUnitTests { private CommandLatencyId sut = CommandLatencyId.create(LocalAddress.ANY, new LocalAddress("me"), CommandKeyword.ADDR); @@ -54,7 +57,7 @@ public byte[] getBytes() { } @Override - public String name() { + public String toString() { return name; } diff --git a/src/test/java/io/lettuce/core/metrics/DefaultCommandLatencyCollectorOptionsUnitTests.java b/src/test/java/io/lettuce/core/metrics/DefaultCommandLatencyCollectorOptionsUnitTests.java index 84c99a9241..606aa1c6f4 100644 --- a/src/test/java/io/lettuce/core/metrics/DefaultCommandLatencyCollectorOptionsUnitTests.java +++ b/src/test/java/io/lettuce/core/metrics/DefaultCommandLatencyCollectorOptionsUnitTests.java @@ -1,14 +1,17 @@ package io.lettuce.core.metrics; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class DefaultCommandLatencyCollectorOptionsUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/metrics/DefaultCommandLatencyCollectorUnitTests.java b/src/test/java/io/lettuce/core/metrics/DefaultCommandLatencyCollectorUnitTests.java index 3fba67c3dc..7206b3c3f3 100644 --- a/src/test/java/io/lettuce/core/metrics/DefaultCommandLatencyCollectorUnitTests.java +++ b/src/test/java/io/lettuce/core/metrics/DefaultCommandLatencyCollectorUnitTests.java @@ -19,11 +19,13 @@ */ package io.lettuce.core.metrics; +import static io.lettuce.TestTags.UNIT_TEST; import static java.util.concurrent.TimeUnit.*; import static org.assertj.core.api.Assertions.*; import java.util.Map; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; @@ -36,6 +38,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) @ExtendWith(MockitoExtension.class) class DefaultCommandLatencyCollectorUnitTests { diff --git a/src/test/java/io/lettuce/core/metrics/MicrometerCommandLatencyRecorderUnitTests.java b/src/test/java/io/lettuce/core/metrics/MicrometerCommandLatencyRecorderUnitTests.java index edd1f63656..1aa514c6d6 100644 --- a/src/test/java/io/lettuce/core/metrics/MicrometerCommandLatencyRecorderUnitTests.java +++ b/src/test/java/io/lettuce/core/metrics/MicrometerCommandLatencyRecorderUnitTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core.metrics; +import static io.lettuce.TestTags.UNIT_TEST; import static io.lettuce.core.metrics.MicrometerCommandLatencyRecorder.*; import static org.assertj.core.api.Assertions.*; @@ -27,6 +28,7 @@ import org.apache.commons.lang3.ArrayUtils; import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; @@ -46,6 +48,7 @@ * * @author Steven Sheehy */ +@Tag(UNIT_TEST) @ExtendWith(MockitoExtension.class) class MicrometerCommandLatencyRecorderUnitTests { diff --git a/src/test/java/io/lettuce/core/metrics/MicrometerOptionsUnitTests.java b/src/test/java/io/lettuce/core/metrics/MicrometerOptionsUnitTests.java index 3035aa1b60..ce0d66445f 100644 --- a/src/test/java/io/lettuce/core/metrics/MicrometerOptionsUnitTests.java +++ b/src/test/java/io/lettuce/core/metrics/MicrometerOptionsUnitTests.java @@ -19,11 +19,13 @@ */ package io.lettuce.core.metrics; +import static io.lettuce.TestTags.UNIT_TEST; import static io.lettuce.core.metrics.MicrometerOptions.*; import static org.assertj.core.api.Assertions.*; import java.time.Duration; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.protocol.Command; @@ -37,6 +39,7 @@ * @author André Tibola * @author Mark Paluch */ +@Tag(UNIT_TEST) class MicrometerOptionsUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/models/command/CommandDetailParserUnitTests.java b/src/test/java/io/lettuce/core/models/command/CommandDetailParserUnitTests.java index 60e54774de..e52cf8c9f9 100644 --- a/src/test/java/io/lettuce/core/models/command/CommandDetailParserUnitTests.java +++ b/src/test/java/io/lettuce/core/models/command/CommandDetailParserUnitTests.java @@ -19,12 +19,14 @@ */ package io.lettuce.core.models.command; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.internal.LettuceLists; @@ -35,6 +37,7 @@ * @author Mark Paluch * @author Mikhael Sokolov */ +@Tag(UNIT_TEST) class CommandDetailParserUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/models/role/RoleParserUnitTests.java b/src/test/java/io/lettuce/core/models/role/RoleParserUnitTests.java index ddd7440502..d68c375180 100644 --- a/src/test/java/io/lettuce/core/models/role/RoleParserUnitTests.java +++ b/src/test/java/io/lettuce/core/models/role/RoleParserUnitTests.java @@ -1,11 +1,13 @@ package io.lettuce.core.models.role; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.ArrayList; import java.util.List; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.internal.HostAndPort; @@ -14,6 +16,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class RoleParserUnitTests { private static final long REPLICATION_OFFSET_1 = 3167038L; diff --git a/src/test/java/io/lettuce/core/models/stream/PendingParserUnitTests.java b/src/test/java/io/lettuce/core/models/stream/PendingParserUnitTests.java index cdcddf71d5..73579a83f0 100644 --- a/src/test/java/io/lettuce/core/models/stream/PendingParserUnitTests.java +++ b/src/test/java/io/lettuce/core/models/stream/PendingParserUnitTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.models.stream; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.time.Duration; @@ -7,6 +8,7 @@ import java.util.Collections; import java.util.List; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.Range; @@ -14,6 +16,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class PendingParserUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/output/BooleanListOutputUnitTests.java b/src/test/java/io/lettuce/core/output/BooleanListOutputUnitTests.java index f3406b867a..8836e03a93 100644 --- a/src/test/java/io/lettuce/core/output/BooleanListOutputUnitTests.java +++ b/src/test/java/io/lettuce/core/output/BooleanListOutputUnitTests.java @@ -1,9 +1,11 @@ package io.lettuce.core.output; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import java.nio.ByteBuffer; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.codec.StringCodec; @@ -11,6 +13,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class BooleanListOutputUnitTests { private BooleanListOutput sut = new BooleanListOutput<>(StringCodec.UTF8); diff --git a/src/test/java/io/lettuce/core/output/GeoCoordinatesListOutputUnitTests.java b/src/test/java/io/lettuce/core/output/GeoCoordinatesListOutputUnitTests.java index 6f14c5271b..9c09e80a36 100644 --- a/src/test/java/io/lettuce/core/output/GeoCoordinatesListOutputUnitTests.java +++ b/src/test/java/io/lettuce/core/output/GeoCoordinatesListOutputUnitTests.java @@ -1,9 +1,11 @@ package io.lettuce.core.output; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import java.nio.ByteBuffer; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.GeoCoordinates; @@ -12,6 +14,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class GeoCoordinatesListOutputUnitTests { private GeoCoordinatesListOutput sut = new GeoCoordinatesListOutput<>(StringCodec.UTF8); diff --git a/src/test/java/io/lettuce/core/output/GeoCoordinatesValueListOutputUnitTests.java b/src/test/java/io/lettuce/core/output/GeoCoordinatesValueListOutputUnitTests.java index 085dcc90b3..0c672db874 100644 --- a/src/test/java/io/lettuce/core/output/GeoCoordinatesValueListOutputUnitTests.java +++ b/src/test/java/io/lettuce/core/output/GeoCoordinatesValueListOutputUnitTests.java @@ -1,9 +1,11 @@ package io.lettuce.core.output; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import java.nio.ByteBuffer; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.GeoCoordinates; @@ -13,6 +15,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class GeoCoordinatesValueListOutputUnitTests { private GeoCoordinatesValueListOutput sut = new GeoCoordinatesValueListOutput<>(StringCodec.UTF8); diff --git a/src/test/java/io/lettuce/core/output/GeoWithinListOutputUnitTests.java b/src/test/java/io/lettuce/core/output/GeoWithinListOutputUnitTests.java index b173391f19..33ab016fd2 100644 --- a/src/test/java/io/lettuce/core/output/GeoWithinListOutputUnitTests.java +++ b/src/test/java/io/lettuce/core/output/GeoWithinListOutputUnitTests.java @@ -1,9 +1,11 @@ package io.lettuce.core.output; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.nio.ByteBuffer; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.GeoCoordinates; @@ -14,6 +16,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class GeoWithinListOutputUnitTests { private GeoWithinListOutput sut = new GeoWithinListOutput<>(StringCodec.UTF8, false, false, false); diff --git a/src/test/java/io/lettuce/core/output/IntegerListOutputUnitTests.java b/src/test/java/io/lettuce/core/output/IntegerListOutputUnitTests.java index 4ed64548c4..8a19682227 100644 --- a/src/test/java/io/lettuce/core/output/IntegerListOutputUnitTests.java +++ b/src/test/java/io/lettuce/core/output/IntegerListOutputUnitTests.java @@ -1,6 +1,7 @@ package io.lettuce.core.output; import io.lettuce.core.codec.StringCodec; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -9,12 +10,14 @@ import java.util.Collection; import java.util.List; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * @author Tihomir Mateev */ +@Tag(UNIT_TEST) class IntegerListOutputUnitTests { static Collection parameters() { diff --git a/src/test/java/io/lettuce/core/output/JsonTypeListOutputUnitTests.java b/src/test/java/io/lettuce/core/output/JsonTypeListOutputUnitTests.java new file mode 100644 index 0000000000..c3bf3788fb --- /dev/null +++ b/src/test/java/io/lettuce/core/output/JsonTypeListOutputUnitTests.java @@ -0,0 +1,51 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.output; + +import io.lettuce.core.codec.StringCodec; +import io.lettuce.core.json.DefaultJsonParser; +import io.lettuce.core.json.JsonType; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static io.lettuce.TestTags.UNIT_TEST; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for {@link JsonTypeListOutput}. + */ +@Tag(UNIT_TEST) +class JsonTypeListOutputUnitTests { + + @Test + void set() { + JsonTypeListOutput sut = new JsonTypeListOutput<>(StringCodec.UTF8); + sut.multi(7); + sut.set(ByteBuffer.wrap("object".getBytes())); + sut.set(ByteBuffer.wrap("array".getBytes())); + sut.set(ByteBuffer.wrap("string".getBytes())); + sut.set(ByteBuffer.wrap("integer".getBytes())); + sut.set(ByteBuffer.wrap("number".getBytes())); + sut.set(ByteBuffer.wrap("boolean".getBytes())); + sut.set(ByteBuffer.wrap("null".getBytes())); + + assertThat(sut.get().isEmpty()).isFalse(); + assertThat(sut.get().size()).isEqualTo(7); + assertThat(sut.get().get(0)).isEqualTo(JsonType.OBJECT); + assertThat(sut.get().get(1)).isEqualTo(JsonType.ARRAY); + assertThat(sut.get().get(2)).isEqualTo(JsonType.STRING); + assertThat(sut.get().get(3)).isEqualTo(JsonType.INTEGER); + assertThat(sut.get().get(4)).isEqualTo(JsonType.NUMBER); + assertThat(sut.get().get(5)).isEqualTo(JsonType.BOOLEAN); + assertThat(sut.get().get(6)).isEqualTo(JsonType.UNKNOWN); + } + +} diff --git a/src/test/java/io/lettuce/core/output/JsonValueListOutputUnitTests.java b/src/test/java/io/lettuce/core/output/JsonValueListOutputUnitTests.java new file mode 100644 index 0000000000..dd27dd1773 --- /dev/null +++ b/src/test/java/io/lettuce/core/output/JsonValueListOutputUnitTests.java @@ -0,0 +1,39 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.output; + +import io.lettuce.core.codec.StringCodec; +import io.lettuce.core.json.DefaultJsonParser; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static io.lettuce.TestTags.UNIT_TEST; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link JsonValueListOutput}. + */ +@Tag(UNIT_TEST) +class JsonValueListOutputUnitTests { + + @Test + void set() { + JsonValueListOutput sut = new JsonValueListOutput<>(StringCodec.UTF8, new DefaultJsonParser()); + sut.multi(2); + sut.set(ByteBuffer.wrap("[1,2,3]".getBytes())); + sut.set(ByteBuffer.wrap("world".getBytes())); + + assertThat(sut.get().isEmpty()).isFalse(); + assertThat(sut.get().size()).isEqualTo(2); + assertThat(sut.get().get(0).toString()).isEqualTo("[1,2,3]"); + assertThat(sut.get().get(1).toString()).isEqualTo("world"); + } + +} diff --git a/src/test/java/io/lettuce/core/output/ListOutputUnitTests.java b/src/test/java/io/lettuce/core/output/ListOutputUnitTests.java index 8c1831f192..de3faa28f4 100644 --- a/src/test/java/io/lettuce/core/output/ListOutputUnitTests.java +++ b/src/test/java/io/lettuce/core/output/ListOutputUnitTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.output; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import java.nio.ByteBuffer; @@ -7,6 +8,7 @@ import java.util.Collection; import java.util.List; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -15,6 +17,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class ListOutputUnitTests { static Collection parameters() { diff --git a/src/test/java/io/lettuce/core/output/MapOutputUnitTests.java b/src/test/java/io/lettuce/core/output/MapOutputUnitTests.java index 0c4aa8951f..d8622dc342 100644 --- a/src/test/java/io/lettuce/core/output/MapOutputUnitTests.java +++ b/src/test/java/io/lettuce/core/output/MapOutputUnitTests.java @@ -1,9 +1,11 @@ package io.lettuce.core.output; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import java.nio.ByteBuffer; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.codec.StringCodec; @@ -13,6 +15,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class MapOutputUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/output/MultiOutputUnitTests.java b/src/test/java/io/lettuce/core/output/MultiOutputUnitTests.java index a7b9792d09..c8d73c2c37 100644 --- a/src/test/java/io/lettuce/core/output/MultiOutputUnitTests.java +++ b/src/test/java/io/lettuce/core/output/MultiOutputUnitTests.java @@ -1,9 +1,11 @@ package io.lettuce.core.output; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.nio.ByteBuffer; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.codec.StringCodec; @@ -13,6 +15,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class MultiOutputUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/output/NestedMultiOutputUnitTests.java b/src/test/java/io/lettuce/core/output/NestedMultiOutputUnitTests.java index 76234eff23..6166c6abf9 100644 --- a/src/test/java/io/lettuce/core/output/NestedMultiOutputUnitTests.java +++ b/src/test/java/io/lettuce/core/output/NestedMultiOutputUnitTests.java @@ -19,10 +19,12 @@ */ package io.lettuce.core.output; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.codec.StringCodec; @@ -30,6 +32,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class NestedMultiOutputUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/output/NumberListOutputUnitTests.java b/src/test/java/io/lettuce/core/output/NumberListOutputUnitTests.java new file mode 100644 index 0000000000..24055d2538 --- /dev/null +++ b/src/test/java/io/lettuce/core/output/NumberListOutputUnitTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.output; + +import io.lettuce.core.codec.StringCodec; +import io.lettuce.core.json.JsonType; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static io.lettuce.TestTags.UNIT_TEST; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for {@link NumberListOutput}. + */ +@Tag(UNIT_TEST) +class NumberListOutputUnitTests { + + @Test + void set() { + NumberListOutput sut = new NumberListOutput<>(StringCodec.UTF8); + sut.multi(4); + sut.set(ByteBuffer.wrap((String.valueOf(Double.MAX_VALUE)).getBytes())); + sut.set(1.2); + sut.set(1L); + sut.setBigNumber(ByteBuffer.wrap(String.valueOf(Double.MAX_VALUE).getBytes())); + + assertThat(sut.get().isEmpty()).isFalse(); + assertThat(sut.get().size()).isEqualTo(4); + assertThat(sut.get().get(0)).isEqualTo(Double.MAX_VALUE); + assertThat(sut.get().get(1)).isEqualTo(1.2); + assertThat(sut.get().get(2)).isEqualTo(1L); + assertThat(sut.get().get(3)).isEqualTo(Double.MAX_VALUE); + } + + @Test + void setNegative() { + NumberListOutput sut = new NumberListOutput<>(StringCodec.UTF8); + sut.multi(1); + sut.set(ByteBuffer.wrap("Not a number".getBytes())); + + assertThat(sut.get().isEmpty()).isFalse(); + assertThat(sut.get().size()).isEqualTo(1); + assertThat(sut.get().get(0)).isEqualTo(0); + } + +} diff --git a/src/test/java/io/lettuce/core/output/ObjectOutputTests.java b/src/test/java/io/lettuce/core/output/ObjectOutputUnitTests.java similarity index 96% rename from src/test/java/io/lettuce/core/output/ObjectOutputTests.java rename to src/test/java/io/lettuce/core/output/ObjectOutputUnitTests.java index 87c0fa9813..8fd36f1a2a 100644 --- a/src/test/java/io/lettuce/core/output/ObjectOutputTests.java +++ b/src/test/java/io/lettuce/core/output/ObjectOutputUnitTests.java @@ -1,10 +1,12 @@ package io.lettuce.core.output; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import java.util.List; import java.util.Map; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.codec.StringCodec; @@ -17,7 +19,8 @@ * * @author Mark Paluch */ -class ObjectOutputTests { +@Tag(UNIT_TEST) +class ObjectOutputUnitTests { @Test void shouldParseHelloWithModules() { diff --git a/src/test/java/io/lettuce/core/output/ReplayOutputUnitTests.java b/src/test/java/io/lettuce/core/output/ReplayOutputUnitTests.java index c6c3dee693..c15f55139c 100644 --- a/src/test/java/io/lettuce/core/output/ReplayOutputUnitTests.java +++ b/src/test/java/io/lettuce/core/output/ReplayOutputUnitTests.java @@ -1,11 +1,13 @@ package io.lettuce.core.output; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.Collections; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.codec.StringCodec; @@ -13,6 +15,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class ReplayOutputUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/output/ScoredValueListOutputUnitTests.java b/src/test/java/io/lettuce/core/output/ScoredValueListOutputUnitTests.java index 2b357b6b1b..f71b1b54cb 100644 --- a/src/test/java/io/lettuce/core/output/ScoredValueListOutputUnitTests.java +++ b/src/test/java/io/lettuce/core/output/ScoredValueListOutputUnitTests.java @@ -1,9 +1,11 @@ package io.lettuce.core.output; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import java.nio.ByteBuffer; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.ScoredValue; @@ -12,6 +14,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class ScoredValueListOutputUnitTests { private ScoredValueListOutput sut = new ScoredValueListOutput<>(StringCodec.UTF8); diff --git a/src/test/java/io/lettuce/core/output/SocketAddressOutputUnitTests.java b/src/test/java/io/lettuce/core/output/SocketAddressOutputUnitTests.java index dcb741abdc..0b1bf7dd3c 100644 --- a/src/test/java/io/lettuce/core/output/SocketAddressOutputUnitTests.java +++ b/src/test/java/io/lettuce/core/output/SocketAddressOutputUnitTests.java @@ -1,10 +1,12 @@ package io.lettuce.core.output; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.net.InetSocketAddress; import java.nio.ByteBuffer; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.codec.StringCodec; @@ -12,6 +14,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class SocketAddressOutputUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/output/StreamReadOutputUnitTests.java b/src/test/java/io/lettuce/core/output/StreamReadOutputUnitTests.java index 3b9eddee7c..af7a7883d0 100644 --- a/src/test/java/io/lettuce/core/output/StreamReadOutputUnitTests.java +++ b/src/test/java/io/lettuce/core/output/StreamReadOutputUnitTests.java @@ -1,9 +1,11 @@ package io.lettuce.core.output; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.nio.ByteBuffer; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.StreamMessage; @@ -14,6 +16,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class StreamReadOutputUnitTests { private StreamReadOutput sut = new StreamReadOutput<>(StringCodec.UTF8); diff --git a/src/test/java/io/lettuce/core/output/StringMatchResultOutputUnitTests.java b/src/test/java/io/lettuce/core/output/StringMatchResultOutputUnitTests.java new file mode 100644 index 0000000000..428b1c0a85 --- /dev/null +++ b/src/test/java/io/lettuce/core/output/StringMatchResultOutputUnitTests.java @@ -0,0 +1,122 @@ +package io.lettuce.core.output; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.netty.buffer.Unpooled; +import org.junit.jupiter.api.Test; + +import io.lettuce.core.StringMatchResult; +import io.lettuce.core.codec.StringCodec; +import io.lettuce.core.protocol.ProtocolVersion; +import io.lettuce.core.protocol.RedisStateMachine; + +public class StringMatchResultOutputUnitTests { + + @Test + void parseManually() { + byte[] rawOne = "%2\r\n$7\r\nmatches\r\n*1\r\n*3\r\n*2\r\n:4\r\n:7\r\n*2\r\n:5\r\n:8\r\n:4\r\n$3\r\nlen\r\n:6\r\n" + .getBytes(StandardCharsets.US_ASCII); + byte[] rawTwo = "%2\r\n$3\r\nlen\r\n:6\r\n$7\r\nmatches\r\n*1\r\n*3\r\n*2\r\n:4\r\n:7\r\n*2\r\n:5\r\n:8\r\n:4\r\n" + .getBytes(StandardCharsets.US_ASCII); + RedisStateMachine rsm = new RedisStateMachine(); + rsm.setProtocolVersion(ProtocolVersion.RESP3); + + StringMatchResultOutput o1 = new StringMatchResultOutput<>(StringCodec.ASCII); + assertThat(rsm.decode(Unpooled.wrappedBuffer(rawOne), o1)).isTrue(); + + StringMatchResultOutput o2 = new StringMatchResultOutput<>(StringCodec.ASCII); + assertThat(rsm.decode(Unpooled.wrappedBuffer(rawTwo), o2)).isTrue(); + + Map res1 = transform(o1.get()); + Map res2 = transform(o2.get()); + + assertThat(res1).isEqualTo(res2); + } + + private Map transform(StringMatchResult result) { + Map obj = new HashMap<>(); + List matches = new ArrayList<>(); + for (StringMatchResult.MatchedPosition match : result.getMatches()) { + Map intra = new HashMap<>(); + Map a = new HashMap<>(); + Map b = new HashMap<>(); + a.put("start", match.getA().getStart()); + a.put("end", match.getA().getEnd()); + + b.put("start", match.getB().getStart()); + b.put("end", match.getB().getEnd()); + intra.put("a", a); + intra.put("b", b); + intra.put("matchLen", match.getMatchLen()); + matches.add(intra); + } + obj.put("matches", matches); + obj.put("len", result.getLen()); + return obj; + } + + @Test + void parseOnlyStringMatch() { + StringMatchResultOutput output = new StringMatchResultOutput<>(StringCodec.ASCII); + + String matchString = "some-string"; + output.set(ByteBuffer.wrap(matchString.getBytes())); + output.complete(0); + + StringMatchResult result = output.get(); + assertThat(result.getMatchString()).isEqualTo(matchString); + assertThat(result.getMatches()).isEmpty(); + assertThat(result.getLen()).isZero(); + } + + @Test + void parseOnlyLen() { + StringMatchResultOutput output = new StringMatchResultOutput<>(StringCodec.ASCII); + + output.set(42); + output.complete(0); + + StringMatchResult result = output.get(); + assertThat(result.getMatchString()).isNull(); + assertThat(result.getMatches()).isEmpty(); + assertThat(result.getLen()).isEqualTo(42); + } + + @Test + void parseLenAndMatchesWithIdx() { + StringMatchResultOutput output = new StringMatchResultOutput<>(StringCodec.ASCII); + + output.set(ByteBuffer.wrap("len".getBytes())); + output.set(42); + + output.set(ByteBuffer.wrap("matches".getBytes())); + output.set(0); + output.set(5); + output.set(10); + output.set(15); + + output.complete(2); + output.complete(0); + + StringMatchResult result = output.get(); + + assertThat(result.getMatchString()).isNull(); + assertThat(result.getLen()).isEqualTo(42); + assertThat(result.getMatches()).hasSize(1).satisfies(m -> assertMatchedPositions(m.get(0), 0, 5, 10, 15)); + } + + private void assertMatchedPositions(StringMatchResult.MatchedPosition match, int... expected) { + assertThat(match.getA().getStart()).isEqualTo(expected[0]); + assertThat(match.getA().getEnd()).isEqualTo(expected[1]); + assertThat(match.getB().getStart()).isEqualTo(expected[2]); + assertThat(match.getB().getEnd()).isEqualTo(expected[3]); + } + +} diff --git a/src/test/java/io/lettuce/core/output/TrackingInfoParserUnitTests.java b/src/test/java/io/lettuce/core/output/TrackingInfoParserUnitTests.java new file mode 100644 index 0000000000..259ab24718 --- /dev/null +++ b/src/test/java/io/lettuce/core/output/TrackingInfoParserUnitTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ + +package io.lettuce.core.output; + +import io.lettuce.core.TrackingInfo; +import io.lettuce.core.protocol.CommandKeyword; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static io.lettuce.TestTags.UNIT_TEST; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@Tag(UNIT_TEST) +class TrackingInfoParserUnitTests { + + @Test + void parseResp3() { + ComplexData flags = new SetComplexData(2); + flags.store(TrackingInfo.TrackingFlag.ON.toString()); + flags.store(TrackingInfo.TrackingFlag.OPTIN.toString()); + + ComplexData prefixes = new ArrayComplexData(0); + + ComplexData input = new MapComplexData(3); + input.store(CommandKeyword.FLAGS.toString().toLowerCase()); + input.storeObject(flags); + input.store(CommandKeyword.REDIRECT.toString().toLowerCase()); + input.store(0L); + input.store(CommandKeyword.PREFIXES.toString().toLowerCase()); + input.storeObject(prefixes); + + TrackingInfo info = TrackingInfoParser.INSTANCE.parse(input); + + assertThat(info.getFlags()).contains(TrackingInfo.TrackingFlag.ON, TrackingInfo.TrackingFlag.OPTIN); + assertThat(info.getRedirect()).isEqualTo(0L); + assertThat(info.getPrefixes()).isEmpty(); + } + + @Test + void parseFailEmpty() { + ComplexData input = new MapComplexData(0); + + Exception exception = assertThrows(IllegalArgumentException.class, () -> { + TrackingInfo info = TrackingInfoParser.INSTANCE.parse(input); + }); + } + + @Test + void parseFailNumberOfElements() { + ComplexData flags = new SetComplexData(2); + flags.store(TrackingInfo.TrackingFlag.ON.toString()); + flags.store(TrackingInfo.TrackingFlag.OPTIN.toString()); + + ComplexData prefixes = new ArrayComplexData(0); + + ComplexData input = new MapComplexData(3); + input.store(CommandKeyword.FLAGS.toString().toLowerCase()); + input.storeObject(flags); + input.store(CommandKeyword.REDIRECT.toString().toLowerCase()); + input.store(-1L); + + Exception exception = assertThrows(IllegalArgumentException.class, () -> { + TrackingInfo info = TrackingInfoParser.INSTANCE.parse(input); + }); + } + + @Test + void parseResp2Compatibility() { + ComplexData flags = new ArrayComplexData(2); + flags.store(TrackingInfo.TrackingFlag.ON.toString()); + flags.store(TrackingInfo.TrackingFlag.OPTIN.toString()); + + ComplexData prefixes = new ArrayComplexData(0); + + ComplexData input = new ArrayComplexData(3); + input.store(CommandKeyword.FLAGS.toString().toLowerCase()); + input.storeObject(flags); + input.store(CommandKeyword.REDIRECT.toString().toLowerCase()); + input.store(0L); + input.store(CommandKeyword.PREFIXES.toString().toLowerCase()); + input.storeObject(prefixes); + + TrackingInfo info = TrackingInfoParser.INSTANCE.parse(input); + + assertThat(info.getFlags()).contains(TrackingInfo.TrackingFlag.ON, TrackingInfo.TrackingFlag.OPTIN); + assertThat(info.getRedirect()).isEqualTo(0L); + assertThat(info.getPrefixes()).isEmpty(); + } + +} diff --git a/src/test/java/io/lettuce/core/protocol/AsyncCommandUnitTests.java b/src/test/java/io/lettuce/core/protocol/AsyncCommandUnitTests.java index 6e85a0a49a..a905e9d97d 100644 --- a/src/test/java/io/lettuce/core/protocol/AsyncCommandUnitTests.java +++ b/src/test/java/io/lettuce/core/protocol/AsyncCommandUnitTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.protocol; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import java.nio.charset.StandardCharsets; @@ -8,6 +9,7 @@ import java.util.concurrent.TimeoutException; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.RedisCommandExecutionException; @@ -25,6 +27,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) public class AsyncCommandUnitTests { private RedisCodec codec = StringCodec.UTF8; diff --git a/src/test/java/io/lettuce/core/protocol/CommandArgsUnitTests.java b/src/test/java/io/lettuce/core/protocol/CommandArgsUnitTests.java index 910461962b..9a7db091f0 100644 --- a/src/test/java/io/lettuce/core/protocol/CommandArgsUnitTests.java +++ b/src/test/java/io/lettuce/core/protocol/CommandArgsUnitTests.java @@ -1,11 +1,13 @@ package io.lettuce.core.protocol; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.codec.ByteArrayCodec; @@ -18,6 +20,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class CommandArgsUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/protocol/CommandHandlerUnitTests.java b/src/test/java/io/lettuce/core/protocol/CommandHandlerUnitTests.java index 243b9f8b55..833625df55 100644 --- a/src/test/java/io/lettuce/core/protocol/CommandHandlerUnitTests.java +++ b/src/test/java/io/lettuce/core/protocol/CommandHandlerUnitTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core.protocol; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.Fail.fail; import static org.mockito.AdditionalMatchers.*; @@ -45,6 +46,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; @@ -87,6 +89,7 @@ * @author Gavin Cook * @author Shaphan */ +@Tag(UNIT_TEST) @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class CommandHandlerUnitTests { diff --git a/src/test/java/io/lettuce/core/protocol/CommandUnitTests.java b/src/test/java/io/lettuce/core/protocol/CommandUnitTests.java index f40b5fac13..167ccf60e7 100644 --- a/src/test/java/io/lettuce/core/protocol/CommandUnitTests.java +++ b/src/test/java/io/lettuce/core/protocol/CommandUnitTests.java @@ -1,10 +1,12 @@ package io.lettuce.core.protocol; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.RedisException; @@ -18,6 +20,7 @@ * @author Will Glozer * @author Mark Paluch */ +@Tag(UNIT_TEST) public class CommandUnitTests { private Command sut; diff --git a/src/test/java/io/lettuce/core/protocol/CommandWrapperUnitTests.java b/src/test/java/io/lettuce/core/protocol/CommandWrapperUnitTests.java index b25706a2d4..0e5b4460cc 100644 --- a/src/test/java/io/lettuce/core/protocol/CommandWrapperUnitTests.java +++ b/src/test/java/io/lettuce/core/protocol/CommandWrapperUnitTests.java @@ -1,11 +1,13 @@ package io.lettuce.core.protocol; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.codec.RedisCodec; @@ -18,6 +20,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class CommandWrapperUnitTests { private RedisCodec codec = StringCodec.UTF8; diff --git a/src/test/java/io/lettuce/core/protocol/ConnectionFailureIntegrationTests.java b/src/test/java/io/lettuce/core/protocol/ConnectionFailureIntegrationTests.java index 38f759c371..d1621181ef 100644 --- a/src/test/java/io/lettuce/core/protocol/ConnectionFailureIntegrationTests.java +++ b/src/test/java/io/lettuce/core/protocol/ConnectionFailureIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.protocol; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.fail; @@ -16,6 +17,7 @@ import javax.inject.Inject; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; @@ -40,6 +42,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) class ConnectionFailureIntegrationTests extends TestSupport { @@ -89,7 +92,8 @@ void invalidFirstByte() throws Exception { @Test void failOnReconnect() throws Exception { - ClientOptions clientOptions = ClientOptions.builder().suspendReconnectOnProtocolFailure(true).build(); + ClientOptions clientOptions = ClientOptions.builder().suspendReconnectOnProtocolFailure(true) + .timeoutOptions(TimeoutOptions.builder().timeoutCommands(false).build()).build(); client.setOptions(clientOptions); RandomResponseServer ts = getRandomResponseServer(); @@ -133,7 +137,8 @@ void failOnReconnect() throws Exception { @Test void failOnReconnectShouldSendEvents() throws Exception { - client.setOptions(ClientOptions.builder().suspendReconnectOnProtocolFailure(false).build()); + client.setOptions(ClientOptions.builder().suspendReconnectOnProtocolFailure(false) + .timeoutOptions(TimeoutOptions.builder().timeoutCommands(false).build()).build()); RandomResponseServer ts = getRandomResponseServer(); @@ -181,7 +186,8 @@ void failOnReconnectShouldSendEvents() throws Exception { @Test void cancelCommandsOnReconnectFailure() throws Exception { - client.setOptions(ClientOptions.builder().cancelCommandsOnReconnectFailure(true).build()); + client.setOptions(ClientOptions.builder().cancelCommandsOnReconnectFailure(true) + .timeoutOptions(TimeoutOptions.builder().timeoutCommands(false).build()).build()); RandomResponseServer ts = getRandomResponseServer(); @@ -235,7 +241,8 @@ void emitEventOnReconnectFailure() throws Exception { RedisURI redisUri = RedisURI.create(defaultRedisUri.toURI()); RedisClient client = RedisClient.create(clientResources); - client.setOptions(ClientOptions.builder().build()); + client.setOptions( + ClientOptions.builder().timeoutOptions(TimeoutOptions.builder().timeoutCommands(false).build()).build()); try { RedisAsyncCommandsImpl connection = (RedisAsyncCommandsImpl) client @@ -329,7 +336,8 @@ public void afterChannelInitialized(Channel channel) { RedisURI redisUri = RedisURI.create(TestSettings.host(), TestSettings.port()); RedisClient client = RedisClient.create(clientResources, redisUri); - client.setOptions(ClientOptions.builder().pingBeforeActivateConnection(true).build()); + client.setOptions(ClientOptions.builder().pingBeforeActivateConnection(true) + .timeoutOptions(TimeoutOptions.builder().timeoutCommands(false).build()).build()); StatefulRedisConnection connection = client.connect(); @@ -365,6 +373,8 @@ public void afterChannelInitialized(Channel channel) { void closingDisconnectedConnectionShouldDisableConnectionWatchdog() { client.setOptions(ClientOptions.create()); + client.setOptions( + ClientOptions.builder().timeoutOptions(TimeoutOptions.builder().timeoutCommands(false).build()).build()); RedisURI redisUri = RedisURI.Builder.redis(TestSettings.host(), TestSettings.port()).withTimeout(Duration.ofMinutes(10)) .build(); diff --git a/src/test/java/io/lettuce/core/protocol/DecodeBufferPoliciesUnitTests.java b/src/test/java/io/lettuce/core/protocol/DecodeBufferPoliciesUnitTests.java index 1b72467c1d..2e6c1b7732 100644 --- a/src/test/java/io/lettuce/core/protocol/DecodeBufferPoliciesUnitTests.java +++ b/src/test/java/io/lettuce/core/protocol/DecodeBufferPoliciesUnitTests.java @@ -1,9 +1,11 @@ package io.lettuce.core.protocol; +import static io.lettuce.TestTags.UNIT_TEST; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -16,6 +18,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) @ExtendWith(MockitoExtension.class) class DecodeBufferPoliciesUnitTests { diff --git a/src/test/java/io/lettuce/core/protocol/DefaultEndpointUnitTests.java b/src/test/java/io/lettuce/core/protocol/DefaultEndpointUnitTests.java index 8080848920..9b0a8cd7be 100644 --- a/src/test/java/io/lettuce/core/protocol/DefaultEndpointUnitTests.java +++ b/src/test/java/io/lettuce/core/protocol/DefaultEndpointUnitTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.protocol; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @@ -23,6 +24,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; @@ -50,6 +52,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class DefaultEndpointUnitTests { diff --git a/src/test/java/io/lettuce/core/protocol/RatioDecodeBufferPolicyTest.java b/src/test/java/io/lettuce/core/protocol/RatioDecodeBufferPolicyUnitTests.java similarity index 93% rename from src/test/java/io/lettuce/core/protocol/RatioDecodeBufferPolicyTest.java rename to src/test/java/io/lettuce/core/protocol/RatioDecodeBufferPolicyUnitTests.java index b48a86dc45..f57ccd0b8b 100644 --- a/src/test/java/io/lettuce/core/protocol/RatioDecodeBufferPolicyTest.java +++ b/src/test/java/io/lettuce/core/protocol/RatioDecodeBufferPolicyUnitTests.java @@ -19,10 +19,12 @@ */ package io.lettuce.core.protocol; +import static io.lettuce.TestTags.UNIT_TEST; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -35,8 +37,9 @@ * * @author Shaphan */ +@Tag(UNIT_TEST) @ExtendWith(MockitoExtension.class) -class RatioDecodeBufferPolicyTest { +class RatioDecodeBufferPolicyUnitTests { @Mock ByteBuf buffer; diff --git a/src/test/java/io/lettuce/core/protocol/RedisStateMachineResp2UnitTests.java b/src/test/java/io/lettuce/core/protocol/RedisStateMachineResp2UnitTests.java index a31c20bea1..ef188425ca 100644 --- a/src/test/java/io/lettuce/core/protocol/RedisStateMachineResp2UnitTests.java +++ b/src/test/java/io/lettuce/core/protocol/RedisStateMachineResp2UnitTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core.protocol; +import static io.lettuce.TestTags.UNIT_TEST; import static io.lettuce.core.protocol.RedisStateMachine.State; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -50,6 +51,7 @@ * @author Will Glozer * @author Mark Paluch */ +@Tag(UNIT_TEST) class RedisStateMachineResp2UnitTests { private RedisCodec codec = StringCodec.UTF8; diff --git a/src/test/java/io/lettuce/core/protocol/RedisStateMachineResp3UnitTests.java b/src/test/java/io/lettuce/core/protocol/RedisStateMachineResp3UnitTests.java index f00c27b8ba..443ec3b75d 100644 --- a/src/test/java/io/lettuce/core/protocol/RedisStateMachineResp3UnitTests.java +++ b/src/test/java/io/lettuce/core/protocol/RedisStateMachineResp3UnitTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core.protocol; +import static io.lettuce.TestTags.UNIT_TEST; import static io.lettuce.core.protocol.RedisStateMachine.State; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -51,6 +52,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class RedisStateMachineResp3UnitTests { private RedisCodec codec = StringCodec.UTF8; diff --git a/src/test/java/io/lettuce/core/protocol/SharedLockUnitTests.java b/src/test/java/io/lettuce/core/protocol/SharedLockUnitTests.java new file mode 100644 index 0000000000..593d319f42 --- /dev/null +++ b/src/test/java/io/lettuce/core/protocol/SharedLockUnitTests.java @@ -0,0 +1,61 @@ +package io.lettuce.core.protocol; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static io.lettuce.TestTags.UNIT_TEST; + +@Tag(UNIT_TEST) +public class SharedLockUnitTests { + + @Test + public void safety_on_reentrant_lock_exclusive_on_writers() throws InterruptedException { + final SharedLock sharedLock = new SharedLock(); + CountDownLatch cnt = new CountDownLatch(1); + try { + sharedLock.incrementWriters(); + + String result = sharedLock.doExclusive(() -> { + return sharedLock.doExclusive(() -> { + return "ok"; + }); + }); + if ("ok".equals(result)) { + cnt.countDown(); + } + } finally { + sharedLock.decrementWriters(); + } + + boolean await = cnt.await(1, TimeUnit.SECONDS); + Assertions.assertTrue(await); + + // verify writers won't be negative after finally decrementWriters + String result = sharedLock.doExclusive(() -> { + return sharedLock.doExclusive(() -> { + return "ok"; + }); + }); + + Assertions.assertEquals("ok", result); + + // and other writers should be passed after exclusive lock released + CountDownLatch cntOtherThread = new CountDownLatch(1); + new Thread(() -> { + try { + sharedLock.incrementWriters(); + cntOtherThread.countDown(); + } finally { + sharedLock.decrementWriters(); + } + }).start(); + + await = cntOtherThread.await(1, TimeUnit.SECONDS); + Assertions.assertTrue(await); + } + +} diff --git a/src/test/java/io/lettuce/core/protocol/StateMachineUnitTests.java b/src/test/java/io/lettuce/core/protocol/StateMachineUnitTests.java index 54b3de051d..81217d8d0d 100644 --- a/src/test/java/io/lettuce/core/protocol/StateMachineUnitTests.java +++ b/src/test/java/io/lettuce/core/protocol/StateMachineUnitTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core.protocol; +import static io.lettuce.TestTags.UNIT_TEST; import static io.lettuce.core.protocol.RedisStateMachine.State; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -46,6 +47,7 @@ * @author Will Glozer * @author Mark Paluch */ +@Tag(UNIT_TEST) class StateMachineUnitTests { private RedisCodec codec = StringCodec.UTF8; diff --git a/src/test/java/io/lettuce/core/protocol/TransactionalCommandUnitTests.java b/src/test/java/io/lettuce/core/protocol/TransactionalCommandUnitTests.java index 838d3498a7..a656c9fc95 100644 --- a/src/test/java/io/lettuce/core/protocol/TransactionalCommandUnitTests.java +++ b/src/test/java/io/lettuce/core/protocol/TransactionalCommandUnitTests.java @@ -1,7 +1,9 @@ package io.lettuce.core.protocol; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.RedisException; @@ -11,6 +13,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class TransactionalCommandUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/pubsub/PubSubCommandBuilderUnitTests.java b/src/test/java/io/lettuce/core/pubsub/PubSubCommandBuilderUnitTests.java index 89e93b234e..a07ecd8c47 100644 --- a/src/test/java/io/lettuce/core/pubsub/PubSubCommandBuilderUnitTests.java +++ b/src/test/java/io/lettuce/core/pubsub/PubSubCommandBuilderUnitTests.java @@ -9,14 +9,17 @@ import io.lettuce.core.protocol.CommandArgs; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import java.util.List; import java.util.Map; +import static io.lettuce.TestTags.UNIT_TEST; import static io.lettuce.core.protocol.CommandType.*; import static org.assertj.core.api.Assertions.assertThat; +@Tag(UNIT_TEST) class PubSubCommandBuilderUnitTests { private PubSubCommandBuilder commandBuilder; diff --git a/src/test/java/io/lettuce/core/pubsub/PubSubCommandHandlerUnitTests.java b/src/test/java/io/lettuce/core/pubsub/PubSubCommandHandlerUnitTests.java index 871915adae..2e2825f95a 100644 --- a/src/test/java/io/lettuce/core/pubsub/PubSubCommandHandlerUnitTests.java +++ b/src/test/java/io/lettuce/core/pubsub/PubSubCommandHandlerUnitTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.pubsub; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @@ -7,6 +8,7 @@ import java.util.Queue; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; @@ -40,6 +42,7 @@ * @author Mark Paluch * @author Giridhar Kannan */ +@Tag(UNIT_TEST) @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class PubSubCommandHandlerUnitTests { diff --git a/src/test/java/io/lettuce/core/pubsub/PubSubCommandTest.java b/src/test/java/io/lettuce/core/pubsub/PubSubCommandIntegrationTests.java similarity index 95% rename from src/test/java/io/lettuce/core/pubsub/PubSubCommandTest.java rename to src/test/java/io/lettuce/core/pubsub/PubSubCommandIntegrationTests.java index c6fd838050..9936ba4283 100644 --- a/src/test/java/io/lettuce/core/pubsub/PubSubCommandTest.java +++ b/src/test/java/io/lettuce/core/pubsub/PubSubCommandIntegrationTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core.pubsub; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import java.nio.ByteBuffer; @@ -32,9 +33,9 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.assertj.core.util.Arrays; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.AbstractRedisClientTest; @@ -68,7 +69,8 @@ * @author Tihomir Mateev * @author Ali Takavci */ -class PubSubCommandTest extends AbstractRedisClientTest { +@Tag(INTEGRATION_TEST) +class PubSubCommandIntegrationTests extends AbstractRedisClientTest { RedisPubSubAsyncCommands pubsub; @@ -84,6 +86,8 @@ class PubSubCommandTest extends AbstractRedisClientTest { BlockingQueue counts = listener.getCounts(); + BlockingQueue shardCounts = listener.getShardCounts(); + String channel = "channel0"; String shardChannel = "shard-channel"; @@ -521,6 +525,24 @@ void resubscribePatternsOnReconnect() throws Exception { assertThat(messages.take()).isEqualTo(message); } + @Test + void resubscribeShardChannelsOnReconnect() throws Exception { + pubsub.ssubscribe(shardChannel); + assertThat(shardChannels.take()).isEqualTo(shardChannel); + assertThat((long) shardCounts.take()).isEqualTo(1); + + pubsub.quit(); + + assertThat(shardChannels.take()).isEqualTo(shardChannel); + assertThat((long) shardCounts.take()).isEqualTo(1); + + Wait.untilTrue(pubsub::isOpen).waitOrTimeout(); + + redis.spublish(shardChannel, shardMessage); + assertThat(shardChannels.take()).isEqualTo(shardChannel); + assertThat(messages.take()).isEqualTo(shardMessage); + } + @Test void adapter() throws Exception { final BlockingQueue localCounts = LettuceFactories.newBlockingQueue(); diff --git a/src/test/java/io/lettuce/core/pubsub/PubSubCommandResp2Test.java b/src/test/java/io/lettuce/core/pubsub/PubSubCommandResp2IntegrationTests.java similarity index 91% rename from src/test/java/io/lettuce/core/pubsub/PubSubCommandResp2Test.java rename to src/test/java/io/lettuce/core/pubsub/PubSubCommandResp2IntegrationTests.java index 7f822f6586..2529608688 100644 --- a/src/test/java/io/lettuce/core/pubsub/PubSubCommandResp2Test.java +++ b/src/test/java/io/lettuce/core/pubsub/PubSubCommandResp2IntegrationTests.java @@ -19,9 +19,11 @@ */ package io.lettuce.core.pubsub; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.ClientOptions; @@ -35,7 +37,8 @@ * * @author Mark Paluch */ -class PubSubCommandResp2Test extends PubSubCommandTest { +@Tag(INTEGRATION_TEST) +class PubSubCommandResp2IntegrationTests extends PubSubCommandIntegrationTests { @Override protected ClientOptions getOptions() { diff --git a/src/test/java/io/lettuce/core/pubsub/PubSubEndpointUnitTests.java b/src/test/java/io/lettuce/core/pubsub/PubSubEndpointUnitTests.java index bf109ee0bb..1cb65875e9 100644 --- a/src/test/java/io/lettuce/core/pubsub/PubSubEndpointUnitTests.java +++ b/src/test/java/io/lettuce/core/pubsub/PubSubEndpointUnitTests.java @@ -1,10 +1,12 @@ package io.lettuce.core.pubsub; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.nio.ByteBuffer; import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.ByteBufferCodec; @@ -19,6 +21,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class PubSubEndpointUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/pubsub/PubSubReactiveTest.java b/src/test/java/io/lettuce/core/pubsub/PubSubReactiveIntegrationTests.java similarity index 98% rename from src/test/java/io/lettuce/core/pubsub/PubSubReactiveTest.java rename to src/test/java/io/lettuce/core/pubsub/PubSubReactiveIntegrationTests.java index 7dd7d11001..6f3250db83 100644 --- a/src/test/java/io/lettuce/core/pubsub/PubSubReactiveTest.java +++ b/src/test/java/io/lettuce/core/pubsub/PubSubReactiveIntegrationTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core.pubsub; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import java.time.Duration; @@ -29,6 +30,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import reactor.core.Disposable; @@ -53,7 +55,8 @@ * @author Mark Paluch * @author Ali Takavci */ -class PubSubReactiveTest extends AbstractRedisClientTest implements RedisPubSubListener { +@Tag(INTEGRATION_TEST) +class PubSubReactiveIntegrationTests extends AbstractRedisClientTest implements RedisPubSubListener { private RedisPubSubReactiveCommands pubsub; diff --git a/src/test/java/io/lettuce/core/pubsub/RedisPubSubAsyncCommandsImplUnitTests.java b/src/test/java/io/lettuce/core/pubsub/RedisPubSubAsyncCommandsImplUnitTests.java index 49dee5c2d8..4b6c3b5d77 100644 --- a/src/test/java/io/lettuce/core/pubsub/RedisPubSubAsyncCommandsImplUnitTests.java +++ b/src/test/java/io/lettuce/core/pubsub/RedisPubSubAsyncCommandsImplUnitTests.java @@ -8,17 +8,20 @@ import io.lettuce.core.protocol.AsyncCommand; import io.lettuce.core.protocol.RedisCommand; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import java.util.concurrent.ExecutionException; +import static io.lettuce.TestTags.UNIT_TEST; import static io.lettuce.core.protocol.CommandType.*; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; +@Tag(UNIT_TEST) class RedisPubSubAsyncCommandsImplUnitTests { private RedisPubSubAsyncCommandsImpl commands; diff --git a/src/test/java/io/lettuce/core/pubsub/StatefulRedisPubSubConnectionImplUnitTests.java b/src/test/java/io/lettuce/core/pubsub/StatefulRedisPubSubConnectionImplUnitTests.java index 160a601125..9eb5528244 100644 --- a/src/test/java/io/lettuce/core/pubsub/StatefulRedisPubSubConnectionImplUnitTests.java +++ b/src/test/java/io/lettuce/core/pubsub/StatefulRedisPubSubConnectionImplUnitTests.java @@ -8,9 +8,11 @@ import io.lettuce.core.resource.ClientResources; import io.lettuce.core.tracing.Tracing; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; +import static io.lettuce.TestTags.UNIT_TEST; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.mockito.Mockito.*; @@ -19,6 +21,7 @@ import java.util.HashSet; import java.util.List; +@Tag(UNIT_TEST) class StatefulRedisPubSubConnectionImplUnitTests { private StatefulRedisPubSubConnectionImpl connection; @@ -78,6 +81,7 @@ void resubscribeChannelSubscription() { when(mockedEndpoint.hasChannelSubscriptions()).thenReturn(true); when(mockedEndpoint.getChannels()).thenReturn(new HashSet<>(Arrays.asList(new String[] { "channel1", "channel2" }))); when(mockedEndpoint.hasPatternSubscriptions()).thenReturn(false); + when(mockedEndpoint.hasShardChannelSubscriptions()).thenReturn(false); List> subscriptions = connection.resubscribe(); RedisFuture commandFuture = subscriptions.get(0); @@ -87,17 +91,35 @@ void resubscribeChannelSubscription() { } @Test - void resubscribeChannelAndPatternSubscription() { + void resubscribeShardChannelSubscription() { + when(mockedEndpoint.hasShardChannelSubscriptions()).thenReturn(true); + when(mockedEndpoint.getShardChannels()) + .thenReturn(new HashSet<>(Arrays.asList(new String[] { "shard_channel1", "shard_channel2" }))); + when(mockedEndpoint.hasChannelSubscriptions()).thenReturn(false); + when(mockedEndpoint.hasPatternSubscriptions()).thenReturn(false); + + List> subscriptions = connection.resubscribe(); + RedisFuture commandFuture = subscriptions.get(0); + + assertEquals(1, subscriptions.size()); + assertInstanceOf(AsyncCommand.class, commandFuture); + } + + @Test + void resubscribeChannelAndPatternAndShardChanelSubscription() { when(mockedEndpoint.hasChannelSubscriptions()).thenReturn(true); - when(mockedEndpoint.getChannels()).thenReturn(new HashSet<>(Arrays.asList(new String[] { "channel1", "channel2" }))); when(mockedEndpoint.hasPatternSubscriptions()).thenReturn(true); + when(mockedEndpoint.hasShardChannelSubscriptions()).thenReturn(true); + when(mockedEndpoint.getChannels()).thenReturn(new HashSet<>(Arrays.asList(new String[] { "channel1", "channel2" }))); when(mockedEndpoint.getPatterns()).thenReturn(new HashSet<>(Arrays.asList(new String[] { "bcast*", "echo" }))); - + when(mockedEndpoint.getShardChannels()) + .thenReturn(new HashSet<>(Arrays.asList(new String[] { "shard_channel1", "shard_channel2" }))); List> subscriptions = connection.resubscribe(); - assertEquals(2, subscriptions.size()); + assertEquals(3, subscriptions.size()); assertInstanceOf(AsyncCommand.class, subscriptions.get(0)); assertInstanceOf(AsyncCommand.class, subscriptions.get(1)); + assertInstanceOf(AsyncCommand.class, subscriptions.get(1)); } } diff --git a/src/test/java/io/lettuce/core/reliability/AtLeastOnceTest.java b/src/test/java/io/lettuce/core/reliability/AtLeastOnceIntegrationTests.java similarity index 97% rename from src/test/java/io/lettuce/core/reliability/AtLeastOnceTest.java rename to src/test/java/io/lettuce/core/reliability/AtLeastOnceIntegrationTests.java index b290075057..7dbac3bf99 100644 --- a/src/test/java/io/lettuce/core/reliability/AtLeastOnceTest.java +++ b/src/test/java/io/lettuce/core/reliability/AtLeastOnceIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.reliability; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assumptions.*; @@ -8,7 +9,9 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import io.lettuce.core.TimeoutOptions; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.AbstractRedisClientTest; @@ -38,13 +41,15 @@ /** * @author Mark Paluch */ -class AtLeastOnceTest extends AbstractRedisClientTest { +@Tag(INTEGRATION_TEST) +class AtLeastOnceIntegrationTests extends AbstractRedisClientTest { private String key = "key"; @BeforeEach void before() { - client.setOptions(ClientOptions.builder().autoReconnect(true).build()); + client.setOptions(ClientOptions.builder().autoReconnect(true) + .timeoutOptions(TimeoutOptions.builder().timeoutCommands(false).build()).build()); // needs to be increased on slow systems...perhaps... client.setDefaultTimeout(3, TimeUnit.SECONDS); diff --git a/src/test/java/io/lettuce/core/reliability/AtMostOnceTest.java b/src/test/java/io/lettuce/core/reliability/AtMostOnceIntegrationTests.java similarity index 96% rename from src/test/java/io/lettuce/core/reliability/AtMostOnceTest.java rename to src/test/java/io/lettuce/core/reliability/AtMostOnceIntegrationTests.java index 7963d5a98e..d377269bad 100644 --- a/src/test/java/io/lettuce/core/reliability/AtMostOnceTest.java +++ b/src/test/java/io/lettuce/core/reliability/AtMostOnceIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.reliability; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static io.lettuce.test.ConnectionTestUtil.*; import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assumptions.*; @@ -9,7 +10,9 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import io.lettuce.core.TimeoutOptions; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.AbstractRedisClientTest; @@ -39,14 +42,16 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @SuppressWarnings("rawtypes") -class AtMostOnceTest extends AbstractRedisClientTest { +class AtMostOnceIntegrationTests extends AbstractRedisClientTest { private String key = "key"; @BeforeEach void before() { - client.setOptions(ClientOptions.builder().autoReconnect(false).build()); + client.setOptions(ClientOptions.builder().autoReconnect(false) + .timeoutOptions(TimeoutOptions.builder().timeoutCommands(false).build()).build()); // needs to be increased on slow systems...perhaps... client.setDefaultTimeout(3, TimeUnit.SECONDS); diff --git a/src/test/java/io/lettuce/core/resource/ConstantDelayUnitTests.java b/src/test/java/io/lettuce/core/resource/ConstantDelayUnitTests.java index 7a171b4891..84f690926c 100644 --- a/src/test/java/io/lettuce/core/resource/ConstantDelayUnitTests.java +++ b/src/test/java/io/lettuce/core/resource/ConstantDelayUnitTests.java @@ -1,16 +1,19 @@ package io.lettuce.core.resource; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.time.Duration; import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class ConstantDelayUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/resource/DecorrelatedJitterDelayUnitTests.java b/src/test/java/io/lettuce/core/resource/DecorrelatedJitterDelayUnitTests.java index 3c0d3faf0e..82999144de 100644 --- a/src/test/java/io/lettuce/core/resource/DecorrelatedJitterDelayUnitTests.java +++ b/src/test/java/io/lettuce/core/resource/DecorrelatedJitterDelayUnitTests.java @@ -19,18 +19,21 @@ */ package io.lettuce.core.resource; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.time.Duration; import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; /** * @author Jongyeol Choi * @author Mark Paluch */ +@Tag(UNIT_TEST) class DecorrelatedJitterDelayUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/resource/DefaultClientResourcesUnitTests.java b/src/test/java/io/lettuce/core/resource/DefaultClientResourcesUnitTests.java index 33dfd39f30..46a89396a0 100644 --- a/src/test/java/io/lettuce/core/resource/DefaultClientResourcesUnitTests.java +++ b/src/test/java/io/lettuce/core/resource/DefaultClientResourcesUnitTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core.resource; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; @@ -26,6 +27,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import reactor.test.StepVerifier; @@ -50,6 +52,7 @@ * @author Mark Paluch * @author Yohei Ueki */ +@Tag(UNIT_TEST) class DefaultClientResourcesUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/resource/DefaultEventLoopGroupProviderUnitTests.java b/src/test/java/io/lettuce/core/resource/DefaultEventLoopGroupProviderUnitTests.java index cd274cb681..de525d04e2 100644 --- a/src/test/java/io/lettuce/core/resource/DefaultEventLoopGroupProviderUnitTests.java +++ b/src/test/java/io/lettuce/core/resource/DefaultEventLoopGroupProviderUnitTests.java @@ -1,9 +1,11 @@ package io.lettuce.core.resource; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.test.TestFutures; @@ -13,6 +15,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class DefaultEventLoopGroupProviderUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/resource/DirContextDnsResolverTests.java b/src/test/java/io/lettuce/core/resource/DirContextDnsResolverTests.java index 412ebaa28b..df85740941 100644 --- a/src/test/java/io/lettuce/core/resource/DirContextDnsResolverTests.java +++ b/src/test/java/io/lettuce/core/resource/DirContextDnsResolverTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.resource; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; @@ -13,6 +14,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; /** @@ -20,6 +22,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) @Disabled("Tests require an internet connection") class DirContextDnsResolverTests { diff --git a/src/test/java/io/lettuce/core/resource/EqualJitterDelayUnitTests.java b/src/test/java/io/lettuce/core/resource/EqualJitterDelayUnitTests.java index 12885548e9..55b7f20263 100644 --- a/src/test/java/io/lettuce/core/resource/EqualJitterDelayUnitTests.java +++ b/src/test/java/io/lettuce/core/resource/EqualJitterDelayUnitTests.java @@ -19,18 +19,21 @@ */ package io.lettuce.core.resource; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.time.Duration; import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; /** * @author Jongyeol Choi * @author Mark Paluch */ +@Tag(UNIT_TEST) class EqualJitterDelayUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/resource/ExponentialDelayUnitTests.java b/src/test/java/io/lettuce/core/resource/ExponentialDelayUnitTests.java index 9a4b6dfe9b..f971b3478c 100644 --- a/src/test/java/io/lettuce/core/resource/ExponentialDelayUnitTests.java +++ b/src/test/java/io/lettuce/core/resource/ExponentialDelayUnitTests.java @@ -1,15 +1,18 @@ package io.lettuce.core.resource; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; /** * @author Mark Paluch */ +@Tag(UNIT_TEST) class ExponentialDelayUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/resource/FullJitterDelayUnitTests.java b/src/test/java/io/lettuce/core/resource/FullJitterDelayUnitTests.java index 440b4f219d..b6875c4481 100644 --- a/src/test/java/io/lettuce/core/resource/FullJitterDelayUnitTests.java +++ b/src/test/java/io/lettuce/core/resource/FullJitterDelayUnitTests.java @@ -19,18 +19,21 @@ */ package io.lettuce.core.resource; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.time.Duration; import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; /** * @author Jongyeol Choi * @author Mark Paluch */ +@Tag(UNIT_TEST) class FullJitterDelayUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/resource/MappingSocketAddressResolverUnitTests.java b/src/test/java/io/lettuce/core/resource/MappingSocketAddressResolverUnitTests.java index 50c79ed570..764d49d7de 100644 --- a/src/test/java/io/lettuce/core/resource/MappingSocketAddressResolverUnitTests.java +++ b/src/test/java/io/lettuce/core/resource/MappingSocketAddressResolverUnitTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.resource; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; @@ -10,6 +11,7 @@ import java.util.function.Function; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -23,6 +25,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class MappingSocketAddressResolverUnitTests { diff --git a/src/test/java/io/lettuce/core/sentinel/SentinelAclIntegrationTests.java b/src/test/java/io/lettuce/core/sentinel/SentinelAclIntegrationTests.java index 12eecc1231..9c515d0051 100644 --- a/src/test/java/io/lettuce/core/sentinel/SentinelAclIntegrationTests.java +++ b/src/test/java/io/lettuce/core/sentinel/SentinelAclIntegrationTests.java @@ -1,10 +1,12 @@ package io.lettuce.core.sentinel; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import javax.inject.Inject; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -23,6 +25,7 @@ * * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) @EnabledOnCommand("ACL") public class SentinelAclIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/sentinel/SentinelCommandIntegrationTests.java b/src/test/java/io/lettuce/core/sentinel/SentinelCommandIntegrationTests.java index 4dc95d7c58..1d37c04dcf 100644 --- a/src/test/java/io/lettuce/core/sentinel/SentinelCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/sentinel/SentinelCommandIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.sentinel; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static io.lettuce.test.settings.TestSettings.*; import static org.assertj.core.api.Assertions.*; @@ -13,6 +14,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -31,6 +33,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) public class SentinelCommandIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/sentinel/SentinelConnectionIntegrationTests.java b/src/test/java/io/lettuce/core/sentinel/SentinelConnectionIntegrationTests.java index 9f269fe9be..1d9077cfa0 100644 --- a/src/test/java/io/lettuce/core/sentinel/SentinelConnectionIntegrationTests.java +++ b/src/test/java/io/lettuce/core/sentinel/SentinelConnectionIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.sentinel; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.util.List; @@ -11,6 +12,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -28,6 +30,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) public class SentinelConnectionIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/sentinel/SentinelServerCommandIntegrationTests.java b/src/test/java/io/lettuce/core/sentinel/SentinelServerCommandIntegrationTests.java index e67abc5cfc..78b07330d7 100644 --- a/src/test/java/io/lettuce/core/sentinel/SentinelServerCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/sentinel/SentinelServerCommandIntegrationTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core.sentinel; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import java.util.regex.Matcher; @@ -28,6 +29,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -45,6 +47,7 @@ * * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) public class SentinelServerCommandIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/sentinel/SentinelSslIntegrationTests.java b/src/test/java/io/lettuce/core/sentinel/SentinelSslIntegrationTests.java index 7adc3221bc..b8fdc03138 100644 --- a/src/test/java/io/lettuce/core/sentinel/SentinelSslIntegrationTests.java +++ b/src/test/java/io/lettuce/core/sentinel/SentinelSslIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.sentinel; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static io.lettuce.test.settings.TestSettings.sslPort; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assumptions.assumeTrue; @@ -9,6 +10,7 @@ import javax.inject.Inject; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -30,6 +32,7 @@ * * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) class SentinelSslIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/sentinel/reactive/SentinelReactiveCommandTest.java b/src/test/java/io/lettuce/core/sentinel/reactive/SentinelReactiveCommandIntegrationTests.java similarity index 70% rename from src/test/java/io/lettuce/core/sentinel/reactive/SentinelReactiveCommandTest.java rename to src/test/java/io/lettuce/core/sentinel/reactive/SentinelReactiveCommandIntegrationTests.java index 03ecca13c9..e2bb9f9b63 100644 --- a/src/test/java/io/lettuce/core/sentinel/reactive/SentinelReactiveCommandTest.java +++ b/src/test/java/io/lettuce/core/sentinel/reactive/SentinelReactiveCommandIntegrationTests.java @@ -2,20 +2,23 @@ import javax.inject.Inject; -import io.lettuce.RedisBug; import io.lettuce.core.RedisClient; import io.lettuce.core.sentinel.SentinelCommandIntegrationTests; import io.lettuce.core.sentinel.api.StatefulRedisSentinelConnection; import io.lettuce.core.sentinel.api.sync.RedisSentinelCommands; import io.lettuce.test.ReactiveSyncInvocationHandler; +import org.junit.jupiter.api.Tag; + +import static io.lettuce.TestTags.INTEGRATION_TEST; /** * @author Mark Paluch */ -public class SentinelReactiveCommandTest extends SentinelCommandIntegrationTests { +@Tag(INTEGRATION_TEST) +public class SentinelReactiveCommandIntegrationTests extends SentinelCommandIntegrationTests { @Inject - public SentinelReactiveCommandTest(RedisClient redisClient) { + public SentinelReactiveCommandIntegrationTests(RedisClient redisClient) { super(redisClient); } diff --git a/src/test/java/io/lettuce/core/sentinel/reactive/SentinelServerReactiveCommandTest.java b/src/test/java/io/lettuce/core/sentinel/reactive/SentinelServerReactiveCommandIntegrationTests.java similarity index 69% rename from src/test/java/io/lettuce/core/sentinel/reactive/SentinelServerReactiveCommandTest.java rename to src/test/java/io/lettuce/core/sentinel/reactive/SentinelServerReactiveCommandIntegrationTests.java index 759ce65bca..05c69e4de0 100644 --- a/src/test/java/io/lettuce/core/sentinel/reactive/SentinelServerReactiveCommandTest.java +++ b/src/test/java/io/lettuce/core/sentinel/reactive/SentinelServerReactiveCommandIntegrationTests.java @@ -7,14 +7,18 @@ import io.lettuce.core.sentinel.api.StatefulRedisSentinelConnection; import io.lettuce.core.sentinel.api.sync.RedisSentinelCommands; import io.lettuce.test.ReactiveSyncInvocationHandler; +import org.junit.jupiter.api.Tag; + +import static io.lettuce.TestTags.INTEGRATION_TEST; /** * @author Mark Paluch */ -public class SentinelServerReactiveCommandTest extends SentinelServerCommandIntegrationTests { +@Tag(INTEGRATION_TEST) +public class SentinelServerReactiveCommandIntegrationTests extends SentinelServerCommandIntegrationTests { @Inject - public SentinelServerReactiveCommandTest(RedisClient redisClient) { + public SentinelServerReactiveCommandIntegrationTests(RedisClient redisClient) { super(redisClient); } diff --git a/src/test/java/io/lettuce/core/support/AsyncConnectionPoolSupportIntegrationTests.java b/src/test/java/io/lettuce/core/support/AsyncConnectionPoolSupportIntegrationTests.java index 798d16c4b5..86b08c28a4 100644 --- a/src/test/java/io/lettuce/core/support/AsyncConnectionPoolSupportIntegrationTests.java +++ b/src/test/java/io/lettuce/core/support/AsyncConnectionPoolSupportIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.support; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; @@ -10,6 +11,7 @@ import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.*; @@ -30,6 +32,7 @@ * * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class AsyncConnectionPoolSupportIntegrationTests extends TestSupport { private static RedisClient client; diff --git a/src/test/java/io/lettuce/core/support/AsyncPoolWithValidationUnitTests.java b/src/test/java/io/lettuce/core/support/AsyncPoolWithValidationUnitTests.java index 1dfdbb9677..64e4b19ce2 100644 --- a/src/test/java/io/lettuce/core/support/AsyncPoolWithValidationUnitTests.java +++ b/src/test/java/io/lettuce/core/support/AsyncPoolWithValidationUnitTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.support; +import static io.lettuce.TestTags.UNIT_TEST; import static io.lettuce.core.internal.Futures.failed; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -13,6 +14,7 @@ import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -26,6 +28,7 @@ /** * @author Mark Paluch */ +@Tag(UNIT_TEST) @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class AsyncPoolWithValidationUnitTests { diff --git a/src/test/java/io/lettuce/core/support/BoundedAsyncPoolUnitTests.java b/src/test/java/io/lettuce/core/support/BoundedAsyncPoolUnitTests.java index a6818a80d3..e4e0ccee77 100644 --- a/src/test/java/io/lettuce/core/support/BoundedAsyncPoolUnitTests.java +++ b/src/test/java/io/lettuce/core/support/BoundedAsyncPoolUnitTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.support; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import java.util.ArrayList; @@ -9,6 +10,7 @@ import java.util.concurrent.CompletionStage; import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.RedisException; @@ -19,6 +21,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class BoundedAsyncPoolUnitTests { private AtomicInteger counter = new AtomicInteger(); diff --git a/src/test/java/io/lettuce/core/support/CdiIntegrationTests.java b/src/test/java/io/lettuce/core/support/CdiIntegrationTests.java index 9edefdb1c0..51d6289c3a 100644 --- a/src/test/java/io/lettuce/core/support/CdiIntegrationTests.java +++ b/src/test/java/io/lettuce/core/support/CdiIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.support; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -9,6 +10,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.AbstractRedisClientTest; @@ -22,6 +24,7 @@ * @author Mark Paluch * @since 3.0 */ +@Tag(INTEGRATION_TEST) class CdiIntegrationTests { private static SeContainer container; diff --git a/src/test/java/io/lettuce/core/support/CommonsPool2ConfigConverterUnitTests.java b/src/test/java/io/lettuce/core/support/CommonsPool2ConfigConverterUnitTests.java index 757156e7d7..82772e35c1 100644 --- a/src/test/java/io/lettuce/core/support/CommonsPool2ConfigConverterUnitTests.java +++ b/src/test/java/io/lettuce/core/support/CommonsPool2ConfigConverterUnitTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.support; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.util.function.BiConsumer; @@ -7,6 +8,7 @@ import org.apache.commons.pool2.impl.BaseObjectPoolConfig; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; /** @@ -14,6 +16,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class CommonsPool2ConfigConverterUnitTests { @Test diff --git a/src/test/java/io/lettuce/core/support/ConnectionPoolSupportIntegrationTests.java b/src/test/java/io/lettuce/core/support/ConnectionPoolSupportIntegrationTests.java index 05b98024c8..ed38435edb 100644 --- a/src/test/java/io/lettuce/core/support/ConnectionPoolSupportIntegrationTests.java +++ b/src/test/java/io/lettuce/core/support/ConnectionPoolSupportIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.support; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; @@ -12,6 +13,7 @@ import org.apache.commons.pool2.impl.SoftReferenceObjectPool; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.test.ReflectionTestUtils; @@ -40,6 +42,7 @@ /** * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) class ConnectionPoolSupportIntegrationTests extends TestSupport { private static RedisClient client; diff --git a/src/test/java/io/lettuce/core/support/caching/ClientsideCachingIntegrationTests.java b/src/test/java/io/lettuce/core/support/caching/ClientsideCachingIntegrationTests.java index bb598e5ea7..8ccd81fa4b 100644 --- a/src/test/java/io/lettuce/core/support/caching/ClientsideCachingIntegrationTests.java +++ b/src/test/java/io/lettuce/core/support/caching/ClientsideCachingIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.support.caching; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.assertThat; import java.util.HashMap; @@ -11,6 +12,7 @@ import javax.inject.Inject; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -33,6 +35,7 @@ * * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) @ExtendWith(LettuceExtension.class) @EnabledOnCommand("ACL") public class ClientsideCachingIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/tracing/BraveTracingIntegrationTests.java b/src/test/java/io/lettuce/core/tracing/BraveTracingIntegrationTests.java index 060ee42e53..f9437e69aa 100644 --- a/src/test/java/io/lettuce/core/tracing/BraveTracingIntegrationTests.java +++ b/src/test/java/io/lettuce/core/tracing/BraveTracingIntegrationTests.java @@ -19,6 +19,7 @@ */ package io.lettuce.core.tracing; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import java.util.ArrayList; @@ -30,6 +31,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import brave.ScopedSpan; @@ -56,6 +58,7 @@ * @author Daniel Albuquerque * @author Anuraag Agrawal */ +@Tag(INTEGRATION_TEST) @EnabledOnCommand("HELLO") class BraveTracingIntegrationTests extends TestSupport { diff --git a/src/test/java/io/lettuce/core/tracing/BraveTracingUnitTests.java b/src/test/java/io/lettuce/core/tracing/BraveTracingUnitTests.java index 5940314e51..5adcb64e7f 100644 --- a/src/test/java/io/lettuce/core/tracing/BraveTracingUnitTests.java +++ b/src/test/java/io/lettuce/core/tracing/BraveTracingUnitTests.java @@ -1,20 +1,20 @@ package io.lettuce.core.tracing; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.assertThat; -import java.util.List; import java.util.Queue; import java.util.concurrent.LinkedBlockingQueue; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.test.ReflectionTestUtils; import io.lettuce.core.protocol.AsyncCommand; import zipkin2.Span; -import brave.Tag; import brave.Tracer; import brave.Tracing; import brave.handler.MutableSpan; @@ -30,6 +30,7 @@ * @author Mark Paluch * @author Daniel Albuquerque */ +@Tag(UNIT_TEST) class BraveTracingUnitTests extends TestSupport { private static Tracing clientTracing; @@ -102,7 +103,7 @@ void shouldCustomizeEndpoint() { void shouldCustomizeSpan() { BraveTracing tracing = BraveTracing.builder().tracing(clientTracing) - .spanCustomizer((command, span) -> span.tag("cmd", command.getType().name())).build(); + .spanCustomizer((command, span) -> span.tag("cmd", command.getType().toString())).build(); BraveTracing.BraveSpan span = (BraveTracing.BraveSpan) tracing.getTracerProvider().getTracer().nextSpan(); span.start(new AsyncCommand<>(new Command<>(CommandType.AUTH, null))); diff --git a/src/test/java/io/lettuce/core/tracing/SynchronousIntegrationTests.java b/src/test/java/io/lettuce/core/tracing/SynchronousIntegrationTests.java index 71bb3872a1..1330316a28 100644 --- a/src/test/java/io/lettuce/core/tracing/SynchronousIntegrationTests.java +++ b/src/test/java/io/lettuce/core/tracing/SynchronousIntegrationTests.java @@ -1,5 +1,6 @@ package io.lettuce.core.tracing; +import static io.lettuce.TestTags.INTEGRATION_TEST; import static org.assertj.core.api.Assertions.*; import java.util.concurrent.LinkedBlockingQueue; @@ -16,12 +17,14 @@ import io.micrometer.observation.ObservationRegistry; import io.micrometer.tracing.exporter.FinishedSpan; import io.micrometer.tracing.test.SampleTestRunner; +import org.junit.jupiter.api.Tag; /** * Collection of tests that log metrics and tracing using the synchronous API. * * @author Mark Paluch */ +@Tag(INTEGRATION_TEST) public class SynchronousIntegrationTests extends SampleTestRunner { SynchronousIntegrationTests() { @@ -75,6 +78,9 @@ public SampleTestRunnerConsumer yourCode() { .containsEntry("net.sock.peer.addr", TestSettings.host()) .containsEntry("net.sock.peer.port", "" + TestSettings.port()); assertThat(finishedSpan.getTags()).containsKeys("db.operation"); + assertThat(finishedSpan.getTags()).containsKeys("server.address"); + assertThat(finishedSpan.getTags()).containsKeys("db.namespace"); + assertThat(finishedSpan.getTags()).containsKeys("user.name"); } assertThat(commands).extracting(RedisCommand::getType).contains(CommandType.PING, CommandType.HELLO); diff --git a/src/test/java/io/lettuce/test/CliParser.java b/src/test/java/io/lettuce/test/CliParser.java index 0beac0e3b4..e12d1586a6 100644 --- a/src/test/java/io/lettuce/test/CliParser.java +++ b/src/test/java/io/lettuce/test/CliParser.java @@ -54,11 +54,11 @@ public static Command> parse(String command) { @Override public byte[] getBytes() { - return name().getBytes(StandardCharsets.UTF_8); + return this.toString().getBytes(StandardCharsets.UTF_8); } @Override - public String name() { + public String toString() { return typeName; } diff --git a/src/test/jmh/io/lettuce/core/dynamic/RedisCommandFactoryBenchmark.java b/src/test/jmh/io/lettuce/core/dynamic/RedisCommandFactoryBenchmark.java index 9147df0d0c..83ce325998 100644 --- a/src/test/jmh/io/lettuce/core/dynamic/RedisCommandFactoryBenchmark.java +++ b/src/test/jmh/io/lettuce/core/dynamic/RedisCommandFactoryBenchmark.java @@ -1,5 +1,6 @@ package io.lettuce.core.dynamic; +import io.lettuce.core.json.DefaultJsonParser; import org.mockito.Mockito; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Scope; @@ -11,6 +12,7 @@ import io.lettuce.core.api.sync.RedisCommands; import io.lettuce.core.codec.StringCodec; import io.lettuce.core.dynamic.batch.BatchSize; +import reactor.core.publisher.Mono; /** * Benchmark for commands executed through {@link RedisCommandFactory}. @@ -30,7 +32,7 @@ public void setup() { redisCommandFactory = new RedisCommandFactory(new MockStatefulConnection(EmptyRedisChannelWriter.INSTANCE)); regularCommands = redisCommandFactory.getCommands(RegularCommands.class); - asyncCommands = new RedisAsyncCommandsImpl<>(EmptyStatefulRedisConnection.INSTANCE, StringCodec.UTF8); + asyncCommands = new RedisAsyncCommandsImpl<>(EmptyStatefulRedisConnection.INSTANCE, StringCodec.UTF8, Mono.just(new DefaultJsonParser())); } @Benchmark diff --git a/src/test/kotlin/io/lettuce/core/CoroutinesIntegrationTests.kt b/src/test/kotlin/io/lettuce/core/CoroutinesIntegrationTests.kt index 150da82af3..2abf2b59f9 100644 --- a/src/test/kotlin/io/lettuce/core/CoroutinesIntegrationTests.kt +++ b/src/test/kotlin/io/lettuce/core/CoroutinesIntegrationTests.kt @@ -1,11 +1,13 @@ package io.lettuce.core +import io.lettuce.TestTags import io.lettuce.core.api.StatefulRedisConnection import io.lettuce.core.api.coroutines import io.lettuce.test.LettuceExtension import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.Timeout @@ -17,6 +19,7 @@ import javax.inject.Inject * * @author Mark Paluch */ +@Tag(TestTags.INTEGRATION_TEST) @ExtendWith(LettuceExtension::class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) @OptIn(ExperimentalLettuceCoroutinesApi::class) diff --git a/src/test/kotlin/io/lettuce/core/ScanFlowIntegrationTests.kt b/src/test/kotlin/io/lettuce/core/ScanFlowIntegrationTests.kt index 58f23c5499..ad83fd5fc1 100644 --- a/src/test/kotlin/io/lettuce/core/ScanFlowIntegrationTests.kt +++ b/src/test/kotlin/io/lettuce/core/ScanFlowIntegrationTests.kt @@ -19,6 +19,7 @@ */ package io.lettuce.core +import io.lettuce.TestTags import io.lettuce.core.api.StatefulRedisConnection import io.lettuce.core.api.coroutines import io.lettuce.test.LettuceExtension @@ -28,6 +29,7 @@ import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.extension.ExtendWith @@ -39,6 +41,7 @@ import javax.inject.Inject * @author Mikhael Sokolov * @author Mark Paluch */ +@Tag(TestTags.INTEGRATION_TEST) @ExtendWith(LettuceExtension::class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class ScanFlowIntegrationTests @Inject constructor(private val connection: StatefulRedisConnection) : TestSupport() { diff --git a/src/test/kotlin/io/lettuce/core/TransactionExtensionsIntegrationTests.kt b/src/test/kotlin/io/lettuce/core/TransactionExtensionsIntegrationTests.kt index af3479c3d7..81b04d45b7 100644 --- a/src/test/kotlin/io/lettuce/core/TransactionExtensionsIntegrationTests.kt +++ b/src/test/kotlin/io/lettuce/core/TransactionExtensionsIntegrationTests.kt @@ -19,6 +19,7 @@ */ package io.lettuce.core; +import io.lettuce.TestTags import io.lettuce.core.api.StatefulRedisConnection import io.lettuce.core.api.async.multi import io.lettuce.core.api.sync.multi @@ -27,6 +28,7 @@ import kotlinx.coroutines.future.await import kotlinx.coroutines.runBlocking import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import javax.inject.Inject @@ -37,6 +39,7 @@ import javax.inject.Inject * @author Mark Paluch * @author Mikhael Sokolov */ +@Tag(TestTags.INTEGRATION_TEST) @ExtendWith(LettuceExtension::class) class TransactionExtensionsIntegrationTests : TestSupport() { diff --git a/src/test/kotlin/io/lettuce/core/api/coroutines/CoroutinesIntegrationTests.kt b/src/test/kotlin/io/lettuce/core/api/coroutines/CoroutinesIntegrationTests.kt index ac55bf2d69..5dee65b309 100644 --- a/src/test/kotlin/io/lettuce/core/api/coroutines/CoroutinesIntegrationTests.kt +++ b/src/test/kotlin/io/lettuce/core/api/coroutines/CoroutinesIntegrationTests.kt @@ -1,5 +1,6 @@ package io.lettuce.core.api.coroutines +import io.lettuce.TestTags import io.lettuce.core.RedisClient import io.lettuce.core.TestSupport import io.lettuce.core.api.StatefulRedisConnection @@ -12,6 +13,7 @@ import io.lettuce.test.LettuceExtension import io.lettuce.test.condition.EnabledOnCommand import kotlinx.coroutines.runBlocking import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import javax.inject.Inject @@ -21,6 +23,7 @@ import javax.inject.Inject * * @author Mark Paluch */ +@Tag(TestTags.INTEGRATION_TEST) @ExtendWith(LettuceExtension::class) class CoroutinesIntegrationTests : TestSupport() { diff --git a/src/test/kotlin/io/lettuce/core/event/jfr/JfrEventRecorderUnitTests.java b/src/test/kotlin/io/lettuce/core/event/jfr/JfrEventRecorderUnitTests.java index c2e459a178..2a105bebbf 100644 --- a/src/test/kotlin/io/lettuce/core/event/jfr/JfrEventRecorderUnitTests.java +++ b/src/test/kotlin/io/lettuce/core/event/jfr/JfrEventRecorderUnitTests.java @@ -1,5 +1,7 @@ package io.lettuce.core.event.jfr; +import static io.lettuce.TestTags.INTEGRATION_TEST; +import static io.lettuce.TestTags.UNIT_TEST; import static org.assertj.core.api.Assertions.*; import java.io.File; @@ -12,6 +14,7 @@ import jdk.jfr.consumer.RecordedEvent; import jdk.jfr.consumer.RecordingFile; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.lettuce.core.event.connection.ConnectionActivatedEvent; @@ -23,6 +26,7 @@ * * @author Mark Paluch */ +@Tag(UNIT_TEST) class JfrEventRecorderUnitTests { @Test diff --git a/src/test/resources/bike-inventory.json b/src/test/resources/bike-inventory.json new file mode 100644 index 0000000000..a69a073ee3 --- /dev/null +++ b/src/test/resources/bike-inventory.json @@ -0,0 +1,46 @@ +{ + "inventory": { + "complete": false, + "owner": null, + "mountain_bikes": [ + { + "id": "bike:1", + "model": "Phoebe", + "description": "This is a mid-travel trail slayer that is a fantastic daily driver or one bike quiver. The Shimano Claris 8-speed groupset gives plenty of gear range to tackle hills and there's room for mudguards and a rack too. This is the bike for the rider who wants trail manners with low fuss ownership.", + "price": 1928, + "specs": { + "material": "carbon", + "weight": 13.1 + }, + "colors": [ + "black", + "silver" + ] + }, + { + "id": "bike:2", + "model": "Quaoar", + "description": "Redesigned for the 2020 model year, this bike impressed our testers and is the best all-around trail bike we've ever tested. The Shimano gear system effectively does away with an external cassette, so is super low maintenance in terms of wear and tear. All in all it's an impressive package for the price, making it very competitive.", + "price": 2072, + "specs": { + "material": "aluminium", + "weight": 7.9 + }, + "colors": [ + "black", + "white" + ] + }, + { + "id": "bike:3", + "model": "Weywot", + "description": "This bike gives kids aged six years and older a durable and uberlight mountain bike for their first experience on tracks and easy cruising through forests and fields. A set of powerful Shimano hydraulic disc brakes provide ample stopping ability. If you're after a budget option, this is one of the best bikes you could get.", + "price": 3264, + "specs": { + "material": "alloy", + "weight": 13.8 + } + } + ] + } +} diff --git a/src/test/resources/docker/Dockerfile b/src/test/resources/docker/Dockerfile new file mode 100644 index 0000000000..fb85a2d529 --- /dev/null +++ b/src/test/resources/docker/Dockerfile @@ -0,0 +1,23 @@ +FROM redis/redis-stack:latest + +RUN mkdir -p /nodes/36379 /nodes/36380 /nodes/36381 /nodes/36382 /nodes/36383 /nodes/36384 + +COPY cluster-nodes/nodes-36379.conf /nodes/36379/nodes.conf +COPY cluster-nodes/nodes-36380.conf /nodes/36380/nodes.conf +COPY cluster-nodes/nodes-36381.conf /nodes/36381/nodes.conf +COPY cluster-nodes/nodes-36382.conf /nodes/36382/nodes.conf +COPY cluster-nodes/nodes-36383.conf /nodes/36383/nodes.conf +COPY cluster-nodes/nodes-36384.conf /nodes/36384/nodes.conf + +COPY cluster-nodes/redis-36379.conf /nodes/36379/redis.conf +COPY cluster-nodes/redis-36380.conf /nodes/36380/redis.conf +COPY cluster-nodes/redis-36381.conf /nodes/36381/redis.conf +COPY cluster-nodes/redis-36382.conf /nodes/36382/redis.conf +COPY cluster-nodes/redis-36383.conf /nodes/36383/redis.conf +COPY cluster-nodes/redis-36384.conf /nodes/36384/redis.conf + +COPY start_cluster.sh /start_cluster.sh + +RUN chmod a+x /start_cluster.sh + +ENTRYPOINT [ "/start_cluster.sh"] \ No newline at end of file diff --git a/src/test/resources/docker/cluster-nodes/nodes-36379.conf b/src/test/resources/docker/cluster-nodes/nodes-36379.conf new file mode 100644 index 0000000000..f895cdc702 --- /dev/null +++ b/src/test/resources/docker/cluster-nodes/nodes-36379.conf @@ -0,0 +1,7 @@ +2ece6db60f8e5665c140c89720d3d259c024d179 127.0.0.1:36381@46381,,tls-port=0,shard-id=6503ed475afaa96064f49a64e6125eac5cfa3fee master - 0 1724417170931 3 connected 10923-16383 +916a157a7772af418664b6b2c7feeede5e1851dd 127.0.0.1:36383@46383,,tls-port=0,shard-id=d0fa2e52119c2ce8e260d5055b7a8315cbf81532 slave 3c5ae3144eb30caa272796173d3d8b314d881b51 1724417171949 1724417168901 1 connected +024a891ec46aeded14511b7c5899460198306353 127.0.0.1:36384@46384,,tls-port=0,shard-id=b267b10ea7526c233843e3a4c165d97cb7da54b5 slave 72d77afa00d3572b7fcb0215181bb46b3e869c42 0 1724417170000 2 connected +72d77afa00d3572b7fcb0215181bb46b3e869c42 127.0.0.1:36380@46380,,tls-port=0,shard-id=b267b10ea7526c233843e3a4c165d97cb7da54b5 master - 0 1724417170000 2 connected 5461-10922 +a7ff9d3ed43a7ff58485c4e780f86c4202b8224f 127.0.0.1:36382@46382,,tls-port=0,shard-id=6503ed475afaa96064f49a64e6125eac5cfa3fee slave 2ece6db60f8e5665c140c89720d3d259c024d179 0 1724417169000 3 connected +3c5ae3144eb30caa272796173d3d8b314d881b51 127.0.0.1:36379@46379,,tls-port=0,shard-id=d0fa2e52119c2ce8e260d5055b7a8315cbf81532 myself,master - 0 1724417168000 1 connected 0-5460 +vars currentEpoch 6 lastVoteEpoch 0 \ No newline at end of file diff --git a/src/test/resources/docker/cluster-nodes/nodes-36380.conf b/src/test/resources/docker/cluster-nodes/nodes-36380.conf new file mode 100644 index 0000000000..a39749dac8 --- /dev/null +++ b/src/test/resources/docker/cluster-nodes/nodes-36380.conf @@ -0,0 +1,7 @@ +72d77afa00d3572b7fcb0215181bb46b3e869c42 127.0.0.1:36380@46380,,tls-port=0,shard-id=b267b10ea7526c233843e3a4c165d97cb7da54b5 myself,master - 0 1724417169000 2 connected 5461-10922 +3c5ae3144eb30caa272796173d3d8b314d881b51 127.0.0.1:36379@46379,,tls-port=0,shard-id=d0fa2e52119c2ce8e260d5055b7a8315cbf81532 master - 0 1724417169000 1 connected 0-5460 +024a891ec46aeded14511b7c5899460198306353 127.0.0.1:36384@46384,,tls-port=0,shard-id=b267b10ea7526c233843e3a4c165d97cb7da54b5 slave 72d77afa00d3572b7fcb0215181bb46b3e869c42 0 1724417167171 2 connected +2ece6db60f8e5665c140c89720d3d259c024d179 127.0.0.1:36381@46381,,tls-port=0,shard-id=6503ed475afaa96064f49a64e6125eac5cfa3fee master - 0 1724417170930 3 connected 10923-16383 +a7ff9d3ed43a7ff58485c4e780f86c4202b8224f 127.0.0.1:36382@46382,,tls-port=0,shard-id=6503ed475afaa96064f49a64e6125eac5cfa3fee slave 2ece6db60f8e5665c140c89720d3d259c024d179 0 1724417169917 3 connected +916a157a7772af418664b6b2c7feeede5e1851dd 127.0.0.1:36383@46383,,tls-port=0,shard-id=d0fa2e52119c2ce8e260d5055b7a8315cbf81532 slave 3c5ae3144eb30caa272796173d3d8b314d881b51 0 1724417168901 1 connected +vars currentEpoch 6 lastVoteEpoch 0 \ No newline at end of file diff --git a/src/test/resources/docker/cluster-nodes/nodes-36381.conf b/src/test/resources/docker/cluster-nodes/nodes-36381.conf new file mode 100644 index 0000000000..b69b042a7c --- /dev/null +++ b/src/test/resources/docker/cluster-nodes/nodes-36381.conf @@ -0,0 +1,7 @@ +024a891ec46aeded14511b7c5899460198306353 127.0.0.1:36384@46384,,tls-port=0,shard-id=b267b10ea7526c233843e3a4c165d97cb7da54b5 slave 72d77afa00d3572b7fcb0215181bb46b3e869c42 0 1724417167171 2 connected +2ece6db60f8e5665c140c89720d3d259c024d179 127.0.0.1:36381@46381,,tls-port=0,shard-id=6503ed475afaa96064f49a64e6125eac5cfa3fee myself,master - 0 1724417166000 3 connected 10923-16383 +3c5ae3144eb30caa272796173d3d8b314d881b51 127.0.0.1:36379@46379,,tls-port=0,shard-id=d0fa2e52119c2ce8e260d5055b7a8315cbf81532 master - 0 1724417168901 1 connected 0-5460 +72d77afa00d3572b7fcb0215181bb46b3e869c42 127.0.0.1:36380@46380,,tls-port=0,shard-id=b267b10ea7526c233843e3a4c165d97cb7da54b5 master - 0 1724417169916 2 connected 5461-10922 +a7ff9d3ed43a7ff58485c4e780f86c4202b8224f 127.0.0.1:36382@46382,,tls-port=0,shard-id=6503ed475afaa96064f49a64e6125eac5cfa3fee slave 2ece6db60f8e5665c140c89720d3d259c024d179 0 1724417167000 3 connected +916a157a7772af418664b6b2c7feeede5e1851dd 127.0.0.1:36383@46383,,tls-port=0,shard-id=d0fa2e52119c2ce8e260d5055b7a8315cbf81532 slave 3c5ae3144eb30caa272796173d3d8b314d881b51 0 1724417168000 1 connected +vars currentEpoch 6 lastVoteEpoch 0 \ No newline at end of file diff --git a/src/test/resources/docker/cluster-nodes/nodes-36382.conf b/src/test/resources/docker/cluster-nodes/nodes-36382.conf new file mode 100644 index 0000000000..415345dbfe --- /dev/null +++ b/src/test/resources/docker/cluster-nodes/nodes-36382.conf @@ -0,0 +1,7 @@ +024a891ec46aeded14511b7c5899460198306353 127.0.0.1:36384@46384,,tls-port=0,shard-id=b267b10ea7526c233843e3a4c165d97cb7da54b5 slave 72d77afa00d3572b7fcb0215181bb46b3e869c42 0 1724417171948 2 connected +2ece6db60f8e5665c140c89720d3d259c024d179 127.0.0.1:36381@46381,,tls-port=0,shard-id=6503ed475afaa96064f49a64e6125eac5cfa3fee master - 0 1724417171000 3 connected 10923-16383 +a7ff9d3ed43a7ff58485c4e780f86c4202b8224f 127.0.0.1:36382@46382,,tls-port=0,shard-id=6503ed475afaa96064f49a64e6125eac5cfa3fee myself,slave 2ece6db60f8e5665c140c89720d3d259c024d179 0 1724417169000 3 connected +3c5ae3144eb30caa272796173d3d8b314d881b51 127.0.0.1:36379@46379,,tls-port=0,shard-id=d0fa2e52119c2ce8e260d5055b7a8315cbf81532 master - 0 1724417170931 1 connected 0-5460 +72d77afa00d3572b7fcb0215181bb46b3e869c42 127.0.0.1:36380@46380,,tls-port=0,shard-id=b267b10ea7526c233843e3a4c165d97cb7da54b5 master - 1724417172965 1724417168902 2 connected 5461-10922 +916a157a7772af418664b6b2c7feeede5e1851dd 127.0.0.1:36383@46383,,tls-port=0,shard-id=d0fa2e52119c2ce8e260d5055b7a8315cbf81532 slave 3c5ae3144eb30caa272796173d3d8b314d881b51 0 1724417168000 1 connected +vars currentEpoch 6 lastVoteEpoch 0 \ No newline at end of file diff --git a/src/test/resources/docker/cluster-nodes/nodes-36383.conf b/src/test/resources/docker/cluster-nodes/nodes-36383.conf new file mode 100644 index 0000000000..0cbe1550b9 --- /dev/null +++ b/src/test/resources/docker/cluster-nodes/nodes-36383.conf @@ -0,0 +1,7 @@ +72d77afa00d3572b7fcb0215181bb46b3e869c42 127.0.0.1:36380@46380,,tls-port=0,shard-id=b267b10ea7526c233843e3a4c165d97cb7da54b5 master - 0 1724417170000 2 connected 5461-10922 +3c5ae3144eb30caa272796173d3d8b314d881b51 127.0.0.1:36379@46379,,tls-port=0,shard-id=d0fa2e52119c2ce8e260d5055b7a8315cbf81532 master - 0 1724417171000 1 connected 0-5460 +024a891ec46aeded14511b7c5899460198306353 127.0.0.1:36384@46384,,tls-port=0,shard-id=b267b10ea7526c233843e3a4c165d97cb7da54b5 slave 72d77afa00d3572b7fcb0215181bb46b3e869c42 0 1724417171000 2 connected +2ece6db60f8e5665c140c89720d3d259c024d179 127.0.0.1:36381@46381,,tls-port=0,shard-id=6503ed475afaa96064f49a64e6125eac5cfa3fee master - 0 1724417171949 3 connected 10923-16383 +916a157a7772af418664b6b2c7feeede5e1851dd 127.0.0.1:36383@46383,,tls-port=0,shard-id=d0fa2e52119c2ce8e260d5055b7a8315cbf81532 myself,slave 3c5ae3144eb30caa272796173d3d8b314d881b51 0 1724417168000 1 connected +a7ff9d3ed43a7ff58485c4e780f86c4202b8224f 127.0.0.1:36382@46382,,tls-port=0,shard-id=6503ed475afaa96064f49a64e6125eac5cfa3fee slave 2ece6db60f8e5665c140c89720d3d259c024d179 0 1724417172966 3 connected +vars currentEpoch 6 lastVoteEpoch 0 \ No newline at end of file diff --git a/src/test/resources/docker/cluster-nodes/nodes-36384.conf b/src/test/resources/docker/cluster-nodes/nodes-36384.conf new file mode 100644 index 0000000000..313b0c30eb --- /dev/null +++ b/src/test/resources/docker/cluster-nodes/nodes-36384.conf @@ -0,0 +1,7 @@ +3c5ae3144eb30caa272796173d3d8b314d881b51 127.0.0.1:36379@46379,,tls-port=0,shard-id=d0fa2e52119c2ce8e260d5055b7a8315cbf81532 master - 0 1724417169000 1 connected 0-5460 +916a157a7772af418664b6b2c7feeede5e1851dd 127.0.0.1:36383@46383,,tls-port=0,shard-id=d0fa2e52119c2ce8e260d5055b7a8315cbf81532 slave 3c5ae3144eb30caa272796173d3d8b314d881b51 0 1724417166981 1 connected +2ece6db60f8e5665c140c89720d3d259c024d179 127.0.0.1:36381@46381,,tls-port=0,shard-id=6503ed475afaa96064f49a64e6125eac5cfa3fee master - 0 1724417168903 3 connected 10923-16383 +024a891ec46aeded14511b7c5899460198306353 127.0.0.1:36384@46384,,tls-port=0,shard-id=b267b10ea7526c233843e3a4c165d97cb7da54b5 myself,slave 72d77afa00d3572b7fcb0215181bb46b3e869c42 0 1724417167000 2 connected +a7ff9d3ed43a7ff58485c4e780f86c4202b8224f 127.0.0.1:36382@46382,,tls-port=0,shard-id=6503ed475afaa96064f49a64e6125eac5cfa3fee slave 2ece6db60f8e5665c140c89720d3d259c024d179 0 1724417169917 3 connected +72d77afa00d3572b7fcb0215181bb46b3e869c42 127.0.0.1:36380@46380,,tls-port=0,shard-id=b267b10ea7526c233843e3a4c165d97cb7da54b5 master - 1724417170930 1724417167000 2 connected 5461-10922 +vars currentEpoch 6 lastVoteEpoch 0 \ No newline at end of file diff --git a/src/test/resources/docker/cluster-nodes/redis-36379.conf b/src/test/resources/docker/cluster-nodes/redis-36379.conf new file mode 100644 index 0000000000..710b18d611 --- /dev/null +++ b/src/test/resources/docker/cluster-nodes/redis-36379.conf @@ -0,0 +1,13 @@ +dir /nodes/36379 +port 36379 +logfile /redis.log +daemonize yes +protected-mode no +cluster-enabled yes +enable-debug-command yes +loadmodule /opt/redis-stack/lib/redisearch.so +# loadmodule /opt/redis-stack/lib/redisgraph.so +loadmodule /opt/redis-stack/lib/redistimeseries.so +loadmodule /opt/redis-stack/lib/rejson.so +loadmodule /opt/redis-stack/lib/redisbloom.so +loadmodule /opt/redis-stack/lib/redisgears.so v8-plugin-path /opt/redis-stack/lib/libredisgears_v8_plugin.so diff --git a/src/test/resources/docker/cluster-nodes/redis-36380.conf b/src/test/resources/docker/cluster-nodes/redis-36380.conf new file mode 100644 index 0000000000..5aab7dfa55 --- /dev/null +++ b/src/test/resources/docker/cluster-nodes/redis-36380.conf @@ -0,0 +1,13 @@ +dir /nodes/36380 +port 36380 +logfile /redis.log +daemonize yes +protected-mode no +cluster-enabled yes +enable-debug-command yes +loadmodule /opt/redis-stack/lib/redisearch.so +# loadmodule /opt/redis-stack/lib/redisgraph.so +loadmodule /opt/redis-stack/lib/redistimeseries.so +loadmodule /opt/redis-stack/lib/rejson.so +loadmodule /opt/redis-stack/lib/redisbloom.so +loadmodule /opt/redis-stack/lib/redisgears.so v8-plugin-path /opt/redis-stack/lib/libredisgears_v8_plugin.so diff --git a/src/test/resources/docker/cluster-nodes/redis-36381.conf b/src/test/resources/docker/cluster-nodes/redis-36381.conf new file mode 100644 index 0000000000..91210574d4 --- /dev/null +++ b/src/test/resources/docker/cluster-nodes/redis-36381.conf @@ -0,0 +1,13 @@ +dir /nodes/36381 +port 36381 +logfile /redis.log +daemonize yes +protected-mode no +cluster-enabled yes +enable-debug-command yes +loadmodule /opt/redis-stack/lib/redisearch.so +# loadmodule /opt/redis-stack/lib/redisgraph.so +loadmodule /opt/redis-stack/lib/redistimeseries.so +loadmodule /opt/redis-stack/lib/rejson.so +loadmodule /opt/redis-stack/lib/redisbloom.so +loadmodule /opt/redis-stack/lib/redisgears.so v8-plugin-path /opt/redis-stack/lib/libredisgears_v8_plugin.so diff --git a/src/test/resources/docker/cluster-nodes/redis-36382.conf b/src/test/resources/docker/cluster-nodes/redis-36382.conf new file mode 100644 index 0000000000..fcf2c7ecd7 --- /dev/null +++ b/src/test/resources/docker/cluster-nodes/redis-36382.conf @@ -0,0 +1,13 @@ +dir /nodes/36382 +port 36382 +logfile /redis.log +daemonize yes +protected-mode no +cluster-enabled yes +enable-debug-command yes +loadmodule /opt/redis-stack/lib/redisearch.so +# loadmodule /opt/redis-stack/lib/redisgraph.so +loadmodule /opt/redis-stack/lib/redistimeseries.so +loadmodule /opt/redis-stack/lib/rejson.so +loadmodule /opt/redis-stack/lib/redisbloom.so +loadmodule /opt/redis-stack/lib/redisgears.so v8-plugin-path /opt/redis-stack/lib/libredisgears_v8_plugin.so diff --git a/src/test/resources/docker/cluster-nodes/redis-36383.conf b/src/test/resources/docker/cluster-nodes/redis-36383.conf new file mode 100644 index 0000000000..a3d4772714 --- /dev/null +++ b/src/test/resources/docker/cluster-nodes/redis-36383.conf @@ -0,0 +1,13 @@ +dir /nodes/36383 +port 36383 +logfile /redis.log +daemonize yes +protected-mode no +cluster-enabled yes +enable-debug-command yes +loadmodule /opt/redis-stack/lib/redisearch.so +# loadmodule /opt/redis-stack/lib/redisgraph.so +loadmodule /opt/redis-stack/lib/redistimeseries.so +loadmodule /opt/redis-stack/lib/rejson.so +loadmodule /opt/redis-stack/lib/redisbloom.so +loadmodule /opt/redis-stack/lib/redisgears.so v8-plugin-path /opt/redis-stack/lib/libredisgears_v8_plugin.so diff --git a/src/test/resources/docker/cluster-nodes/redis-36384.conf b/src/test/resources/docker/cluster-nodes/redis-36384.conf new file mode 100644 index 0000000000..70c5428e4d --- /dev/null +++ b/src/test/resources/docker/cluster-nodes/redis-36384.conf @@ -0,0 +1,13 @@ +dir /nodes/36384 +port 36384 +logfile /redis.log +daemonize yes +protected-mode no +cluster-enabled yes +enable-debug-command yes +loadmodule /opt/redis-stack/lib/redisearch.so +# loadmodule /opt/redis-stack/lib/redisgraph.so +loadmodule /opt/redis-stack/lib/redistimeseries.so +loadmodule /opt/redis-stack/lib/rejson.so +loadmodule /opt/redis-stack/lib/redisbloom.so +loadmodule /opt/redis-stack/lib/redisgears.so v8-plugin-path /opt/redis-stack/lib/libredisgears_v8_plugin.so diff --git a/src/test/resources/docker/docker-compose.yml b/src/test/resources/docker/docker-compose.yml new file mode 100644 index 0000000000..aede734651 --- /dev/null +++ b/src/test/resources/docker/docker-compose.yml @@ -0,0 +1,17 @@ +--- +services: + + standalone-stack: + image: redis/redis-stack:latest + ports: + - "16379:6379" + + clustered-stack: + image: tihomirmateev339/cae-infra:latest + ports: + - "36379:36379" + - "36380:36380" + - "36381:36381" + - "36382:36382" + - "36383:36383" + - "36384:36384" \ No newline at end of file diff --git a/src/test/resources/docker/start_cluster.sh b/src/test/resources/docker/start_cluster.sh new file mode 100644 index 0000000000..85e3ac94ec --- /dev/null +++ b/src/test/resources/docker/start_cluster.sh @@ -0,0 +1,31 @@ +#! /bin/bash + +# +# Copyright 2024, Redis Ltd. and Contributors +# All rights reserved. +# +# Licensed under the MIT License. +# + +if [ -z ${START_PORT} ]; then + START_PORT=36379 +fi +if [ -z ${END_PORT} ]; then + END_PORT=36384 +fi +if [ ! -z "$3" ]; then + START_PORT=$2 + START_PORT=$3 +fi + +for PORT in `seq ${START_PORT} ${END_PORT}`; do + echo ">>> Starting Redis server at port ${PORT}" + /opt/redis-stack/bin/redis-server /nodes/$PORT/redis.conf > /nodes/$PORT/console.log + if [ $? -ne 0 ]; then + echo "Redis failed to start, exiting." + continue + fi + echo 127.0.0.1:$PORT >> /nodes/nodemap +done + +tail -f /redis.log \ No newline at end of file diff --git a/src/test/resources/log4j2-test.xml b/src/test/resources/log4j2-test.xml index 7969c32138..efb5371536 100644 --- a/src/test/resources/log4j2-test.xml +++ b/src/test/resources/log4j2-test.xml @@ -12,6 +12,8 @@ + +