diff --git a/.github/workflows/csharp.yml b/.github/workflows/csharp.yml index e1dfcc5c86..e7916a82e8 100644 --- a/.github/workflows/csharp.yml +++ b/.github/workflows/csharp.yml @@ -8,12 +8,20 @@ on: - glide-core/src/** - submodules/** - .github/workflows/csharp.yml + - .github/workflows/install-shared-dependencies/action.yml + - .github/workflows/install-redis/action.yml + - .github/workflows/test-benchmark/action.yml + - .github/workflows/lint-rust/action.yml pull_request: paths: - csharp/** - glide-core/src/** - submodules/** - .github/workflows/csharp.yml + - .github/workflows/install-shared-dependencies/action.yml + - .github/workflows/install-redis/action.yml + - .github/workflows/test-benchmark/action.yml + - .github/workflows/lint-rust/action.yml permissions: contents: read @@ -21,7 +29,6 @@ permissions: jobs: run-tests: timeout-minutes: 15 - runs-on: ubuntu-latest strategy: fail-fast: false matrix: @@ -29,8 +36,12 @@ jobs: - 6.2.14 - 7.2.3 dotnet: - - 6.0 - - 8.0 + - '6.0' + - '8.0' + os: + - ubuntu-latest + - macos-latest + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -38,35 +49,47 @@ jobs: submodules: recursive - name: Install redis + # TODO: make this step macos compatible: https://github.com/aws/glide-for-redis/issues/781 + if: ${{ matrix.os == 'ubuntu-latest' }} uses: ./.github/workflows/install-redis with: redis-version: ${{ matrix.redis }} - - name: Install protoc (protobuf) - uses: arduino/setup-protoc@v3 + - name: Install shared software dependencies + uses: ./.github/workflows/install-shared-dependencies with: - version: "25.1" + os: ${{ matrix.os }} + target: ${{ matrix.os == 'ubuntu-latest' && 'x86_64-unknown-linux-gnu' || 'x86_64-apple-darwin' }} + github-token: ${{ secrets.GITHUB_TOKEN }} - name: Set up dotnet ${{ matrix.dotnet }} - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: ${{ matrix.dotnet }} - - name: Start redis server - run: redis-server & - - name: Format working-directory: ./csharp run: dotnet format --verify-no-changes --verbosity diagnostic - name: Test dotnet ${{ matrix.dotnet }} working-directory: ./csharp - run: dotnet test --framework net${{ matrix.dotnet }} /warnaserror + run: dotnet test --framework net${{ matrix.dotnet }} "-l:html;LogFileName=TestReport.html" --results-directory . -warnaserror - uses: ./.github/workflows/test-benchmark with: language-flag: -csharp + - name: Upload test reports + if: always() + continue-on-error: true + uses: actions/upload-artifact@v4 + with: + name: test-reports-dotnet-${{ matrix.dotnet }}-redis-${{ matrix.redis }}-${{ matrix.os }} + path: | + csharp/TestReport.html + benchmarks/results/* + utils/clusters/** + lint-rust: timeout-minutes: 10 runs-on: ubuntu-latest diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 80246396b4..1beb65f518 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -24,8 +24,8 @@ jobs: fail-fast: false matrix: go: - - '1.18' - - '1.21' + - '1.18.10' + - '1.22.0' redis: - 6.2.14 - 7.2.3 @@ -60,9 +60,9 @@ jobs: with: redis-version: ${{ matrix.redis }} - - name: Install client dependencies + - name: Install tools for Go ${{ matrix.go }} working-directory: ./go - run: make install-tools + run: make install-tools-go${{ matrix.go }} - name: Build client working-directory: ./go @@ -70,11 +70,11 @@ jobs: - name: Run linters working-directory: ./go - run: make lint + run: make lint-ci - - name: Run unit tests + - name: Run tests working-directory: ./go - run: make unit-test-report + run: make test-and-report - name: Upload test reports if: always() @@ -83,7 +83,7 @@ jobs: with: name: test-reports-go-${{ matrix.go }}-redis-${{ matrix.redis }}-${{ matrix.os }} path: | - go/reports/unit-test-report.html + go/reports/test-report.html build-amazonlinux-latest: if: github.repository_owner == 'aws' @@ -93,7 +93,7 @@ jobs: matrix: go: - 1.18.10 - - 1.21.6 + - 1.22.0 runs-on: ubuntu-latest container: amazonlinux:latest timeout-minutes: 15 @@ -135,9 +135,9 @@ jobs: echo "/usr/local/go/bin" >> $GITHUB_PATH echo "$HOME/go/bin" >> $GITHUB_PATH - - name: Install client dependencies + - name: Install tools for Go ${{ matrix.go }} working-directory: ./go - run: make install-tools + run: make install-tools-go${{ matrix.go }} - name: Build client working-directory: ./go @@ -145,11 +145,11 @@ jobs: - name: Run linters working-directory: ./go - run: make lint + run: make lint-ci - - name: Run unit tests + - name: Run tests working-directory: ./go - run: make unit-test-report + run: make test-and-report - name: Upload test reports if: always() @@ -157,7 +157,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: test-reports-go-${{ matrix.go }}-amazon-linux-latest - path: go/reports/unit-test-report.html + path: go/reports/test-report.html lint-rust: timeout-minutes: 15 diff --git a/.github/workflows/install-shared-dependencies/action.yml b/.github/workflows/install-shared-dependencies/action.yml index 2174952692..e09981ea7c 100644 --- a/.github/workflows/install-shared-dependencies/action.yml +++ b/.github/workflows/install-shared-dependencies/action.yml @@ -31,7 +31,7 @@ runs: shell: bash if: "${{ inputs.os == 'macos-latest' }}" run: | - brew install git gcc pkgconfig openssl redis + brew install git gcc pkgconfig openssl redis coreutils - name: Install software dependencies for Ubuntu shell: bash diff --git a/.github/workflows/java-benchmark.yml b/.github/workflows/java-benchmark.yml deleted file mode 100644 index 21d7d7fea1..0000000000 --- a/.github/workflows/java-benchmark.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: Java client benchmarks - -on: - workflow_dispatch: - inputs: - name: - required: false - type: string - -run-name: ${{ inputs.name == '' && format('{0} @ {1}', github.ref_name, github.sha) || inputs.name }} - -jobs: - java-benchmark: - timeout-minutes: 25 - strategy: - # Run all jobs - fail-fast: false - matrix: - java: - - 11 - - 17 - redis: - - 6.2.14 - - 7.2.3 - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Set up JDK ${{ matrix.java }} - uses: actions/setup-java@v4 - with: - distribution: "temurin" - java-version: ${{ matrix.java }} - - - name: Install redis - uses: ./.github/workflows/install-redis - with: - redis-version: ${{ matrix.redis }} - - - name: benchmark - uses: ./.github/workflows/test-benchmark - with: - language-flag: -java - - - name: Upload test reports - if: always() - continue-on-error: true - uses: actions/upload-artifact@v4 - with: - name: test-reports-java-${{ matrix.java }}-redis-${{ matrix.redis }} - path: | - java/benchmarks/build/reports/** - benchmarks/results/** diff --git a/.github/workflows/java.yml b/.github/workflows/java.yml index 941ad2b349..f0d3c1a01c 100644 --- a/.github/workflows/java.yml +++ b/.github/workflows/java.yml @@ -6,14 +6,22 @@ on: paths: - glide-core/src/** - submodules/** - - "java/**" - - ".github/workflows/java.yml" + - java/** + - .github/workflows/java.yml + - .github/workflows/install-shared-dependencies/action.yml + - .github/workflows/install-redis/action.yml + - .github/workflows/test-benchmark/action.yml + - .github/workflows/lint-rust/action.yml pull_request: paths: - glide-core/src/** - submodules/** - - "java/**" - - ".github/workflows/java.yml" + - java/** + - .github/workflows/java.yml + - .github/workflows/install-shared-dependencies/action.yml + - .github/workflows/install-redis/action.yml + - .github/workflows/test-benchmark/action.yml + - .github/workflows/lint-rust/action.yml jobs: build-and-test-java-client: @@ -67,6 +75,10 @@ jobs: working-directory: java run: ./gradlew spotlessDiagnose | grep 'All formatters are well behaved for all files' + - uses: ./.github/workflows/test-benchmark + with: + language-flag: -java + - name: Upload test reports if: always() continue-on-error: true @@ -77,6 +89,7 @@ jobs: java/client/build/reports/** java/integTest/build/reports/** utils/clusters/** + benchmarks/results/** build-amazonlinux-latest: if: github.repository_owner == 'aws' diff --git a/.github/workflows/npm-cd.yml b/.github/workflows/npm-cd.yml index 68dd3b3b82..d768e542e9 100644 --- a/.github/workflows/npm-cd.yml +++ b/.github/workflows/npm-cd.yml @@ -112,7 +112,7 @@ jobs: run: | # Remove the "cpu" and "os" fileds so the base package would be able to install it on ubuntu SED_FOR_MACOS=`if [[ "${{ matrix.build.OS }}" =~ .*"macos".* ]]; then echo "''"; fi` - sed -i $SED_FOR_MACOS '/"cpu":/d' ./package.json && sed -i $SED_FOR_MACOS '/"os":/d' ./package.json + sed -i $SED_FOR_MACOS '/"\/\/\/cpu": \[/,/]/d' ./package.json && sed -i $SED_FOR_MACOS '/"\/\/\/os": \[/,/]/d' ./package.json mkdir -p bin npm pack --pack-destination ./bin ls ./bin diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b2a4b4025..f0fc01fa5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,22 @@ #### Changes -- Node: Allow routing Cluster requests by address. ([#1021](https://github.com/aws/glide-for-redis/pull/1021)) +* Python Node: Allow routing Cluster requests by address. ([#1021](https://github.com/aws/glide-for-redis/pull/1021)) +* Python: Added HSETNX command. ([#954](https://github.com/aws/glide-for-redis/pull/954)) +* Python: Added SISMEMBER command ([#971](https://github.com/aws/glide-for-redis/pull/971)) +* Python, Node: Added TYPE command ([#945](https://github.com/aws/glide-for-redis/pull/945), [#980](https://github.com/aws/glide-for-redis/pull/980)) +* Python, Node: Added HLEN command ([#944](https://github.com/aws/glide-for-redis/pull/944), [#981](https://github.com/aws/glide-for-redis/pull/981)) +* Python, Node: Added ZCOUNT command ([#878](https://github.com/aws/glide-for-redis/pull/878)) ([#909](https://github.com/aws/glide-for-redis/pull/909)) +* Python: Added ECHO command ([#953](https://github.com/aws/glide-for-redis/pull/953)) +* Python, Node: Added ZPOPMIN command ([#975](https://github.com/aws/glide-for-redis/pull/975), [#1008](https://github.com/aws/glide-for-redis/pull/1008)) +* Node: Added STRLEN command ([#993](https://github.com/aws/glide-for-redis/pull/993)) +* Node: Added LINDEX command ([#999](https://github.com/aws/glide-for-redis/pull/999)) +* Python, Node: Added ZPOPMAX command ([#996](https://github.com/aws/glide-for-redis/pull/996), [#1009](https://github.com/aws/glide-for-redis/pull/1009)) +* Python: Added ZRANGE command ([#906](https://github.com/aws/glide-for-redis/pull/906)) +* Python, Node: Added PTTL command ([#1036](https://github.com/aws/glide-for-redis/pull/1036), [#1082](https://github.com/aws/glide-for-redis/pull/1082)) + +#### Features + +* Python: Allow chaining function calls on transaction. ([#987](https://github.com/aws/glide-for-redis/pull/987)) ## 0.2.0 (2024-02-11) @@ -13,18 +29,10 @@ * Python, Node: Added RPOPCOUNT and LPOPCOUNT to transaction ([#874](https://github.com/aws/glide-for-redis/pull/874)) * Standalone client: Improve connection errors. ([#854](https://github.com/aws/glide-for-redis/pull/854)) * Python, Node: When recieving LPOP/RPOP with count, convert result to Array. ([#811](https://github.com/aws/glide-for-redis/pull/811)) -* Python, Node: Added TYPE command ([#945](https://github.com/aws/glide-for-redis/pull/945), [#980](https://github.com/aws/glide-for-redis/pull/980)) -* Python, Node: Added HLEN command ([#944](https://github.com/aws/glide-for-redis/pull/944), [#981](https://github.com/aws/glide-for-redis/pull/981)) -* Node: Added ZCOUNT command ([#909](https://github.com/aws/glide-for-redis/pull/909)) -* Python: Added ECHO command ([#953](https://github.com/aws/glide-for-redis/pull/953)) -* Python, Node: Added ZPOPMIN command ([#975](https://github.com/aws/glide-for-redis/pull/975), [#1008](https://github.com/aws/glide-for-redis/pull/1008)) -* Node: Added STRLEN command ([#993](https://github.com/aws/glide-for-redis/pull/993)) -* Node: Added LINDEX command ([#999](https://github.com/aws/glide-for-redis/pull/999)) -* Python, Node: Added ZPOPMAX command ([#996](https://github.com/aws/glide-for-redis/pull/996), [#1009](https://github.com/aws/glide-for-redis/pull/1009)) #### Features * Python, Node: Added support in Lua Scripts ([#775](https://github.com/aws/glide-for-redis/pull/775), [#860](https://github.com/aws/glide-for-redis/pull/860)) -* Python, Node: Allow chaining function calls on transaction. ([#902](https://github.com/aws/glide-for-redis/pull/902)), ([#987](https://github.com/aws/glide-for-redis/pull/987)) +* Node: Allow chaining function calls on transaction. ([#902](https://github.com/aws/glide-for-redis/pull/902)) #### Fixes * Core: Fixed `Connection Refused` error not to close the client ([#872](https://github.com/aws/glide-for-redis/pull/872)) diff --git a/benchmarks/install_and_test.sh b/benchmarks/install_and_test.sh index 72b56fb2cb..ebd650f39e 100755 --- a/benchmarks/install_and_test.sh +++ b/benchmarks/install_and_test.sh @@ -33,7 +33,6 @@ chosenClients="all" host="localhost" port=6379 tlsFlag="--tls" -javaTlsFlag="-tls" function runPythonBenchmark(){ # generate protobuf files @@ -74,7 +73,7 @@ function runCSharpBenchmark(){ function runJavaBenchmark(){ cd ${BENCH_FOLDER}/../java - ./gradlew :benchmarks:run --args="-resultsFile \"${BENCH_FOLDER}/$1\" -dataSize \"$2\" -concurrentTasks \"$concurrentTasks\" -clients \"$chosenClients\" -host $host $javaPortFlag -clientCount \"$clientCount\" $javaTlsFlag $javaClusterFlag" + ./gradlew :benchmarks:run --args="-resultsFile \"${BENCH_FOLDER}/$1\" --dataSize \"$2\" --concurrentTasks \"$concurrentTasks\" --clients \"$chosenClients\" --host $host $portFlag --clientCount \"$clientCount\" $tlsFlag $clusterFlag $minimalFlag" } function runRustBenchmark(){ @@ -218,15 +217,12 @@ do -no-csv) writeResultsCSV=0 ;; -no-tls) tlsFlag= - javaTlsFlag= ;; -is-cluster) clusterFlag="--clusterModeEnabled" - javaClusterFlag="-clusterModeEnabled" ;; -port) portFlag="--port "$2 - javaPortFlag="-port "$2 shift ;; -minimal) diff --git a/csharp/.gitignore b/csharp/.gitignore index 92e9f50cc3..71475a2a33 100644 --- a/csharp/.gitignore +++ b/csharp/.gitignore @@ -94,6 +94,7 @@ ClientBin/ *~ *.dbmdl *.[Pp]ublish.xml +*.html *.publishsettings diff --git a/csharp/DEVELOPER.md b/csharp/DEVELOPER.md index ca5c9369b4..1042aae2e4 100644 --- a/csharp/DEVELOPER.md +++ b/csharp/DEVELOPER.md @@ -4,30 +4,73 @@ This document describes how to set up your development environment to build and ### Development Overview -The GLIDE C# wrapper consists of both C# and Rust code. +We're excited to share that the GLIDE C# client is currently in development! However, it's important to note that this client is a work in progress and is not yet complete or fully tested. Your contributions and feedback are highly encouraged as we work towards refining and improving this implementation. Thank you for your interest and understanding as we continue to develop this C# wrapper. + +The C# client contains the following parts: + +1. Rust part of the C# client located in `lib/src`; it communicates with [GLIDE core rust library](../glide-core/README.md). +2. C# part of the client located in `lib`; it translates Rust async API into .Net async API. +3. Integration tests for the C# client located in `tests` directory. +4. A dedicated benchmarking tool designed to evaluate and compare the performance of GLIDE for Redis and other .Net clients. It is located in `/benchmarks/csharp`. + +TODO: examples, UT, design docs ### Build from source +Software Dependencies: + +- .Net SDK 6 or later +- git +- rustup +- redis + +Please also install the following packages to build [GLIDE core rust library](../glide-core/README.md): + +- GCC +- protoc (protobuf compiler) +- pkg-config +- openssl +- openssl-dev + #### Prerequisites -Software Dependencies +**.Net** + +It is recommended to visit https://dotnet.microsoft.com/en-us/download/dotnet to download .Net installer. +You can also use a package manager to install the .Net SDK: + +```bash +brew install dotnet@6 # MacOS +sudo apt-get install dotnet6 # Linux +``` + +**Protoc installation** + +Download a binary matching your system from the [official release page](https://github.com/protocolbuffers/protobuf/releases/tag/v25.1) and make it accessible in your $PATH by moving it or creating a symlink. +For example, on Linux you can copy it to `/usr/bin`: -- .net sdk 6 or later -- git -- GCC -- pkg-config -- protoc (protobuf compiler) -- openssl -- openssl-dev -- rustup +```bash +sudo cp protoc /usr/bin/ +``` + +**Redis installation** + +To install `redis-server` and `redis-cli` on your host, follow the [Redis Installation Guide](https://redis.io/docs/install/install-redis/). + +**Dependencies installation for Ubuntu** + +```bash +sudo apt-get update -y +sudo apt-get install -y openssl openssl-dev gcc +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source "$HOME/.cargo/env" +``` **Dependencies installation for MacOS** -visit https://dotnet.microsoft.com/en-us/download/dotnet -to download .net installer ```bash brew update -brew install git gcc pkgconfig protobuf openssl +brew install git gcc pkgconfig openssl curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source "$HOME/.cargo/env" ``` @@ -36,34 +79,70 @@ source "$HOME/.cargo/env" Before starting this step, make sure you've installed all software requirments. -1. Clone the repository: +1. Clone the repository + ```bash VERSION=0.1.0 # You can modify this to other released version or set it to "main" to get the unstable branch git clone --branch ${VERSION} https://github.com/aws/glide-for-redis.git cd glide-for-redis ``` -2. Initialize git submodule: + +2. Initialize git submodule + ```bash git submodule update --init --recursive ``` -3. Build the c# wrapper: - Choose a build option from the following and run it from the `csharp` folder: - Build in release mode, stripped from all debug symbols: +3. Build the C# wrapper + +```bash +dotnet build +``` + +4. Run tests + +Run test suite from `csharp` directory: + +```bash +dotnet test +``` + +5. Run benchmark + + 1. Ensure that you have installed `redis-server` and `redis-cli` on your host. You can find the Redis installation guide at the following link: [Redis Installation Guide](https://redis.io/docs/install/install-redis/install-redis-on-linux/). + + 2. Execute the following command from the root project folder: ```bash - dotnet build + cd /benchmarks/csharp + dotnet run --framework net8.0 --dataSize 1024 --resultsFile test.json --concurrentTasks 4 --clients all --host localhost --clientCount 4 ``` -4. Run benchmark: + 3. Use a [helper script](../benchmarks/README.md) which runs end-to-end benchmarking workflow: - 1. Ensure that you have installed redis-server and redis-cli on your host. You can find the Redis installation guide at the following link: [Redis Installation Guide](https://redis.io/docs/install/install-redis/install-redis-on-linux/). + ```bash + cd /benchmarks + ./install_and_test.sh -csharp + ``` - 2. Execute the following command from the root project folder: - ```bash - cd benchmarks/csharp - dotnet run --framework net8.0 --dataSize 1024 --resultsFile test.json --concurrentTasks 4 --clients all --host localhost --clientCount 4 - ``` + Run benchmarking script with `-h` flag to get list and help about all command line parameters. + +6. Lint the code + +Before making a contribution ensure that all new user API and non-obvious places in code is well documented and run a code linter. + +C# linter: + +```bash +dotnet format --verify-no-changes --verbosity diagnostic +``` + +Rust linter: + +```bash +cargo clippy --all-features --all-targets -- -D warnings +cargo fmt --all -- --check +``` ### Submodules diff --git a/csharp/README.md b/csharp/README.md index 860f5e67c0..5ebc9e1961 100644 --- a/csharp/README.md +++ b/csharp/README.md @@ -36,4 +36,4 @@ glideClient.Dispose(); ### Building & Testing -Development instructions for local building & testing the package are in the [DEVELOPER.md](https://github.com/aws/glide-for-redis/blob/main/csharp/DEVELOPER.md#build-from-source) file. +Development instructions for local building & testing the package are in the [DEVELOPER.md](DEVELOPER.md#build-from-source) file. diff --git a/csharp/lib/Cargo.toml b/csharp/lib/Cargo.toml index f0f9f0889c..6bf4183914 100644 --- a/csharp/lib/Cargo.toml +++ b/csharp/lib/Cargo.toml @@ -15,10 +15,7 @@ crate-type = ["cdylib"] redis = { path = "../../submodules/redis-rs/redis", features = ["aio", "tokio-comp","tokio-native-tls-comp"] } glide-core = { path = "../../glide-core" } tokio = { version = "^1", features = ["rt", "macros", "rt-multi-thread", "time"] } -num-derive = "0.4.0" -num-traits = "0.2.15" logger_core = {path = "../../logger_core"} -tracing-subscriber = "0.3.16" [profile.release] lto = true diff --git a/csharp/tests/AsyncClientTests.cs b/csharp/tests/Integration/GetAndSet.cs similarity index 80% rename from csharp/tests/AsyncClientTests.cs rename to csharp/tests/Integration/GetAndSet.cs index e9adfdf97b..ed37512337 100644 --- a/csharp/tests/AsyncClientTests.cs +++ b/csharp/tests/Integration/GetAndSet.cs @@ -2,19 +2,14 @@ * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ -namespace tests; +namespace tests.Integration; using Glide; -// TODO - need to start a new redis server for each test? -public class AsyncClientTests -{ - [OneTimeSetUp] - public void Setup() - { - Glide.Logger.SetLoggerConfig(Glide.Level.Info); - } +using static tests.Integration.IntegrationTestBase; +public class GetAndSet +{ private async Task GetAndSetRandomValues(AsyncClient client) { var key = Guid.NewGuid().ToString(); @@ -27,7 +22,7 @@ private async Task GetAndSetRandomValues(AsyncClient client) [Test] public async Task GetReturnsLastSet() { - using (var client = new AsyncClient("localhost", 6379, false)) + using (var client = new AsyncClient("localhost", TestConfiguration.STANDALONE_PORTS[0], false)) { await GetAndSetRandomValues(client); } @@ -36,7 +31,7 @@ public async Task GetReturnsLastSet() [Test] public async Task GetAndSetCanHandleNonASCIIUnicode() { - using (var client = new AsyncClient("localhost", 6379, false)) + using (var client = new AsyncClient("localhost", TestConfiguration.STANDALONE_PORTS[0], false)) { var key = Guid.NewGuid().ToString(); var value = "שלום hello 汉字"; @@ -49,7 +44,7 @@ public async Task GetAndSetCanHandleNonASCIIUnicode() [Test] public async Task GetReturnsNull() { - using (var client = new AsyncClient("localhost", 6379, false)) + using (var client = new AsyncClient("localhost", TestConfiguration.STANDALONE_PORTS[0], false)) { var result = await client.GetAsync(Guid.NewGuid().ToString()); Assert.That(result, Is.EqualTo(null)); @@ -59,7 +54,7 @@ public async Task GetReturnsNull() [Test] public async Task GetReturnsEmptyString() { - using (var client = new AsyncClient("localhost", 6379, false)) + using (var client = new AsyncClient("localhost", TestConfiguration.STANDALONE_PORTS[0], false)) { var key = Guid.NewGuid().ToString(); var value = ""; @@ -72,7 +67,7 @@ public async Task GetReturnsEmptyString() [Test] public async Task HandleVeryLargeInput() { - using (var client = new AsyncClient("localhost", 6379, false)) + using (var client = new AsyncClient("localhost", TestConfiguration.STANDALONE_PORTS[0], false)) { var key = Guid.NewGuid().ToString(); var value = Guid.NewGuid().ToString(); @@ -92,7 +87,7 @@ public async Task HandleVeryLargeInput() [Test] public void ConcurrentOperationsWork() { - using (var client = new AsyncClient("localhost", 6379, false)) + using (var client = new AsyncClient("localhost", TestConfiguration.STANDALONE_PORTS[0], false)) { var operations = new List(); diff --git a/csharp/tests/Integration/IntegrationTestBase.cs b/csharp/tests/Integration/IntegrationTestBase.cs new file mode 100644 index 0000000000..635e0544cf --- /dev/null +++ b/csharp/tests/Integration/IntegrationTestBase.cs @@ -0,0 +1,134 @@ +/** + * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + */ + +using System.Diagnostics; + +// Note: All IT should be in the same namespace +namespace tests.Integration; + +[SetUpFixture] +public class IntegrationTestBase +{ + internal class TestConfiguration + { + public static List STANDALONE_PORTS { get; internal set; } = new(); + public static List CLUSTER_PORTS { get; internal set; } = new(); + public static Version REDIS_VERSION { get; internal set; } = new(); + } + + [OneTimeSetUp] + public void SetUp() + { + // Stop all if weren't stopped on previous test run + StopRedis(false); + + // Delete dirs if stop failed due to https://github.com/aws/glide-for-redis/issues/849 + Directory.Delete(Path.Combine(_scriptDir, "clusters"), true); + + // Start cluster + TestConfiguration.CLUSTER_PORTS = StartRedis(true); + // Start standalone + TestConfiguration.STANDALONE_PORTS = StartRedis(false); + // Get redis version + TestConfiguration.REDIS_VERSION = GetRedisVersion(); + + TestContext.Progress.WriteLine($"Cluster ports = {string.Join(',', TestConfiguration.CLUSTER_PORTS)}"); + TestContext.Progress.WriteLine($"Standalone ports = {string.Join(',', TestConfiguration.STANDALONE_PORTS)}"); + TestContext.Progress.WriteLine($"Redis version = {TestConfiguration.REDIS_VERSION}"); + } + + [OneTimeTearDown] + public void TearDown() + { + // Stop all + StopRedis(true); + } + + private readonly string _scriptDir; + + // Nunit requires a public default constructor. These variables would be set in SetUp method. + public IntegrationTestBase() + { + string? projectDir = Directory.GetCurrentDirectory(); + while (!(Path.GetFileName(projectDir) == "csharp" || projectDir == null)) + projectDir = Path.GetDirectoryName(projectDir); + + if (projectDir == null) + throw new FileNotFoundException("Can't detect the project dir. Are you running tests from `csharp` directory?"); + + _scriptDir = Path.Combine(projectDir, "..", "utils"); + } + + internal List StartRedis(bool cluster, bool tls = false, string? name = null) + { + string cmd = $"start {(cluster ? "--cluster-mode" : "-r 0")} {(tls ? " --tls" : "")} {(name != null ? " --prefix " + name : "")}"; + return ParsePortsFromOutput(RunClusterManager(cmd, false)); + } + + /// + /// Stop all instances on the given . + /// + internal void StopRedis(bool keepLogs, string? name = null) + { + string cmd = $"stop --prefix {name ?? "redis-cluster"} {(keepLogs ? "--keep-folder" : "")}"; + RunClusterManager(cmd, true); + } + + private string RunClusterManager(string cmd, bool ignoreExitCode) + { + ProcessStartInfo info = new() + { + WorkingDirectory = _scriptDir, + FileName = "python3", + Arguments = "cluster_manager.py " + cmd, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + Process? script = Process.Start(info); + script?.WaitForExit(); + string? error = script?.StandardError.ReadToEnd(); + string? output = script?.StandardOutput.ReadToEnd(); + int? exit_code = script?.ExitCode; + + TestContext.Progress.WriteLine($"cluster_manager.py stdout\n====\n{output}\n====\ncluster_manager.py stderr\n====\n{error}\n====\n"); + + if (!ignoreExitCode && exit_code != 0) + throw new ApplicationException($"cluster_manager.py script failed: exit code {exit_code}."); + + return output ?? ""; + } + + private static List ParsePortsFromOutput(string output) + { + List ports = new(); + foreach (string line in output.Split("\n")) + { + if (!line.StartsWith("CLUSTER_NODES=")) + continue; + + string[] addresses = line.Split("=")[1].Split(","); + foreach (string address in addresses) + ports.Add(uint.Parse(address.Split(":")[1])); + } + return ports; + } + + private static Version GetRedisVersion() + { + ProcessStartInfo info = new() + { + FileName = "redis-server", + Arguments = "-v", + UseShellExecute = false, + RedirectStandardOutput = true, + }; + Process? proc = Process.Start(info); + proc?.WaitForExit(); + string output = proc?.StandardOutput.ReadToEnd() ?? ""; + + // Redis server v=7.2.3 sha=00000000:0 malloc=jemalloc-5.3.0 bits=64 build=7504b1fedf883f2 + return new Version(output.Split(" ")[2].Split("=")[1]); + } +} diff --git a/glide-core/Cargo.toml b/glide-core/Cargo.toml index 5f4a1fe097..55a0e82fee 100644 --- a/glide-core/Cargo.toml +++ b/glide-core/Cargo.toml @@ -10,7 +10,6 @@ authors = ["Amazon Web Services"] [dependencies] bytes = "^1.3" futures = "^0.3" -num-traits = "^0.2" redis = { path = "../submodules/redis-rs/redis", features = ["aio", "tokio-comp", "tokio-rustls-comp", "connection-manager","cluster", "cluster-async"] } signal-hook = "^0.3" signal-hook-tokio = {version = "^0.3", features = ["futures-v0_3"] } diff --git a/glide-core/THIRD_PARTY_LICENSES_RUST b/glide-core/THIRD_PARTY_LICENSES_RUST index f263209929..96a819d93d 100644 --- a/glide-core/THIRD_PARTY_LICENSES_RUST +++ b/glide-core/THIRD_PARTY_LICENSES_RUST @@ -683,7 +683,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: ahash:0.8.9 +Package: ahash:0.8.11 The following copyrights and licenses were found in the source code of this package: @@ -1599,7 +1599,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: arc-swap:1.6.0 +Package: arc-swap:1.7.0 The following copyrights and licenses were found in the source code of this package: @@ -5816,7 +5816,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: crossbeam-channel:0.5.11 +Package: crossbeam-channel:0.5.12 The following copyrights and licenses were found in the source code of this package: @@ -12482,7 +12482,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: hermit-abi:0.3.8 +Package: hermit-abi:0.3.9 The following copyrights and licenses were found in the source code of this package: @@ -14361,7 +14361,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: js-sys:0.3.68 +Package: js-sys:0.3.69 The following copyrights and licenses were found in the source code of this package: @@ -15751,7 +15751,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: log:0.4.20 +Package: log:0.4.21 The following copyrights and licenses were found in the source code of this package: @@ -16489,7 +16489,7 @@ the following restrictions: ---- -Package: mio:0.8.10 +Package: mio:0.8.11 The following copyrights and licenses were found in the source code of this package: @@ -19337,7 +19337,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: pin-project:1.1.4 +Package: pin-project:1.1.5 The following copyrights and licenses were found in the source code of this package: @@ -19566,7 +19566,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: pin-project-internal:1.1.4 +Package: pin-project-internal:1.1.5 The following copyrights and licenses were found in the source code of this package: @@ -21627,7 +21627,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: protobuf:3.3.0 +Package: protobuf:3.4.0 The following copyrights and licenses were found in the source code of this package: @@ -21652,7 +21652,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: protobuf-support:3.3.0 +Package: protobuf-support:3.4.0 The following copyrights and licenses were found in the source code of this package: @@ -23838,7 +23838,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-pemfile:2.1.0 +Package: rustls-pemfile:2.1.1 The following copyrights and licenses were found in the source code of this package: @@ -24081,7 +24081,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-pki-types:1.3.0 +Package: rustls-pki-types:1.3.1 The following copyrights and licenses were found in the source code of this package: @@ -27472,7 +27472,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: syn:2.0.50 +Package: syn:2.0.52 The following copyrights and licenses were found in the source code of this package: @@ -31750,7 +31750,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen:0.2.91 +Package: wasm-bindgen:0.2.92 The following copyrights and licenses were found in the source code of this package: @@ -31979,7 +31979,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen-backend:0.2.91 +Package: wasm-bindgen-backend:0.2.92 The following copyrights and licenses were found in the source code of this package: @@ -32208,7 +32208,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen-macro:0.2.91 +Package: wasm-bindgen-macro:0.2.92 The following copyrights and licenses were found in the source code of this package: @@ -32437,7 +32437,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen-macro-support:0.2.91 +Package: wasm-bindgen-macro-support:0.2.92 The following copyrights and licenses were found in the source code of this package: @@ -32666,7 +32666,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen-shared:0.2.91 +Package: wasm-bindgen-shared:0.2.92 The following copyrights and licenses were found in the source code of this package: @@ -34498,7 +34498,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows-targets:0.52.3 +Package: windows-targets:0.52.4 The following copyrights and licenses were found in the source code of this package: @@ -34956,7 +34956,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_aarch64_gnullvm:0.52.3 +Package: windows_aarch64_gnullvm:0.52.4 The following copyrights and licenses were found in the source code of this package: @@ -35414,7 +35414,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_aarch64_msvc:0.52.3 +Package: windows_aarch64_msvc:0.52.4 The following copyrights and licenses were found in the source code of this package: @@ -35872,7 +35872,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_gnu:0.52.3 +Package: windows_i686_gnu:0.52.4 The following copyrights and licenses were found in the source code of this package: @@ -36330,7 +36330,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_msvc:0.52.3 +Package: windows_i686_msvc:0.52.4 The following copyrights and licenses were found in the source code of this package: @@ -36788,7 +36788,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_gnu:0.52.3 +Package: windows_x86_64_gnu:0.52.4 The following copyrights and licenses were found in the source code of this package: @@ -37246,7 +37246,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_gnullvm:0.52.3 +Package: windows_x86_64_gnullvm:0.52.4 The following copyrights and licenses were found in the source code of this package: @@ -37704,7 +37704,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_msvc:0.52.3 +Package: windows_x86_64_msvc:0.52.4 The following copyrights and licenses were found in the source code of this package: diff --git a/glide-core/src/client/standalone_client.rs b/glide-core/src/client/standalone_client.rs index 8889102315..f2a337e07d 100644 --- a/glide-core/src/client/standalone_client.rs +++ b/glide-core/src/client/standalone_client.rs @@ -225,7 +225,7 @@ impl StandaloneClient { let mut connection = reconnecting_connection.get_connection().await?; let result = connection.send_packed_command(cmd).await; match result { - Err(err) if err.is_connection_dropped() => { + Err(err) if err.is_unrecoverable_error() => { log_warn("send request", format!("received disconnect error `{err}`")); reconnecting_connection.reconnect(); Err(err) @@ -321,7 +321,7 @@ impl StandaloneClient { .send_packed_commands(pipeline, offset, count) .await; match result { - Err(err) if err.is_connection_dropped() => { + Err(err) if err.is_unrecoverable_error() => { log_warn( "pipeline request", format!("received disconnect error `{err}`"), diff --git a/glide-core/src/client/value_conversion.rs b/glide-core/src/client/value_conversion.rs index c4864310d0..9538791422 100644 --- a/glide-core/src/client/value_conversion.rs +++ b/glide-core/src/client/value_conversion.rs @@ -51,7 +51,7 @@ pub(crate) fn convert_to_expected_type( match inner_value { Value::BulkString(_) => Ok(( key_str, - Value::Double(from_owned_redis_value::(inner_value)?.into()), + Value::Double(from_owned_redis_value::(inner_value)?), )), Value::Double(_) => Ok((key_str, inner_value)), _ => Err(( @@ -89,13 +89,11 @@ pub(crate) fn convert_to_expected_type( ) .into()), }, - ExpectedReturnType::Double => { - Ok(Value::Double(from_owned_redis_value::(value)?.into())) - } + ExpectedReturnType::Double => Ok(Value::Double(from_owned_redis_value::(value)?)), ExpectedReturnType::Boolean => Ok(Value::Boolean(from_owned_redis_value::(value)?)), ExpectedReturnType::DoubleOrNull => match value { Value::Nil => Ok(value), - _ => Ok(Value::Double(from_owned_redis_value::(value)?.into())), + _ => Ok(Value::Double(from_owned_redis_value::(value)?)), }, ExpectedReturnType::ZrankReturnType => match value { Value::Nil => Ok(value), @@ -201,9 +199,8 @@ pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option { Some(ExpectedReturnType::Map) } b"INCRBYFLOAT" | b"HINCRBYFLOAT" => Some(ExpectedReturnType::Double), - b"HEXISTS" | b"EXPIRE" | b"EXPIREAT" | b"PEXPIRE" | b"PEXPIREAT" => { - Some(ExpectedReturnType::Boolean) - } + b"HEXISTS" | b"HSETNX" | b"EXPIRE" | b"EXPIREAT" | b"PEXPIRE" | b"PEXPIREAT" + | b"SISMEMBER" => Some(ExpectedReturnType::Boolean), b"SMEMBERS" => Some(ExpectedReturnType::Set), b"ZSCORE" => Some(ExpectedReturnType::DoubleOrNull), b"ZPOPMIN" | b"ZPOPMAX" => Some(ExpectedReturnType::MapOfStringToDouble), @@ -307,10 +304,7 @@ mod tests { Value::BulkString(b"key2".to_vec()), Value::BulkString(b"20.8".to_vec()), ), - ( - Value::Double(20.5.into()), - Value::BulkString(b"30.2".to_vec()), - ), + (Value::Double(20.5), Value::BulkString(b"30.2".to_vec())), ]; let converted_map = convert_to_expected_type( @@ -329,15 +323,15 @@ mod tests { let (key, value) = &converted_map[0]; assert_eq!(*key, Value::BulkString(b"key1".to_vec())); - assert_eq!(*value, Value::Double(10.5.into())); + assert_eq!(*value, Value::Double(10.5)); let (key, value) = &converted_map[1]; assert_eq!(*key, Value::BulkString(b"key2".to_vec())); - assert_eq!(*value, Value::Double(20.8.into())); + assert_eq!(*value, Value::Double(20.8)); let (key, value) = &converted_map[2]; assert_eq!(*key, Value::BulkString(b"20.5".to_vec())); - assert_eq!(*value, Value::Double(30.2.into())); + assert_eq!(*value, Value::Double(30.2)); let array_of_arrays = vec![ Value::Array(vec![ @@ -346,7 +340,7 @@ mod tests { ]), Value::Array(vec![ Value::BulkString(b"key2".to_vec()), - Value::Double(20.5.into()), + Value::Double(20.5), ]), ]; @@ -366,11 +360,11 @@ mod tests { let (key, value) = &converted_map[0]; assert_eq!(*key, Value::BulkString(b"key1".to_vec())); - assert_eq!(*value, Value::Double(10.5.into())); + assert_eq!(*value, Value::Double(10.5)); let (key, value) = &converted_map[1]; assert_eq!(*key, Value::BulkString(b"key2".to_vec())); - assert_eq!(*value, Value::Double(20.5.into())); + assert_eq!(*value, Value::Double(20.5)); let array_of_arrays_err: Vec = vec![Value::Array(vec![ Value::BulkString(b"key".to_vec()), @@ -411,7 +405,7 @@ mod tests { assert_eq!(array_result.len(), 2); assert_eq!(array_result[0], Value::BulkString(b"key".to_vec())); - assert_eq!(array_result[1], Value::Double(20.5.into())); + assert_eq!(array_result[1], Value::Double(20.5)); let array_err = vec![Value::BulkString(b"key".to_vec())]; assert!(convert_to_expected_type( diff --git a/glide-core/src/errors.rs b/glide-core/src/errors.rs new file mode 100644 index 0000000000..1c05aad84b --- /dev/null +++ b/glide-core/src/errors.rs @@ -0,0 +1,34 @@ +/* + * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + */ + +use redis::RedisError; + +#[repr(C)] +pub enum RequestErrorType { + Unspecified = 0, + ExecAbort = 1, + Timeout = 2, + Disconnect = 3, +} + +pub fn error_type(error: &RedisError) -> RequestErrorType { + if error.is_timeout() { + RequestErrorType::Timeout + } else if error.is_unrecoverable_error() { + RequestErrorType::Disconnect + } else if matches!(error.kind(), redis::ErrorKind::ExecAbortError) { + RequestErrorType::ExecAbort + } else { + RequestErrorType::Unspecified + } +} + +pub fn error_message(error: &RedisError) -> String { + let error_message = error.to_string(); + if matches!(error_type(error), RequestErrorType::Disconnect) { + format!("Received connection error `{error_message}`. Will attempt to reconnect") + } else { + error_message + } +} diff --git a/glide-core/src/lib.rs b/glide-core/src/lib.rs index 8fe89abd35..aa25cb89b1 100644 --- a/glide-core/src/lib.rs +++ b/glide-core/src/lib.rs @@ -8,4 +8,5 @@ mod retry_strategies; pub mod rotating_buffer; mod socket_listener; pub use socket_listener::*; +pub mod errors; pub mod scripts_container; diff --git a/glide-core/src/protobuf/redis_request.proto b/glide-core/src/protobuf/redis_request.proto index 0389ce82d3..ceaa83c11a 100644 --- a/glide-core/src/protobuf/redis_request.proto +++ b/glide-core/src/protobuf/redis_request.proto @@ -123,6 +123,10 @@ enum RequestType { XTrim = 79; XGroupCreate = 80; XGroupDestroy = 81; + HSetNX = 82; + SIsMember = 83; + Hvals = 84; + PTTL = 85; } message Command { diff --git a/glide-core/src/socket_listener.rs b/glide-core/src/socket_listener.rs index d925c80172..21994da38c 100644 --- a/glide-core/src/socket_listener.rs +++ b/glide-core/src/socket_listener.rs @@ -4,6 +4,7 @@ use super::rotating_buffer::RotatingBuffer; use crate::client::Client; use crate::connection_request::ConnectionRequest; +use crate::errors::{error_message, error_type, RequestErrorType}; use crate::redis_request::{ command, redis_request, Command, RedisRequest, RequestType, Routes, ScriptInvocation, SlotTypes, Transaction, @@ -217,28 +218,20 @@ async fn write_result( Some(response::response::Value::RequestError(request_error)) } Err(ClienUsageError::Redis(err)) => { - let error_message = err.to_string(); + let error_message = error_message(&err); log_warn("received error", error_message.as_str()); log_debug("received error", format!("for callback {}", callback_index)); - let mut request_error = response::RequestError::default(); - if err.is_connection_dropped() { - request_error.type_ = response::RequestErrorType::Disconnect.into(); - request_error.message = format!( - "Received connection error `{error_message}`. Will attempt to reconnect" - ) - .into(); - } else if err.is_timeout() { - request_error.type_ = response::RequestErrorType::Timeout.into(); - request_error.message = error_message.into(); - } else { - request_error.type_ = match err.kind() { - redis::ErrorKind::ExecAbortError => { - response::RequestErrorType::ExecAbort.into() - } - _ => response::RequestErrorType::Unspecified.into(), - }; - request_error.message = error_message.into(); - } + let request_error = response::RequestError { + type_: match error_type(&err) { + RequestErrorType::Unspecified => response::RequestErrorType::Unspecified, + RequestErrorType::ExecAbort => response::RequestErrorType::ExecAbort, + RequestErrorType::Timeout => response::RequestErrorType::Timeout, + RequestErrorType::Disconnect => response::RequestErrorType::Disconnect, + } + .into(), + message: error_message.into(), + ..Default::default() + }; Some(response::response::Value::RequestError(request_error)) } }; @@ -360,6 +353,10 @@ fn get_command(request: &Command) -> Option { RequestType::XGroupCreate => Some(get_two_word_command("XGROUP", "CREATE")), RequestType::XGroupDestroy => Some(get_two_word_command("XGROUP", "DESTROY")), RequestType::XTrim => Some(cmd("XTRIM")), + RequestType::HSetNX => Some(cmd("HSETNX")), + RequestType::SIsMember => Some(cmd("SISMEMBER")), + RequestType::Hvals => Some(cmd("HVALS")), + RequestType::PTTL => Some(cmd("PTTL")), } } diff --git a/glide-core/tests/test_client.rs b/glide-core/tests/test_client.rs index 42cfa9a13c..6aa1c5c040 100644 --- a/glide-core/tests/test_client.rs +++ b/glide-core/tests/test_client.rs @@ -458,7 +458,7 @@ pub(crate) mod shared_client_tests { Value::Boolean(true), Value::Int(1), Value::Okay, - Value::Double(0.5.into()), + Value::Double(0.5), Value::Int(1), ]),) ); diff --git a/go/DEVELOPER.md b/go/DEVELOPER.md new file mode 100644 index 0000000000..6f365c756d --- /dev/null +++ b/go/DEVELOPER.md @@ -0,0 +1,213 @@ +# Developer Guide + +This document describes how to set up your development environment to build and test the GLIDE for Redis Go wrapper. + +### Development Overview + +We're excited to share that the GLIDE Go client is currently in development! However, it's important to note that this client is a work in progress and is not yet complete or fully tested. Your contributions and feedback are highly encouraged as we work towards refining and improving this implementation. Thank you for your interest and understanding as we continue to develop this Go wrapper. + +The GLIDE for Redis Go wrapper consists of both Go and Rust code. The Go and Rust components communicate in two ways: +1. Using the [protobuf](https://github.com/protocolbuffers/protobuf) protocol. +2. Using shared C objects. [cgo](https://pkg.go.dev/cmd/cgo) is used to interact with the C objects from Go code. + +### Build from source + +#### Prerequisites + +Software Dependencies + +- Go +- GNU Make +- git +- GCC +- pkg-config +- protoc (protobuf compiler) >= v3.20.0 +- openssl +- openssl-dev +- rustup +- redis + +**Redis installation** + +To install redis-server and redis-cli on your host, follow the [Redis Installation Guide](https://redis.io/docs/install/install-redis/). + +**Dependencies installation for Ubuntu** + +```bash +sudo apt update -y +sudo apt install -y git gcc pkg-config openssl libssl-dev unzip make +# Install Go +sudo snap install go --classic +export PATH="$PATH:$HOME/go/bin" +# Install rust +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source "$HOME/.cargo/env" +# Check that the Rust compiler is installed +rustc --version +# Install protobuf compiler +PB_REL="https://github.com/protocolbuffers/protobuf/releases" +curl -LO $PB_REL/download/v3.20.3/protoc-3.20.3-linux-x86_64.zip +unzip protoc-3.20.3-linux-x86_64.zip -d $HOME/.local +export PATH="$PATH:$HOME/.local/bin" +# Check that the protobuf compiler is installed. A minimum version of 3.20.0 is required. +protoc --version +``` + +**Dependencies installation for CentOS** + +```bash +sudo yum update -y +sudo yum install -y git gcc pkgconfig openssl openssl-devel unzip wget tar +# Install Go +wget https://go.dev/dl/go1.22.0.linux-amd64.tar.gz +sudo tar -C /usr/local -xzf go1.22.0.linux-amd64.tar.gz +export PATH="$PATH:/usr/local/go/bin" +export PATH="$PATH:$HOME/go/bin" +# Install rust +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source "$HOME/.cargo/env" +# Check that the Rust compiler is installed +rustc --version +# Install protobuf compiler +PB_REL="https://github.com/protocolbuffers/protobuf/releases" +curl -LO $PB_REL/download/v3.20.3/protoc-3.20.3-linux-x86_64.zip +unzip protoc-3.20.3-linux-x86_64.zip -d $HOME/.local +export PATH="$PATH:$HOME/.local/bin" +# Check that the protobuf compiler is installed. A minimum version of 3.20.0 is required. +protoc --version +``` + +**Dependencies installation for MacOS** + +```bash +brew update +brew install go make git gcc pkgconfig protobuf@3 openssl +export PATH="$PATH:$HOME/go/bin" +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source "$HOME/.cargo/env" +# Check that the protobuf compiler is installed. A minimum version of 3.20.0 is required. +protoc --version +# Check that the Rust compiler is installed +rustc --version +``` + +#### Building and installation steps + +Before starting this step, make sure you've installed all software requirements. + +1. Clone the repository: + ```bash + VERSION=0.1.0 # You can modify this to other released version or set it to "main" to get the unstable branch + git clone --branch ${VERSION} https://github.com/aws/glide-for-redis.git + cd glide-for-redis + ``` +2. Initialize git submodule: + ```bash + git submodule update --init --recursive + ``` +3. Install build dependencies: + ```bash + cd go + make install-build-tools + ``` +4. Build the Go wrapper: + ```bash + make build + ``` +5. Run tests: + 1. Ensure that you have installed redis-server and redis-cli on your host. You can find the Redis installation guide at the following link: [Redis Installation Guide](https://redis.io/docs/install/install-redis/install-redis-on-linux/). + 2. Execute the following command from the go folder: + ```bash + go test -race ./... + ``` +6. Install Go development tools with: + + ```bash + make install-dev-tools + ``` + +### Test + +To run tests, use the following command: + +```bash +go test -race ./... +``` + +For more detailed test output, add the `-v` flag: + +```bash +go test -race ./... -v +``` + +To execute a specific test, include `-run `. For example: + +```bash +go test -race ./... -run TestConnectionRequestProtobufGeneration_allFieldsSet -v +``` + +### Submodules + +After pulling new changes, ensure that you update the submodules by running the following command: + +```bash +git submodule update +``` + +### Generate protobuf files + +During the initial build, Go protobuf files were created in `go/protobuf`. If modifications are made to the protobuf definition files (.proto files located in `glide-core/src/protobuf`), it becomes necessary to regenerate the Go protobuf files. To do so, run: + +```bash +make generate-protobuf +``` + + +### Linters + +Development on the Go wrapper may involve changes in either the Go or Rust code. Each language has distinct linter tests that must be passed before committing changes. + +#### Language-specific Linters + +**Go:** + +- go vet +- gofumpt +- staticcheck +- golines + +**Rust:** + +- clippy +- fmt + +#### Running the linters + +Run from the main `/go` folder + +1. Go + ```bash + make install-dev-tools + make lint + ``` +2. Rust + ```bash + rustup component add clippy rustfmt + cargo clippy --all-features --all-targets -- -D warnings + cargo fmt --manifest-path ./Cargo.toml --all + ``` + +#### Fixing lint formatting errors + +The following command can be used to fix Go formatting errors reported by gofumpt or golines. Note that golines does not always format comments well if they surpass the max line length (127 characters). + +Run from the main `/go` folder + +```bash +make format +``` + +### Recommended extensions for VS Code + +- [Go](https://marketplace.visualstudio.com/items?itemName=golang.Go) +- [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) diff --git a/go/Makefile b/go/Makefile index bd3e101b7b..1ec0145dfd 100644 --- a/go/Makefile +++ b/go/Makefile @@ -1,5 +1,25 @@ -install-tools: - @cat tools.go | grep _ | awk -F'"' '{print $$2}' | xargs -tI % go install % +install-build-tools: + go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.32.0 + +install-dev-tools-go1.18.10: + go install github.com/vakenbolt/go-test-report@v0.9.3 + go install mvdan.cc/gofumpt@v0.4.0 + go install github.com/segmentio/golines@v0.11.0 + go install honnef.co/go/tools/cmd/staticcheck@v0.3.3 + +install-dev-tools-go1.22.0: + go install github.com/vakenbolt/go-test-report@v0.9.3 + go install mvdan.cc/gofumpt@v0.6.0 + go install github.com/segmentio/golines@v0.12.2 + go install honnef.co/go/tools/cmd/staticcheck@v0.4.6 + +install-dev-tools: install-dev-tools-go1.22.0 + +install-tools-go1.18.10: install-build-tools install-dev-tools-go1.18.10 + +install-tools-go1.22.0: install-build-tools install-dev-tools-go1.22.0 + +install-tools: install-tools-go1.22.0 build: build-glide-core build-glide-client generate-protobuf go build ./... @@ -23,7 +43,22 @@ generate-protobuf: lint: go vet ./... staticcheck ./... + gofumpt -d . + golines --dry-run --shorten-comments -m 127 . + +lint-ci: + go vet ./... + staticcheck ./... + if [ "$$(gofumpt -l . | wc -l)" -gt 0 ]; then exit 1; fi + if [ "$$(golines -l --shorten-comments -m 127 . | wc -l)" -gt 0 ]; then exit 1; fi + +format: + gofumpt -w . + golines -w --shorten-comments -m 127 . + +test: + go test -v -race `go list ./... | grep -v protobuf` -unit-test-report: +test-and-report: mkdir -p reports - go test -race ./... -json | go-test-report -o reports/unit-test-report.html + go test -v -race `go list ./... | grep -v protobuf` -json | go-test-report -o reports/test-report.html diff --git a/go/api/config.go b/go/api/config.go new file mode 100644 index 0000000000..9d2417b429 --- /dev/null +++ b/go/api/config.go @@ -0,0 +1,309 @@ +// Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + +package api + +import "github.com/aws/glide-for-redis/go/glide/protobuf" + +const ( + defaultHost = "localhost" + defaultPort = 6379 +) + +// NodeAddress represents the host address and port of a node in the cluster. +type NodeAddress struct { + Host string // If not supplied, "localhost" will be used. + Port int // If not supplied, 6379 will be used. +} + +func (addr *NodeAddress) toProtobuf() *protobuf.NodeAddress { + if addr.Host == "" { + addr.Host = defaultHost + } + + if addr.Port == 0 { + addr.Port = defaultPort + } + + return &protobuf.NodeAddress{Host: addr.Host, Port: uint32(addr.Port)} +} + +// RedisCredentials represents the credentials for connecting to a Redis server. +type RedisCredentials struct { + // The username that will be used for authenticating connections to the Redis servers. If not supplied, "default" + // will be used. + username string + // The password that will be used for authenticating connections to the Redis servers. + password string +} + +// NewRedisCredentials returns a [RedisCredentials] struct with the given username and password. +func NewRedisCredentials(username string, password string) *RedisCredentials { + return &RedisCredentials{username, password} +} + +// NewRedisCredentialsWithDefaultUsername returns a [RedisCredentials] struct with a default username of "default" and the +// given password. +func NewRedisCredentialsWithDefaultUsername(password string) *RedisCredentials { + return &RedisCredentials{password: password} +} + +func (creds *RedisCredentials) toProtobuf() *protobuf.AuthenticationInfo { + return &protobuf.AuthenticationInfo{Username: creds.username, Password: creds.password} +} + +// ReadFrom represents the client's read from strategy. +type ReadFrom int + +const ( + // Primary - Always get from primary, in order to get the freshest data. + Primary ReadFrom = iota + // PreferReplica - Spread the requests between all replicas in a round-robin manner. If no replica is available, route the + // requests to the primary. + PreferReplica +) + +func mapReadFrom(readFrom ReadFrom) protobuf.ReadFrom { + if readFrom == PreferReplica { + return protobuf.ReadFrom_PreferReplica + } + + return protobuf.ReadFrom_Primary +} + +type baseClientConfiguration struct { + addresses []NodeAddress + useTLS bool + credentials *RedisCredentials + readFrom ReadFrom + requestTimeout int + clientName string +} + +func (config *baseClientConfiguration) toProtobuf() *protobuf.ConnectionRequest { + request := protobuf.ConnectionRequest{} + for _, address := range config.addresses { + request.Addresses = append(request.Addresses, address.toProtobuf()) + } + + if config.useTLS { + request.TlsMode = protobuf.TlsMode_SecureTls + } else { + request.TlsMode = protobuf.TlsMode_NoTls + } + + if config.credentials != nil { + request.AuthenticationInfo = config.credentials.toProtobuf() + } + + request.ReadFrom = mapReadFrom(config.readFrom) + if config.requestTimeout != 0 { + request.RequestTimeout = uint32(config.requestTimeout) + } + + if config.clientName != "" { + request.ClientName = config.clientName + } + + return &request +} + +// BackoffStrategy represents the strategy used to determine how and when to reconnect, in case of connection failures. The +// time between attempts grows exponentially, to the formula: +// +// rand(0 ... factor * (exponentBase ^ N)) +// +// where N is the number of failed attempts. +// +// Once the maximum value is reached, that will remain the time between retry attempts until a reconnect attempt is successful. +// The client will attempt to reconnect indefinitely. +type BackoffStrategy struct { + // Number of retry attempts that the client should perform when disconnected from the server, where the time + // between retries increases. Once the retries have reached the maximum value, the time between retries will remain + // constant until a reconnect attempt is successful. + numOfRetries int + // The multiplier that will be applied to the waiting time between each retry. + factor int + // The exponent base configured for the strategy. + exponentBase int +} + +// NewBackoffStrategy returns a [BackoffStrategy] with the given configuration parameters. +func NewBackoffStrategy(numOfRetries int, factor int, exponentBase int) *BackoffStrategy { + return &BackoffStrategy{numOfRetries, factor, exponentBase} +} + +func (strategy *BackoffStrategy) toProtobuf() *protobuf.ConnectionRetryStrategy { + return &protobuf.ConnectionRetryStrategy{ + NumberOfRetries: uint32(strategy.numOfRetries), + Factor: uint32(strategy.factor), + ExponentBase: uint32(strategy.exponentBase), + } +} + +// RedisClientConfiguration represents the configuration settings for a Standalone Redis client. baseClientConfiguration is an +// embedded struct that contains shared settings for standalone and cluster clients. +type RedisClientConfiguration struct { + baseClientConfiguration + reconnectStrategy *BackoffStrategy + databaseId int +} + +// NewRedisClientConfiguration returns a [RedisClientConfiguration] with default configuration settings. For further +// configuration, use the [RedisClientConfiguration] With* methods. +func NewRedisClientConfiguration() *RedisClientConfiguration { + return &RedisClientConfiguration{} +} + +func (config *RedisClientConfiguration) toProtobuf() *protobuf.ConnectionRequest { + request := config.baseClientConfiguration.toProtobuf() + request.ClusterModeEnabled = false + if config.reconnectStrategy != nil { + request.ConnectionRetryStrategy = config.reconnectStrategy.toProtobuf() + } + + if config.databaseId != 0 { + request.DatabaseId = uint32(config.databaseId) + } + + return request +} + +// WithAddress adds an address for a known node in the cluster to this configuration's list of addresses. WithAddress can be +// called multiple times to add multiple addresses to the list. If the server is in cluster mode the list can be partial, as +// the client will attempt to map out the cluster and find all nodes. If the server is in standalone mode, only nodes whose +// addresses were provided will be used by the client. For example: +// +// config := NewRedisClientConfiguration(). +// WithAddress(&NodeAddress{ +// Host: "sample-address-0001.use1.cache.amazonaws.com", Port: 6379}). +// WithAddress(&NodeAddress{ +// Host: "sample-address-0002.use1.cache.amazonaws.com", Port: 6379}) +func (config *RedisClientConfiguration) WithAddress(address *NodeAddress) *RedisClientConfiguration { + config.addresses = append(config.addresses, *address) + return config +} + +// WithUseTLS configures the TLS settings for this configuration. Set to true if communication with the cluster should use +// Transport Level Security. This setting should match the TLS configuration of the server/cluster, otherwise the connection +// attempt will fail. +func (config *RedisClientConfiguration) WithUseTLS(useTLS bool) *RedisClientConfiguration { + config.useTLS = useTLS + return config +} + +// WithCredentials sets the credentials for the authentication process. If none are set, the client will not authenticate +// itself with the server. +func (config *RedisClientConfiguration) WithCredentials(credentials *RedisCredentials) *RedisClientConfiguration { + config.credentials = credentials + return config +} + +// WithReadFrom sets the client's [ReadFrom] strategy. If not set, [Primary] will be used. +func (config *RedisClientConfiguration) WithReadFrom(readFrom ReadFrom) *RedisClientConfiguration { + config.readFrom = readFrom + return config +} + +// WithRequestTimeout sets the duration in milliseconds that the client should wait for a request to complete. This duration +// encompasses sending the request, awaiting for a response from the server, and any required reconnections or retries. If the +// specified timeout is exceeded for a pending request, it will result in a timeout error. If not set, a default value will be +// used. +func (config *RedisClientConfiguration) WithRequestTimeout(requestTimeout int) *RedisClientConfiguration { + config.requestTimeout = requestTimeout + return config +} + +// WithClientName sets the client name to be used for the client. Will be used with CLIENT SETNAME command during connection +// establishment. +func (config *RedisClientConfiguration) WithClientName(clientName string) *RedisClientConfiguration { + config.clientName = clientName + return config +} + +// WithReconnectStrategy sets the [BackoffStrategy] used to determine how and when to reconnect, in case of connection +// failures. If not set, a default backoff strategy will be used. +func (config *RedisClientConfiguration) WithReconnectStrategy(strategy *BackoffStrategy) *RedisClientConfiguration { + config.reconnectStrategy = strategy + return config +} + +// WithDatabaseId sets the index of the logical database to connect to. +func (config *RedisClientConfiguration) WithDatabaseId(id int) *RedisClientConfiguration { + config.databaseId = id + return config +} + +// RedisClusterClientConfiguration represents the configuration settings for a Cluster Redis client. +// Note: Currently, the reconnection strategy in cluster mode is not configurable, and exponential backoff with fixed values is +// used. +type RedisClusterClientConfiguration struct { + baseClientConfiguration +} + +// NewRedisClusterClientConfiguration returns a [RedisClusterClientConfiguration] with default configuration settings. For +// further configuration, use the [RedisClientConfiguration] With* methods. +func NewRedisClusterClientConfiguration() *RedisClusterClientConfiguration { + return &RedisClusterClientConfiguration{ + baseClientConfiguration: baseClientConfiguration{}, + } +} + +func (config *RedisClusterClientConfiguration) toProtobuf() *protobuf.ConnectionRequest { + request := config.baseClientConfiguration.toProtobuf() + request.ClusterModeEnabled = true + return request +} + +// WithAddress adds an address for a known node in the cluster to this configuration's list of addresses. WithAddress can be +// called multiple times to add multiple addresses to the list. If the server is in cluster mode the list can be partial, as +// the client will attempt to map out the cluster and find all nodes. If the server is in standalone mode, only nodes whose +// addresses were provided will be used by the client. For example: +// +// config := NewRedisClusterClientConfiguration(). +// WithAddress(&NodeAddress{ +// Host: "sample-address-0001.use1.cache.amazonaws.com", Port: 6379}). +// WithAddress(&NodeAddress{ +// Host: "sample-address-0002.use1.cache.amazonaws.com", Port: 6379}) +func (config *RedisClusterClientConfiguration) WithAddress(address *NodeAddress) *RedisClusterClientConfiguration { + config.addresses = append(config.addresses, *address) + return config +} + +// WithUseTLS configures the TLS settings for this configuration. Set to true if communication with the cluster should use +// Transport Level Security. This setting should match the TLS configuration of the server/cluster, otherwise the connection +// attempt will fail. +func (config *RedisClusterClientConfiguration) WithUseTLS(useTLS bool) *RedisClusterClientConfiguration { + config.useTLS = useTLS + return config +} + +// WithCredentials sets the credentials for the authentication process. If none are set, the client will not authenticate +// itself with the server. +func (config *RedisClusterClientConfiguration) WithCredentials( + credentials *RedisCredentials, +) *RedisClusterClientConfiguration { + config.credentials = credentials + return config +} + +// WithReadFrom sets the client's [ReadFrom] strategy. If not set, [Primary] will be used. +func (config *RedisClusterClientConfiguration) WithReadFrom(readFrom ReadFrom) *RedisClusterClientConfiguration { + config.readFrom = readFrom + return config +} + +// WithRequestTimeout sets the duration in milliseconds that the client should wait for a request to complete. This duration +// encompasses sending the request, awaiting for a response from the server, and any required reconnections or retries. If the +// specified timeout is exceeded for a pending request, it will result in a timeout error. If not set, a default value will be +// used. +func (config *RedisClusterClientConfiguration) WithRequestTimeout(requestTimeout int) *RedisClusterClientConfiguration { + config.requestTimeout = requestTimeout + return config +} + +// WithClientName sets the client name to be used for the client. Will be used with CLIENT SETNAME command during connection +// establishment. +func (config *RedisClusterClientConfiguration) WithClientName(clientName string) *RedisClusterClientConfiguration { + config.clientName = clientName + return config +} diff --git a/go/api/config_test.go b/go/api/config_test.go new file mode 100644 index 0000000000..53a18e5308 --- /dev/null +++ b/go/api/config_test.go @@ -0,0 +1,129 @@ +// Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + +package api + +import ( + "fmt" + "testing" + + "github.com/aws/glide-for-redis/go/glide/protobuf" + "github.com/stretchr/testify/assert" +) + +func TestDefaultStandaloneConfig(t *testing.T) { + config := NewRedisClientConfiguration() + expected := &protobuf.ConnectionRequest{ + TlsMode: protobuf.TlsMode_NoTls, + ClusterModeEnabled: false, + ReadFrom: protobuf.ReadFrom_Primary, + } + + result := config.toProtobuf() + + assert.Equal(t, expected, result) +} + +func TestDefaultClusterConfig(t *testing.T) { + config := NewRedisClusterClientConfiguration() + expected := &protobuf.ConnectionRequest{ + TlsMode: protobuf.TlsMode_NoTls, + ClusterModeEnabled: true, + ReadFrom: protobuf.ReadFrom_Primary, + } + + result := config.toProtobuf() + + assert.Equal(t, expected, result) +} + +func TestConfig_allFieldsSet(t *testing.T) { + hosts := []string{"host1", "host2"} + ports := []int{1234, 5678} + username := "username" + password := "password" + timeout := 3 + clientName := "client name" + retries, factor, base := 5, 10, 50 + databaseId := 1 + + config := NewRedisClientConfiguration(). + WithUseTLS(true). + WithReadFrom(PreferReplica). + WithCredentials(NewRedisCredentials(username, password)). + WithRequestTimeout(timeout). + WithClientName(clientName). + WithReconnectStrategy(NewBackoffStrategy(retries, factor, base)). + WithDatabaseId(databaseId) + + expected := &protobuf.ConnectionRequest{ + TlsMode: protobuf.TlsMode_SecureTls, + ReadFrom: protobuf.ReadFrom_PreferReplica, + ClusterModeEnabled: false, + AuthenticationInfo: &protobuf.AuthenticationInfo{Username: username, Password: password}, + RequestTimeout: uint32(timeout), + ClientName: clientName, + ConnectionRetryStrategy: &protobuf.ConnectionRetryStrategy{ + NumberOfRetries: uint32(retries), + Factor: uint32(factor), + ExponentBase: uint32(base), + }, + DatabaseId: uint32(databaseId), + } + + assert.Equal(t, len(hosts), len(ports)) + for i := 0; i < len(hosts); i++ { + config.WithAddress(&NodeAddress{hosts[i], ports[i]}) + expected.Addresses = append( + expected.Addresses, + &protobuf.NodeAddress{Host: hosts[i], Port: uint32(ports[i])}, + ) + } + + result := config.toProtobuf() + + assert.Equal(t, expected, result) +} + +func TestNodeAddress(t *testing.T) { + parameters := []struct { + input NodeAddress + expected *protobuf.NodeAddress + }{ + {NodeAddress{}, &protobuf.NodeAddress{Host: defaultHost, Port: defaultPort}}, + {NodeAddress{Host: "host"}, &protobuf.NodeAddress{Host: "host", Port: defaultPort}}, + {NodeAddress{Port: 1234}, &protobuf.NodeAddress{Host: defaultHost, Port: 1234}}, + {NodeAddress{"host", 1234}, &protobuf.NodeAddress{Host: "host", Port: 1234}}, + } + + for i, parameter := range parameters { + t.Run(fmt.Sprintf("Testing [%v]", i), func(t *testing.T) { + result := parameter.input.toProtobuf() + + assert.Equal(t, parameter.expected, result) + }) + } +} + +func TestRedisCredentials(t *testing.T) { + parameters := []struct { + input *RedisCredentials + expected *protobuf.AuthenticationInfo + }{ + { + NewRedisCredentials("username", "password"), + &protobuf.AuthenticationInfo{Username: "username", Password: "password"}, + }, + { + NewRedisCredentialsWithDefaultUsername("password"), + &protobuf.AuthenticationInfo{Password: "password"}, + }, + } + + for i, parameter := range parameters { + t.Run(fmt.Sprintf("Testing [%v]", i), func(t *testing.T) { + result := parameter.input.toProtobuf() + + assert.Equal(t, parameter.expected, result) + }) + } +} diff --git a/go/go.mod b/go/go.mod index 2e4ede8147..bb2f9a7c48 100644 --- a/go/go.mod +++ b/go/go.mod @@ -3,9 +3,8 @@ module github.com/aws/glide-for-redis/go/glide go 1.18 require ( - github.com/vakenbolt/go-test-report v0.9.3 + github.com/stretchr/testify v1.8.4 google.golang.org/protobuf v1.32.0 - honnef.co/go/tools v0.3.3 ) require ( @@ -19,4 +18,11 @@ require ( golang.org/x/sync v0.6.0 // indirect golang.org/x/sys v0.11.0 // indirect golang.org/x/tools v0.12.1-0.20230825192346-2191a27a6dc5 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go/go.sum b/go/go.sum index 26671898ba..4c28101968 100644 --- a/go/go.sum +++ b/go/go.sum @@ -1,40 +1,4 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= -github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= @@ -300,26 +264,24 @@ google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiq google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.3.3 h1:oDx7VAwstgpYpb3wv0oxiZlxY+foCpRAwY7Vk6XpAgA= -honnef.co/go/tools v0.3.3/go.mod h1:jzwdWgg7Jdq75wlfblQxO4neNaFFSvgc1tD5Wv8U0Yw= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/go/tests/glide_test.go b/go/tests/glide_test.go deleted file mode 100644 index 32103c6fc5..0000000000 --- a/go/tests/glide_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package tests - -import ( - "testing" -) - -// TODO: Replace this test with real tests when glide client implementation is started -func TestArbitraryLogic(t *testing.T) { - someVar := true - if !someVar { - t.Fatalf("Expected someVar to be true, but was false.") - } -} diff --git a/go/tools.go b/go/tools.go deleted file mode 100644 index b6b28536d0..0000000000 --- a/go/tools.go +++ /dev/null @@ -1,10 +0,0 @@ -//go:build tools -// +build tools - -package main - -import ( - _ "github.com/vakenbolt/go-test-report" - _ "google.golang.org/protobuf/cmd/protoc-gen-go" - _ "honnef.co/go/tools/cmd/staticcheck" -) diff --git a/java/README.md b/java/README.md index fbedc1b499..b7b9a3dcaa 100644 --- a/java/README.md +++ b/java/README.md @@ -111,8 +111,8 @@ assert getResponse.get() == "foobar" : "Failed on client.get("key") request"; You can run benchmarks using `./gradlew run`. You can set arguments using the args flag like: ```shell -./gradlew run --args="-help" -./gradlew run --args="-resultsFile=output -dataSize \"100 1000\" -concurrentTasks \"10 100\" -clients all -host localhost -port 6279 -clientCount \"1 5\" -tls" +./gradlew run --args="--help" +./gradlew run --args="--resultsFile=output --dataSize \"100 1000\" --concurrentTasks \"10 100\" --clients all --host localhost --port 6279 --clientCount \"1 5\" --tls" ``` The following arguments are accepted: diff --git a/java/benchmarks/src/main/java/glide/benchmarks/BenchmarkingApp.java b/java/benchmarks/src/main/java/glide/benchmarks/BenchmarkingApp.java index 9c5d196699..594c82c030 100644 --- a/java/benchmarks/src/main/java/glide/benchmarks/BenchmarkingApp.java +++ b/java/benchmarks/src/main/java/glide/benchmarks/BenchmarkingApp.java @@ -66,36 +66,66 @@ private static Options getOptions() { // create the Options Options options = new Options(); - options.addOption(Option.builder("help").desc("print this message").build()); + options.addOption(Option.builder("h").longOpt("help").desc("Print this message").build()); options.addOption( - Option.builder("configuration").hasArg(true).desc("Configuration flag [Release]").build()); + Option.builder() + .longOpt("configuration") + .hasArg(true) + .desc("Configuration flag [Release]") + .build()); options.addOption( - Option.builder("resultsFile") + Option.builder() + .longOpt("resultsFile") .hasArg(true) .desc("Result filepath (stdout if empty) []") .build()); options.addOption( - Option.builder("dataSize").hasArg(true).desc("Data block size [100 4000]").build()); + Option.builder() + .longOpt("dataSize") + .hasArg(true) + .desc("Data block size [100 4000]") + .build()); options.addOption( - Option.builder("concurrentTasks") + Option.builder() + .longOpt("concurrentTasks") .hasArg(true) .desc("Number of concurrent tasks [100, 1000]") .build()); options.addOption( - Option.builder("clients").hasArg(true).desc("one of: all|jedis|lettuce|glide").build()); - options.addOption(Option.builder("host").hasArg(true).desc("Hostname [localhost]").build()); - options.addOption(Option.builder("port").hasArg(true).desc("Port number [6379]").build()); + Option.builder() + .longOpt("clients") + .hasArg(true) + .desc("one of: all|jedis|lettuce|glide") + .build()); options.addOption( - Option.builder("clientCount").hasArg(true).desc("Number of clients to run [1]").build()); - options.addOption(Option.builder("tls").hasArg(false).desc("TLS [false]").build()); + Option.builder().longOpt("host").hasArg(true).desc("Hostname [localhost]").build()); options.addOption( - Option.builder("clusterModeEnabled") + Option.builder().longOpt("port").hasArg(true).desc("Port number [6379]").build()); + options.addOption( + Option.builder() + .longOpt("clientCount") + .hasArg(true) + .desc("Number of clients to run [1]") + .build()); + options.addOption(Option.builder().longOpt("tls").hasArg(false).desc("TLS [false]").build()); + options.addOption( + Option.builder() + .longOpt("clusterModeEnabled") .hasArg(false) .desc("Is cluster-mode enabled, other standalone mode is used [false]") .build()); - Option.builder("minimal").hasArg(false).desc("Run·benchmark·in·minimal·mode").build(); options.addOption( - Option.builder("debugLogging").hasArg(false).desc("Verbose logs [false]").build()); + Option.builder() + .longOpt("minimal") + .hasArg(false) + .desc("Run benchmark in minimal mode") + .build()); + options.addOption( + Option.builder() + .longOpt("debugLogging") + .hasArg(false) + .desc("Verbose logs [false]") + .build()); return options; } diff --git a/java/benchmarks/src/main/java/glide/benchmarks/clients/jedis/JedisClient.java b/java/benchmarks/src/main/java/glide/benchmarks/clients/jedis/JedisClient.java index a79b757a2c..a2fb669334 100644 --- a/java/benchmarks/src/main/java/glide/benchmarks/clients/jedis/JedisClient.java +++ b/java/benchmarks/src/main/java/glide/benchmarks/clients/jedis/JedisClient.java @@ -6,18 +6,24 @@ import java.util.Set; import redis.clients.jedis.DefaultJedisClientConfig; import redis.clients.jedis.HostAndPort; +import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisCluster; import redis.clients.jedis.JedisPool; -import redis.clients.jedis.commands.JedisCommands; /** A Jedis client with sync capabilities. See: https://github.com/redis/jedis */ public class JedisClient implements SyncClient { - - private JedisCommands jedis; + boolean isClusterMode; + private JedisPool jedisStandalonePool; + private JedisCluster jedisCluster; @Override public void closeConnection() { - // nothing to do + if (jedisCluster != null) { + jedisCluster.close(); + } + if (jedisStandalonePool != null) { + jedisStandalonePool.close(); + } } @Override @@ -27,27 +33,38 @@ public String getName() { @Override public void connectToRedis(ConnectionSettings connectionSettings) { - if (connectionSettings.clusterMode) { - jedis = + isClusterMode = connectionSettings.clusterMode; + if (isClusterMode) { + jedisCluster = new JedisCluster( Set.of(new HostAndPort(connectionSettings.host, connectionSettings.port)), DefaultJedisClientConfig.builder().ssl(connectionSettings.useSsl).build()); } else { - try (JedisPool pool = + jedisStandalonePool = new JedisPool( - connectionSettings.host, connectionSettings.port, connectionSettings.useSsl)) { - jedis = pool.getResource(); - } + connectionSettings.host, connectionSettings.port, connectionSettings.useSsl); } } @Override public void set(String key, String value) { - jedis.set(key, value); + if (isClusterMode) { + jedisCluster.set(key, value); + } else { + try (Jedis jedis = jedisStandalonePool.getResource()) { + jedis.set(key, value); + } + } } @Override public String get(String key) { - return jedis.get(key); + if (isClusterMode) { + return jedisCluster.get(key); + } else { + try (Jedis jedis = jedisStandalonePool.getResource()) { + return jedis.get(key); + } + } } } diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index 6d56840502..72d0785360 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -8,6 +8,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.DecrBy; import static redis_request.RedisRequestOuterClass.RequestType.Del; import static redis_request.RedisRequestOuterClass.RequestType.Exists; +import static redis_request.RedisRequestOuterClass.RequestType.Expire; +import static redis_request.RedisRequestOuterClass.RequestType.ExpireAt; import static redis_request.RedisRequestOuterClass.RequestType.GetString; import static redis_request.RedisRequestOuterClass.RequestType.HashDel; import static redis_request.RedisRequestOuterClass.RequestType.HashExists; @@ -20,10 +22,15 @@ import static redis_request.RedisRequestOuterClass.RequestType.Incr; import static redis_request.RedisRequestOuterClass.RequestType.IncrBy; import static redis_request.RedisRequestOuterClass.RequestType.IncrByFloat; +import static redis_request.RedisRequestOuterClass.RequestType.LLen; import static redis_request.RedisRequestOuterClass.RequestType.LPop; import static redis_request.RedisRequestOuterClass.RequestType.LPush; +import static redis_request.RedisRequestOuterClass.RequestType.LRange; +import static redis_request.RedisRequestOuterClass.RequestType.LTrim; import static redis_request.RedisRequestOuterClass.RequestType.MGet; import static redis_request.RedisRequestOuterClass.RequestType.MSet; +import static redis_request.RedisRequestOuterClass.RequestType.PExpire; +import static redis_request.RedisRequestOuterClass.RequestType.PExpireAt; import static redis_request.RedisRequestOuterClass.RequestType.RPop; import static redis_request.RedisRequestOuterClass.RequestType.RPush; import static redis_request.RedisRequestOuterClass.RequestType.SAdd; @@ -31,6 +38,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.SMembers; import static redis_request.RedisRequestOuterClass.RequestType.SRem; import static redis_request.RedisRequestOuterClass.RequestType.SetString; +import static redis_request.RedisRequestOuterClass.RequestType.TTL; import static redis_request.RedisRequestOuterClass.RequestType.Unlink; import glide.api.commands.GenericBaseCommands; @@ -38,6 +46,7 @@ import glide.api.commands.ListBaseCommands; import glide.api.commands.SetCommands; import glide.api.commands.StringCommands; +import glide.api.models.commands.ExpireOptions; import glide.api.models.commands.SetOptions; import glide.api.models.configuration.BaseClientConfiguration; import glide.api.models.exceptions.RedisException; @@ -70,6 +79,7 @@ public abstract class BaseClient HashCommands, ListBaseCommands, SetCommands { + /** Redis simple string response with "OK" */ public static final String OK = ConstantResponse.OK.toString(); @@ -205,7 +215,7 @@ protected Object[] handleArrayOrNullResponse(Response response) throws RedisExce /** * @param response A Protobuf response * @return A map of String to V - * @param Value type could be even map too + * @param Value type */ @SuppressWarnings("unchecked") // raw Map cast to Map protected Map handleMapResponse(Response response) throws RedisException { @@ -353,6 +363,27 @@ public CompletableFuture lpopCount(@NonNull String key, long count) { response -> castArray(handleArrayResponse(response), String.class)); } + @Override + public CompletableFuture lrange(@NonNull String key, long start, long end) { + return commandManager.submitNewCommand( + LRange, + new String[] {key, Long.toString(start), Long.toString(end)}, + response -> castArray(handleArrayOrNullResponse(response), String.class)); + } + + @Override + public CompletableFuture ltrim(@NonNull String key, long start, long end) { + return commandManager.submitNewCommand( + LTrim, + new String[] {key, Long.toString(start), Long.toString(end)}, + this::handleStringResponse); + } + + @Override + public CompletableFuture llen(@NonNull String key) { + return commandManager.submitNewCommand(LLen, new String[] {key}, this::handleLongResponse); + } + @Override public CompletableFuture rpush(@NonNull String key, @NonNull String[] elements) { String[] arguments = ArrayUtils.addFirst(elements, key); @@ -374,24 +405,24 @@ public CompletableFuture rpopCount(@NonNull String key, long count) { } @Override - public CompletableFuture sadd(String key, String[] members) { + public CompletableFuture sadd(@NonNull String key, @NonNull String[] members) { String[] arguments = ArrayUtils.addFirst(members, key); return commandManager.submitNewCommand(SAdd, arguments, this::handleLongResponse); } @Override - public CompletableFuture srem(String key, String[] members) { + public CompletableFuture srem(@NonNull String key, @NonNull String[] members) { String[] arguments = ArrayUtils.addFirst(members, key); return commandManager.submitNewCommand(SRem, arguments, this::handleLongResponse); } @Override - public CompletableFuture> smembers(String key) { + public CompletableFuture> smembers(@NonNull String key) { return commandManager.submitNewCommand(SMembers, new String[] {key}, this::handleSetResponse); } @Override - public CompletableFuture scard(String key) { + public CompletableFuture scard(@NonNull String key) { return commandManager.submitNewCommand(SCard, new String[] {key}, this::handleLongResponse); } @@ -404,4 +435,68 @@ public CompletableFuture exists(@NonNull String[] keys) { public CompletableFuture unlink(@NonNull String[] keys) { return commandManager.submitNewCommand(Unlink, keys, this::handleLongResponse); } + + @Override + public CompletableFuture expire(@NonNull String key, long seconds) { + return commandManager.submitNewCommand( + Expire, new String[] {key, Long.toString(seconds)}, this::handleBooleanResponse); + } + + @Override + public CompletableFuture expire( + @NonNull String key, long seconds, @NonNull ExpireOptions expireOptions) { + String[] arguments = + ArrayUtils.addAll(new String[] {key, Long.toString(seconds)}, expireOptions.toArgs()); + return commandManager.submitNewCommand(Expire, arguments, this::handleBooleanResponse); + } + + @Override + public CompletableFuture expireAt(@NonNull String key, long unixSeconds) { + return commandManager.submitNewCommand( + ExpireAt, new String[] {key, Long.toString(unixSeconds)}, this::handleBooleanResponse); + } + + @Override + public CompletableFuture expireAt( + @NonNull String key, long unixSeconds, @NonNull ExpireOptions expireOptions) { + String[] arguments = + ArrayUtils.addAll(new String[] {key, Long.toString(unixSeconds)}, expireOptions.toArgs()); + return commandManager.submitNewCommand(ExpireAt, arguments, this::handleBooleanResponse); + } + + @Override + public CompletableFuture pexpire(@NonNull String key, long milliseconds) { + return commandManager.submitNewCommand( + PExpire, new String[] {key, Long.toString(milliseconds)}, this::handleBooleanResponse); + } + + @Override + public CompletableFuture pexpire( + @NonNull String key, long milliseconds, @NonNull ExpireOptions expireOptions) { + String[] arguments = + ArrayUtils.addAll(new String[] {key, Long.toString(milliseconds)}, expireOptions.toArgs()); + return commandManager.submitNewCommand(PExpire, arguments, this::handleBooleanResponse); + } + + @Override + public CompletableFuture pexpireAt(@NonNull String key, long unixMilliseconds) { + return commandManager.submitNewCommand( + PExpireAt, + new String[] {key, Long.toString(unixMilliseconds)}, + this::handleBooleanResponse); + } + + @Override + public CompletableFuture pexpireAt( + @NonNull String key, long unixMilliseconds, @NonNull ExpireOptions expireOptions) { + String[] arguments = + ArrayUtils.addAll( + new String[] {key, Long.toString(unixMilliseconds)}, expireOptions.toArgs()); + return commandManager.submitNewCommand(PExpireAt, arguments, this::handleBooleanResponse); + } + + @Override + public CompletableFuture ttl(@NonNull String key) { + return commandManager.submitNewCommand(TTL, new String[] {key}, this::handleLongResponse); + } } diff --git a/java/client/src/main/java/glide/api/RedisClient.java b/java/client/src/main/java/glide/api/RedisClient.java index 5b4d28ab4a..5d106bf131 100644 --- a/java/client/src/main/java/glide/api/RedisClient.java +++ b/java/client/src/main/java/glide/api/RedisClient.java @@ -1,6 +1,10 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api; +import static redis_request.RedisRequestOuterClass.RequestType.ClientGetName; +import static redis_request.RedisRequestOuterClass.RequestType.ClientId; +import static redis_request.RedisRequestOuterClass.RequestType.ConfigResetStat; +import static redis_request.RedisRequestOuterClass.RequestType.ConfigRewrite; import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; import static redis_request.RedisRequestOuterClass.RequestType.Info; import static redis_request.RedisRequestOuterClass.RequestType.Ping; @@ -45,7 +49,7 @@ public CompletableFuture customCommand(@NonNull String[] args) { } @Override - public CompletableFuture exec(Transaction transaction) { + public CompletableFuture exec(@NonNull Transaction transaction) { return commandManager.submitNewCommand(transaction, this::handleArrayOrNullResponse); } @@ -75,4 +79,27 @@ public CompletableFuture select(long index) { return commandManager.submitNewCommand( Select, new String[] {Long.toString(index)}, this::handleStringResponse); } + + @Override + public CompletableFuture clientId() { + return commandManager.submitNewCommand(ClientId, new String[0], this::handleLongResponse); + } + + @Override + public CompletableFuture clientGetName() { + return commandManager.submitNewCommand( + ClientGetName, new String[0], this::handleStringOrNullResponse); + } + + @Override + public CompletableFuture configRewrite() { + return commandManager.submitNewCommand( + ConfigRewrite, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture configResetStat() { + return commandManager.submitNewCommand( + ConfigResetStat, new String[0], this::handleStringResponse); + } } diff --git a/java/client/src/main/java/glide/api/RedisClusterClient.java b/java/client/src/main/java/glide/api/RedisClusterClient.java index 719fd07451..6947555ecc 100644 --- a/java/client/src/main/java/glide/api/RedisClusterClient.java +++ b/java/client/src/main/java/glide/api/RedisClusterClient.java @@ -1,6 +1,10 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api; +import static redis_request.RedisRequestOuterClass.RequestType.ClientGetName; +import static redis_request.RedisRequestOuterClass.RequestType.ClientId; +import static redis_request.RedisRequestOuterClass.RequestType.ConfigResetStat; +import static redis_request.RedisRequestOuterClass.RequestType.ConfigRewrite; import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; import static redis_request.RedisRequestOuterClass.RequestType.Info; import static redis_request.RedisRequestOuterClass.RequestType.Ping; @@ -53,7 +57,8 @@ public CompletableFuture> customCommand(@NonNull String[] a } @Override - public CompletableFuture> customCommand(String[] args, Route route) { + public CompletableFuture> customCommand( + @NonNull String[] args, @NonNull Route route) { return commandManager.submitNewCommand( CustomCommand, args, route, response -> handleCustomCommandResponse(route, response)); } @@ -69,14 +74,14 @@ protected ClusterValue handleCustomCommandResponse(Route route, Response } @Override - public CompletableFuture exec(ClusterTransaction transaction) { + public CompletableFuture exec(@NonNull ClusterTransaction transaction) { return commandManager.submitNewCommand( transaction, Optional.empty(), this::handleArrayOrNullResponse); } @Override public CompletableFuture[]> exec( - ClusterTransaction transaction, Route route) { + @NonNull ClusterTransaction transaction, Route route) { return commandManager .submitNewCommand(transaction, Optional.ofNullable(route), this::handleArrayOrNullResponse) .thenApply( @@ -143,4 +148,63 @@ public CompletableFuture> info( ? ClusterValue.of(handleStringResponse(response)) : ClusterValue.of(handleMapResponse(response))); } + + @Override + public CompletableFuture clientId() { + return commandManager.submitNewCommand(ClientId, new String[0], this::handleLongResponse); + } + + @Override + public CompletableFuture> clientId(@NonNull Route route) { + return commandManager.submitNewCommand( + ClientId, + new String[0], + route, + response -> + route.isSingleNodeRoute() + ? ClusterValue.of(handleLongResponse(response)) + : ClusterValue.of(handleMapResponse(response))); + } + + @Override + public CompletableFuture clientGetName() { + return commandManager.submitNewCommand( + ClientGetName, new String[0], this::handleStringOrNullResponse); + } + + @Override + public CompletableFuture> clientGetName(@NonNull Route route) { + return commandManager.submitNewCommand( + ClientGetName, + new String[0], + route, + response -> + route.isSingleNodeRoute() + ? ClusterValue.of(handleStringOrNullResponse(response)) + : ClusterValue.of(handleMapResponse(response))); + } + + @Override + public CompletableFuture configRewrite() { + return commandManager.submitNewCommand( + ConfigRewrite, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture configRewrite(@NonNull Route route) { + return commandManager.submitNewCommand( + ConfigRewrite, new String[0], route, this::handleStringResponse); + } + + @Override + public CompletableFuture configResetStat() { + return commandManager.submitNewCommand( + ConfigResetStat, new String[0], this::handleStringResponse); + } + + @Override + public CompletableFuture configResetStat(@NonNull Route route) { + return commandManager.submitNewCommand( + ConfigResetStat, new String[0], route, this::handleStringResponse); + } } diff --git a/java/client/src/main/java/glide/api/commands/ConnectionManagementClusterCommands.java b/java/client/src/main/java/glide/api/commands/ConnectionManagementClusterCommands.java index 5620052dfe..49774f0c09 100644 --- a/java/client/src/main/java/glide/api/commands/ConnectionManagementClusterCommands.java +++ b/java/client/src/main/java/glide/api/commands/ConnectionManagementClusterCommands.java @@ -1,13 +1,14 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.commands; +import glide.api.models.ClusterValue; import glide.api.models.configuration.RequestRoutingConfiguration.Route; import java.util.concurrent.CompletableFuture; /** - * Connection Management Commands interface. + * Connection Management Commands interface for cluster client. * - * @see: Connection Management Commands + * @see Connection Management Commands */ public interface ConnectionManagementClusterCommands { @@ -48,4 +49,74 @@ public interface ConnectionManagementClusterCommands { * @return String with a copy of the argument message. */ CompletableFuture ping(String message, Route route); + + /** + * Gets the current connection id.
+ * The command will be routed to a random node. + * + * @see redis.io for details. + * @return The id of the client. + * @example + *
{@code
+     * long id = client.clientId().get();
+     * assert id > 0
+     * }
+ */ + CompletableFuture clientId(); + + /** + * Gets the current connection id. + * + * @see redis.io for details. + * @param route Routing configuration for the command. Client will route the command to the nodes + * defined. + * @return A {@link ClusterValue} which holds a single value if single node route is used or a + * dictionary where each address is the key and its corresponding node response is the value. + * The value is the id of the client on that node. + * @example + *
{@code
+     * long id = client.clientId(new SlotIdRoute(...)).get().getSingleValue();
+     * assert id > 0;
+     *
+     * Map idPerNode = client.clientId(ALL_NODES).get().getMultiValue();
+     * assert idPerNode.get("") > 0;
+     * 
+ */ + CompletableFuture> clientId(Route route); + + /** + * Gets the name of the current connection.
+ * The command will be routed a random node. + * + * @see redis.io for details. + * @return The name of the client connection as a string if a name is set, or null if + * no name is assigned. + * @example + *
{@code
+     * String clientName = client.clientGetName().get();
+     * assert clientName != null;
+     * }
+ */ + CompletableFuture clientGetName(); + + /** + * Gets the name of the current connection. + * + * @see redis.io for details. + * @param route Routing configuration for the command. Client will route the command to the nodes + * defined. + * @return A {@link ClusterValue} which holds a single value if single node route is used or a + * dictionary where each address is the key and its corresponding node response is the value. + * The value is the name of the client connection as a string if a name is set, or null if no + * name is assigned. + * @example + *
{@code
+     * String clientName = client.clientGetName(new SlotIdRoute(...)).get().getSingleValue();
+     * assert clientName != null;
+     *
+     * Map clientNamePerNode = client.clientGetName(ALL_NODES).get().getMultiValue();
+     * assert clientNamePerNode.get("") != null
+     * }
+ */ + CompletableFuture> clientGetName(Route route); } diff --git a/java/client/src/main/java/glide/api/commands/ConnectionManagementCommands.java b/java/client/src/main/java/glide/api/commands/ConnectionManagementCommands.java index a38cff926e..f1840921c6 100644 --- a/java/client/src/main/java/glide/api/commands/ConnectionManagementCommands.java +++ b/java/client/src/main/java/glide/api/commands/ConnectionManagementCommands.java @@ -4,9 +4,9 @@ import java.util.concurrent.CompletableFuture; /** - * Connection Management Commands interface. + * Connection Management Commands interface for standalone client. * - * @see: Connection Management Commands + * @see Connection Management Commands */ public interface ConnectionManagementCommands { @@ -26,4 +26,31 @@ public interface ConnectionManagementCommands { * @return String with a copy of the argument message. */ CompletableFuture ping(String message); + + /** + * Gets the current connection id. + * + * @see redis.io for details. + * @return The id of the client. + * @example + *
{@code
+     * Long id = client.clientId().get();
+     * assert id > 0;
+     * }
+ */ + CompletableFuture clientId(); + + /** + * Gets the name of the current connection. + * + * @see redis.io for details. + * @return The name of the client connection as a string if a name is set, or null if + * no name is assigned. + * @example + *
{@code
+     * String clientName = client.clientGetName().get();
+     * assert clientName != null;
+     * }
+ */ + CompletableFuture clientGetName(); } diff --git a/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java b/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java index 13a5b05353..d8c29319aa 100644 --- a/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/GenericBaseCommands.java @@ -1,11 +1,11 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.commands; +import glide.api.models.commands.ExpireOptions; import java.util.concurrent.CompletableFuture; /** - * Generic Commands interface to handle generic commands for all server requests for both standalone - * and cluster clients. + * Generic Commands interface to handle generic commands for all server requests. * * @see Generic Commands */ @@ -18,6 +18,11 @@ public interface GenericBaseCommands { * @see redis.io for details. * @param keys The keys we wanted to remove. * @return The number of keys that were removed. + * @example + *
{@code
+     * Long num = client.del(new String[] {"key1", "key2"}).get();
+     * assert num == 2l;
+     * }
*/ CompletableFuture del(String[] keys); @@ -29,10 +34,10 @@ public interface GenericBaseCommands { * @return The number of keys that exist. If the same existing key is mentioned in keys * multiple times, it will be counted multiple times. * @example - *

- * long result = client.exists(new String[] {"my_key", "invalid_key"}).get(); + *

{@code
+     * Long result = client.exists(new String[] {"my_key", "invalid_key"}).get();
      * assert result == 1L;
-     * 
+     * }
*/ CompletableFuture exists(String[] keys); @@ -46,11 +51,221 @@ public interface GenericBaseCommands { * @param keys The list of keys to unlink. * @return The number of keys that were unlinked. * @example - *

- *

-     * long result = client.unlink("my_key").get();
+     *     
{@code
+     * Long result = client.unlink("my_key").get();
      * assert result == 1L;
-     * 
+ * }
*/ CompletableFuture unlink(String[] keys); + + /** + * Sets a timeout on key in seconds. After the timeout has expired, the key + * will automatically be deleted.
+ * If key already has an existing expire + * set, the time to live is updated to the new value.
+ * If seconds is a non-positive number, the key will be deleted rather + * than expired.
+ * The timeout will only be cleared by commands that delete or overwrite the contents of key + * . + * + * @see redis.io for details. + * @param key The key to set timeout on it. + * @param seconds The timeout in seconds. + * @return true if the timeout was set. false if the timeout was not + * set. e.g. key doesn't exist. + * @example + *
{@code
+     * Boolean isSet = client.expire("my_key", 60).get();
+     * assert isSet; //Indicates that a timeout of 60 seconds has been set for "my_key."
+     * }
+ */ + CompletableFuture expire(String key, long seconds); + + /** + * Sets a timeout on key in seconds. After the timeout has expired, the key + * will automatically be deleted.
+ * If key already has an existing expire + * set, the time to live is updated to the new value.
+ * If seconds is a non-positive number, the key will be deleted rather + * than expired.
+ * The timeout will only be cleared by commands that delete or overwrite the contents of key + * . + * + * @see redis.io for details. + * @param key The key to set timeout on it. + * @param seconds The timeout in seconds. + * @param expireOptions The expire options. + * @return true if the timeout was set. false if the timeout was not + * set. e.g. key doesn't exist, or operation skipped due to the provided + * arguments. + * @example + *
{@code
+     * Boolean isSet = client.expire("my_key", 60, ExpireOptions.HAS_NO_EXPIRY).get();
+     * assert isSet; //Indicates that a timeout of 60 seconds has been set for "my_key."
+     * }
+ */ + CompletableFuture expire(String key, long seconds, ExpireOptions expireOptions); + + /** + * Sets a timeout on key. It takes an absolute Unix timestamp (seconds since January + * 1, 1970) instead of specifying the number of seconds.
+ * A timestamp in the past will delete the key immediately. After the timeout has + * expired, the key will automatically be deleted.
+ * If key already has an existing expire set, the time to live is + * updated to the new value.
+ * The timeout will only be cleared by commands that delete or overwrite the contents of key + * . + * + * @see redis.io for details. + * @param key The key to set timeout on it. + * @param unixSeconds The timeout in an absolute Unix timestamp. + * @return true if the timeout was set. false if the timeout was not + * set. e.g. key doesn't exist. + * @example + *
{@code
+     * Boolean isSet = client.expireAt("my_key", Instant.now().getEpochSecond() + 10).get();
+     * assert isSet;
+     * }
+ */ + CompletableFuture expireAt(String key, long unixSeconds); + + /** + * Sets a timeout on key. It takes an absolute Unix timestamp (seconds since January + * 1, 1970) instead of specifying the number of seconds.
+ * A timestamp in the past will delete the key immediately. After the timeout has + * expired, the key will automatically be deleted.
+ * If key already has an existing expire set, the time to live is + * updated to the new value.
+ * The timeout will only be cleared by commands that delete or overwrite the contents of key + * . + * + * @see redis.io for details. + * @param key The key to set timeout on it. + * @param unixSeconds The timeout in an absolute Unix timestamp. + * @param expireOptions The expire options. + * @return true if the timeout was set. false if the timeout was not + * set. e.g. key doesn't exist, or operation skipped due to the provided + * arguments. + * @example + *
{@code
+     * Boolean isSet = client.expireAt("my_key", Instant.now().getEpochSecond() + 10, ExpireOptions.HasNoExpiry).get();
+     * assert isSet;
+     * }
+ */ + CompletableFuture expireAt(String key, long unixSeconds, ExpireOptions expireOptions); + + /** + * Sets a timeout on key in milliseconds. After the timeout has expired, the + * key will automatically be deleted.
+ * If key already has an existing + * expire set, the time to live is updated to the new value.
+ * If milliseconds is a non-positive number, the key will be deleted + * rather than expired.
+ * The timeout will only be cleared by commands that delete or overwrite the contents of key + * . + * + * @see redis.io for details. + * @param key The key to set timeout on it. + * @param milliseconds The timeout in milliseconds. + * @return true if the timeout was set. false if the timeout was not + * set. e.g. key doesn't exist. + * @example + *
{@code
+     * Boolean isSet = client.pexpire("my_key", 60000).get();
+     * assert isSet;
+     * }
+ */ + CompletableFuture pexpire(String key, long milliseconds); + + /** + * Sets a timeout on key in milliseconds. After the timeout has expired, the + * key will automatically be deleted.
+ * If key already has an existing expire set, the time to live is updated to the new + * value.
+ * If milliseconds is a non-positive number, the key will be deleted + * rather than expired.
+ * The timeout will only be cleared by commands that delete or overwrite the contents of key + * . + * + * @see redis.io for details. + * @param key The key to set timeout on it. + * @param milliseconds The timeout in milliseconds. + * @param expireOptions The expire options. + * @return true if the timeout was set. false if the timeout was not + * set. e.g. key doesn't exist, or operation skipped due to the provided + * arguments. + * @example + *
{@code
+     * Boolean isSet = client.pexpire("my_key", 60000, ExpireOptions.HasNoExpiry).get();
+     * assert isSet;
+     * }
+ */ + CompletableFuture pexpire(String key, long milliseconds, ExpireOptions expireOptions); + + /** + * Sets a timeout on key. It takes an absolute Unix timestamp (milliseconds since + * January 1, 1970) instead of specifying the number of milliseconds.
+ * A timestamp in the past will delete the key immediately. After the timeout has + * expired, the key will automatically be deleted.
+ * If key already has an existing expire set, the time to live is + * updated to the new value.
+ * The timeout will only be cleared by commands that delete or overwrite the contents of key + * . + * + * @see redis.io for details. + * @param key The key to set timeout on it. + * @param unixMilliseconds The timeout in an absolute Unix timestamp. + * @return true if the timeout was set. false if the timeout was not + * set. e.g. key doesn't exist. + * @example + *
{@code
+     * Boolean isSet = client.pexpireAt("my_key", Instant.now().toEpochMilli() + 10).get();
+     * assert isSet;
+     * }
+ */ + CompletableFuture pexpireAt(String key, long unixMilliseconds); + + /** + * Sets a timeout on key. It takes an absolute Unix timestamp (milliseconds since + * January 1, 1970) instead of specifying the number of milliseconds.
+ * A timestamp in the past will delete the key immediately. After the timeout has + * expired, the key will automatically be deleted.
+ * If key already has an existing expire set, the time to live is + * updated to the new value.
+ * The timeout will only be cleared by commands that delete or overwrite the contents of key + * . + * + * @see redis.io for details. + * @param key The key to set timeout on it. + * @param unixMilliseconds The timeout in an absolute Unix timestamp. + * @param expireOptions The expire option. + * @return true if the timeout was set. false if the timeout was not + * set. e.g. key doesn't exist, or operation skipped due to the provided + * arguments. + * @example + *
{@code
+     * Boolean isSet = client.pexpireAt("my_key", Instant.now().toEpochMilli() + 10, ExpireOptions.HasNoExpiry).get();
+     * assert isSet;
+     * }
+ */ + CompletableFuture pexpireAt( + String key, long unixMilliseconds, ExpireOptions expireOptions); + + /** + * Returns the remaining time to live of key that has a timeout. + * + * @see redis.io for details. + * @param key The key to return its timeout. + * @return TTL in seconds, -2 if key does not exist, or -1 + * if key exists but has no associated expire. + * @example + *
{@code
+     * Long timeRemaining = client.ttl("my_key").get()
+     * assert timeRemaining == 3600L //Indicates that "my_key" has a remaining time to live of 3600 seconds.
+     *
+     * Long timeRemaining = client.ttl("nonexistent_key").get();
+     * assert timeRemaining == -2L; //Returns -2 for a non-existing key.
+     * }
+ */ + CompletableFuture ttl(String key); } diff --git a/java/client/src/main/java/glide/api/commands/GenericClusterCommands.java b/java/client/src/main/java/glide/api/commands/GenericClusterCommands.java index 1fbca154cc..95e56335d6 100644 --- a/java/client/src/main/java/glide/api/commands/GenericClusterCommands.java +++ b/java/client/src/main/java/glide/api/commands/GenericClusterCommands.java @@ -26,12 +26,13 @@ public interface GenericClusterCommands { * response (such as XREAD), or that change the client's behavior (such as entering * pub/sub mode on RESP2 connections) shouldn't be called using * this function. - * @example Returns a list of all pub/sub clients: - *

- * Object result = client.customCommand(new String[]{ "CLIENT", "LIST", "TYPE", "PUBSUB" }).get(); - * * @param args Arguments for the custom command including the command name. * @return Response from Redis containing an Object. + * @example + *

{@code
+     * ClusterValue data = client.customCommand(new String[] {"ping"}).get();
+     * assert ((String) data.getSingleValue()).equals("PONG");
+     * }
      */
     CompletableFuture> customCommand(String[] args);
 
@@ -46,13 +47,16 @@ public interface GenericClusterCommands {
      *     response (such as XREAD), or that change the client's behavior (such as entering
      *     pub/sub mode on RESP2 connections) shouldn't be called using
      *     this function.
-     * @example Returns a list of all pub/sub clients:
-     *     

- * Object result = client.customCommand(new String[]{ "CLIENT", "LIST", "TYPE", "PUBSUB" }, RANDOM).get(); - * * @param args Arguments for the custom command including the command name * @param route Routing configuration for the command * @return Response from Redis containing an Object. + * @example + *

{@code
+     * ClusterValue result = clusterClient.customCommand(new String[]{ "CONFIG", "GET", "maxmemory"}, ALL_NODES).get();
+     * Map payload = result.getMultiValue();
+     * assert ((String) payload.get("node1")).equals("1GB");
+     * assert ((String) payload.get("node2")).equals("100MB");
+     * }
      */
     CompletableFuture> customCommand(String[] args, Route route);
 
@@ -73,6 +77,13 @@ public interface GenericClusterCommands {
      *       
  • If the transaction failed due to a WATCH command, exec will * return null. * + * + * @example + *
    {@code
    +     * ClusterTransaction transaction = new ClusterTransaction().customCommand(new String[] {"info"});
    +     * Object[] result = clusterClient.exec(transaction).get();
    +     * assert ((String) result[0]).contains("# Stats");
    +     * }
    */ CompletableFuture exec(ClusterTransaction transaction); @@ -92,6 +103,14 @@ public interface GenericClusterCommands { *
  • If the transaction failed due to a WATCH command, exec will * return null. * + * + * @example + *
    {@code
    +     * ClusterTransaction transaction = new ClusterTransaction().ping().info();
    +     * ClusterValue[] result = clusterClient.exec(transaction, RANDOM).get();
    +     * assert ((String) result[0].getSingleValue()).equals("PONG");
    +     * assert ((String) result[1].getSingleValue()).contains("# Stats");
    +     * }
          */
         CompletableFuture[]> exec(ClusterTransaction transaction, Route route);
     }
    diff --git a/java/client/src/main/java/glide/api/commands/GenericCommands.java b/java/client/src/main/java/glide/api/commands/GenericCommands.java
    index 126188457f..d76a7f4f7b 100644
    --- a/java/client/src/main/java/glide/api/commands/GenericCommands.java
    +++ b/java/client/src/main/java/glide/api/commands/GenericCommands.java
    @@ -28,6 +28,11 @@ public interface GenericCommands {
          *
          * @param args Arguments for the custom command.
          * @return Response from Redis containing an Object.
    +     * @example
    +     *     
    {@code
    +     * Object response = (String) client.customCommand(new String[] {"ping", "GLIDE"}).get()
    +     * assert ((String) response).equals("GLIDE");
    +     * }
    */ CompletableFuture customCommand(String[] args); @@ -45,6 +50,13 @@ public interface GenericCommands { *
  • If the transaction failed due to a WATCH command, exec will * return null. * + * + * @example + *
    {@code
    +     * Transaction transaction = new Transaction().customCommand(new String[] {"info"});
    +     * Object[] result = client.exec(transaction).get();
    +     * assert ((String) result[0]).contains("# Stats");
    +     * }
    */ CompletableFuture exec(Transaction transaction); } diff --git a/java/client/src/main/java/glide/api/commands/HashCommands.java b/java/client/src/main/java/glide/api/commands/HashCommands.java index 5ffe906882..35d1bbc9ac 100644 --- a/java/client/src/main/java/glide/api/commands/HashCommands.java +++ b/java/client/src/main/java/glide/api/commands/HashCommands.java @@ -19,6 +19,14 @@ public interface HashCommands { * @param field The field in the hash stored at key to retrieve from the database. * @return The value associated with field, or null when field * is not present in the hash or key does not exist. + * @example + *
    {@code
    +     * String payload = client.hget("my_hash", "field1").get();
    +     * assert payload.equals("value");
    +     *
    +     * String payload = client.hget("my_hash", "nonexistent_field").get();
    +     * assert payload.equals(null);
    +     * }
    */ CompletableFuture hget(String key, String field); @@ -30,6 +38,11 @@ public interface HashCommands { * @param fieldValueMap A field-value map consisting of fields and their corresponding values to * be set in the hash stored at the specified key. * @return The number of fields that were added. + * @example + *
    {@code
    +     * Long num = client.hset("my_hash", Map.of("field", "value", "field2", "value2")).get();
    +     * assert num == 2L;
    +     * }
    */ CompletableFuture hset(String key, Map fieldValueMap); @@ -43,6 +56,11 @@ public interface HashCommands { * @return The number of fields that were removed from the hash, not including specified but * non-existing fields.
    * If key does not exist, it is treated as an empty hash and it returns 0.
    + * @example + *
    {@code
    +     * Long num = client.hdel("my_hash", new String[] {}).get("field1", "field2");
    +     * assert num == 2L; //Indicates that two fields were successfully removed from the hash.
    +     * }
    */ CompletableFuture hdel(String key, String[] fields); @@ -58,10 +76,10 @@ public interface HashCommands { * If key does not exist, it is treated as an empty hash, and it returns an array * of null values.
    * @example - *
    +     *     
    {@code
          * String[] values = client.hmget("my_hash", new String[] {"field1", "field2"}).get()
    -     * assert values == new String[] {"value1", "value2"}
    -     * 
    + * assert values.equals(new String[] {"value1", "value2"}); + * }
    */ CompletableFuture hmget(String key, String[] fields); @@ -74,12 +92,13 @@ public interface HashCommands { * @return True if the hash contains the specified field. If the hash does not * contain the field, or if the key does not exist, it returns False. * @example - *
    -     * Boolean exists = client.hexists("my_hash", "field1").get()
    -     * assert exists
    -     * Boolean exists = client.hexists("my_hash", "non_existent_field").get()
    -     * assert !exists
    -     * 
    + *
    {@code
    +     * Boolean exists = client.hexists("my_hash", "field1").get();
    +     * assert exists;
    +     *
    +     * Boolean exists = client.hexists("my_hash", "non_existent_field").get();
    +     * assert !exists;
    +     * }
    */ CompletableFuture hexists(String key, String field); @@ -92,10 +111,10 @@ public interface HashCommands { * the map is associated with its corresponding value.
    * If key does not exist, it returns an empty map. * @example - *
    -     * Map fieldValueMap = client.hgetall("my_hash").get()
    -     * assert fieldValueMap.equals(Map.of(field1", "value1", "field2", "value2"))
    -     * 
    + *
    {@code
    +     * Map fieldValueMap = client.hgetall("my_hash").get();
    +     * assert fieldValueMap.equals(Map.of(field1", "value1", "field2", "value2"));
    +     * }
    */ CompletableFuture> hgetall(String key); @@ -114,10 +133,10 @@ public interface HashCommands { * @return The value of field in the hash stored at key after the * increment or decrement. * @example - *
    -     * Long num = client.hincrBy("my_hash", "field1", 5).get()
    -     * assert num == 5L
    -     * 
    + *
    {@code
    +     * Long num = client.hincrBy("my_hash", "field1", 5).get();
    +     * assert num == 5L;
    +     * }
    */ CompletableFuture hincrBy(String key, String field, long amount); @@ -137,10 +156,10 @@ public interface HashCommands { * @returns The value of field in the hash stored at key after the * increment or decrement. * @example - *
    -     * Double num = client.hincrByFloat("my_hash", "field1", 2.5).get()
    -     * assert num == 2.5
    -     * 
    + *
    {@code
    +     * Double num = client.hincrByFloat("my_hash", "field1", 2.5).get();
    +     * assert num == 2.5;
    +     * }
    */ CompletableFuture hincrByFloat(String key, String field, double amount); } diff --git a/java/client/src/main/java/glide/api/commands/ListBaseCommands.java b/java/client/src/main/java/glide/api/commands/ListBaseCommands.java index 989ec73785..053f0d6697 100644 --- a/java/client/src/main/java/glide/api/commands/ListBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/ListBaseCommands.java @@ -67,6 +67,76 @@ public interface ListBaseCommands { */ CompletableFuture lpopCount(String key, long count); + /** + * Returns the specified elements of the list stored at key.
    + * The offsets start and end are zero-based indexes, with 0 being the + * first element of the list, 1 being the next element and so on. These offsets can also be + * negative numbers indicating offsets starting at the end of the list, with -1 being the last + * element of the list, -2 being the penultimate, and so on. + * + * @see redis.io for details. + * @param key The key of the list. + * @param start The starting point of the range. + * @param end The end of the range. + * @return Array of elements in the specified range.
    + * If start exceeds the end of the list, or if start is greater than + * end, an empty array will be returned.
    + * If end exceeds the actual end of the list, the range will stop at the actual + * end of the list.
    + * If key does not exist an empty array will be returned.
    + * @example + *
    +     * String[] payload = lient.lrange("my_list", 0, 2).get()
    +     * assert payload.equals(new String[] {"value1", "value2", "value3"})
    +     * String[] payload = client.lrange("my_list", -2, -1).get()
    +     * assert payload.equals(new String[] {"value2", "value3"})
    +     * String[] payload = client.lrange("non_exiting_key", 0, 2).get()
    +     * assert payload.equals(new String[] {})
    +     * 
    + */ + CompletableFuture lrange(String key, long start, long end); + + /** + * Trims an existing list so that it will contain only the specified range of elements specified. + *
    + * The offsets start and end are zero-based indexes, with 0 being the + * first element of the list, 1 being the next element and so on.
    + * These offsets can also be negative numbers indicating offsets starting at the end of the list, + * with -1 being the last element of the list, -2 being the penultimate, and so on. + * + * @see redis.io for details. + * @param key The key of the list. + * @param start The starting point of the range. + * @param end The end of the range. + * @return Always OK.
    + * If start exceeds the end of the list, or if start is greater than + * end, the result will be an empty list (which causes key to be removed).
    + * If end exceeds the actual end of the list, it will be treated like the last + * element of the list.
    + * If key does not exist, OK will be returned without changes to the database. + * @example + *
    +     * String payload = client.ltrim("my_list", 0, 1).get()
    +     * assert payload.equals("OK")
    +     * 
    + */ + CompletableFuture ltrim(String key, long start, long end); + + /** + * Returns the length of the list stored at key. + * + * @see redis.io for details. + * @param key The key of the list. + * @return The length of the list at key.
    + * If key does not exist, it is interpreted as an empty list and 0 is returned. + * @example + *
    +     * Long lenList = client.llen("my_list").get()
    +     * assert lenList == 3L //Indicates that there are 3 elements in the list.
    +     * 
    + */ + CompletableFuture llen(String key); + /** * Inserts all the specified values at the tail of the list stored at key.
    * elements are inserted one after the other to the tail of the list, from the diff --git a/java/client/src/main/java/glide/api/commands/ServerManagementClusterCommands.java b/java/client/src/main/java/glide/api/commands/ServerManagementClusterCommands.java index f511cc81fb..33d3c3a4ed 100644 --- a/java/client/src/main/java/glide/api/commands/ServerManagementClusterCommands.java +++ b/java/client/src/main/java/glide/api/commands/ServerManagementClusterCommands.java @@ -8,7 +8,7 @@ import java.util.concurrent.CompletableFuture; /** - * Server Management Commands interface. + * Server Management Commands interface for cluster client. * * @see Server Management Commands */ @@ -71,4 +71,68 @@ public interface ServerManagementClusterCommands { * value is the information of the sections requested for the node. */ CompletableFuture> info(InfoOptions options, Route route); + + /** + * Rewrites the configuration file with the current configuration.
    + * The command will be routed automatically to all nodes. + * + * @see redis.io for details. + * @return OK when the configuration was rewritten properly, otherwise an error is + * thrown. + * @example + *
    {@code
    +     * String response = client.configRewrite().get();
    +     * assert response.equals("OK")
    +     * }
    + */ + CompletableFuture configRewrite(); + + /** + * Rewrites the configuration file with the current configuration. + * + * @see redis.io for details. + * @param route Routing configuration for the command. Client will route the command to the nodes + * defined. + * @return OK when the configuration was rewritten properly, otherwise an error is + * thrown. + * @example + *
    {@code
    +     * String response = client.configRewrite(ALL_PRIMARIES).get();
    +     * assert response.equals("OK")
    +     * }
    + */ + CompletableFuture configRewrite(Route route); + + /** + * Resets the statistics reported by Redis using the INFO and LATENCY HISTOGRAM commands.
    + * The command will be routed automatically to all nodes. + * + * @see redis.io for details. + * @return OK to confirm that the statistics were successfully reset. + * @example + *
    {@code
    +     * String response = client.configResetStat().get();
    +     * assert response.equals("OK")
    +     * }
    + */ + CompletableFuture configResetStat(); + + /** + * Resets the statistics reported by Redis using the INFO and LATENCY HISTOGRAM commands. + * + * @see redis.io for details. + * @param route Routing configuration for the command. Client will route the command to the nodes + * defined. + * @return OK to confirm that the statistics were successfully reset. + * @example + *
    {@code
    +     * String response = client.configResetStat(ALL_PRIMARIES).get();
    +     * assert response.equals("OK")
    +     * }
    + */ + CompletableFuture configResetStat(Route route); } diff --git a/java/client/src/main/java/glide/api/commands/ServerManagementCommands.java b/java/client/src/main/java/glide/api/commands/ServerManagementCommands.java index 5dd94f93e9..3f5cc514b5 100644 --- a/java/client/src/main/java/glide/api/commands/ServerManagementCommands.java +++ b/java/client/src/main/java/glide/api/commands/ServerManagementCommands.java @@ -6,7 +6,7 @@ import java.util.concurrent.CompletableFuture; /** - * Server Management Commands interface. + * Server Management Commands interface for standalone client. * * @see Server Management Commands */ @@ -40,4 +40,33 @@ public interface ServerManagementCommands { * @return A simple OK response. */ CompletableFuture select(long index); + + /** + * Rewrites the configuration file with the current configuration. + * + * @see redis.io for details. + * @return OK when the configuration was rewritten properly, otherwise an error is + * thrown. + * @example + *
    {@code
    +     * String response = client.configRewrite().get();
    +     * assert response.equals("OK");
    +     * }
    + */ + CompletableFuture configRewrite(); + + /** + * Resets the statistics reported by Redis using the INFO and LATENCY HISTOGRAM commands. + * + * @see redis.io for details. + * @return OK to confirm that the statistics were successfully reset. + * @example + *
    {@code
    +     * String response = client.configResetStat().get();
    +     * assert response.equals("OK");
    +     * }
    + */ + CompletableFuture configResetStat(); } diff --git a/java/client/src/main/java/glide/api/commands/SetCommands.java b/java/client/src/main/java/glide/api/commands/SetCommands.java index f2098c9b08..c5463ca18c 100644 --- a/java/client/src/main/java/glide/api/commands/SetCommands.java +++ b/java/client/src/main/java/glide/api/commands/SetCommands.java @@ -21,10 +21,10 @@ public interface SetCommands { * @remarks If key does not exist, a new set is created before adding members * . * @example - *

    - * int result = client.sadd("my_set", new String[]{"member1", "member2"}).get(); - * // result: 2 - * + *

    {@code
    +     * Long result = client.sadd("my_set", new String[]{"member1", "member2"}).get();
    +     * assert result == 2L;
    +     * }
    */ CompletableFuture sadd(String key, String[] members); @@ -39,10 +39,10 @@ public interface SetCommands { * @remarks If key does not exist, it is treated as an empty set and this command * returns 0. * @example - *

    - * int result = client.srem("my_set", new String[]{"member1", "member2"}).get(); - * // result: 2 - * + *

    {@code
    +     * Long result = client.srem("my_set", new String[]{"member1", "member2"}).get();
    +     * assert result == 2L;
    +     * }
    */ CompletableFuture srem(String key, String[] members); @@ -54,10 +54,10 @@ public interface SetCommands { * @return A Set of all members of the set. * @remarks If key does not exist an empty set will be returned. * @example - *

    - * {@literal Set} result = client.smembers("my_set").get(); - * // result: {"member1", "member2", "member3"} - * + *

    {@code
    +     * Set result = client.smembers("my_set").get();
    +     * assert result.equals(Set.of("member1", "member2", "member3"));
    +     * }
    */ CompletableFuture> smembers(String key); @@ -68,10 +68,10 @@ public interface SetCommands { * @param key The key from which to retrieve the number of set members. * @return The cardinality (number of elements) of the set, or 0 if the key does not exist. * @example - *

    - * int result = client.scard("my_set").get(); - * // result: 3 - * + *

    {@code
    +     * Long result = client.scard("my_set").get();
    +     * assert result == 3L;
    +     * }
    */ CompletableFuture scard(String key); } diff --git a/java/client/src/main/java/glide/api/commands/StringCommands.java b/java/client/src/main/java/glide/api/commands/StringCommands.java index 97bb98fa31..7431927947 100644 --- a/java/client/src/main/java/glide/api/commands/StringCommands.java +++ b/java/client/src/main/java/glide/api/commands/StringCommands.java @@ -22,6 +22,14 @@ public interface StringCommands { * @param key The key to retrieve from the database. * @return Response from Redis. If key exists, returns the value of * key as a String. Otherwise, return null. + * @example + *
    {@code
    +     * String payload = client.get("key").get();
    +     * assert payload.equals("value");
    +     *
    +     * String payload = client.get("non_existing_key").get();
    +     * assert payload.equals(null);
    +     * }
    */ CompletableFuture get(String key); @@ -32,6 +40,11 @@ public interface StringCommands { * @param key The key to store. * @param value The value to store with the given key. * @return Response from Redis containing "OK". + * @example + *
    {@code
    +     * String payload = client.set("key", "value").get();
    +     * assert payload.equals("OK");
    +     * }
    */ CompletableFuture set(String key, String value); @@ -47,6 +60,16 @@ public interface StringCommands { * {@link ConditionalSet#ONLY_IF_EXISTS} or {@link ConditionalSet#ONLY_IF_DOES_NOT_EXIST} * conditions, return null. If {@link SetOptionsBuilder#returnOldValue(boolean)} * is set, return the old value as a String. + * @example + *
    {@code
    +     * String payload =
    +     *         client.set("key", "value", SetOptions.builder()
    +     *                 .conditionalSet(ONLY_IF_EXISTS)
    +     *                 .expiry(SetOptions.Expiry.Seconds(5L))
    +     *                 .build())
    +     *                 .get();
    +     * assert payload.equals("OK");
    +     * }
    */ CompletableFuture set(String key, String value, SetOptions options); @@ -58,6 +81,11 @@ public interface StringCommands { * @return An array of values corresponding to the provided keys.
    * If a keyis not found, its corresponding value in the list will be null * . + * @example + *
    {@code
    +     * String payload = client.mget(new String[] {"key1", "key2"}).get();
    +     * assert payload.equals(new String[] {"value1", "value2"});
    +     * }
    */ CompletableFuture mget(String[] keys); @@ -67,6 +95,11 @@ public interface StringCommands { * @see redis.io for details. * @param keyValueMap A key-value map consisting of keys and their respective values to set. * @return Always OK. + * @example + *
    {@code
    +     * String payload = client.mset(Map.of("key1", "value1", "key2", "value2"}).get();
    +     * assert payload.equals("OK"));
    +     * }
    */ CompletableFuture mset(Map keyValueMap); @@ -77,6 +110,11 @@ public interface StringCommands { * @see redis.io for details. * @param key The key to increment its value. * @return The value of key after the increment. + * @example + *
    {@code
    +     * Long num = client.incr("key").get();
    +     * assert num == 5L;
    +     * }
    */ CompletableFuture incr(String key); @@ -88,6 +126,11 @@ public interface StringCommands { * @param key The key to increment its value. * @param amount The amount to increment. * @return The value of key after the increment. + * @example + *
    {@code
    +     * Long num = client.incrBy("key", 2).get();
    +     * assert num == 7L;
    +     * }
    */ CompletableFuture incrBy(String key, long amount); @@ -101,6 +144,11 @@ public interface StringCommands { * @param key The key to increment its value. * @param amount The amount to increment. * @return The value of key after the increment. + * @example + *
    {@code
    +     * Long num = client.incrByFloat("key", 0.5).get();
    +     * assert num == 7.5;
    +     * }
    */ CompletableFuture incrByFloat(String key, double amount); @@ -111,6 +159,11 @@ public interface StringCommands { * @see redis.io for details. * @param key The key to decrement its value. * @return The value of key after the decrement. + * @example + *
    {@code
    +     * Long num = client.decr("key").get();
    +     * assert num == 4L;
    +     * }
    */ CompletableFuture decr(String key); @@ -122,6 +175,11 @@ public interface StringCommands { * @param key The key to decrement its value. * @param amount The amount to decrement. * @return The value of key after the decrement. + * @example + *
    {@code
    +     * Long num = client.decrBy("key", 2).get();
    +     * assert num == 2L;
    +     * }
    */ CompletableFuture decrBy(String key, long amount); } diff --git a/java/client/src/main/java/glide/api/models/BaseTransaction.java b/java/client/src/main/java/glide/api/models/BaseTransaction.java index 7c18c3dec3..d51fcd1a16 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -2,11 +2,17 @@ package glide.api.models; import static glide.utils.ArrayTransformUtils.convertMapToArgArray; +import static redis_request.RedisRequestOuterClass.RequestType.ClientGetName; +import static redis_request.RedisRequestOuterClass.RequestType.ClientId; +import static redis_request.RedisRequestOuterClass.RequestType.ConfigResetStat; +import static redis_request.RedisRequestOuterClass.RequestType.ConfigRewrite; import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; import static redis_request.RedisRequestOuterClass.RequestType.Decr; import static redis_request.RedisRequestOuterClass.RequestType.DecrBy; import static redis_request.RedisRequestOuterClass.RequestType.Del; import static redis_request.RedisRequestOuterClass.RequestType.Exists; +import static redis_request.RedisRequestOuterClass.RequestType.Expire; +import static redis_request.RedisRequestOuterClass.RequestType.ExpireAt; import static redis_request.RedisRequestOuterClass.RequestType.GetString; import static redis_request.RedisRequestOuterClass.RequestType.HashDel; import static redis_request.RedisRequestOuterClass.RequestType.HashExists; @@ -20,10 +26,15 @@ import static redis_request.RedisRequestOuterClass.RequestType.IncrBy; import static redis_request.RedisRequestOuterClass.RequestType.IncrByFloat; import static redis_request.RedisRequestOuterClass.RequestType.Info; +import static redis_request.RedisRequestOuterClass.RequestType.LLen; import static redis_request.RedisRequestOuterClass.RequestType.LPop; import static redis_request.RedisRequestOuterClass.RequestType.LPush; +import static redis_request.RedisRequestOuterClass.RequestType.LRange; +import static redis_request.RedisRequestOuterClass.RequestType.LTrim; import static redis_request.RedisRequestOuterClass.RequestType.MGet; import static redis_request.RedisRequestOuterClass.RequestType.MSet; +import static redis_request.RedisRequestOuterClass.RequestType.PExpire; +import static redis_request.RedisRequestOuterClass.RequestType.PExpireAt; import static redis_request.RedisRequestOuterClass.RequestType.Ping; import static redis_request.RedisRequestOuterClass.RequestType.RPop; import static redis_request.RedisRequestOuterClass.RequestType.RPush; @@ -32,8 +43,10 @@ import static redis_request.RedisRequestOuterClass.RequestType.SMembers; import static redis_request.RedisRequestOuterClass.RequestType.SRem; import static redis_request.RedisRequestOuterClass.RequestType.SetString; +import static redis_request.RedisRequestOuterClass.RequestType.TTL; import static redis_request.RedisRequestOuterClass.RequestType.Unlink; +import glide.api.models.commands.ExpireOptions; import glide.api.models.commands.InfoOptions; import glide.api.models.commands.InfoOptions.Section; import glide.api.models.commands.SetOptions; @@ -108,7 +121,7 @@ public T ping() { * @param msg The ping argument that will be returned. * @return A response from Redis with a String. */ - public T ping(String msg) { + public T ping(@NonNull String msg) { ArgsArray commandArgs = buildArgs(msg); protobufTransaction.addCommands(buildCommand(Ping, commandArgs)); @@ -135,7 +148,7 @@ public T info() { * @return Response from Redis with a String containing the requested {@link * Section}s. */ - public T info(InfoOptions options) { + public T info(@NonNull InfoOptions options) { ArgsArray commandArgs = buildArgs(options.toArgs()); protobufTransaction.addCommands(buildCommand(Info, commandArgs)); @@ -150,7 +163,7 @@ public T info(InfoOptions options) { * @param keys The keys we wanted to remove. * @return Command Response - The number of keys that were removed. */ - public T del(String[] keys) { + public T del(@NonNull String[] keys) { ArgsArray commandArgs = buildArgs(keys); protobufTransaction.addCommands(buildCommand(Del, commandArgs)); @@ -165,7 +178,7 @@ public T del(String[] keys) { * @return Response from Redis. key exists, returns the value of * key as a String. Otherwise, return null. */ - public T get(String key) { + public T get(@NonNull String key) { ArgsArray commandArgs = buildArgs(key); protobufTransaction.addCommands(buildCommand(GetString, commandArgs)); @@ -180,7 +193,7 @@ public T get(String key) { * @param value The value to store with the given key. * @return Response from Redis. */ - public T set(String key, String value) { + public T set(@NonNull String key, @NonNull String value) { ArgsArray commandArgs = buildArgs(key, value); protobufTransaction.addCommands(buildCommand(SetString, commandArgs)); @@ -200,7 +213,7 @@ public T set(String key, String value) { * {@link ConditionalSet#ONLY_IF_DOES_NOT_EXIST} conditions, return null. * Otherwise, return OK. */ - public T set(String key, String value, SetOptions options) { + public T set(@NonNull String key, @NonNull String value, @NonNull SetOptions options) { ArgsArray commandArgs = buildArgs(ArrayUtils.addAll(new String[] {key, value}, options.toArgs())); @@ -372,57 +385,6 @@ public T hdel(@NonNull String key, @NonNull String[] fields) { return getThis(); } - /** - * Inserts all the specified values at the tail of the list stored at key.
    - * elements are inserted one after the other to the tail of the list, from the - * leftmost element to the rightmost element. If key does not exist, it is created as - * an empty list before performing the push operations. - * - * @see redis.io for details. - * @param key The key of the list. - * @param elements The elements to insert at the tail of the list stored at key. - * @return Command Response - The length of the list after the push operations. - */ - public T rpush(@NonNull String key, @NonNull String[] elements) { - ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(elements, key)); - - protobufTransaction.addCommands(buildCommand(RPush, commandArgs)); - return getThis(); - } - - /** - * Removes and returns the last elements of the list stored at key.
    - * The command pops a single element from the end of the list. - * - * @see redis.io for details. - * @param key The key of the list. - * @return Command Response - The value of the last element.
    - * If key does not exist, null will be returned.
    - */ - public T rpop(@NonNull String key) { - ArgsArray commandArgs = buildArgs(key); - - protobufTransaction.addCommands(buildCommand(RPop, commandArgs)); - return getThis(); - } - - /** - * Removes and returns up to count elements from the list stored at key, - * depending on the list's length. - * - * @see redis.io for details. - * @param count The count of the elements to pop from the list. - * @returns Command Response - An array of popped elements will be returned depending on the - * list's length.
    - * If key does not exist, null will be returned.
    - */ - public T rpopCount(@NonNull String key, long count) { - ArgsArray commandArgs = buildArgs(key, Long.toString(count)); - - protobufTransaction.addCommands(buildCommand(RPop, commandArgs)); - return getThis(); - } - /** * Returns the values associated with the specified fields in the hash stored at key. * @@ -572,6 +534,123 @@ public T lpopCount(@NonNull String key, long count) { return getThis(); } + /** + * Returns the specified elements of the list stored at key.
    + * The offsets start and end are zero-based indexes, with 0 being the + * first element of the list, 1 being the next element and so on. These offsets can also be + * negative numbers indicating offsets starting at the end of the list, with -1 being the last + * element of the list, -2 being the penultimate, and so on. + * + * @see redis.io for details. + * @param key The key of the list. + * @param start The starting point of the range. + * @param end The end of the range. + * @return Command Response - Array of elements in the specified range.
    + * If start exceeds the end of the list, or if start is greater than + * end, an empty array will be returned.
    + * If end exceeds the actual end of the list, the range will stop at the actual + * end of the list.
    + * If key does not exist an empty array will be returned.
    + */ + public T lrange(@NonNull String key, long start, long end) { + ArgsArray commandArgs = buildArgs(key, Long.toString(start), Long.toString(end)); + + protobufTransaction.addCommands(buildCommand(LRange, commandArgs)); + return getThis(); + } + + /** + * Trims an existing list so that it will contain only the specified range of elements specified. + *
    + * The offsets start and end are zero-based indexes, with 0 being the + * first element of the list, 1 being the next element and so on.
    + * These offsets can also be negative numbers indicating offsets starting at the end of the list, + * with -1 being the last element of the list, -2 being the penultimate, and so on. + * + * @see redis.io for details. + * @param key The key of the list. + * @param start The starting point of the range. + * @param end The end of the range. + * @return Command Response - Always OK.
    + * If start exceeds the end of the list, or if start is greater than + * end, the result will be an empty list (which causes key to be removed).
    + * If end exceeds the actual end of the list, it will be treated like the last + * element of the list.
    + * If key does not exist, OK will be returned without changes to the database. + */ + public T ltrim(@NonNull String key, long start, long end) { + ArgsArray commandArgs = buildArgs(key, Long.toString(start), Long.toString(end)); + + protobufTransaction.addCommands(buildCommand(LTrim, commandArgs)); + return getThis(); + } + + /** + * Returns the length of the list stored at key. + * + * @see redis.io for details. + * @param key The key of the list. + * @return Command Response - The length of the list at key.
    + * If key does not exist, it is interpreted as an empty list and 0 is returned. + */ + public T llen(@NonNull String key) { + ArgsArray commandArgs = buildArgs(key); + + protobufTransaction.addCommands(buildCommand(LLen, commandArgs)); + return getThis(); + } + + /** + * Inserts all the specified values at the tail of the list stored at key.
    + * elements are inserted one after the other to the tail of the list, from the + * leftmost element to the rightmost element. If key does not exist, it is created as + * an empty list before performing the push operations. + * + * @see redis.io for details. + * @param key The key of the list. + * @param elements The elements to insert at the tail of the list stored at key. + * @return Command Response - The length of the list after the push operations. + */ + public T rpush(@NonNull String key, @NonNull String[] elements) { + ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(elements, key)); + + protobufTransaction.addCommands(buildCommand(RPush, commandArgs)); + return getThis(); + } + + /** + * Removes and returns the last elements of the list stored at key.
    + * The command pops a single element from the end of the list. + * + * @see redis.io for details. + * @param key The key of the list. + * @return Command Response - The value of the last element.
    + * If key does not exist, null will be returned.
    + */ + public T rpop(@NonNull String key) { + ArgsArray commandArgs = buildArgs(key); + + protobufTransaction.addCommands(buildCommand(RPop, commandArgs)); + return getThis(); + } + + /** + * Removes and returns up to count elements from the list stored at key, + * depending on the list's length. + * + * @see redis.io for details. + * @param count The count of the elements to pop from the list. + * @return Command Response - An array of popped elements will be returned depending on the list's + * length.
    + * If key does not exist, null will be returned.
    + */ + public T rpopCount(@NonNull String key, long count) { + ArgsArray commandArgs = buildArgs(key, Long.toString(count)); + + protobufTransaction.addCommands(buildCommand(RPop, commandArgs)); + return getThis(); + } + /** * Add specified members to the set stored at key. Specified members that are already * a member of this set are ignored. @@ -584,7 +663,7 @@ public T lpopCount(@NonNull String key, long count) { * @remarks If key does not exist, a new set is created before adding members * . */ - public T sadd(String key, String[] members) { + public T sadd(@NonNull String key, @NonNull String[] members) { ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(members, key)); protobufTransaction.addCommands(buildCommand(SAdd, commandArgs)); @@ -603,7 +682,7 @@ public T sadd(String key, String[] members) { * @remarks If key does not exist, it is treated as an empty set and this command * returns 0. */ - public T srem(String key, String[] members) { + public T srem(@NonNull String key, @NonNull String[] members) { ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(members, key)); protobufTransaction.addCommands(buildCommand(SRem, commandArgs)); @@ -618,7 +697,7 @@ public T srem(String key, String[] members) { * @return Command Response - A Set of all members of the set. * @remarks If key does not exist an empty set will be returned. */ - public T smembers(String key) { + public T smembers(@NonNull String key) { ArgsArray commandArgs = buildArgs(key); protobufTransaction.addCommands(buildCommand(SMembers, commandArgs)); @@ -633,7 +712,7 @@ public T smembers(String key) { * @return Command Response - The cardinality (number of elements) of the set, or 0 if the key * does not exist. */ - public T scard(String key) { + public T scard(@NonNull String key) { ArgsArray commandArgs = buildArgs(key); protobufTransaction.addCommands(buildCommand(SCard, commandArgs)); @@ -648,7 +727,7 @@ public T scard(String key) { * @return Command Response - The number of keys that exist. If the same existing key is mentioned * in keys multiple times, it will be counted multiple times. */ - public T exists(String[] keys) { + public T exists(@NonNull String[] keys) { ArgsArray commandArgs = buildArgs(keys); protobufTransaction.addCommands(buildCommand(Exists, commandArgs)); @@ -665,13 +744,280 @@ public T exists(String[] keys) { * @param keys The list of keys to unlink. * @return Command Response - The number of keys that were unlinked. */ - public T unlink(String[] keys) { + public T unlink(@NonNull String[] keys) { ArgsArray commandArgs = buildArgs(keys); protobufTransaction.addCommands(buildCommand(Unlink, commandArgs)); return getThis(); } + /** + * Sets a timeout on key in seconds. After the timeout has expired, the key + * will automatically be deleted.
    + * If key already has an existing expire + * set, the time to live is updated to the new value.
    + * If seconds is a non-positive number, the key will be deleted rather + * than expired.
    + * The timeout will only be cleared by commands that delete or overwrite the contents of key + * . + * + * @see redis.io for details. + * @param key The key to set timeout on it. + * @param seconds The timeout in seconds. + * @return Command response - true if the timeout was set. false if the + * timeout was not set. e.g. key doesn't exist. + */ + public T expire(@NonNull String key, long seconds) { + ArgsArray commandArgs = buildArgs(key, Long.toString(seconds)); + + protobufTransaction.addCommands(buildCommand(Expire, commandArgs)); + return getThis(); + } + + /** + * Sets a timeout on key in seconds. After the timeout has expired, the key + * will automatically be deleted.
    + * If key already has an existing expire + * set, the time to live is updated to the new value.
    + * If seconds is a non-positive number, the key will be deleted rather + * than expired.
    + * The timeout will only be cleared by commands that delete or overwrite the contents of key + * . + * + * @see redis.io for details. + * @param key The key to set timeout on it. + * @param seconds The timeout in seconds. + * @param expireOptions The expire options. + * @return Command response - true if the timeout was set. false if the + * timeout was not set. e.g. key doesn't exist, or operation skipped due to the + * provided arguments. + */ + public T expire(@NonNull String key, long seconds, @NonNull ExpireOptions expireOptions) { + ArgsArray commandArgs = + buildArgs( + ArrayUtils.addAll(new String[] {key, Long.toString(seconds)}, expireOptions.toArgs())); + + protobufTransaction.addCommands(buildCommand(Expire, commandArgs)); + return getThis(); + } + + /** + * Sets a timeout on key. It takes an absolute Unix timestamp (seconds since January + * 1, 1970) instead of specifying the number of seconds.
    + * A timestamp in the past will delete the key immediately. After the timeout has + * expired, the key will automatically be deleted.
    + * If key already has an existing expire set, the time to live is + * updated to the new value.
    + * The timeout will only be cleared by commands that delete or overwrite the contents of key + * . + * + * @see redis.io for details. + * @param key The key to set timeout on it. + * @param unixSeconds The timeout in an absolute Unix timestamp. + * @return Command response - true if the timeout was set. false if the + * timeout was not set. e.g. key doesn't exist. + */ + public T expireAt(@NonNull String key, long unixSeconds) { + ArgsArray commandArgs = buildArgs(key, Long.toString(unixSeconds)); + + protobufTransaction.addCommands(buildCommand(ExpireAt, commandArgs)); + return getThis(); + } + + /** + * Sets a timeout on key. It takes an absolute Unix timestamp (seconds since January + * 1, 1970) instead of specifying the number of seconds.
    + * A timestamp in the past will delete the key immediately. After the timeout has + * expired, the key will automatically be deleted.
    + * If key already has an existing expire set, the time to live is + * updated to the new value.
    + * The timeout will only be cleared by commands that delete or overwrite the contents of key + * . + * + * @see redis.io for details. + * @param key The key to set timeout on it. + * @param unixSeconds The timeout in an absolute Unix timestamp. + * @param expireOptions The expire options. + * @return Command response - true if the timeout was set. false if the + * timeout was not set. e.g. key doesn't exist, or operation skipped due to the + * provided arguments. + */ + public T expireAt(@NonNull String key, long unixSeconds, @NonNull ExpireOptions expireOptions) { + ArgsArray commandArgs = + buildArgs( + ArrayUtils.addAll( + new String[] {key, Long.toString(unixSeconds)}, expireOptions.toArgs())); + + protobufTransaction.addCommands(buildCommand(ExpireAt, commandArgs)); + return getThis(); + } + + /** + * Sets a timeout on key in milliseconds. After the timeout has expired, the + * key will automatically be deleted.
    + * If key already has an existing + * expire set, the time to live is updated to the new value.
    + * If milliseconds is a non-positive number, the key will be deleted + * rather than expired.
    + * The timeout will only be cleared by commands that delete or overwrite the contents of key + * . + * + * @see redis.io for details. + * @param key The key to set timeout on it. + * @param milliseconds The timeout in milliseconds. + * @return Command response - true if the timeout was set. false if the + * timeout was not set. e.g. key doesn't exist. + */ + public T pexpire(@NonNull String key, long milliseconds) { + ArgsArray commandArgs = buildArgs(key, Long.toString(milliseconds)); + + protobufTransaction.addCommands(buildCommand(PExpire, commandArgs)); + return getThis(); + } + + /** + * Sets a timeout on key in milliseconds. After the timeout has expired, the + * key will automatically be deleted.
    + * If key already has an existing expire set, the time to live is updated to the new + * value.
    + * If milliseconds is a non-positive number, the key will be deleted + * rather than expired.
    + * The timeout will only be cleared by commands that delete or overwrite the contents of key + * . + * + * @see redis.io for details. + * @param key The key to set timeout on it. + * @param milliseconds The timeout in milliseconds. + * @param expireOptions The expire options. + * @return Command response - true if the timeout was set. false if the + * timeout was not set. e.g. key doesn't exist, or operation skipped due to the + * provided arguments. + */ + public T pexpire(@NonNull String key, long milliseconds, @NonNull ExpireOptions expireOptions) { + ArgsArray commandArgs = + buildArgs( + ArrayUtils.addAll( + new String[] {key, Long.toString(milliseconds)}, expireOptions.toArgs())); + + protobufTransaction.addCommands(buildCommand(PExpire, commandArgs)); + return getThis(); + } + + /** + * Sets a timeout on key. It takes an absolute Unix timestamp (milliseconds since + * January 1, 1970) instead of specifying the number of milliseconds.
    + * A timestamp in the past will delete the key immediately. After the timeout has + * expired, the key will automatically be deleted.
    + * If key already has an existing expire set, the time to live is + * updated to the new value.
    + * The timeout will only be cleared by commands that delete or overwrite the contents of key + * . + * + * @see redis.io for details. + * @param key The key to set timeout on it. + * @param unixMilliseconds The timeout in an absolute Unix timestamp. + * @return Command response - true if the timeout was set. false if the + * timeout was not set. e.g. key doesn't exist. + */ + public T pexpireAt(@NonNull String key, long unixMilliseconds) { + ArgsArray commandArgs = buildArgs(key, Long.toString(unixMilliseconds)); + + protobufTransaction.addCommands(buildCommand(PExpireAt, commandArgs)); + return getThis(); + } + + /** + * Sets a timeout on key. It takes an absolute Unix timestamp (milliseconds since + * January 1, 1970) instead of specifying the number of milliseconds.
    + * A timestamp in the past will delete the key immediately. After the timeout has + * expired, the key will automatically be deleted.
    + * If key already has an existing expire set, the time to live is + * updated to the new value.
    + * The timeout will only be cleared by commands that delete or overwrite the contents of key + * . + * + * @see redis.io for details. + * @param key The key to set timeout on it. + * @param unixMilliseconds The timeout in an absolute Unix timestamp. + * @param expireOptions The expire option. + * @return Command response - true if the timeout was set. false if the + * timeout was not set. e.g. key doesn't exist, or operation skipped due to the + * provided arguments. + */ + public T pexpireAt( + @NonNull String key, long unixMilliseconds, @NonNull ExpireOptions expireOptions) { + ArgsArray commandArgs = + buildArgs( + ArrayUtils.addAll( + new String[] {key, Long.toString(unixMilliseconds)}, expireOptions.toArgs())); + + protobufTransaction.addCommands(buildCommand(PExpireAt, commandArgs)); + return getThis(); + } + + /** + * Returns the remaining time to live of key that has a timeout. + * + * @see redis.io for details. + * @param key The key to return its timeout. + * @return Command response - TTL in seconds, -2 if key does not exist, + * or -1 if key exists but has no associated expire. + */ + public T ttl(@NonNull String key) { + ArgsArray commandArgs = buildArgs(key); + + protobufTransaction.addCommands(buildCommand(TTL, commandArgs)); + return getThis(); + } + + /** + * Get the current connection id. + * + * @see redis.io for details. + * @return Command response - The id of the client. + */ + public T clientId() { + protobufTransaction.addCommands(buildCommand(ClientId)); + return getThis(); + } + + /** + * Get the name of the current connection. + * + * @see redis.io for details. + * @return Command response - The name of the client connection as a string if a name is set, or + * null if no name is assigned. + */ + public T clientGetName() { + protobufTransaction.addCommands(buildCommand(ClientGetName)); + return getThis(); + } + + /** + * Rewrites the configuration file with the current configuration. + * + * @see redis.io for details. + * @return OK is returned when the configuration was rewritten properly. Otherwise, + * the transaction fails with an error. + */ + public T configRewrite() { + protobufTransaction.addCommands(buildCommand(ConfigRewrite)); + return getThis(); + } + + /** + * Resets the statistics reported by Redis using the INFO and LATENCY HISTOGRAM commands. + * + * @see redis.io for details. + * @return OK to confirm that the statistics were successfully reset. + */ + public T configResetStat() { + protobufTransaction.addCommands(buildCommand(ConfigResetStat)); + return getThis(); + } + /** Build protobuf {@link Command} object for given command and arguments. */ protected Command buildCommand(RequestType requestType) { return buildCommand(requestType, buildArgs()); diff --git a/java/client/src/main/java/glide/api/models/commands/ExpireOptions.java b/java/client/src/main/java/glide/api/models/commands/ExpireOptions.java new file mode 100644 index 0000000000..2f51745af5 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/ExpireOptions.java @@ -0,0 +1,45 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands; + +import glide.api.commands.GenericBaseCommands; +import lombok.RequiredArgsConstructor; + +/** + * Optional arguments for {@link GenericBaseCommands#expire(String, long, ExpireOptions)}, and + * similar commands. + * + * @see redis.io + */ +@RequiredArgsConstructor +public enum ExpireOptions { + /** + * Sets expiry only when the key has no expiry. Equivalent to NX in the Redis API. + */ + HAS_NO_EXPIRY("NX"), + /** + * Sets expiry only when the key has an existing expiry. Equivalent to XX in the + * Redis API. + */ + HAS_EXISTING_EXPIRY("XX"), + /** + * Sets expiry only when the new expiry is greater than current one. Equivalent to GT + * in the Redis API. + */ + NEW_EXPIRY_GREATER_THAN_CURRENT("GT"), + /** + * Sets expiry only when the new expiry is less than current one. Equivalent to LT in + * the Redis API. + */ + NEW_EXPIRY_LESS_THAN_CURRENT("LT"); + + private final String redisApi; + + /** + * Converts ExpireOptions into a String[]. + * + * @return String[] + */ + public String[] toArgs() { + return new String[] {this.redisApi}; + } +} diff --git a/java/client/src/main/java/glide/api/models/configuration/BaseClientConfiguration.java b/java/client/src/main/java/glide/api/models/configuration/BaseClientConfiguration.java index 747bf806b1..f22002f183 100644 --- a/java/client/src/main/java/glide/api/models/configuration/BaseClientConfiguration.java +++ b/java/client/src/main/java/glide/api/models/configuration/BaseClientConfiguration.java @@ -53,6 +53,12 @@ public abstract class BaseClientConfiguration { */ private final Integer requestTimeout; + /** + * Client name to be used for the client. Will be used with CLIENT SETNAME command during + * connection establishment. + */ + private final String clientName; + /** * Advanced users can pass an extended {@link ThreadPoolResource} to pass a user-defined event * loop group. If set, users are responsible for shutting the resource down when no longer in use. diff --git a/java/client/src/main/java/glide/connectors/handlers/CallbackDispatcher.java b/java/client/src/main/java/glide/connectors/handlers/CallbackDispatcher.java index dfaf01bbe7..6c5e86e2d2 100644 --- a/java/client/src/main/java/glide/connectors/handlers/CallbackDispatcher.java +++ b/java/client/src/main/java/glide/connectors/handlers/CallbackDispatcher.java @@ -129,7 +129,8 @@ public void distributeClosingException(String message) { } public void shutdownGracefully() { - responses.values().forEach(future -> future.cancel(false)); + String msg = "Operation terminated: The closing process has been initiated for the resource."; + responses.values().forEach(future -> future.completeExceptionally(new ClosingException(msg))); responses.clear(); } } diff --git a/java/client/src/main/java/glide/connectors/handlers/ChannelHandler.java b/java/client/src/main/java/glide/connectors/handlers/ChannelHandler.java index 2874fc13b8..4800316803 100644 --- a/java/client/src/main/java/glide/connectors/handlers/ChannelHandler.java +++ b/java/client/src/main/java/glide/connectors/handlers/ChannelHandler.java @@ -6,8 +6,12 @@ import io.netty.bootstrap.Bootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; import io.netty.channel.unix.DomainSocketAddress; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; import redis_request.RedisRequestOuterClass.RedisRequest; import response.ResponseOuterClass.Response; @@ -17,10 +21,13 @@ */ public class ChannelHandler { - private static final String THREAD_POOL_NAME = "glide-channel"; - protected final Channel channel; protected final CallbackDispatcher callbackDispatcher; + private AtomicBoolean isClosed = new AtomicBoolean(false); + + public boolean isClosed() { + return this.isClosed.get() || !this.channel.isOpen(); + } /** * Open a new channel for a new client and running it on the provided EventLoopGroup. @@ -41,6 +48,8 @@ public ChannelHandler( .channel(threadPoolResource.getDomainSocketChannelClass()) .handler(new ProtobufSocketChannelInitializer(callbackDispatcher)) .connect(new DomainSocketAddress(socketPath)) + // TODO .addListener(new NettyFutureErrorHandler()) + // we need to use connection promise here for that ^ .sync() .channel(); this.callbackDispatcher = callbackDispatcher; @@ -58,9 +67,11 @@ public CompletableFuture write(RedisRequest.Builder request, boolean f request.setCallbackIdx(commandId.getKey()); if (flush) { - channel.writeAndFlush(request.build()); + channel + .writeAndFlush(request.build()) + .addListener(new NettyFutureErrorHandler(commandId.getValue())); } else { - channel.write(request.build()); + channel.write(request.build()).addListener(new NettyFutureErrorHandler(commandId.getValue())); } return commandId.getValue(); } @@ -73,13 +84,35 @@ public CompletableFuture write(RedisRequest.Builder request, boolean f */ public CompletableFuture connect(ConnectionRequest request) { var future = callbackDispatcher.registerConnection(); - channel.writeAndFlush(request); + channel.writeAndFlush(request).addListener(new NettyFutureErrorHandler(future)); return future; } /** Closes the UDS connection and frees corresponding resources. */ public ChannelFuture close() { + this.isClosed.set(true); callbackDispatcher.shutdownGracefully(); return channel.close(); } + + /** + * Propagate an error from Netty's {@link ChannelFuture} and complete the {@link + * CompletableFuture} promise. + */ + @RequiredArgsConstructor + private static class NettyFutureErrorHandler implements ChannelFutureListener { + + private final CompletableFuture promise; + + @Override + public void operationComplete(@NonNull ChannelFuture channelFuture) throws Exception { + if (channelFuture.isCancelled()) { + promise.cancel(false); + } + var cause = channelFuture.cause(); + if (cause != null) { + promise.completeExceptionally(cause); + } + } + } } diff --git a/java/client/src/main/java/glide/connectors/handlers/ReadHandler.java b/java/client/src/main/java/glide/connectors/handlers/ReadHandler.java index 44e8d75a1c..29b7f4c01b 100644 --- a/java/client/src/main/java/glide/connectors/handlers/ReadHandler.java +++ b/java/client/src/main/java/glide/connectors/handlers/ReadHandler.java @@ -29,8 +29,10 @@ public void channelRead(@NonNull ChannelHandlerContext ctx, @NonNull Object msg) /** Handles uncaught exceptions from {@link #channelRead(ChannelHandlerContext, Object)}. */ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + // TODO: log thru logger System.out.printf("=== exceptionCaught %s %s %n", ctx, cause); - cause.printStackTrace(System.err); - super.exceptionCaught(ctx, cause); + + callbackDispatcher.distributeClosingException( + "An unhandled error while reading from UDS channel: " + cause); } } diff --git a/java/client/src/main/java/glide/managers/CommandManager.java b/java/client/src/main/java/glide/managers/CommandManager.java index 770830ce2f..253a83f317 100644 --- a/java/client/src/main/java/glide/managers/CommandManager.java +++ b/java/client/src/main/java/glide/managers/CommandManager.java @@ -109,6 +109,13 @@ public CompletableFuture submitNewCommand( */ protected CompletableFuture submitCommandToChannel( RedisRequest.Builder command, RedisExceptionCheckedFunction responseHandler) { + if (channel.isClosed()) { + var errorFuture = new CompletableFuture(); + errorFuture.completeExceptionally( + new ClosingException("Channel closed: Unable to submit command.")); + return errorFuture; + } + // write command request to channel // when complete, convert the response to our expected type T using the given responseHandler return channel diff --git a/java/client/src/main/java/glide/managers/ConnectionManager.java b/java/client/src/main/java/glide/managers/ConnectionManager.java index d0b3ad36ed..d9a8f58574 100644 --- a/java/client/src/main/java/glide/managers/ConnectionManager.java +++ b/java/client/src/main/java/glide/managers/ConnectionManager.java @@ -111,6 +111,10 @@ private ConnectionRequest.Builder setupConnectionRequestBuilderBaseConfiguration connectionRequestBuilder.setRequestTimeout(configuration.getRequestTimeout()); } + if (configuration.getClientName() != null) { + connectionRequestBuilder.setClientName(configuration.getClientName()); + } + return connectionRequestBuilder; } diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index 06d0a145e9..2d69888112 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -12,11 +12,17 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static redis_request.RedisRequestOuterClass.RequestType.ClientGetName; +import static redis_request.RedisRequestOuterClass.RequestType.ClientId; +import static redis_request.RedisRequestOuterClass.RequestType.ConfigResetStat; +import static redis_request.RedisRequestOuterClass.RequestType.ConfigRewrite; import static redis_request.RedisRequestOuterClass.RequestType.CustomCommand; import static redis_request.RedisRequestOuterClass.RequestType.Decr; import static redis_request.RedisRequestOuterClass.RequestType.DecrBy; import static redis_request.RedisRequestOuterClass.RequestType.Del; import static redis_request.RedisRequestOuterClass.RequestType.Exists; +import static redis_request.RedisRequestOuterClass.RequestType.Expire; +import static redis_request.RedisRequestOuterClass.RequestType.ExpireAt; import static redis_request.RedisRequestOuterClass.RequestType.GetString; import static redis_request.RedisRequestOuterClass.RequestType.HashDel; import static redis_request.RedisRequestOuterClass.RequestType.HashExists; @@ -30,10 +36,15 @@ import static redis_request.RedisRequestOuterClass.RequestType.IncrBy; import static redis_request.RedisRequestOuterClass.RequestType.IncrByFloat; import static redis_request.RedisRequestOuterClass.RequestType.Info; +import static redis_request.RedisRequestOuterClass.RequestType.LLen; import static redis_request.RedisRequestOuterClass.RequestType.LPop; import static redis_request.RedisRequestOuterClass.RequestType.LPush; +import static redis_request.RedisRequestOuterClass.RequestType.LRange; +import static redis_request.RedisRequestOuterClass.RequestType.LTrim; import static redis_request.RedisRequestOuterClass.RequestType.MGet; import static redis_request.RedisRequestOuterClass.RequestType.MSet; +import static redis_request.RedisRequestOuterClass.RequestType.PExpire; +import static redis_request.RedisRequestOuterClass.RequestType.PExpireAt; import static redis_request.RedisRequestOuterClass.RequestType.Ping; import static redis_request.RedisRequestOuterClass.RequestType.RPop; import static redis_request.RedisRequestOuterClass.RequestType.RPush; @@ -43,8 +54,10 @@ import static redis_request.RedisRequestOuterClass.RequestType.SRem; import static redis_request.RedisRequestOuterClass.RequestType.Select; import static redis_request.RedisRequestOuterClass.RequestType.SetString; +import static redis_request.RedisRequestOuterClass.RequestType.TTL; import static redis_request.RedisRequestOuterClass.RequestType.Unlink; +import glide.api.models.commands.ExpireOptions; import glide.api.models.commands.InfoOptions; import glide.api.models.commands.SetOptions; import glide.api.models.commands.SetOptions.Expiry; @@ -319,6 +332,215 @@ public void exists_returns_long_success() { assertEquals(numberExisting, result); } + @SneakyThrows + @Test + public void expire_returns_success() { + // setup + String key = "testKey"; + long seconds = 10L; + String[] arguments = new String[] {key, Long.toString(seconds)}; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(true); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Expire), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.expire(key, seconds); + + // verify + assertEquals(testResponse, response); + assertEquals(true, response.get()); + } + + @SneakyThrows + @Test + public void expire_with_expireOptions_returns_success() { + // setup + String key = "testKey"; + long seconds = 10L; + String[] arguments = new String[] {key, Long.toString(seconds), "NX"}; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(false); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(Expire), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.expire(key, seconds, ExpireOptions.HAS_NO_EXPIRY); + + // verify + assertEquals(testResponse, response); + assertEquals(false, response.get()); + } + + @SneakyThrows + @Test + public void expireAt_returns_success() { + // setup + String key = "testKey"; + long unixSeconds = 100000L; + String[] arguments = new String[] {key, Long.toString(unixSeconds)}; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(true); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ExpireAt), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.expireAt(key, unixSeconds); + + // verify + assertEquals(testResponse, response); + assertEquals(true, response.get()); + } + + @SneakyThrows + @Test + public void expireAt_with_expireOptions_returns_success() { + // setup + String key = "testKey"; + long unixSeconds = 100000L; + String[] arguments = new String[] {key, Long.toString(unixSeconds), "XX"}; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(false); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ExpireAt), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.expireAt(key, unixSeconds, ExpireOptions.HAS_EXISTING_EXPIRY); + + // verify + assertEquals(testResponse, response); + assertEquals(false, response.get()); + } + + @SneakyThrows + @Test + public void pexpire_returns_success() { + // setup + String key = "testKey"; + long milliseconds = 50000L; + String[] arguments = new String[] {key, Long.toString(milliseconds)}; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(true); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(PExpire), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.pexpire(key, milliseconds); + + // verify + assertEquals(testResponse, response); + assertEquals(true, response.get()); + } + + @SneakyThrows + @Test + public void pexpire_with_expireOptions_returns_success() { + // setup + String key = "testKey"; + long milliseconds = 50000L; + String[] arguments = new String[] {key, Long.toString(milliseconds), "LT"}; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(false); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(PExpire), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.pexpire(key, milliseconds, ExpireOptions.NEW_EXPIRY_LESS_THAN_CURRENT); + + // verify + assertEquals(testResponse, response); + assertEquals(false, response.get()); + } + + @SneakyThrows + @Test + public void pexpireAt_returns_success() { + // setup + String key = "testKey"; + long unixMilliseconds = 999999L; + String[] arguments = new String[] {key, Long.toString(unixMilliseconds)}; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(true); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(PExpireAt), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.pexpireAt(key, unixMilliseconds); + + // verify + assertEquals(testResponse, response); + assertEquals(true, response.get()); + } + + @SneakyThrows + @Test + public void pexpireAt_with_expireOptions_returns_success() { + // setup + String key = "testKey"; + long unixMilliseconds = 999999L; + String[] arguments = new String[] {key, Long.toString(unixMilliseconds), "GT"}; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(false); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(PExpireAt), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.pexpireAt(key, unixMilliseconds, ExpireOptions.NEW_EXPIRY_GREATER_THAN_CURRENT); + + // verify + assertEquals(testResponse, response); + assertEquals(false, response.get()); + } + + @SneakyThrows + @Test + public void ttl_returns_success() { + // setup + String key = "testKey"; + long ttl = 999L; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(ttl); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(TTL), eq(new String[] {key}), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.ttl(key); + + // verify + assertEquals(testResponse, response); + assertEquals(ttl, response.get()); + } + @SneakyThrows @Test public void info_returns_success() { @@ -825,6 +1047,80 @@ public void lpopCount_returns_success() { assertEquals(value, payload); } + @SneakyThrows + @Test + public void lrange_returns_success() { + // setup + String key = "testKey"; + long start = 2L; + long end = 4L; + String[] args = new String[] {key, Long.toString(start), Long.toString(end)}; + String[] value = new String[] {"value1", "value2"}; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LRange), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.lrange(key, start, end); + String[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void ltrim_returns_success() { + // setup + String key = "testKey"; + long start = 2L; + long end = 2L; + String[] args = new String[] {key, Long.toString(end), Long.toString(start)}; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LTrim), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.ltrim(key, start, end); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void llen_returns_success() { + // setup + String key = "testKey"; + String[] args = new String[] {key}; + long value = 2L; + + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(LLen), eq(args), any())).thenReturn(testResponse); + + // exercise + CompletableFuture response = service.llen(key); + Long payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void rpush_returns_success() { @@ -994,4 +1290,82 @@ public void scard_returns_success() { assertEquals(testResponse, response); assertEquals(value, payload); } + + @SneakyThrows + @Test + public void clientId_returns_success() { + // setup + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(42L); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ClientId), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.clientId(); + + // verify + assertEquals(testResponse, response); + assertEquals(42L, response.get()); + } + + @SneakyThrows + @Test + public void clientGetName_returns_success() { + // setup + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn("TEST"); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ClientGetName), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.clientGetName(); + + // verify + assertEquals(testResponse, response); + assertEquals("TEST", response.get()); + } + + @SneakyThrows + @Test + public void configRewrite_returns_success() { + // setup + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ConfigRewrite), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.configRewrite(); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void configResetStat_returns_success() { + // setup + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ConfigResetStat), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.configResetStat(); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } } diff --git a/java/client/src/test/java/glide/api/RedisClusterClientTest.java b/java/client/src/test/java/glide/api/RedisClusterClientTest.java index 341cd40025..e478e11917 100644 --- a/java/client/src/test/java/glide/api/RedisClusterClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClusterClientTest.java @@ -1,6 +1,7 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api; +import static glide.api.BaseClient.OK; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleRoute.ALL_NODES; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleRoute.ALL_PRIMARIES; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleRoute.RANDOM; @@ -11,6 +12,10 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static redis_request.RedisRequestOuterClass.RequestType.ClientGetName; +import static redis_request.RedisRequestOuterClass.RequestType.ClientId; +import static redis_request.RedisRequestOuterClass.RequestType.ConfigResetStat; +import static redis_request.RedisRequestOuterClass.RequestType.ConfigRewrite; import static redis_request.RedisRequestOuterClass.RequestType.Info; import static redis_request.RedisRequestOuterClass.RequestType.Ping; @@ -118,7 +123,6 @@ public TestClient(CommandManager commandManager, Object objectToReturn) { object = objectToReturn; } - @SuppressWarnings("unchecked") @Override protected T handleRedisResponse(Class classType, boolean isNullable, Response response) { return (T) object; @@ -356,4 +360,174 @@ public void info_with_options_and_multi_node_route_returns_multi_value() { assertAll( () -> assertTrue(value.hasMultiData()), () -> assertEquals(data, value.getMultiValue())); } + + @SneakyThrows + @Test + public void clientId_returns_success() { + // setup + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(42L); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ClientId), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.clientId(); + + // verify + assertEquals(testResponse, response); + assertEquals(42L, response.get()); + } + + @Test + @SneakyThrows + public void clientId_with_multi_node_route_returns_success() { + var commandManager = new TestCommandManager(null); + + var data = Map.of("n1", 42L); + var client = new TestClient(commandManager, data); + + var value = client.clientId(ALL_NODES).get(); + assertEquals(data, value.getMultiValue()); + } + + @Test + @SneakyThrows + public void clientId_with_single_node_route_returns_success() { + var commandManager = new TestCommandManager(null); + + var client = new TestClient(commandManager, 42L); + + var value = client.clientId(RANDOM).get(); + assertEquals(42, value.getSingleValue()); + } + + @SneakyThrows + @Test + public void clientGetName_returns_success() { + // setup + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn("TEST"); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ClientGetName), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.clientGetName(); + + // verify + assertEquals(testResponse, response); + assertEquals("TEST", response.get()); + } + + @Test + @SneakyThrows + public void clientGetName_with_single_node_route_returns_success() { + var commandManager = new TestCommandManager(null); + + var client = new TestClient(commandManager, "TEST"); + + var value = client.clientGetName(RANDOM).get(); + assertEquals("TEST", value.getSingleValue()); + } + + @Test + @SneakyThrows + public void clientGetName_with_multi_node_route_returns_success() { + var commandManager = new TestCommandManager(null); + + var data = Map.of("n1", "TEST"); + var client = new TestClient(commandManager, data); + + var value = client.clientGetName(ALL_NODES).get(); + assertEquals(data, value.getMultiValue()); + } + + @SneakyThrows + @Test + public void configRewrite_without_route_returns_success() { + // setup + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ConfigRewrite), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.configRewrite(); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void configRewrite_with_route_returns_success() { + // setup + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(OK); + + Route route = ALL_NODES; + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(ConfigRewrite), eq(new String[0]), eq(route), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.configRewrite(route); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void configResetStat_without_route_returns_success() { + // setup + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(OK); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(ConfigResetStat), eq(new String[0]), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.configResetStat(); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } + + @SneakyThrows + @Test + public void configResetStat_with_route_returns_success() { + // setup + CompletableFuture testResponse = mock(CompletableFuture.class); + when(testResponse.get()).thenReturn(OK); + + Route route = ALL_NODES; + + // match on protobuf request + when(commandManager.submitNewCommand( + eq(ConfigResetStat), eq(new String[0]), eq(route), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.configResetStat(route); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(OK, payload); + } } diff --git a/java/client/src/test/java/glide/api/models/TransactionTests.java b/java/client/src/test/java/glide/api/models/TransactionTests.java index c43a224016..2e60763ac3 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -3,10 +3,16 @@ import static glide.api.models.commands.SetOptions.RETURN_OLD_VALUE; import static org.junit.jupiter.api.Assertions.assertEquals; +import static redis_request.RedisRequestOuterClass.RequestType.ClientGetName; +import static redis_request.RedisRequestOuterClass.RequestType.ClientId; +import static redis_request.RedisRequestOuterClass.RequestType.ConfigResetStat; +import static redis_request.RedisRequestOuterClass.RequestType.ConfigRewrite; import static redis_request.RedisRequestOuterClass.RequestType.Decr; import static redis_request.RedisRequestOuterClass.RequestType.DecrBy; import static redis_request.RedisRequestOuterClass.RequestType.Del; import static redis_request.RedisRequestOuterClass.RequestType.Exists; +import static redis_request.RedisRequestOuterClass.RequestType.Expire; +import static redis_request.RedisRequestOuterClass.RequestType.ExpireAt; import static redis_request.RedisRequestOuterClass.RequestType.GetString; import static redis_request.RedisRequestOuterClass.RequestType.HashDel; import static redis_request.RedisRequestOuterClass.RequestType.HashExists; @@ -20,10 +26,15 @@ import static redis_request.RedisRequestOuterClass.RequestType.IncrBy; import static redis_request.RedisRequestOuterClass.RequestType.IncrByFloat; import static redis_request.RedisRequestOuterClass.RequestType.Info; +import static redis_request.RedisRequestOuterClass.RequestType.LLen; import static redis_request.RedisRequestOuterClass.RequestType.LPop; import static redis_request.RedisRequestOuterClass.RequestType.LPush; +import static redis_request.RedisRequestOuterClass.RequestType.LRange; +import static redis_request.RedisRequestOuterClass.RequestType.LTrim; import static redis_request.RedisRequestOuterClass.RequestType.MGet; import static redis_request.RedisRequestOuterClass.RequestType.MSet; +import static redis_request.RedisRequestOuterClass.RequestType.PExpire; +import static redis_request.RedisRequestOuterClass.RequestType.PExpireAt; import static redis_request.RedisRequestOuterClass.RequestType.Ping; import static redis_request.RedisRequestOuterClass.RequestType.RPop; import static redis_request.RedisRequestOuterClass.RequestType.RPush; @@ -32,8 +43,10 @@ import static redis_request.RedisRequestOuterClass.RequestType.SMembers; import static redis_request.RedisRequestOuterClass.RequestType.SRem; import static redis_request.RedisRequestOuterClass.RequestType.SetString; +import static redis_request.RedisRequestOuterClass.RequestType.TTL; import static redis_request.RedisRequestOuterClass.RequestType.Unlink; +import glide.api.models.commands.ExpireOptions; import glide.api.models.commands.InfoOptions; import glide.api.models.commands.SetOptions; import java.util.LinkedList; @@ -74,15 +87,9 @@ public void transaction_builds_protobuf_request(BaseTransaction transaction) .addArgs(RETURN_OLD_VALUE) .build())); - transaction.exists(new String[] {"key1", "key2"}); - results.add(Pair.of(Exists, ArgsArray.newBuilder().addArgs("key1").addArgs("key2").build())); - transaction.del(new String[] {"key1", "key2"}); results.add(Pair.of(Del, ArgsArray.newBuilder().addArgs("key1").addArgs("key2").build())); - transaction.unlink(new String[] {"key1", "key2"}); - results.add(Pair.of(Unlink, ArgsArray.newBuilder().addArgs("key1").addArgs("key2").build())); - transaction.ping(); results.add(Pair.of(Ping, ArgsArray.newBuilder().build())); @@ -165,6 +172,17 @@ public void transaction_builds_protobuf_request(BaseTransaction transaction) transaction.lpopCount("key", 2); results.add(Pair.of(LPop, ArgsArray.newBuilder().addArgs("key").addArgs("2").build())); + transaction.lrange("key", 1, 2); + results.add( + Pair.of(LRange, ArgsArray.newBuilder().addArgs("key").addArgs("1").addArgs("2").build())); + + transaction.ltrim("key", 1, 2); + results.add( + Pair.of(LTrim, ArgsArray.newBuilder().addArgs("key").addArgs("1").addArgs("2").build())); + + transaction.llen("key"); + results.add(Pair.of(LLen, ArgsArray.newBuilder().addArgs("key").build())); + transaction.rpush("key", new String[] {"element"}); results.add(Pair.of(RPush, ArgsArray.newBuilder().addArgs("key").addArgs("element").build())); @@ -186,6 +204,87 @@ public void transaction_builds_protobuf_request(BaseTransaction transaction) transaction.scard("key"); results.add(Pair.of(SCard, ArgsArray.newBuilder().addArgs("key").build())); + transaction.exists(new String[] {"key1", "key2"}); + results.add(Pair.of(Exists, ArgsArray.newBuilder().addArgs("key1").addArgs("key2").build())); + + transaction.unlink(new String[] {"key1", "key2"}); + results.add(Pair.of(Unlink, ArgsArray.newBuilder().addArgs("key1").addArgs("key2").build())); + + transaction.expire("key", 9L); + results.add( + Pair.of(Expire, ArgsArray.newBuilder().addArgs("key").addArgs(Long.toString(9L)).build())); + + transaction.expire("key", 99L, ExpireOptions.NEW_EXPIRY_GREATER_THAN_CURRENT); + results.add( + Pair.of( + Expire, + ArgsArray.newBuilder() + .addArgs("key") + .addArgs(Long.toString(99L)) + .addArgs("GT") + .build())); + + transaction.expireAt("key", 999L); + results.add( + Pair.of( + ExpireAt, ArgsArray.newBuilder().addArgs("key").addArgs(Long.toString(999L)).build())); + + transaction.expireAt("key", 9999L, ExpireOptions.NEW_EXPIRY_LESS_THAN_CURRENT); + results.add( + Pair.of( + ExpireAt, + ArgsArray.newBuilder() + .addArgs("key") + .addArgs(Long.toString(9999L)) + .addArgs("LT") + .build())); + + transaction.pexpire("key", 99999L); + results.add( + Pair.of( + PExpire, ArgsArray.newBuilder().addArgs("key").addArgs(Long.toString(99999L)).build())); + + transaction.pexpire("key", 999999L, ExpireOptions.HAS_EXISTING_EXPIRY); + results.add( + Pair.of( + PExpire, + ArgsArray.newBuilder() + .addArgs("key") + .addArgs(Long.toString(999999L)) + .addArgs("XX") + .build())); + + transaction.pexpireAt("key", 9999999L); + results.add( + Pair.of( + PExpireAt, + ArgsArray.newBuilder().addArgs("key").addArgs(Long.toString(9999999L)).build())); + + transaction.pexpireAt("key", 99999999L, ExpireOptions.HAS_NO_EXPIRY); + results.add( + Pair.of( + PExpireAt, + ArgsArray.newBuilder() + .addArgs("key") + .addArgs(Long.toString(99999999L)) + .addArgs("NX") + .build())); + + transaction.ttl("key"); + results.add(Pair.of(TTL, ArgsArray.newBuilder().addArgs("key").build())); + + transaction.clientId(); + results.add(Pair.of(ClientId, ArgsArray.newBuilder().build())); + + transaction.clientGetName(); + results.add(Pair.of(ClientGetName, ArgsArray.newBuilder().build())); + + transaction.configRewrite(); + results.add(Pair.of(ConfigRewrite, ArgsArray.newBuilder().build())); + + transaction.configResetStat(); + results.add(Pair.of(ConfigResetStat, ArgsArray.newBuilder().build())); + var protobufTransaction = transaction.getProtobufTransaction().build(); for (int idx = 0; idx < protobufTransaction.getCommandsCount(); idx++) { diff --git a/java/client/src/test/java/glide/connection/ConnectionWithGlideMockTests.java b/java/client/src/test/java/glide/connection/ConnectionWithGlideMockTests.java new file mode 100644 index 0000000000..331af6fa39 --- /dev/null +++ b/java/client/src/test/java/glide/connection/ConnectionWithGlideMockTests.java @@ -0,0 +1,190 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.connection; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import connection_request.ConnectionRequestOuterClass.ConnectionRequest; +import connection_request.ConnectionRequestOuterClass.NodeAddress; +import glide.api.RedisClient; +import glide.api.models.exceptions.ClosingException; +import glide.connectors.handlers.CallbackDispatcher; +import glide.connectors.handlers.ChannelHandler; +import glide.connectors.resources.Platform; +import glide.managers.CommandManager; +import glide.managers.ConnectionManager; +import glide.utils.RustCoreLibMockTestBase; +import glide.utils.RustCoreMock; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeoutException; +import lombok.SneakyThrows; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import redis_request.RedisRequestOuterClass.RedisRequest; +import response.ResponseOuterClass.Response; + +public class ConnectionWithGlideMockTests extends RustCoreLibMockTestBase { + + private ChannelHandler channelHandler = null; + + @BeforeEach + @SneakyThrows + public void createTestClient() { + channelHandler = + new ChannelHandler( + new CallbackDispatcher(), socketPath, Platform.getThreadPoolResourceSupplier().get()); + } + + @AfterEach + public void closeTestClient() { + channelHandler.close(); + } + + private Future testConnection() { + return channelHandler.connect(createConnectionRequest()); + } + + private static ConnectionRequest createConnectionRequest() { + return ConnectionRequest.newBuilder() + .addAddresses(NodeAddress.newBuilder().setHost("dummyhost").setPort(42).build()) + .build(); + } + + @BeforeAll + public static void init() { + startRustCoreLibMock(null); + } + + @Test + @SneakyThrows + // as of #710 https://github.com/aws/babushka/pull/710 - connection response is empty + public void can_connect_with_empty_response() { + RustCoreMock.updateGlideMock( + new RustCoreMock.GlideMockProtobuf() { + @Override + public Response connection(ConnectionRequest request) { + return Response.newBuilder().build(); + } + + @Override + public Response.Builder redisRequest(RedisRequest request) { + return null; + } + }); + + var connectionResponse = testConnection().get(); + assertAll( + () -> assertFalse(connectionResponse.hasClosingError()), + () -> assertFalse(connectionResponse.hasRequestError()), + () -> assertFalse(connectionResponse.hasRespPointer())); + } + + @Test + @SneakyThrows + public void can_connect_with_ok_response() { + RustCoreMock.updateGlideMock( + new RustCoreMock.GlideMockProtobuf() { + @Override + public Response connection(ConnectionRequest request) { + return OK().build(); + } + + @Override + public Response.Builder redisRequest(RedisRequest request) { + return null; + } + }); + + var connectionResponse = testConnection().get(); + assertAll( + () -> assertTrue(connectionResponse.hasConstantResponse()), + () -> assertFalse(connectionResponse.hasClosingError()), + () -> assertFalse(connectionResponse.hasRequestError()), + () -> assertFalse(connectionResponse.hasRespPointer())); + } + + @Test + public void cant_connect_when_no_response() { + RustCoreMock.updateGlideMock( + new RustCoreMock.GlideMockProtobuf() { + @Override + public Response connection(ConnectionRequest request) { + return null; + } + + @Override + public Response.Builder redisRequest(RedisRequest request) { + return null; + } + }); + + assertThrows(TimeoutException.class, () -> testConnection().get(1, SECONDS)); + } + + @Test + @SneakyThrows + public void cant_connect_when_negative_response() { + RustCoreMock.updateGlideMock( + new RustCoreMock.GlideMockProtobuf() { + @Override + public Response connection(ConnectionRequest request) { + return Response.newBuilder().setClosingError("You shall not pass!").build(); + } + + @Override + public Response.Builder redisRequest(RedisRequest request) { + return null; + } + }); + + var exception = assertThrows(ExecutionException.class, () -> testConnection().get(1, SECONDS)); + assertAll( + () -> assertTrue(exception.getCause() instanceof ClosingException), + () -> assertEquals("You shall not pass!", exception.getCause().getMessage())); + } + + @Test + @SneakyThrows + public void rethrow_error_on_read_when_malformed_packet_received() { + RustCoreMock.updateGlideMock(request -> new byte[] {-1}); + + var exception = assertThrows(ExecutionException.class, () -> testConnection().get(1, SECONDS)); + assertAll( + () -> assertTrue(exception.getCause() instanceof ClosingException), + () -> + assertTrue( + exception + .getCause() + .getMessage() + .contains("An unhandled error while reading from UDS channel"))); + } + + @Test + @SneakyThrows + public void rethrow_error_if_UDS_channel_closed() { + var client = new TestClient(channelHandler); + stopRustCoreLibMock(); + try { + var exception = + assertThrows(ExecutionException.class, () -> client.customCommand(new String[0]).get()); + assertTrue(exception.getCause() instanceof ClosingException); + } finally { + // restart mock to let other tests pass if this one failed + startRustCoreLibMock(null); + } + } + + private static class TestClient extends RedisClient { + + public TestClient(ChannelHandler channelHandler) { + super(new ConnectionManager(channelHandler), new CommandManager(channelHandler)); + } + } +} diff --git a/java/client/src/test/java/glide/managers/CommandManagerTest.java b/java/client/src/test/java/glide/managers/CommandManagerTest.java index 0f7f539ebb..a64c6499ad 100644 --- a/java/client/src/test/java/glide/managers/CommandManagerTest.java +++ b/java/client/src/test/java/glide/managers/CommandManagerTest.java @@ -60,6 +60,7 @@ public void submitNewCommand_return_Object_result() { CompletableFuture future = new CompletableFuture<>(); future.complete(respPointerResponse); when(channelHandler.write(any(), anyBoolean())).thenReturn(future); + when(channelHandler.isClosed()).thenReturn(false); // exercise CompletableFuture result = @@ -81,6 +82,7 @@ public void submitNewCommand_return_Null_result() { CompletableFuture future = new CompletableFuture<>(); future.complete(respPointerResponse); when(channelHandler.write(any(), anyBoolean())).thenReturn(future); + when(channelHandler.isClosed()).thenReturn(false); // exercise CompletableFuture result = @@ -107,6 +109,7 @@ public void submitNewCommand_return_String_result() { CompletableFuture future = new CompletableFuture<>(); future.complete(respPointerResponse); when(channelHandler.write(any(), anyBoolean())).thenReturn(future); + when(channelHandler.isClosed()).thenReturn(false); // exercise CompletableFuture result = @@ -126,6 +129,7 @@ public void submitNewCommand_return_String_result() { public void prepare_request_with_simple_routes(SimpleRoute routeType) { CompletableFuture future = new CompletableFuture<>(); when(channelHandler.write(any(), anyBoolean())).thenReturn(future); + when(channelHandler.isClosed()).thenReturn(false); ArgumentCaptor captor = ArgumentCaptor.forClass(RedisRequest.Builder.class); @@ -156,6 +160,7 @@ public void prepare_request_with_simple_routes(SimpleRoute routeType) { public void prepare_request_with_slot_id_routes(SlotType slotType) { CompletableFuture future = new CompletableFuture<>(); when(channelHandler.write(any(), anyBoolean())).thenReturn(future); + when(channelHandler.isClosed()).thenReturn(false); ArgumentCaptor captor = ArgumentCaptor.forClass(RedisRequest.Builder.class); @@ -188,6 +193,7 @@ public void prepare_request_with_slot_id_routes(SlotType slotType) { public void prepare_request_with_slot_key_routes(SlotType slotType) { CompletableFuture future = new CompletableFuture<>(); when(channelHandler.write(any(), anyBoolean())).thenReturn(future); + when(channelHandler.isClosed()).thenReturn(false); ArgumentCaptor captor = ArgumentCaptor.forClass(RedisRequest.Builder.class); @@ -239,6 +245,7 @@ public void submitNewCommand_with_Transaction_sends_protobuf_request() { CompletableFuture future = new CompletableFuture<>(); when(channelHandler.write(any(), anyBoolean())).thenReturn(future); + when(channelHandler.isClosed()).thenReturn(false); ArgumentCaptor captor = ArgumentCaptor.forClass(RedisRequest.Builder.class); @@ -279,6 +286,7 @@ public void submitNewCommand_with_ClusterTransaction_with_route_sends_protobuf_r CompletableFuture future = new CompletableFuture<>(); when(channelHandler.write(any(), anyBoolean())).thenReturn(future); + when(channelHandler.isClosed()).thenReturn(false); ArgumentCaptor captor = ArgumentCaptor.forClass(RedisRequest.Builder.class); diff --git a/java/client/src/test/java/glide/managers/ConnectionManagerTest.java b/java/client/src/test/java/glide/managers/ConnectionManagerTest.java index 2877739ea5..b04dd5b312 100644 --- a/java/client/src/test/java/glide/managers/ConnectionManagerTest.java +++ b/java/client/src/test/java/glide/managers/ConnectionManagerTest.java @@ -54,6 +54,8 @@ public class ConnectionManagerTest { private static int REQUEST_TIMEOUT = 3; + private static String CLIENT_NAME = "ClientName"; + @BeforeEach public void setUp() { channel = mock(ChannelHandler.class); @@ -132,6 +134,7 @@ public void connection_request_protobuf_generation_with_all_fields_set() { .factor(FACTOR) .build()) .databaseId(DATABASE_ID) + .clientName(CLIENT_NAME) .build(); ConnectionRequest expectedProtobufConnectionRequest = ConnectionRequest.newBuilder() @@ -158,6 +161,7 @@ public void connection_request_protobuf_generation_with_all_fields_set() { .setExponentBase(EXPONENT_BASE) .build()) .setDatabaseId(DATABASE_ID) + .setClientName(CLIENT_NAME) .build(); CompletableFuture completedFuture = new CompletableFuture<>(); Response response = Response.newBuilder().setConstantResponse(ConstantResponse.OK).build(); diff --git a/java/client/src/test/java/glide/utils/RustCoreLibMockTestBase.java b/java/client/src/test/java/glide/utils/RustCoreLibMockTestBase.java new file mode 100644 index 0000000000..ecf59e4a17 --- /dev/null +++ b/java/client/src/test/java/glide/utils/RustCoreLibMockTestBase.java @@ -0,0 +1,49 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.utils; + +import glide.connectors.handlers.ChannelHandler; +import glide.ffi.resolvers.SocketListenerResolver; +import lombok.SneakyThrows; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +public class RustCoreLibMockTestBase { + + /** + * Pass this socket path to {@link ChannelHandler} or mock {@link + * SocketListenerResolver#getSocket()} to return it. + */ + protected static String socketPath = null; + + @SneakyThrows + public static void startRustCoreLibMock(RustCoreMock.GlideMock rustCoreLibMock) { + assert socketPath == null + : "Previous `RustCoreMock` wasn't stopped. Ensure that your test class inherits" + + " `RustCoreLibMockTestBase`."; + + socketPath = RustCoreMock.start(rustCoreLibMock); + } + + @BeforeEach + public void preTestCheck() { + assert socketPath != null + : "You missed to call `startRustCoreLibMock` in a `@BeforeAll` method of your test class" + + " inherited from `RustCoreLibMockTestBase`."; + } + + @AfterEach + public void afterTestCheck() { + assert !RustCoreMock.failed() : "Error occurred in `RustCoreMock`"; + } + + @AfterAll + @SneakyThrows + public static void stopRustCoreLibMock() { + assert socketPath != null + : "You missed to call `startRustCoreLibMock` in a `@AfterAll` method of your test class" + + " inherited from `RustCoreLibMockTestBase`."; + RustCoreMock.stop(); + socketPath = null; + } +} diff --git a/java/client/src/test/java/glide/utils/RustCoreMock.java b/java/client/src/test/java/glide/utils/RustCoreMock.java new file mode 100644 index 0000000000..8ef787948e --- /dev/null +++ b/java/client/src/test/java/glide/utils/RustCoreMock.java @@ -0,0 +1,189 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.utils; + +import connection_request.ConnectionRequestOuterClass.ConnectionRequest; +import glide.connectors.resources.Platform; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.epoll.EpollServerDomainSocketChannel; +import io.netty.channel.kqueue.KQueueServerDomainSocketChannel; +import io.netty.channel.unix.DomainSocketAddress; +import io.netty.channel.unix.DomainSocketChannel; +import io.netty.handler.codec.protobuf.ProtobufEncoder; +import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder; +import io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender; +import java.nio.file.Files; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import redis_request.RedisRequestOuterClass.RedisRequest; +import response.ResponseOuterClass.ConstantResponse; +import response.ResponseOuterClass.Response; + +public class RustCoreMock { + + @FunctionalInterface + public interface GlideMock { + default boolean isRaw() { + return true; + } + + byte[] handle(byte[] request); + } + + public abstract static class GlideMockProtobuf implements GlideMock { + @Override + public boolean isRaw() { + return false; + } + + @Override + public byte[] handle(byte[] request) { + return new byte[0]; + } + + /** Return `null` to do not reply. */ + public abstract Response connection(ConnectionRequest request); + + /** Return `null` to do not reply. */ + public abstract Response.Builder redisRequest(RedisRequest request); + + public Response redisRequestWithCallbackId(RedisRequest request) { + var responseDraft = redisRequest(request); + return responseDraft == null + ? null + : responseDraft.setCallbackIdx(request.getCallbackIdx()).build(); + } + + public static Response.Builder OK() { + return Response.newBuilder().setConstantResponse(ConstantResponse.OK); + } + } + + public abstract static class GlideMockConnectAll extends GlideMockProtobuf { + @Override + public Response connection(ConnectionRequest request) { + return Response.newBuilder().build(); + } + } + + /** Thread pool supplied to Netty to perform all async IO. */ + private final EventLoopGroup group; + + private final Channel channel; + + private final String socketPath; + + private static RustCoreMock instance; + + private GlideMock messageProcessor; + + /** Update {@link GlideMock} into a running {@link RustCoreMock}. */ + public static void updateGlideMock(GlideMock newMock) { + instance.messageProcessor = newMock; + } + + private final AtomicBoolean failed = new AtomicBoolean(false); + + /** Get and clear failure status. */ + public static boolean failed() { + return instance.failed.compareAndSet(true, false); + } + + @SneakyThrows + private RustCoreMock() { + var threadPoolResource = Platform.getThreadPoolResourceSupplier().get(); + socketPath = Files.createTempFile("GlideCoreMock", null).toString(); + group = threadPoolResource.getEventLoopGroup(); + channel = + new ServerBootstrap() + .group(group) + .channel( + Platform.getCapabilities().isEPollAvailable() + ? EpollServerDomainSocketChannel.class + : KQueueServerDomainSocketChannel.class) + .childHandler( + new ChannelInitializer() { + + @Override + protected void initChannel(DomainSocketChannel ch) throws Exception { + ch.pipeline() + // https://netty.io/4.1/api/io/netty/handler/codec/protobuf/ProtobufEncoder.html + .addLast("frameDecoder", new ProtobufVarint32FrameDecoder()) + .addLast("frameEncoder", new ProtobufVarint32LengthFieldPrepender()) + .addLast("protobufEncoder", new ProtobufEncoder()) + .addLast(new UdsServer(ch)); + } + }) + .bind(new DomainSocketAddress(socketPath)) + .syncUninterruptibly() + .channel(); + } + + public static String start(GlideMock messageProcessor) { + if (instance != null) { + stop(); + } + instance = new RustCoreMock(); + instance.messageProcessor = messageProcessor; + return instance.socketPath; + } + + @SneakyThrows + public static void stop() { + if (instance != null) { + instance.channel.close().syncUninterruptibly(); + instance.group.shutdownGracefully().get(5, TimeUnit.SECONDS); + instance = null; + } + } + + @RequiredArgsConstructor + private class UdsServer extends ChannelInboundHandlerAdapter { + + private final Channel ch; + + // This works with only one connected client. + // TODO Rework with `channelActive` override. + private final AtomicBoolean anybodyConnected = new AtomicBoolean(false); + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + var buf = (ByteBuf) msg; + var bytes = new byte[buf.readableBytes()]; + buf.readBytes(bytes); + buf.release(); + if (messageProcessor.isRaw()) { + ch.writeAndFlush(Unpooled.copiedBuffer(messageProcessor.handle(bytes))); + return; + } + var handler = (GlideMockProtobuf) messageProcessor; + Response response = null; + if (!anybodyConnected.get()) { + var connection = ConnectionRequest.parseFrom(bytes); + response = handler.connection(connection); + anybodyConnected.setPlain(true); + } else { + var request = RedisRequest.parseFrom(bytes); + response = handler.redisRequestWithCallbackId(request); + } + if (response != null) { + ctx.writeAndFlush(response); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + cause.printStackTrace(); + ctx.close(); + failed.setPlain(true); + } + } +} diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index a2f13dc324..e8742fc103 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -2,6 +2,7 @@ package glide; import static glide.TestConfiguration.CLUSTER_PORTS; +import static glide.TestConfiguration.REDIS_VERSION; import static glide.TestConfiguration.STANDALONE_PORTS; import static glide.api.BaseClient.OK; import static glide.api.models.commands.SetOptions.ConditionalSet.ONLY_IF_DOES_NOT_EXIST; @@ -18,11 +19,13 @@ import glide.api.BaseClient; import glide.api.RedisClient; import glide.api.RedisClusterClient; +import glide.api.models.commands.ExpireOptions; import glide.api.models.commands.SetOptions; import glide.api.models.configuration.NodeAddress; import glide.api.models.configuration.RedisClientConfiguration; import glide.api.models.configuration.RedisClusterClientConfiguration; import glide.api.models.exceptions.RequestException; +import java.time.Instant; import java.util.List; import java.util.Map; import java.util.Set; @@ -293,7 +296,7 @@ public void set_missing_value_and_returnOldValue_is_null(BaseClient client) { assertEquals(OK, ok); SetOptions options = SetOptions.builder().returnOldValue(true).build(); - String data = client.set("another", ANOTHER_VALUE, options).get(); + String data = client.set(UUID.randomUUID().toString(), ANOTHER_VALUE, options).get(); assertNull(data); } @@ -542,20 +545,22 @@ public void hincrBy_hincrByFloat_type_error(BaseClient client) { @SneakyThrows @ParameterizedTest @MethodSource("getClients") - public void lpush_lpop_existing_non_existing_key(BaseClient client) { + public void lpush_lpop_lrange_existing_non_existing_key(BaseClient client) { String key = UUID.randomUUID().toString(); String[] valueArray = new String[] {"value4", "value3", "value2", "value1"}; assertEquals(4, client.lpush(key, valueArray).get()); assertEquals("value1", client.lpop(key).get()); + assertArrayEquals(new String[] {"value2", "value3", "value4"}, client.lrange(key, 0, -1).get()); assertArrayEquals(new String[] {"value2", "value3"}, client.lpopCount(key, 2).get()); + assertArrayEquals(new String[] {}, client.lrange("non_existing_key", 0, -1).get()); assertNull(client.lpop("non_existing_key").get()); } @SneakyThrows @ParameterizedTest @MethodSource("getClients") - public void lpush_lpop_type_error(BaseClient client) { + public void lpush_lpop_lrange_type_error(BaseClient client) { String key = UUID.randomUUID().toString(); assertEquals(OK, client.set(key, "foo").get()); @@ -570,6 +575,51 @@ public void lpush_lpop_type_error(BaseClient client) { Exception lpopCountException = assertThrows(ExecutionException.class, () -> client.lpopCount(key, 2).get()); assertTrue(lpopCountException.getCause() instanceof RequestException); + + Exception lrangeException = + assertThrows(ExecutionException.class, () -> client.lrange(key, 0, -1).get()); + assertTrue(lrangeException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void ltrim_existing_non_existing_key_and_type_error(BaseClient client) { + String key = UUID.randomUUID().toString(); + String[] valueArray = new String[] {"value4", "value3", "value2", "value1"}; + + assertEquals(4, client.lpush(key, valueArray).get()); + assertEquals(OK, client.ltrim(key, 0, 1).get()); + assertArrayEquals(new String[] {"value1", "value2"}, client.lrange(key, 0, -1).get()); + + // `start` is greater than `end` so the key will be removed. + assertEquals(OK, client.ltrim(key, 4, 2).get()); + assertArrayEquals(new String[] {}, client.lrange(key, 0, -1).get()); + + assertEquals(OK, client.set(key, "foo").get()); + + Exception ltrimException = + assertThrows(ExecutionException.class, () -> client.ltrim(key, 0, 1).get()); + assertTrue(ltrimException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void llen_existing_non_existing_key_and_type_error(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + String[] valueArray = new String[] {"value4", "value3", "value2", "value1"}; + + assertEquals(4, client.lpush(key1, valueArray).get()); + assertEquals(4, client.llen(key1).get()); + assertEquals(0, client.llen("non_existing_key").get()); + + assertEquals(OK, client.set(key2, "foo").get()); + + Exception lrangeException = + assertThrows(ExecutionException.class, () -> client.llen(key2).get()); + assertTrue(lrangeException.getCause() instanceof RequestException); } @SneakyThrows @@ -664,4 +714,124 @@ public void exists_multiple_keys(BaseClient client) { client.exists(new String[] {key1, key2, key1, UUID.randomUUID().toString()}).get(); assertEquals(3L, existsKeysNum); } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void expire_pexpire_and_ttl_with_positive_timeout(BaseClient client) { + String key = UUID.randomUUID().toString(); + assertEquals(OK, client.set(key, "expire_timeout").get()); + assertTrue(client.expire(key, 10L).get()); + assertTrue(client.ttl(key).get() <= 10L); + + // set command clears the timeout. + assertEquals(OK, client.set(key, "pexpire_timeout").get()); + if (REDIS_VERSION.feature() < 7) { + assertTrue(client.pexpire(key, 10000L).get()); + } else { + assertTrue(client.pexpire(key, 10000L, ExpireOptions.HAS_NO_EXPIRY).get()); + } + assertTrue(client.ttl(key).get() <= 10L); + + // TTL will be updated to the new value = 15 + if (REDIS_VERSION.feature() < 7) { + assertTrue(client.expire(key, 15L).get()); + } else { + assertTrue(client.expire(key, 15L, ExpireOptions.HAS_EXISTING_EXPIRY).get()); + } + assertTrue(client.ttl(key).get() <= 15L); + } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void expireAt_pexpireAt_and_ttl_with_positive_timeout(BaseClient client) { + String key = UUID.randomUUID().toString(); + assertEquals(OK, client.set(key, "expireAt_timeout").get()); + assertTrue(client.expireAt(key, Instant.now().getEpochSecond() + 10L).get()); + assertTrue(client.ttl(key).get() <= 10L); + + // extend TTL + if (REDIS_VERSION.feature() < 7) { + assertTrue(client.expireAt(key, Instant.now().getEpochSecond() + 50L).get()); + } else { + assertTrue( + client + .expireAt( + key, + Instant.now().getEpochSecond() + 50L, + ExpireOptions.NEW_EXPIRY_GREATER_THAN_CURRENT) + .get()); + } + assertTrue(client.ttl(key).get() <= 50L); + + if (REDIS_VERSION.feature() < 7) { + assertTrue(client.pexpireAt(key, Instant.now().toEpochMilli() + 50000L).get()); + } else { + // set command clears the timeout. + assertEquals(OK, client.set(key, "pexpireAt_timeout").get()); + assertFalse( + client + .pexpireAt( + key, Instant.now().toEpochMilli() + 50000L, ExpireOptions.HAS_EXISTING_EXPIRY) + .get()); + } + } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void expire_pexpire_with_timestamp_in_the_past_or_negative_timeout(BaseClient client) { + String key = UUID.randomUUID().toString(); + + assertEquals(OK, client.set(key, "expire_with_past_timestamp").get()); + assertEquals(-1L, client.ttl(key).get()); + assertTrue(client.expire(key, -10L).get()); + assertEquals(-2L, client.ttl(key).get()); + + assertEquals(OK, client.set(key, "pexpire_with_past_timestamp").get()); + assertTrue(client.pexpire(key, -10000L).get()); + assertEquals(-2L, client.ttl(key).get()); + } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void expireAt_pexpireAt_with_timestamp_in_the_past_or_negative_timeout(BaseClient client) { + String key = UUID.randomUUID().toString(); + + assertEquals(OK, client.set(key, "expireAt_with_past_timestamp").get()); + // set timeout in the past + assertTrue(client.expireAt(key, Instant.now().getEpochSecond() - 50L).get()); + assertEquals(-2L, client.ttl(key).get()); + + assertEquals(OK, client.set(key, "pexpireAt_with_past_timestamp").get()); + // set timeout in the past + assertTrue(client.pexpireAt(key, Instant.now().toEpochMilli() - 50000L).get()); + assertEquals(-2L, client.ttl(key).get()); + } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void expire_pexpire_and_ttl_with_non_existing_key(BaseClient client) { + String key = UUID.randomUUID().toString(); + + assertFalse(client.expire(key, 10L).get()); + assertFalse(client.pexpire(key, 10000L).get()); + + assertEquals(-2L, client.ttl(key).get()); + } + + @SneakyThrows + @ParameterizedTest + @MethodSource("getClients") + public void expireAt_pexpireAt_and_ttl_with_non_existing_key(BaseClient client) { + String key = UUID.randomUUID().toString(); + + assertFalse(client.expireAt(key, Instant.now().getEpochSecond() + 10L).get()); + assertFalse(client.pexpireAt(key, Instant.now().toEpochMilli() + 10000L).get()); + + assertEquals(-2L, client.ttl(key).get()); + } } diff --git a/java/integTest/src/test/java/glide/TestUtilities.java b/java/integTest/src/test/java/glide/TestUtilities.java new file mode 100644 index 0000000000..8c18e8b98f --- /dev/null +++ b/java/integTest/src/test/java/glide/TestUtilities.java @@ -0,0 +1,26 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide; + +import static org.junit.jupiter.api.Assertions.fail; + +import glide.api.models.ClusterValue; +import lombok.experimental.UtilityClass; + +@UtilityClass +public class TestUtilities { + /** Extract integer parameter value from INFO command output */ + public static int getValueFromInfo(String data, String value) { + for (var line : data.split("\r\n")) { + if (line.contains(value)) { + return Integer.parseInt(line.split(":")[1]); + } + } + fail(); + return 0; + } + + /** Extract first value from {@link ClusterValue} assuming it contains a multi-value. */ + public static T getFirstEntryFromMultiValue(ClusterValue data) { + return data.getMultiValue().get(data.getMultiValue().keySet().toArray(String[]::new)[0]); + } +} diff --git a/java/integTest/src/test/java/glide/TransactionTestUtilities.java b/java/integTest/src/test/java/glide/TransactionTestUtilities.java index bcdbb8af78..94f54998bf 100644 --- a/java/integTest/src/test/java/glide/TransactionTestUtilities.java +++ b/java/integTest/src/test/java/glide/TransactionTestUtilities.java @@ -1,6 +1,8 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide; +import static glide.api.BaseClient.OK; + import glide.api.models.BaseTransaction; import glide.api.models.commands.SetOptions; import java.util.Map; @@ -61,7 +63,10 @@ public static BaseTransaction transactionTest(BaseTransaction baseTransact baseTransaction.hincrBy(key4, field3, 5); baseTransaction.hincrByFloat(key4, field3, 5.5); - baseTransaction.lpush(key5, new String[] {value1, value2, value3}); + baseTransaction.lpush(key5, new String[] {value1, value2, value3, value3}); + baseTransaction.llen(key5); + baseTransaction.ltrim(key5, 1, -1); + baseTransaction.lrange(key5, 0, -2); baseTransaction.lpop(key5); baseTransaction.lpopCount(key5, 2); @@ -74,12 +79,14 @@ public static BaseTransaction transactionTest(BaseTransaction baseTransact baseTransaction.scard(key7); baseTransaction.smembers(key7); + baseTransaction.configResetStat(); + return baseTransaction; } public static Object[] transactionTestResult() { return new Object[] { - "OK", + OK, value1, null, new String[] {value1, value2}, @@ -88,7 +95,7 @@ public static Object[] transactionTestResult() { null, 1L, null, - "OK", + OK, new String[] {value2, value1}, 1L, 3L, @@ -104,7 +111,10 @@ public static Object[] transactionTestResult() { 1L, 5L, 10.5, - 3L, + 4L, + 4L, + OK, + new String[] {value3, value2}, value3, new String[] {value2, value1}, 3L, @@ -114,6 +124,7 @@ public static Object[] transactionTestResult() { 1L, 1L, Set.of("baz"), + OK }; } } diff --git a/java/integTest/src/test/java/glide/cluster/ClientTests.java b/java/integTest/src/test/java/glide/cluster/ClientTests.java new file mode 100644 index 0000000000..3924747391 --- /dev/null +++ b/java/integTest/src/test/java/glide/cluster/ClientTests.java @@ -0,0 +1,48 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.cluster; + +import static glide.TestConfiguration.CLUSTER_PORTS; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import glide.api.RedisClusterClient; +import glide.api.models.configuration.NodeAddress; +import glide.api.models.configuration.RedisClusterClientConfiguration; +import glide.api.models.exceptions.ClosingException; +import java.util.concurrent.ExecutionException; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; + +public class ClientTests { + @Test + @SneakyThrows + public void custom_command_info() { + RedisClusterClient client = + RedisClusterClient.CreateClient( + RedisClusterClientConfiguration.builder() + .address(NodeAddress.builder().port(CLUSTER_PORTS[0]).build()) + .clientName("TEST_CLIENT_NAME") + .build()) + .get(); + + String clientInfo = + (String) client.customCommand(new String[] {"CLIENT", "INFO"}).get().getSingleValue(); + assertTrue(clientInfo.contains("name=TEST_CLIENT_NAME")); + } + + @Test + @SneakyThrows + public void close_client_throws_ExecutionException_with_ClosingException_cause() { + RedisClusterClient client = + RedisClusterClient.CreateClient( + RedisClusterClientConfiguration.builder() + .address(NodeAddress.builder().port(CLUSTER_PORTS[0]).build()) + .build()) + .get(); + + client.close(); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.set("foo", "bar").get()); + assertTrue(executionException.getCause() instanceof ClosingException); + } +} diff --git a/java/integTest/src/test/java/glide/cluster/CommandTests.java b/java/integTest/src/test/java/glide/cluster/CommandTests.java index 408cabb8a6..6af733d8dc 100644 --- a/java/integTest/src/test/java/glide/cluster/CommandTests.java +++ b/java/integTest/src/test/java/glide/cluster/CommandTests.java @@ -3,6 +3,9 @@ import static glide.TestConfiguration.CLUSTER_PORTS; import static glide.TestConfiguration.REDIS_VERSION; +import static glide.TestUtilities.getFirstEntryFromMultiValue; +import static glide.TestUtilities.getValueFromInfo; +import static glide.api.BaseClient.OK; import static glide.api.models.commands.InfoOptions.Section.CLIENTS; import static glide.api.models.commands.InfoOptions.Section.CLUSTER; import static glide.api.models.commands.InfoOptions.Section.COMMANDSTATS; @@ -10,12 +13,14 @@ import static glide.api.models.commands.InfoOptions.Section.EVERYTHING; import static glide.api.models.commands.InfoOptions.Section.MEMORY; import static glide.api.models.commands.InfoOptions.Section.REPLICATION; +import static glide.api.models.commands.InfoOptions.Section.STATS; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleRoute.ALL_NODES; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleRoute.ALL_PRIMARIES; import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleRoute.RANDOM; import static glide.api.models.configuration.RequestRoutingConfiguration.SlotType.PRIMARY; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import glide.api.RedisClusterClient; @@ -24,7 +29,9 @@ import glide.api.models.configuration.NodeAddress; import glide.api.models.configuration.RedisClusterClientConfiguration; import glide.api.models.configuration.RequestRoutingConfiguration.SlotKeyRoute; +import glide.api.models.exceptions.RequestException; import java.util.List; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import lombok.SneakyThrows; import org.junit.jupiter.api.AfterAll; @@ -274,4 +281,89 @@ public void info_with_multi_node_route_and_options() { } } } + + @Test + @SneakyThrows + public void clientId() { + var id = clusterClient.clientId().get(); + assertTrue(id > 0); + } + + @Test + @SneakyThrows + public void clientId_with_single_node_route() { + var data = clusterClient.clientId(RANDOM).get(); + assertTrue(data.getSingleValue() > 0L); + } + + @Test + @SneakyThrows + public void clientId_with_multi_node_route() { + var data = clusterClient.clientId(ALL_NODES).get(); + data.getMultiValue().values().forEach(id -> assertTrue(id > 0)); + } + + @Test + @SneakyThrows + public void clientGetName() { + // TODO replace with the corresponding command once implemented + clusterClient.customCommand(new String[] {"client", "setname", "clientGetName"}).get(); + + var name = clusterClient.clientGetName().get(); + + assertEquals("clientGetName", name); + } + + @Test + @SneakyThrows + public void clientGetName_with_single_node_route() { + // TODO replace with the corresponding command once implemented + clusterClient + .customCommand( + new String[] {"client", "setname", "clientGetName_with_single_node_route"}, ALL_NODES) + .get(); + + var name = clusterClient.clientGetName(RANDOM).get(); + + assertEquals("clientGetName_with_single_node_route", name.getSingleValue()); + } + + @Test + @SneakyThrows + public void clientGetName_with_multi_node_route() { + // TODO replace with the corresponding command once implemented + clusterClient + .customCommand( + new String[] {"client", "setname", "clientGetName_with_multi_node_route"}, ALL_NODES) + .get(); + + var name = clusterClient.clientGetName(ALL_NODES).get(); + + assertEquals("clientGetName_with_multi_node_route", getFirstEntryFromMultiValue(name)); + } + + @Test + @SneakyThrows + public void config_reset_stat() { + var data = clusterClient.info(InfoOptions.builder().section(STATS).build()).get(); + String firstNodeInfo = getFirstEntryFromMultiValue(data); + int value_before = getValueFromInfo(firstNodeInfo, "total_net_input_bytes"); + + var result = clusterClient.configResetStat().get(); + assertEquals(OK, result); + + data = clusterClient.info(InfoOptions.builder().section(STATS).build()).get(); + firstNodeInfo = getFirstEntryFromMultiValue(data); + int value_after = getValueFromInfo(firstNodeInfo, "total_net_input_bytes"); + assertTrue(value_after < value_before); + } + + @Test + @SneakyThrows + public void config_rewrite_non_existent_config_file() { + // The setup for the Integration Tests server does not include a configuration file for Redis. + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> clusterClient.configRewrite().get()); + assertTrue(executionException.getCause() instanceof RequestException); + } } diff --git a/java/integTest/src/test/java/glide/standalone/ClientTests.java b/java/integTest/src/test/java/glide/standalone/ClientTests.java new file mode 100644 index 0000000000..960696b06e --- /dev/null +++ b/java/integTest/src/test/java/glide/standalone/ClientTests.java @@ -0,0 +1,47 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.standalone; + +import static glide.TestConfiguration.STANDALONE_PORTS; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import glide.api.RedisClient; +import glide.api.models.configuration.NodeAddress; +import glide.api.models.configuration.RedisClientConfiguration; +import glide.api.models.exceptions.ClosingException; +import java.util.concurrent.ExecutionException; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; + +public class ClientTests { + @Test + @SneakyThrows + public void custom_command_info() { + RedisClient client = + RedisClient.CreateClient( + RedisClientConfiguration.builder() + .address(NodeAddress.builder().port(STANDALONE_PORTS[0]).build()) + .clientName("TEST_CLIENT_NAME") + .build()) + .get(); + + String clientInfo = (String) client.customCommand(new String[] {"CLIENT", "INFO"}).get(); + assertTrue(clientInfo.contains("name=TEST_CLIENT_NAME")); + } + + @Test + @SneakyThrows + public void close_client_throws_ExecutionException_with_ClosingException_cause() { + RedisClient client = + RedisClient.CreateClient( + RedisClientConfiguration.builder() + .address(NodeAddress.builder().port(STANDALONE_PORTS[0]).build()) + .build()) + .get(); + + client.close(); + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.set("key", "value").get()); + assertTrue(executionException.getCause() instanceof ClosingException); + } +} diff --git a/java/integTest/src/test/java/glide/standalone/CommandTests.java b/java/integTest/src/test/java/glide/standalone/CommandTests.java index 747c6e152d..5ded6e00ce 100644 --- a/java/integTest/src/test/java/glide/standalone/CommandTests.java +++ b/java/integTest/src/test/java/glide/standalone/CommandTests.java @@ -3,11 +3,13 @@ import static glide.TestConfiguration.REDIS_VERSION; import static glide.TestConfiguration.STANDALONE_PORTS; +import static glide.TestUtilities.getValueFromInfo; import static glide.api.BaseClient.OK; import static glide.api.models.commands.InfoOptions.Section.CLUSTER; import static glide.api.models.commands.InfoOptions.Section.CPU; import static glide.api.models.commands.InfoOptions.Section.EVERYTHING; import static glide.api.models.commands.InfoOptions.Section.MEMORY; +import static glide.api.models.commands.InfoOptions.Section.STATS; import static glide.cluster.CommandTests.DEFAULT_INFO_SECTIONS; import static glide.cluster.CommandTests.EVERYTHING_INFO_SECTIONS; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -142,4 +144,45 @@ public void select_test_gives_error() { assertThrows(ExecutionException.class, () -> regularClient.select(-1).get()); assertTrue(e.getCause() instanceof RequestException); } + + @Test + @SneakyThrows + public void clientId() { + var id = regularClient.clientId().get(); + assertTrue(id > 0); + } + + @Test + @SneakyThrows + public void clientGetName() { + // TODO replace with the corresponding command once implemented + regularClient.customCommand(new String[] {"client", "setname", "clientGetName"}).get(); + + var name = regularClient.clientGetName().get(); + + assertEquals("clientGetName", name); + } + + @Test + @SneakyThrows + public void config_reset_stat() { + String data = regularClient.info(InfoOptions.builder().section(STATS).build()).get(); + int value_before = getValueFromInfo(data, "total_net_input_bytes"); + + var result = regularClient.configResetStat().get(); + assertEquals(OK, result); + + data = regularClient.info(InfoOptions.builder().section(STATS).build()).get(); + int value_after = getValueFromInfo(data, "total_net_input_bytes"); + assertTrue(value_after < value_before); + } + + @Test + @SneakyThrows + public void config_rewrite_non_existent_config_file() { + // The setup for the Integration Tests server does not include a configuration file for Redis. + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> regularClient.configRewrite().get()); + assertTrue(executionException.getCause() instanceof RequestException); + } } diff --git a/java/src/lib.rs b/java/src/lib.rs index cb381b0ebb..32b88969b2 100644 --- a/java/src/lib.rs +++ b/java/src/lib.rs @@ -62,7 +62,7 @@ fn redis_value_to_java<'local>(env: &mut JNIEnv<'local>, val: Value) -> JObject< hashmap } Value::Double(float) => env - .new_object("java/lang/Double", "(D)V", &[float.into_inner().into()]) + .new_object("java/lang/Double", "(D)V", &[float.into()]) .unwrap(), Value::Boolean(bool) => env .new_object("java/lang/Boolean", "(Z)V", &[bool.into()]) diff --git a/node/DEVELOPER.md b/node/DEVELOPER.md index dd596d5db1..e903a6191d 100644 --- a/node/DEVELOPER.md +++ b/node/DEVELOPER.md @@ -12,10 +12,9 @@ The GLIDE Node wrapper consists of both TypeScript and Rust code. Rust bindings Software Dependencies -> Note: Currently, we only support npm major version 8. f you have a later version installed, you can downgrade it with `npm i -g npm@8`. > If your NodeJS version is below the supported version specified in the client's [documentation](https://github.com/aws/glide-for-redis/blob/main/node/README.md#nodejs-supported-version), you can upgrade it using [NVM](https://github.com/nvm-sh/nvm?tab=readme-ov-file#install--update-script). -- npm v8 +- npm - git - GCC - pkg-config @@ -29,7 +28,6 @@ Software Dependencies ```bash sudo apt update -y sudo apt install -y nodejs npm git gcc pkg-config protobuf-compiler openssl libssl-dev -npm i -g npm@8 curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source "$HOME/.cargo/env" ``` @@ -39,7 +37,6 @@ source "$HOME/.cargo/env" ```bash sudo yum update -y sudo yum install -y nodejs git gcc pkgconfig protobuf-compiler openssl openssl-devel gettext -npm i -g npm@8 curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source "$HOME/.cargo/env" ``` @@ -49,7 +46,6 @@ source "$HOME/.cargo/env" ```bash brew update brew install nodejs git gcc pkgconfig protobuf openssl -npm i -g npm@8 curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source "$HOME/.cargo/env" ``` @@ -113,6 +109,10 @@ Before starting this step, make sure you've installed all software requirments. > Note: Once building completed, you'll find the compiled JavaScript code in the `node/build-ts` folder. +### Troubleshooting + +- If the build fails after running `npx tsc` because `glide-rs` isn't found, check if your npm version is in the range 9.0.0-9.4.1, and if so, upgrade it. 9.4.2 contains a fix to a change introduced in 9.0.0 that is required in order to build the library. + ### Test To run tests, use the following command: diff --git a/node/THIRD_PARTY_LICENSES_NODE b/node/THIRD_PARTY_LICENSES_NODE index d0a094c8ba..86a5f9f5a8 100644 --- a/node/THIRD_PARTY_LICENSES_NODE +++ b/node/THIRD_PARTY_LICENSES_NODE @@ -683,7 +683,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: ahash:0.8.9 +Package: ahash:0.8.11 The following copyrights and licenses were found in the source code of this package: @@ -1651,7 +1651,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: arc-swap:1.6.0 +Package: arc-swap:1.7.0 The following copyrights and licenses were found in the source code of this package: @@ -5945,7 +5945,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: crossbeam-channel:0.5.11 +Package: crossbeam-channel:0.5.12 The following copyrights and licenses were found in the source code of this package: @@ -6403,7 +6403,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: ctor:0.2.6 +Package: ctor:0.2.7 The following copyrights and licenses were found in the source code of this package: @@ -13048,7 +13048,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: hermit-abi:0.3.8 +Package: hermit-abi:0.3.9 The following copyrights and licenses were found in the source code of this package: @@ -14927,7 +14927,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: js-sys:0.3.68 +Package: js-sys:0.3.69 The following copyrights and licenses were found in the source code of this package: @@ -15614,7 +15614,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: libloading:0.8.1 +Package: libloading:0.8.3 The following copyrights and licenses were found in the source code of this package: @@ -16335,7 +16335,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: log:0.4.20 +Package: log:0.4.21 The following copyrights and licenses were found in the source code of this package: @@ -17073,7 +17073,7 @@ the following restrictions: ---- -Package: mio:0.8.10 +Package: mio:0.8.11 The following copyrights and licenses were found in the source code of this package: @@ -17098,7 +17098,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: napi:2.15.4 +Package: napi:2.16.0 The following copyrights and licenses were found in the source code of this package: @@ -17148,7 +17148,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: napi-derive:2.15.3 +Package: napi-derive:2.16.0 The following copyrights and licenses were found in the source code of this package: @@ -17173,7 +17173,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: napi-derive-backend:1.0.61 +Package: napi-derive-backend:1.0.62 The following copyrights and licenses were found in the source code of this package: @@ -20046,7 +20046,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: pin-project:1.1.4 +Package: pin-project:1.1.5 The following copyrights and licenses were found in the source code of this package: @@ -20275,7 +20275,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: pin-project-internal:1.1.4 +Package: pin-project-internal:1.1.5 The following copyrights and licenses were found in the source code of this package: @@ -22336,7 +22336,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: protobuf:3.3.0 +Package: protobuf:3.4.0 The following copyrights and licenses were found in the source code of this package: @@ -22361,7 +22361,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: protobuf-support:3.3.0 +Package: protobuf-support:3.4.0 The following copyrights and licenses were found in the source code of this package: @@ -23612,7 +23612,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: regex-automata:0.4.5 +Package: regex-automata:0.4.6 The following copyrights and licenses were found in the source code of this package: @@ -25234,7 +25234,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-pemfile:2.1.0 +Package: rustls-pemfile:2.1.1 The following copyrights and licenses were found in the source code of this package: @@ -25477,7 +25477,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-pki-types:1.3.0 +Package: rustls-pki-types:1.3.1 The following copyrights and licenses were found in the source code of this package: @@ -29097,7 +29097,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: syn:2.0.50 +Package: syn:2.0.52 The following copyrights and licenses were found in the source code of this package: @@ -34062,7 +34062,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen:0.2.91 +Package: wasm-bindgen:0.2.92 The following copyrights and licenses were found in the source code of this package: @@ -34291,7 +34291,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen-backend:0.2.91 +Package: wasm-bindgen-backend:0.2.92 The following copyrights and licenses were found in the source code of this package: @@ -34520,7 +34520,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen-macro:0.2.91 +Package: wasm-bindgen-macro:0.2.92 The following copyrights and licenses were found in the source code of this package: @@ -34749,7 +34749,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen-macro-support:0.2.91 +Package: wasm-bindgen-macro-support:0.2.92 The following copyrights and licenses were found in the source code of this package: @@ -34978,7 +34978,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen-shared:0.2.91 +Package: wasm-bindgen-shared:0.2.92 The following copyrights and licenses were found in the source code of this package: @@ -36810,7 +36810,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows-targets:0.52.3 +Package: windows-targets:0.52.4 The following copyrights and licenses were found in the source code of this package: @@ -37268,7 +37268,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_aarch64_gnullvm:0.52.3 +Package: windows_aarch64_gnullvm:0.52.4 The following copyrights and licenses were found in the source code of this package: @@ -37726,7 +37726,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_aarch64_msvc:0.52.3 +Package: windows_aarch64_msvc:0.52.4 The following copyrights and licenses were found in the source code of this package: @@ -38184,7 +38184,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_gnu:0.52.3 +Package: windows_i686_gnu:0.52.4 The following copyrights and licenses were found in the source code of this package: @@ -38642,7 +38642,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_msvc:0.52.3 +Package: windows_i686_msvc:0.52.4 The following copyrights and licenses were found in the source code of this package: @@ -39100,7 +39100,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_gnu:0.52.3 +Package: windows_x86_64_gnu:0.52.4 The following copyrights and licenses were found in the source code of this package: @@ -39558,7 +39558,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_gnullvm:0.52.3 +Package: windows_x86_64_gnullvm:0.52.4 The following copyrights and licenses were found in the source code of this package: @@ -40016,7 +40016,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_msvc:0.52.3 +Package: windows_x86_64_msvc:0.52.4 The following copyrights and licenses were found in the source code of this package: @@ -41602,7 +41602,7 @@ THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---- -Package: @types:node:20.11.20 +Package: @types:node:20.11.24 The following copyrights and licenses were found in the source code of this package: diff --git a/node/jest.config.js b/node/jest.config.js index b83febd3e8..a5a0e5c267 100644 --- a/node/jest.config.js +++ b/node/jest.config.js @@ -4,4 +4,5 @@ module.exports = { testEnvironment: "node", testRegex: "/tests/.*\\.(test|spec)?\\.(ts|tsx)$", moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], + testTimeout: 20000, }; diff --git a/node/package.json b/node/package.json index b943253f51..d3fc6932d1 100644 --- a/node/package.json +++ b/node/package.json @@ -12,6 +12,7 @@ "glide-rs": "file:rust-client", "long": "^5.2.3", "npmignore": "^0.3.0", + "prettier": "^3.2.5", "protobufjs": "^7.2.2" }, "bundleDependencies": [ @@ -32,7 +33,9 @@ "test": "jest --verbose --runInBand --testPathIgnorePatterns='RedisModules'", "lint": "eslint -f unix \"src/**/*.{ts,tsx}\"", "prepack": "npmignore --auto", - "test-modules": "jest --verbose --runInBand 'tests/RedisModules.test.ts'" + "test-modules": "jest --verbose --runInBand 'tests/RedisModules.test.ts'", + "prettier:check:ci": "./node_modules/.bin/prettier --check . --ignore-unknown '!**/*.{js,d.ts}'", + "prettier:format": "./node_modules/.bin/prettier --write . --ignore-unknown '!**/*.{js,d.ts}'" }, "devDependencies": { "@babel/preset-env": "^7.20.2", @@ -70,8 +73,12 @@ "//": [ "The fields below have been commented out and are only necessary for publishing the package." ], - "///cpu": ["${node_arch}"], - "///os": ["${node_os}"], + "///cpu": [ + "${node_arch}" + ], + "///os": [ + "${node_os}" + ], "///name": "${scope}${pkg_name}", "///version": "${package_version}" } diff --git a/node/rust-client/src/lib.rs b/node/rust-client/src/lib.rs index 78d7e7196b..3b8c1fd384 100644 --- a/node/rust-client/src/lib.rs +++ b/node/rust-client/src/lib.rs @@ -184,9 +184,7 @@ fn redis_value_to_js(val: Value, js_env: Env) -> Result { } Ok(obj.into_unknown()) } - Value::Double(float) => js_env - .create_double(float.into()) - .map(|val| val.into_unknown()), + Value::Double(float) => js_env.create_double(float).map(|val| val.into_unknown()), Value::Boolean(bool) => js_env.get_boolean(bool).map(|val| val.into_unknown()), // format is ignored, as per the RESP3 recommendations - // "Normal client libraries may ignore completely the difference between this" @@ -330,7 +328,7 @@ pub fn create_leaked_bigint(big_int: BigInt) -> [u32; 2] { /// Should NOT be used in production. #[cfg(feature = "testing_utilities")] pub fn create_leaked_double(float: f64) -> [u32; 2] { - let pointer = Box::leak(Box::new(Value::Double(float.into()))) as *mut Value; + let pointer = Box::leak(Box::new(Value::Double(float))) as *mut Value; split_pointer(pointer) } diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index a73c40f47e..61454210d0 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -18,6 +18,7 @@ import { createDecr, createDecrBy, createDel, + createEcho, createExists, createExpire, createExpireAt, @@ -31,6 +32,7 @@ import { createHLen, createHMGet, createHSet, + createHvals, createIncr, createIncrBy, createIncrByFloat, @@ -45,6 +47,7 @@ import { createMSet, createPExpire, createPExpireAt, + createPttl, createRPop, createRPush, createSAdd, @@ -631,6 +634,16 @@ export class BaseClient { return this.createWritePromise(createHLen(key)); } + /** Returns all values in the hash stored at key. + * See https://redis.io/commands/hvals/ for more details. + * + * @param key - The key of the hash. + * @returns a list of values in the hash, or an empty list when the key does not exist. + */ + public hvals(key: string): Promise { + return this.createWritePromise(createHvals(key)); + } + /** Inserts all the specified values at the head of the list stored at `key`. * `elements` are inserted one after the other to the head of the list, from the leftmost element to the rightmost element. * If `key` does not exist, it is created as empty list before performing the push operations. @@ -1138,6 +1151,26 @@ export class BaseClient { return this.createWritePromise(createZpopmax(key, count)); } + /** Echoes the provided `message` back. + * See https://redis.io/commands/echo for more details. + * + * @param message - The message to be echoed back. + * @returns The provided `message`. + */ + public echo(message: string): Promise { + return this.createWritePromise(createEcho(message)); + } + + /** Returns the remaining time to live of `key` that has a timeout, in milliseconds. + * See https://redis.io/commands/pttl for more details. + * + * @param key - The key to return its timeout. + * @returns TTL in milliseconds. -2 if `key` does not exist, -1 if `key` exists but has no associated expire. + */ + public pttl(key: string): Promise { + return this.createWritePromise(createPttl(key)); + } + private readonly MAP_READ_FROM_STRATEGY: Record< ReadFrom, connection_request.ReadFrom diff --git a/node/src/Commands.ts b/node/src/Commands.ts index ffd24f4731..b70685b001 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -592,6 +592,13 @@ export function createHLen(key: string): redis_request.Command { return createCommand(RequestType.HLen, [key]); } +/** + * @internal + */ +export function createHvals(key: string): redis_request.Command { + return createCommand(RequestType.Hvals, [key]); +} + /** * @internal */ @@ -863,3 +870,17 @@ export function createZpopmax(key: string, count?: number): redis_request.Comman const args: string[] = count == undefined ? [key] : [key, count.toString()]; return createCommand(RequestType.ZPopMax, args); } + +/** + * @internal + */ +export function createEcho(message: string): redis_request.Command { + return createCommand(RequestType.Echo, [message]); +} + +/** + * @internal + */ +export function createPttl(key: string): redis_request.Command { + return createCommand(RequestType.PTTL, [key]); +} diff --git a/node/src/RedisClusterClient.ts b/node/src/RedisClusterClient.ts index f35b2bfd85..06091a3c64 100644 --- a/node/src/RedisClusterClient.ts +++ b/node/src/RedisClusterClient.ts @@ -16,6 +16,7 @@ import { createInfo, createPing, } from "./Commands"; +import { RequestError } from "./Errors"; import { connection_request, redis_request } from "./ProtobufMessage"; import { ClusterTransaction } from "./Transaction"; @@ -56,11 +57,11 @@ export type SlotKeyTypes = { export type RouteByAddress = { type: "routeByAddress"; /** - * DNS name of the host. + *The endpoint of the node. If `port` is not provided, should be in the `${address}:${port}` format, where `address` is the preferred endpoint as shown in the output of the `CLUSTER SLOTS` command. */ host: string; /** - * The port to access on the node. If port is not provided, `host` is assumed to be in the format `{hostname}:{port}`. + * The port to access on the node. If port is not provided, `host` is assumed to be in the format `${address}:${port}`. */ port?: number; }; @@ -146,7 +147,7 @@ function toProtobufRoute( const split = host.split(":"); if (split.length !== 2) { - throw new Error( + throw new RequestError( "No port provided, expected host to be formatted as `{hostname}:{port}`. Received " + host ); @@ -243,7 +244,7 @@ export class RedisClusterClient extends BaseClient { /** Ping the Redis server. * See https://redis.io/commands/ping/ for details. * - * @param message - An optional message to include in the PING command. + * @param message - An optional message to include in the PING command. * If not provided, the server will respond with "PONG". * If provided, the server will respond with a copy of the message. * @param route - The command will be routed to all primaries, unless `route` is provided, in which @@ -251,7 +252,10 @@ export class RedisClusterClient extends BaseClient { * @returns - "PONG" if `message` is not provided, otherwise return a copy of `message`. */ public ping(message?: string, route?: Routes): Promise { - return this.createWritePromise(createPing(message), toProtobufRoute(route)); + return this.createWritePromise( + createPing(message), + toProtobufRoute(route) + ); } /** Get information and statistics about the Redis server. diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index cf6f3826b9..de8a087703 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -18,6 +18,7 @@ import { createDecr, createDecrBy, createDel, + createEcho, createExists, createExpire, createExpireAt, @@ -31,6 +32,7 @@ import { createHLen, createHMGet, createHSet, + createHvals, createIncr, createIncrBy, createIncrByFloat, @@ -47,6 +49,7 @@ import { createPExpire, createPExpireAt, createPing, + createPttl, createRPop, createRPush, createSAdd, @@ -425,6 +428,17 @@ export class BaseTransaction> { return this.addAndReturn(createHLen(key)); } + /** Returns all values in the hash stored at key. + * See https://redis.io/commands/hvals/ for more details. + * + * @param key - The key of the hash. + * + * Command Response - a list of values in the hash, or an empty list when the key does not exist. + */ + public hvals(key: string): T { + return this.addAndReturn(createHvals(key)); + } + /** Inserts all the specified values at the head of the list stored at `key`. * `elements` are inserted one after the other to the head of the list, from the leftmost element to the rightmost element. * If `key` does not exist, it is created as empty list before performing the push operations. @@ -900,6 +914,28 @@ export class BaseTransaction> { return this.addAndReturn(createZpopmax(key, count)); } + /** Echoes the provided `message` back. + * See https://redis.io/commands/echo for more details. + * + * @param message - The message to be echoed back. + * + * Command Response - The provided `message`. + */ + public echo(message: string): T { + return this.addAndReturn(createEcho(message)); + } + + /** Returns the remaining time to live of `key` that has a timeout, in milliseconds. + * See https://redis.io/commands/pttl for more details. + * + * @param key - The key to return its timeout. + * + * Command Response - TTL in milliseconds. -2 if `key` does not exist, -1 if `key` exists but has no associated expire. + */ + public pttl(key: string): T { + return this.addAndReturn(createPttl(key)); + } + /** Executes a single command, without checking inputs. Every part of the command, including subcommands, * should be added as a separate value in args. * diff --git a/node/tests/RedisClient.test.ts b/node/tests/RedisClient.test.ts index 5c83c18917..451ab214a9 100644 --- a/node/tests/RedisClient.test.ts +++ b/node/tests/RedisClient.test.ts @@ -11,7 +11,6 @@ import { it, } from "@jest/globals"; import { BufferReader, BufferWriter } from "protobufjs"; -import RedisServer from "redis-server"; import { v4 as uuidv4 } from "uuid"; import { BaseClientConfiguration, @@ -21,41 +20,32 @@ import { } from ".."; import { redis_request } from "../src/ProtobufMessage"; import { runBaseTests } from "./SharedTests"; -import { flushallOnPort, transactionTest } from "./TestUtilities"; +import { RedisCluster, flushallOnPort, transactionTest } from "./TestUtilities"; /* eslint-disable @typescript-eslint/no-var-requires */ -const FreePort = require("find-free-port"); type Context = { client: RedisClient; }; -const PORT_NUMBER = 3000; +const TIMEOUT = 10000; describe("RedisClient", () => { - let server: RedisServer; + let testsFailed = 0; + let cluster: RedisCluster; let port: number; beforeAll(async () => { - port = await FreePort(PORT_NUMBER).then( - ([free_port]: number[]) => free_port - ); - server = await new Promise((resolve, reject) => { - const server = new RedisServer(port); - server.open(async (err: Error | null) => { - if (err) { - reject(err); - } - - resolve(server); - }); - }); - }); + cluster = await RedisCluster.createCluster(false, 1, 1); + port = cluster.ports()[0]; + }, 20000); afterEach(async () => { - await flushallOnPort(port); + await flushallOnPort(cluster.ports()[0]); }); - afterAll(() => { - server.close(); + afterAll(async () => { + if (testsFailed === 0) { + await cluster.close(); + } }); const getAddress = (port: number) => { @@ -204,12 +194,17 @@ describe("RedisClient", () => { const options = getOptions(port, protocol); options.protocol = protocol; options.clientName = clientName; + testsFailed += 1; const client = await RedisClient.createClient(options); - return { client, context: { client } }; }, - close: async (context: Context) => { + close: (context: Context, testSucceeded: boolean) => { + if (testSucceeded) { + testsFailed -= 1; + } + context.client.close(); }, + timeout: TIMEOUT, }); }); diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index 43533434b9..51aca01470 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -35,7 +35,7 @@ describe("RedisClusterClient", () => { let testsFailed = 0; let cluster: RedisCluster; beforeAll(async () => { - cluster = await RedisCluster.createCluster(3, 0); + cluster = await RedisCluster.createCluster(true, 3, 0); }, 20000); afterEach(async () => { diff --git a/node/tests/RedisModules.test.ts b/node/tests/RedisModules.test.ts index e6c1d33378..506c572c61 100644 --- a/node/tests/RedisModules.test.ts +++ b/node/tests/RedisModules.test.ts @@ -34,7 +34,12 @@ describe("RedisModules", () => { arg.startsWith("--load-module=") ); const loadModuleValues = loadModuleArgs.map((arg) => arg.split("=")[1]); - cluster = await RedisCluster.createCluster(3, 0, loadModuleValues); + cluster = await RedisCluster.createCluster( + true, + 3, + 0, + loadModuleValues + ); }, 20000); afterEach(async () => { diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index ef6ae73aa8..3920334eb4 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -772,6 +772,28 @@ export function runBaseTests(config: { config.timeout ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `hvals test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key1 = uuidv4(); + const field1 = uuidv4(); + const field2 = uuidv4(); + const fieldValueMap = { + [field1]: "value1", + [field2]: "value2", + }; + + expect(await client.hset(key1, fieldValueMap)).toEqual(2); + expect(await client.hvals(key1)).toEqual(["value1", "value2"]); + expect(await client.hdel(key1, [field1])).toEqual(1); + expect(await client.hvals(key1)).toEqual(["value2"]); + expect(await client.hvals("nonExistingHash")).toEqual([]); + }, protocol); + }, + config.timeout + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `lpush, lpop and lrange with existing and non existing key_%p`, async (protocol) => { @@ -1481,7 +1503,7 @@ export function runBaseTests(config: { "positiveInfinity" ) ).toEqual(0); - + expect(await client.set(key2, "foo")).toEqual("OK"); await expect( client.zcount(key2, "negativeInfinity", "positiveInfinity") @@ -1532,6 +1554,17 @@ export function runBaseTests(config: { config.timeout ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `echo test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const message = uuidv4(); + expect(await client.echo(message)).toEqual(message); + }, protocol); + }, + config.timeout + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `strlen test_%p`, async (protocol) => { @@ -1618,6 +1651,35 @@ export function runBaseTests(config: { }, config.timeout ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `Pttl test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient) => { + const key = uuidv4(); + expect(await client.pttl(key)).toEqual(-2); + + expect(await client.set(key, "value")).toEqual("OK"); + expect(await client.pttl(key)).toEqual(-1); + + expect(await client.expire(key, 10)).toEqual(true); + let result = await client.pttl(key); + expect(result).toBeGreaterThan(0); + expect(result).toBeLessThanOrEqual(10000); + + expect(await client.expireAt(key, Math.floor(Date.now() / 1000) + 20)).toEqual(true); + result = await client.pttl(key); + expect(result).toBeGreaterThan(0); + expect(result).toBeLessThanOrEqual(20000); + + expect(await client.pexpireAt(key, Date.now() + 30000)).toEqual(true); + result = await client.pttl(key); + expect(result).toBeGreaterThan(0); + expect(result).toBeLessThanOrEqual(30000); + }, protocol); + }, + config.timeout + ); } export function runCommonTests(config: { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index fadf6e5fde..f468e83921 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -68,6 +68,8 @@ export function transactionTest( args.push("OK"); baseTransaction.type(key1); args.push("string"); + baseTransaction.echo(value); + args.push(value); baseTransaction.set(key2, "baz", { returnOldValue: true, }); @@ -86,6 +88,8 @@ export function transactionTest( args.push(1); baseTransaction.hlen(key4); args.push(1); + baseTransaction.hvals(key4); + args.push([value]); baseTransaction.hget(key4, field); args.push(value); baseTransaction.hgetall(key4); @@ -144,9 +148,9 @@ export function transactionTest( baseTransaction.zcount(key8, { bound: 2 }, "positiveInfinity"); args.push(2); baseTransaction.zpopmin(key8); - args.push({"member2": 3.0}); + args.push({ member2: 3.0 }); baseTransaction.zpopmax(key8); - args.push({"member3": 3.5}); + args.push({ member3: 3.5 }); return args; } @@ -185,12 +189,17 @@ export class RedisCluster { } public static createCluster( + cluster_mode: boolean, shardCount: number, replicaCount: number, loadModule?: string[] ): Promise { return new Promise((resolve, reject) => { - let command = `python3 ../utils/cluster_manager.py start --cluster-mode -r ${replicaCount} -n ${shardCount}`; + let command = `python3 ../utils/cluster_manager.py start -r ${replicaCount} -n ${shardCount}`; + + if (cluster_mode) { + command += " --cluster-mode"; + } if (loadModule) { if (loadModule.length === 0) { diff --git a/python/Cargo.toml b/python/Cargo.toml index 24c14fbbc9..d8a0e3c6a8 100644 --- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -12,12 +12,9 @@ crate-type = ["cdylib"] [dependencies] pyo3 = { version = "^0.20", features = ["extension-module", "num-bigint"] } -pyo3-asyncio = { version = "^0.20", features = ["tokio-runtime"] } redis = { path = "../submodules/redis-rs/redis", features = ["aio", "tokio-comp", "connection-manager","tokio-rustls-comp"] } glide-core = { path = "../glide-core" } -tokio = { version = "^1", features = ["rt", "macros", "rt-multi-thread", "time"] } logger_core = {path = "../logger_core"} -tracing-subscriber = "0.3.16" [package.metadata.maturin] python-source = "python" diff --git a/python/THIRD_PARTY_LICENSES_PYTHON b/python/THIRD_PARTY_LICENSES_PYTHON index c78134feed..5d847a0c42 100644 --- a/python/THIRD_PARTY_LICENSES_PYTHON +++ b/python/THIRD_PARTY_LICENSES_PYTHON @@ -683,7 +683,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: ahash:0.8.9 +Package: ahash:0.8.11 The following copyrights and licenses were found in the source code of this package: @@ -1599,7 +1599,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: arc-swap:1.6.0 +Package: arc-swap:1.7.0 The following copyrights and licenses were found in the source code of this package: @@ -5816,7 +5816,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: crossbeam-channel:0.5.11 +Package: crossbeam-channel:0.5.12 The following copyrights and licenses were found in the source code of this package: @@ -12919,7 +12919,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: hermit-abi:0.3.8 +Package: hermit-abi:0.3.9 The following copyrights and licenses were found in the source code of this package: @@ -15027,7 +15027,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: js-sys:0.3.68 +Package: js-sys:0.3.69 The following copyrights and licenses were found in the source code of this package: @@ -16417,7 +16417,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: log:0.4.20 +Package: log:0.4.21 The following copyrights and licenses were found in the source code of this package: @@ -17180,7 +17180,7 @@ the following restrictions: ---- -Package: mio:0.8.10 +Package: mio:0.8.11 The following copyrights and licenses were found in the source code of this package: @@ -20028,7 +20028,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: pin-project:1.1.4 +Package: pin-project:1.1.5 The following copyrights and licenses were found in the source code of this package: @@ -20257,7 +20257,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: pin-project-internal:1.1.4 +Package: pin-project-internal:1.1.5 The following copyrights and licenses were found in the source code of this package: @@ -22547,7 +22547,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: protobuf:3.3.0 +Package: protobuf:3.4.0 The following copyrights and licenses were found in the source code of this package: @@ -22572,7 +22572,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: protobuf-support:3.3.0 +Package: protobuf-support:3.4.0 The following copyrights and licenses were found in the source code of this package: @@ -22826,214 +22826,6 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: pyo3-asyncio:0.20.0 - -The following copyrights and licenses were found in the source code of this package: - - Apache License - Version 2.0, January 2004 - http://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 - - http://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: pyo3-build-config:0.20.3 The following copyrights and licenses were found in the source code of this package: @@ -26111,7 +25903,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-pemfile:2.1.0 +Package: rustls-pemfile:2.1.1 The following copyrights and licenses were found in the source code of this package: @@ -26354,7 +26146,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: rustls-pki-types:1.3.0 +Package: rustls-pki-types:1.3.1 The following copyrights and licenses were found in the source code of this package: @@ -29745,7 +29537,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: syn:2.0.50 +Package: syn:2.0.52 The following copyrights and licenses were found in the source code of this package: @@ -34476,7 +34268,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen:0.2.91 +Package: wasm-bindgen:0.2.92 The following copyrights and licenses were found in the source code of this package: @@ -34705,7 +34497,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen-backend:0.2.91 +Package: wasm-bindgen-backend:0.2.92 The following copyrights and licenses were found in the source code of this package: @@ -34934,7 +34726,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen-macro:0.2.91 +Package: wasm-bindgen-macro:0.2.92 The following copyrights and licenses were found in the source code of this package: @@ -35163,7 +34955,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen-macro-support:0.2.91 +Package: wasm-bindgen-macro-support:0.2.92 The following copyrights and licenses were found in the source code of this package: @@ -35392,7 +35184,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: wasm-bindgen-shared:0.2.91 +Package: wasm-bindgen-shared:0.2.92 The following copyrights and licenses were found in the source code of this package: @@ -37224,7 +37016,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows-targets:0.52.3 +Package: windows-targets:0.52.4 The following copyrights and licenses were found in the source code of this package: @@ -37682,7 +37474,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_aarch64_gnullvm:0.52.3 +Package: windows_aarch64_gnullvm:0.52.4 The following copyrights and licenses were found in the source code of this package: @@ -38140,7 +37932,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_aarch64_msvc:0.52.3 +Package: windows_aarch64_msvc:0.52.4 The following copyrights and licenses were found in the source code of this package: @@ -38598,7 +38390,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_gnu:0.52.3 +Package: windows_i686_gnu:0.52.4 The following copyrights and licenses were found in the source code of this package: @@ -39056,7 +38848,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_i686_msvc:0.52.3 +Package: windows_i686_msvc:0.52.4 The following copyrights and licenses were found in the source code of this package: @@ -39514,7 +39306,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_gnu:0.52.3 +Package: windows_x86_64_gnu:0.52.4 The following copyrights and licenses were found in the source code of this package: @@ -39972,7 +39764,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_gnullvm:0.52.3 +Package: windows_x86_64_gnullvm:0.52.4 The following copyrights and licenses were found in the source code of this package: @@ -40430,7 +40222,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: windows_x86_64_msvc:0.52.3 +Package: windows_x86_64_msvc:0.52.4 The following copyrights and licenses were found in the source code of this package: @@ -41650,7 +41442,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: cachetools:5.3.2 +Package: cachetools:5.3.3 The following copyrights and licenses were found in the source code of this package: @@ -45526,7 +45318,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- -Package: types-protobuf:4.24.0.20240129 +Package: types-protobuf:4.24.0.20240302 The following copyrights and licenses were found in the source code of this package: diff --git a/python/python/glide/__init__.py b/python/python/glide/__init__.py index b4448ffad2..d06fc99276 100644 --- a/python/python/glide/__init__.py +++ b/python/python/glide/__init__.py @@ -5,11 +5,18 @@ ExpireOptions, ExpirySet, ExpiryType, - InfBound, InfoSection, - ScoreLimit, UpdateOptions, ) +from glide.async_commands.sorted_set import ( + InfBound, + LexBoundary, + Limit, + RangeByIndex, + RangeByLex, + RangeByScore, + ScoreBoundary, +) from glide.async_commands.transaction import ClusterTransaction, Transaction from glide.config import ( BaseClientConfiguration, @@ -33,6 +40,7 @@ from glide.routes import ( AllNodes, AllPrimaries, + ByAddressRoute, RandomNode, SlotIdRoute, SlotKeyRoute, @@ -45,13 +53,18 @@ "BaseClientConfiguration", "ClusterClientConfiguration", "RedisClientConfiguration", - "ScoreLimit", + "ScoreBoundary", "ConditionalChange", "ExpireOptions", "ExpirySet", "ExpiryType", "InfBound", "InfoSection", + "LexBoundary", + "Limit", + "RangeByIndex", + "RangeByLex", + "RangeByScore", "UpdateOptions", "Logger", "LogLevel", @@ -68,6 +81,7 @@ "SlotType", "AllNodes", "AllPrimaries", + "ByAddressRoute", "RandomNode", "SlotKeyRoute", "SlotIdRoute", diff --git a/python/python/glide/async_commands/core.py b/python/python/glide/async_commands/core.py index a0d4c71e25..956286b72e 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/python/glide/async_commands/core.py @@ -16,6 +16,14 @@ get_args, ) +from glide.async_commands.sorted_set import ( + InfBound, + RangeByIndex, + RangeByLex, + RangeByScore, + ScoreBoundary, + _create_zrange_args, +) from glide.constants import TOK, TResult from glide.protobuf.redis_request_pb2 import RequestType from glide.routes import Route @@ -123,29 +131,6 @@ class UpdateOptions(Enum): GREATER_THAN = "GT" -class InfBound(Enum): - """ - Enumeration representing positive and negative infinity bounds for sorted set scores. - """ - - POS_INF = "+inf" - NEG_INF = "-inf" - - -class ScoreLimit: - """ - Represents a score limit in a sorted set. - - Args: - value (float): The score value. - is_inclusive (bool): Whether the score value is inclusive. Defaults to False. - """ - - def __init__(self, value: float, is_inclusive: bool = True): - """Convert the score limit to the Redis protocol format.""" - self.value = str(value) if is_inclusive else f"({value}" - - class ExpirySet: """SET option: Represents the expiry type and value to be executed with "SET" command.""" @@ -419,7 +404,7 @@ async def hset(self, key: str, field_value_map: Mapping[str, str]) -> int: int: The number of fields that were added to the hash. Example: - >>> hset("my_hash", {"field": "value", "field2": "value2"}) + >>> await client.hset("my_hash", {"field": "value", "field2": "value2"}) 2 """ field_value_list: List[str] = [key] @@ -444,9 +429,9 @@ async def hget(self, key: str, field: str) -> Optional[str]: Returns None if `field` is not presented in the hash or `key` does not exist. Examples: - >>> hget("my_hash", "field") + >>> await client.hget("my_hash", "field") "value" - >>> hget("my_hash", "nonexistent_field") + >>> await client.hget("my_hash", "nonexistent_field") None """ return cast( @@ -454,6 +439,37 @@ async def hget(self, key: str, field: str) -> Optional[str]: await self._execute_command(RequestType.HashGet, [key, field]), ) + async def hsetnx( + self, + key: str, + field: str, + value: str, + ) -> bool: + """ + Sets `field` in the hash stored at `key` to `value`, only if `field` does not yet exist. + If `key` does not exist, a new key holding a hash is created. + If `field` already exists, this operation has no effect. + See https://redis.io/commands/hsetnx/ for more details. + + Args: + key (str): The key of the hash. + field (str): The field to set the value for. + value (str): The value to set. + + Returns: + bool: True if the field was set, False if the field already existed and was not set. + + Examples: + >>> await client.hsetnx("my_hash", "field", "value") + True # Indicates that the field "field" was set successfully in the hash "my_hash". + >>> await client.hsetnx("my_hash", "field", "new_value") + False # Indicates that the field "field" already existed in the hash "my_hash" and was not set again. + """ + return cast( + bool, + await self._execute_command(RequestType.HSetNX, [key, field, value]), + ) + async def hincrby(self, key: str, field: str, amount: int) -> int: """ Increment or decrement the value of a `field` in the hash stored at `key` by the specified amount. @@ -871,6 +887,35 @@ async def scard(self, key: str) -> int: """ return cast(int, await self._execute_command(RequestType.SCard, [key])) + async def sismember( + self, + key: str, + member: str, + ) -> bool: + """ + Returns if `member` is a member of the set stored at `key`. + + See https://redis.io/commands/sismember/ for more details. + + Args: + key (str): The key of the set. + member (str): The member to check for existence in the set. + + Returns: + bool: True if the member exists in the set, False otherwise. + If `key` doesn't exist, it is treated as an empty set and the command returns False. + + Examples: + >>> await client.sismember("my_set", "member1") + True # Indicates that "member1" exists in the set "my_set". + >>> await client.sismember("my_set", "non_existing_member") + False # Indicates that "non_existing_member" does not exist in the set "my_set". + """ + return cast( + bool, + await self._execute_command(RequestType.SIsMember, [key, member]), + ) + async def ltrim(self, key: str, start: int, end: int) -> TOK: """ Trim an existing list so that it will contain only the specified range of elements specified. @@ -1005,7 +1050,7 @@ async def expire( Examples: >>> await client.expire("my_key", 60) - 1 # Indicates that a timeout of 60 seconds has been set for "my_key." + True # Indicates that a timeout of 60 seconds has been set for "my_key." """ args: List[str] = ( [key, str(seconds)] if option is None else [key, str(seconds), option.value] @@ -1035,7 +1080,7 @@ async def expireat( Examples: >>> await client.expireAt("my_key", 1672531200, ExpireOptions.HasNoExpiry) - 1 + True """ args = ( [key, str(unix_seconds)] @@ -1065,7 +1110,7 @@ async def pexpire( Examples: >>> await client.pexpire("my_key", 60000, ExpireOptions.HasNoExpiry) - 1 # Indicates that a timeout of 60,000 milliseconds has been set for "my_key." + True # Indicates that a timeout of 60,000 milliseconds has been set for "my_key." """ args = ( [key, str(milliseconds)] @@ -1097,7 +1142,7 @@ async def pexpireat( Examples: >>> await client.pexpireAt("my_key", 1672531200000, ExpireOptions.HasNoExpiry) - 1 + True """ args = ( [key, str(unix_milliseconds)] @@ -1125,6 +1170,31 @@ async def ttl(self, key: str) -> int: """ return cast(int, await self._execute_command(RequestType.TTL, [key])) + async def pttl( + self, + key: str, + ) -> int: + """ + Returns the remaining time to live of `key` that has a timeout, in milliseconds. + See https://redis.io/commands/pttl for more details. + + Args: + key (str): The key to return its timeout. + + Returns: + int: TTL in milliseconds. -2 if `key` does not exist, -1 if `key` exists but has no associated expire. + + Examples: + >>> await client.pttl("my_key") + 5000 # Indicates that the key "my_key" has a remaining time to live of 5000 milliseconds. + >>> await client.pttl("non_existing_key") + -2 # Indicates that the key "non_existing_key" does not exist. + """ + return cast( + int, + await self._execute_command(RequestType.PTTL, [key]), + ) + async def echo(self, message: str) -> str: """ Echoes the provided `message` back. @@ -1193,9 +1263,9 @@ async def zadd( If `changed` is set, returns the number of elements updated in the sorted set. Examples: - >>> await zadd("my_sorted_set", {"member1": 10.5, "member2": 8.2}) + >>> await client.zadd("my_sorted_set", {"member1": 10.5, "member2": 8.2}) 2 # Indicates that two elements have been added or updated in the sorted set "my_sorted_set." - >>> await zadd("existing_sorted_set", {"member1": 15.0, "member2": 5.5}, existing_options=ConditionalChange.XX) + >>> await client.zadd("existing_sorted_set", {"member1": 15.0, "member2": 5.5}, existing_options=ConditionalChange.XX) 2 # Updates the scores of two existing members in the sorted set "existing_sorted_set." """ args = [key] @@ -1256,9 +1326,9 @@ async def zadd_incr( If there was a conflict with choosing the XX/NX/LT/GT options, the operation aborts and None is returned. Examples: - >>> await zaddIncr("my_sorted_set", member , 5.0) + >>> await client.zaddIncr("my_sorted_set", member , 5.0) 5.0 - >>> await zaddIncr("existing_sorted_set", member , "3.0" , UpdateOptions.LESS_THAN) + >>> await client.zaddIncr("existing_sorted_set", member , "3.0" , UpdateOptions.LESS_THAN) None """ args = [key] @@ -1297,9 +1367,9 @@ async def zcard(self, key: str) -> int: If `key` does not exist, it is treated as an empty sorted set, and the command returns 0. Examples: - >>> await zcard("my_sorted_set") + >>> await client.zcard("my_sorted_set") 3 # Indicates that there are 3 elements in the sorted set "my_sorted_set". - >>> await zcard("non_existing_key") + >>> await client.zcard("non_existing_key") 0 """ return cast(int, await self._execute_command(RequestType.Zcard, [key])) @@ -1307,8 +1377,8 @@ async def zcard(self, key: str) -> int: async def zcount( self, key: str, - min_score: Union[InfBound, ScoreLimit], - max_score: Union[InfBound, ScoreLimit], + min_score: Union[InfBound, ScoreBoundary], + max_score: Union[InfBound, ScoreBoundary], ) -> int: """ Returns the number of members in the sorted set stored at `key` with scores between `min_score` and `max_score`. @@ -1317,12 +1387,12 @@ async def zcount( Args: key (str): The key of the sorted set. - min_score (Union[InfBound, ScoreLimit]): The minimum score to count from. + min_score (Union[InfBound, ScoreBoundary]): The minimum score to count from. Can be an instance of InfBound representing positive/negative infinity, - or ScoreLimit representing a specific score and inclusivity. - max_score (Union[InfBound, ScoreLimit]): The maximum score to count up to. + or ScoreBoundary representing a specific score and inclusivity. + max_score (Union[InfBound, ScoreBoundary]): The maximum score to count up to. Can be an instance of InfBound representing positive/negative infinity, - or ScoreLimit representing a specific score and inclusivity. + or ScoreBoundary representing a specific score and inclusivity. Returns: int: The number of members in the specified score range. @@ -1330,15 +1400,25 @@ async def zcount( If `max_score` < `min_score`, 0 is returned. Examples: - >>> await client.zcount("my_sorted_set", ScoreLimit(5.0 , is_inclusive=true) , InfBound.POS_INF) + >>> await client.zcount("my_sorted_set", ScoreBoundary(5.0 , is_inclusive=true) , InfBound.POS_INF) 2 # Indicates that there are 2 members with scores between 5.0 (not exclusive) and +inf in the sorted set "my_sorted_set". - >>> await client.zcount("my_sorted_set", ScoreLimit(5.0 , is_inclusive=true) , ScoreLimit(10.0 , is_inclusive=false)) - 1 # Indicates that there is one ScoreLimit with 5.0 < score <= 10.0 in the sorted set "my_sorted_set". + >>> await client.zcount("my_sorted_set", ScoreBoundary(5.0 , is_inclusive=true) , ScoreBoundary(10.0 , is_inclusive=false)) + 1 # Indicates that there is one ScoreBoundary with 5.0 < score <= 10.0 in the sorted set "my_sorted_set". """ + score_min = ( + min_score.value["score_arg"] + if type(min_score) == InfBound + else min_score.value + ) + score_max = ( + max_score.value["score_arg"] + if type(max_score) == InfBound + else max_score.value + ) return cast( int, await self._execute_command( - RequestType.Zcount, [key, min_score.value, max_score.value] + RequestType.Zcount, [key, score_min, score_max] ), ) @@ -1406,6 +1486,78 @@ async def zpopmin( ), ) + async def zrange( + self, + key: str, + range_query: Union[RangeByIndex, RangeByLex, RangeByScore], + reverse: bool = False, + ) -> List[str]: + """ + Returns the specified range of elements in the sorted set stored at `key`. + + ZRANGE can perform different types of range queries: by index (rank), by the score, or by lexicographical order. + + See https://redis.io/commands/zrange/ for more details. + + To get the elements with their scores, see zrange_withscores. + + Args: + key (str): The key of the sorted set. + range_query (Union[RangeByIndex, RangeByLex, RangeByScore]): The range query object representing the type of range query to perform. + - For range queries by index (rank), use RangeByIndex. + - For range queries by lexicographical order, use RangeByLex. + - For range queries by score, use RangeByScore. + reverse (bool): If True, reverses the sorted set, with index 0 as the element with the highest score. + + Returns: + List[str]: A list of elements within the specified range. + If `key` does not exist, it is treated as an empty sorted set, and the command returns an empty array. + + Examples: + >>> await client.zrange("my_sorted_set", RangeByIndex(0, -1)) + ['member1', 'member2', 'member3'] # Returns all members in ascending order. + >>> await client.zrange("my_sorted_set", RangeByScore(start=InfBound.NEG_INF, stop=ScoreBoundary(3))) + ['member2', 'member3'] # Returns members with scores within the range of negative infinity to 3, in ascending order. + """ + args = _create_zrange_args(key, range_query, reverse, with_scores=False) + + return cast(List[str], await self._execute_command(RequestType.Zrange, args)) + + async def zrange_withscores( + self, + key: str, + range_query: Union[RangeByIndex, RangeByScore], + reverse: bool = False, + ) -> Mapping[str, float]: + """ + Returns the specified range of elements with their scores in the sorted set stored at `key`. + Similar to ZRANGE but with a WITHSCORE flag. + + See https://redis.io/commands/zrange/ for more details. + + Args: + key (str): The key of the sorted set. + range_query (Union[RangeByIndex, RangeByScore]): The range query object representing the type of range query to perform. + - For range queries by index (rank), use RangeByIndex. + - For range queries by score, use RangeByScore. + reverse (bool): If True, reverses the sorted set, with index 0 as the element with the highest score. + + Returns: + Mapping[str , float]: A map of elements and their scores within the specified range. + If `key` does not exist, it is treated as an empty sorted set, and the command returns an empty map. + + Examples: + >>> await client.zrange_withscores("my_sorted_set", RangeByScore(ScoreBoundary(10), ScoreBoundary(20))) + {'member1': 10.5, 'member2': 15.2} # Returns members with scores between 10 and 20 with their scores. + >>> await client.zrange("my_sorted_set", RangeByScore(start=InfBound.NEG_INF, stop=ScoreBoundary(3))) + {'member4': -2.0, 'member7': 1.5} # Returns members with with scores within the range of negative infinity to 3, with their scores. + """ + args = _create_zrange_args(key, range_query, reverse, with_scores=True) + + return cast( + Mapping[str, float], await self._execute_command(RequestType.Zrange, args) + ) + async def zrem( self, key: str, diff --git a/python/python/glide/async_commands/sorted_set.py b/python/python/glide/async_commands/sorted_set.py new file mode 100644 index 0000000000..83c6037341 --- /dev/null +++ b/python/python/glide/async_commands/sorted_set.py @@ -0,0 +1,160 @@ +# Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 + +from enum import Enum +from typing import List, Optional, Union + + +class InfBound(Enum): + """ + Enumeration representing numeric and lexicographic positive and negative infinity bounds for sorted set. + """ + + POS_INF = {"score_arg": "+inf", "lex_arg": "+"} + """ + Positive infinity bound for sorted set. + score_arg: represents numeric positive infinity (+inf). + lex_arg: represents lexicographic positive infinity (+). + """ + NEG_INF = {"score_arg": "-inf", "lex_arg": "-"} + """ + Negative infinity bound for sorted set. + score_arg: represents numeric negative infinity (-inf). + lex_arg: represents lexicographic negative infinity (-). + """ + + +class ScoreBoundary: + """ + Represents a specific numeric score boundary in a sorted set. + + Args: + value (float): The score value. + is_inclusive (bool): Whether the score value is inclusive. Defaults to True. + """ + + def __init__(self, value: float, is_inclusive: bool = True): + # Convert the score boundary to the Redis protocol format + self.value = str(value) if is_inclusive else f"({value}" + + +class LexBoundary: + """ + Represents a specific lexicographic boundary in a sorted set. + + Args: + value (str): The lex value. + is_inclusive (bool): Whether the score value is inclusive. Defaults to True. + """ + + def __init__(self, value: str, is_inclusive: bool = True): + # Convert the lexicographic boundary to the Redis protocol format + self.value = f"[{value}" if is_inclusive else f"({value}" + + +class Limit: + """ + Represents a limit argument for a range query in a sorted set to be used in [ZRANGE](https://redis.io/commands/zrange) command. + + The optional LIMIT argument can be used to obtain a sub-range from the matching elements + (similar to SELECT LIMIT offset, count in SQL). + Args: + offset (int): The offset from the start of the range. + count (int): The number of elements to include in the range. + A negative count returns all elements from the offset. + """ + + def __init__(self, offset: int, count: int): + self.offset = offset + self.count = count + + +class RangeByIndex: + """ + Represents a range by index (rank) in a sorted set. + + The `start` and `stop` arguments represent zero-based indexes. + + Args: + start (int): The start index of the range. + stop (int): The stop index of the range. + """ + + def __init__(self, start: int, stop: int): + self.start = start + self.stop = stop + + +class RangeByScore: + """ + Represents a range by score in a sorted set. + + The `start` and `stop` arguments represent score boundaries. + + Args: + start (Union[InfBound, ScoreBoundary]): The start score boundary. + stop (Union[InfBound, ScoreBoundary]): The stop score boundary. + limit (Optional[Limit]): The limit argument for a range query. Defaults to None. See `Limit` class for more information. + """ + + def __init__( + self, + start: Union[InfBound, ScoreBoundary], + stop: Union[InfBound, ScoreBoundary], + limit: Optional[Limit] = None, + ): + self.start = ( + start.value["score_arg"] if type(start) == InfBound else start.value + ) + self.stop = stop.value["score_arg"] if type(stop) == InfBound else stop.value + self.limit = limit + + +class RangeByLex: + """ + Represents a range by lexicographical order in a sorted set. + + The `start` and `stop` arguments represent lexicographical boundaries. + + Args: + start (Union[InfBound, LexBoundary]): The start lexicographic boundary. + stop (Union[InfBound, LexBoundary]): The stop lexicographic boundary. + limit (Optional[Limit]): The limit argument for a range query. Defaults to None. See `Limit` class for more information. + """ + + def __init__( + self, + start: Union[InfBound, LexBoundary], + stop: Union[InfBound, LexBoundary], + limit: Optional[Limit] = None, + ): + self.start = start.value["lex_arg"] if type(start) == InfBound else start.value + self.stop = stop.value["lex_arg"] if type(stop) == InfBound else stop.value + self.limit = limit + + +def _create_zrange_args( + key: str, + range_query: Union[RangeByLex, RangeByScore, RangeByIndex], + reverse: bool, + with_scores: bool, +) -> List[str]: + args = [key, str(range_query.start), str(range_query.stop)] + + if isinstance(range_query, RangeByScore): + args.append("BYSCORE") + elif isinstance(range_query, RangeByLex): + args.append("BYLEX") + if reverse: + args.append("REV") + if hasattr(range_query, "limit") and range_query.limit is not None: + args.extend( + [ + "LIMIT", + str(range_query.limit.offset), + str(range_query.limit.count), + ] + ) + if with_scores: + args.append("WITHSCORES") + + return args diff --git a/python/python/glide/async_commands/transaction.py b/python/python/glide/async_commands/transaction.py index 89550c5bee..9cebba38d9 100644 --- a/python/python/glide/async_commands/transaction.py +++ b/python/python/glide/async_commands/transaction.py @@ -7,11 +7,17 @@ ConditionalChange, ExpireOptions, ExpirySet, - InfBound, InfoSection, - ScoreLimit, UpdateOptions, ) +from glide.async_commands.sorted_set import ( + InfBound, + RangeByIndex, + RangeByLex, + RangeByScore, + ScoreBoundary, + _create_zrange_args, +) from glide.protobuf.redis_request_pb2 import RequestType TTransaction = TypeVar("TTransaction", bound="BaseTransaction") @@ -373,6 +379,28 @@ def hget(self: TTransaction, key: str, field: str) -> TTransaction: """ return self.append_command(RequestType.HashGet, [key, field]) + def hsetnx( + self: TTransaction, + key: str, + field: str, + value: str, + ) -> TTransaction: + """ + Sets `field` in the hash stored at `key` to `value`, only if `field` does not yet exist. + If `key` does not exist, a new key holding a hash is created. + If `field` already exists, this operation has no effect. + See https://redis.io/commands/hsetnx/ for more details. + + Args: + key (str): The key of the hash. + field (str): The field to set the value for. + value (str): The value to set. + + Commands response: + bool: True if the field was set, False if the field already existed and was not set. + """ + return self.append_command(RequestType.HSetNX, [key, field, value]) + def hincrby(self: TTransaction, key: str, field: str, amount: int) -> TTransaction: """ Increment or decrement the value of a `field` in the hash stored at `key` by the specified amount. @@ -673,6 +701,26 @@ def scard(self: TTransaction, key: str) -> TTransaction: """ return self.append_command(RequestType.SCard, [key]) + def sismember( + self: TTransaction, + key: str, + member: str, + ) -> TTransaction: + """ + Returns if `member` is a member of the set stored at `key`. + + See https://redis.io/commands/sismember/ for more details. + + Args: + key (str): The key of the set. + member (str): The member to check for existence in the set. + + Commands response: + bool: True if the member exists in the set, False otherwise. + If `key` doesn't exist, it is treated as an empty set and the command returns False. + """ + return self.append_command(RequestType.SIsMember, [key, member]) + def ltrim(self: TTransaction, key: str, start: int, end: int) -> TTransaction: """ Trim an existing list so that it will contain only the specified range of elements specified. @@ -891,6 +939,22 @@ def ttl(self: TTransaction, key: str) -> TTransaction: """ return self.append_command(RequestType.TTL, [key]) + def pttl( + self: TTransaction, + key: str, + ) -> TTransaction: + """ + Returns the remaining time to live of `key` that has a timeout, in milliseconds. + See https://redis.io/commands/pttl for more details. + + Args: + key (str): The key to return its timeout. + + Commands Response: + int: TTL in milliseconds. -2 if `key` does not exist, -1 if `key` exists but has no associated expire. + """ + return self.append_command(RequestType.PTTL, [key]) + def echo(self: TTransaction, message: str) -> TTransaction: """ Echoes the provided `message` back. @@ -1040,8 +1104,8 @@ def zcard(self: TTransaction, key: str) -> TTransaction: def zcount( self: TTransaction, key: str, - min_score: Union[InfBound, ScoreLimit], - max_score: Union[InfBound, ScoreLimit], + min_score: Union[InfBound, ScoreBoundary], + max_score: Union[InfBound, ScoreBoundary], ) -> TTransaction: """ Returns the number of members in the sorted set stored at `key` with scores between `min_score` and `max_score`. @@ -1050,21 +1114,29 @@ def zcount( Args: key (str): The key of the sorted set. - min_score (Union[InfBound, ScoreLimit]): The minimum score to count from. + min_score (Union[InfBound, ScoreBoundary]): The minimum score to count from. Can be an instance of InfBound representing positive/negative infinity, - or ScoreLimit representing a specific score and inclusivity. - max_score (Union[InfBound, ScoreLimit]): The maximum score to count up to. + or ScoreBoundary representing a specific score and inclusivity. + max_score (Union[InfBound, ScoreBoundary]): The maximum score to count up to. Can be an instance of InfBound representing positive/negative infinity, - or ScoreLimit representing a specific score and inclusivity. + or ScoreBoundary representing a specific score and inclusivity. Commands response: int: The number of members in the specified score range. If key does not exist, 0 is returned. If `max_score` < `min_score`, 0 is returned. """ - return self.append_command( - RequestType.Zcount, [key, min_score.value, max_score.value] + score_min = ( + min_score.value["score_arg"] + if type(min_score) == InfBound + else min_score.value + ) + score_max = ( + max_score.value["score_arg"] + if type(max_score) == InfBound + else max_score.value ) + return self.append_command(RequestType.Zcount, [key, score_min, score_max]) def zpopmax( self: TTransaction, key: str, count: Optional[int] = None @@ -1112,6 +1184,62 @@ def zpopmin( RequestType.ZPopMin, [key, str(count)] if count else [key] ) + def zrange( + self: TTransaction, + key: str, + range_query: Union[RangeByIndex, RangeByLex, RangeByScore], + reverse: bool = False, + ) -> TTransaction: + """ + Returns the specified range of elements in the sorted set stored at `key`. + + ZRANGE can perform different types of range queries: by index (rank), by the score, or by lexicographical order. + + See https://redis.io/commands/zrange/ for more details. + + Args: + key (str): The key of the sorted set. + range_query (Union[RangeByIndex, RangeByLex, RangeByScore]): The range query object representing the type of range query to perform. + - For range queries by index (rank), use RangeByIndex. + - For range queries by lexicographical order, use RangeByLex. + - For range queries by score, use RangeByScore. + reverse (bool): If True, reverses the sorted set, with index 0 as the element with the highest score. + + Commands response: + List[str]: A list of elements within the specified range. + If `key` does not exist, it is treated as an empty sorted set, and the command returns an empty array. + """ + args = _create_zrange_args(key, range_query, reverse, with_scores=False) + + return self.append_command(RequestType.Zrange, args) + + def zrange_withscores( + self: TTransaction, + key: str, + range_query: Union[RangeByIndex, RangeByScore], + reverse: bool = False, + ) -> TTransaction: + """ + Returns the specified range of elements with their scores in the sorted set stored at `key`. + Similar to ZRANGE but with a WITHSCORE flag. + + See https://redis.io/commands/zrange/ for more details. + + Args: + key (str): The key of the sorted set. + range_query (Union[RangeByIndex, RangeByScore]): The range query object representing the type of range query to perform. + - For range queries by index (rank), use RangeByIndex. + - For range queries by score, use RangeByScore. + reverse (bool): If True, reverses the sorted set, with index 0 as the element with the highest score. + + Commands response: + Mapping[str , float]: A map of elements and their scores within the specified range. + If `key` does not exist, it is treated as an empty sorted set, and the command returns an empty map. + """ + args = _create_zrange_args(key, range_query, reverse, with_scores=True) + + return self.append_command(RequestType.Zrange, args) + def zrem( self: TTransaction, key: str, diff --git a/python/python/glide/constants.py b/python/python/glide/constants.py index 7dce4b2d95..1526ee1167 100644 --- a/python/python/glide/constants.py +++ b/python/python/glide/constants.py @@ -4,7 +4,7 @@ from glide.protobuf.connection_request_pb2 import ConnectionRequest from glide.protobuf.redis_request_pb2 import RedisRequest -from glide.routes import RandomNode, SlotIdRoute, SlotKeyRoute +from glide.routes import ByAddressRoute, RandomNode, SlotIdRoute, SlotKeyRoute OK: str = "OK" DEFAULT_READ_BYTES_SIZE: int = pow(2, 16) @@ -26,4 +26,4 @@ # When routing to a single node, response will be T # Otherwise, response will be : {Address : response , ... } with type of Dict[str, T]. TClusterResponse = Union[T, Dict[str, T]] -TSingleNodeRoute = Union[RandomNode, SlotKeyRoute, SlotIdRoute] +TSingleNodeRoute = Union[RandomNode, SlotKeyRoute, SlotIdRoute, ByAddressRoute] diff --git a/python/python/glide/routes.py b/python/python/glide/routes.py index 3784764255..29cf8b364c 100644 --- a/python/python/glide/routes.py +++ b/python/python/glide/routes.py @@ -3,6 +3,7 @@ from enum import Enum from typing import Optional +from glide.exceptions import RequestError from glide.protobuf.redis_request_pb2 import RedisRequest, SimpleRoutes from glide.protobuf.redis_request_pb2 import SlotTypes as ProtoSlotTypes @@ -48,6 +49,29 @@ def __init__(self, slot_type: SlotType, slot_id: int) -> None: self.slot_id = slot_id +class ByAddressRoute(Route): + def __init__(self, host: str, port: Optional[int] = None) -> None: + """Routes a request to a node by its address + + Args: + host (str): The endpoint of the node. If `port` is not provided, should be in the f"{address}:{port}" format, where `address` is the preferred endpoint as shown in the output of the `CLUSTER SLOTS` command. + port (Optional[int]): The port to access on the node. If port is not provided, `host` is assumed to be in the format f"{address}:{port}". + """ + super().__init__() + if port is None: + split = host.split(":") + if len(split) < 2: + raise RequestError( + "No port provided, expected host to be formatted as {hostname}:{port}`. Received " + + host + ) + self.host = split[0] + self.port = int(split[1]) + else: + self.host = host + self.port = port + + def to_protobuf_slot_type(slot_type: SlotType) -> ProtoSlotTypes.ValueType: return ( ProtoSlotTypes.Primary @@ -71,5 +95,8 @@ def set_protobuf_route(request: RedisRequest, route: Optional[Route]) -> None: elif isinstance(route, SlotIdRoute): request.route.slot_id_route.slot_type = to_protobuf_slot_type(route.slot_type) request.route.slot_id_route.slot_id = route.slot_id + elif isinstance(route, ByAddressRoute): + request.route.by_address_route.host = route.host + request.route.by_address_route.port = route.port else: - raise Exception(f"Received invalid route type: {type(route)}") + raise RequestError(f"Received invalid route type: {type(route)}") diff --git a/python/python/tests/test_async_client.py b/python/python/tests/test_async_client.py index 029e73e510..6cc73c5459 100644 --- a/python/python/tests/test_async_client.py +++ b/python/python/tests/test_async_client.py @@ -18,15 +18,24 @@ ExpiryType, InfBound, InfoSection, - ScoreLimit, UpdateOptions, ) +from glide.async_commands.sorted_set import ( + InfBound, + LexBoundary, + Limit, + RangeByIndex, + RangeByLex, + RangeByScore, + ScoreBoundary, +) from glide.config import ProtocolVersion, RedisCredentials from glide.constants import OK from glide.redis_client import RedisClient, RedisClusterClient, TRedisClient from glide.routes import ( AllNodes, AllPrimaries, + ByAddressRoute, RandomNode, Route, SlotIdRoute, @@ -634,6 +643,20 @@ async def test_hdel(self, redis_client: TRedisClient): assert await redis_client.hdel(key, ["nonExistingField"]) == 0 assert await redis_client.hdel("nonExistingKey", [field3]) == 0 + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_hsetnx(self, redis_client: TRedisClient): + key = get_random_string(10) + field = get_random_string(5) + + assert await redis_client.hsetnx(key, field, "value") == True + assert await redis_client.hsetnx(key, field, "new value") == False + assert await redis_client.hget(key, field) == "value" + key = get_random_string(5) + assert await redis_client.set(key, "value") == OK + with pytest.raises(RequestError): + await redis_client.hsetnx(key, field, "value") + @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_hmget(self, redis_client: TRedisClient): @@ -843,6 +866,16 @@ async def test_sadd_srem_smembers_scard_wrong_type_raise_error( await redis_client.smembers(key) assert "Operation against a key holding the wrong kind of value" in str(e) + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_sismember(self, redis_client: TRedisClient): + key = get_random_string(10) + member = get_random_string(5) + assert await redis_client.sadd(key, [member]) == 1 + assert await redis_client.sismember(key, member) + assert not await redis_client.sismember(key, get_random_string(5)) + assert not await redis_client.sismember("non_existing_key", member) + @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_ltrim(self, redis_client: TRedisClient): @@ -1016,6 +1049,25 @@ async def test_expire_pexpire_expireAt_pexpireAt_ttl_non_existing_key( assert not await redis_client.pexpireat(key, int(time.time() * 1000) + 50000) assert await redis_client.ttl(key) == -2 + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_pttl(self, redis_client: TRedisClient): + key = get_random_string(10) + assert await redis_client.pttl(key) == -2 + current_time = int(time.time()) + + assert await redis_client.set(key, "value") == OK + assert await redis_client.pttl(key) == -1 + + assert await redis_client.expire(key, 10) + assert 0 < await redis_client.pttl(key) <= 10000 + + assert await redis_client.expireat(key, current_time + 20) + assert 0 < await redis_client.pttl(key) <= 20000 + + assert await redis_client.pexpireat(key, current_time * 1000 + 30000) + assert 0 < await redis_client.pttl(key) <= 30000 + @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_zadd_zaddincr(self, redis_client: TRedisClient): @@ -1146,18 +1198,32 @@ async def test_zcount(self, redis_client: TRedisClient): assert await redis_client.zcount(key, InfBound.NEG_INF, InfBound.POS_INF) == 3 assert ( - await redis_client.zcount(key, ScoreLimit(1, False), ScoreLimit(3, False)) + await redis_client.zcount( + key, + ScoreBoundary(1, is_inclusive=False), + ScoreBoundary(3, is_inclusive=False), + ) == 1 ) assert ( - await redis_client.zcount(key, ScoreLimit(1, False), ScoreLimit(3, True)) + await redis_client.zcount( + key, + ScoreBoundary(1, is_inclusive=False), + ScoreBoundary(3, is_inclusive=True), + ) == 2 ) assert ( - await redis_client.zcount(key, InfBound.NEG_INF, ScoreLimit(3, True)) == 3 + await redis_client.zcount( + key, InfBound.NEG_INF, ScoreBoundary(3, is_inclusive=True) + ) + == 3 ) assert ( - await redis_client.zcount(key, InfBound.POS_INF, ScoreLimit(3, True)) == 0 + await redis_client.zcount( + key, InfBound.POS_INF, ScoreBoundary(3, is_inclusive=True) + ) + == 0 ) assert ( await redis_client.zcount( @@ -1210,6 +1276,194 @@ async def test_zpopmax(self, redis_client: TRedisClient): assert await redis_client.zpopmax("non_exisitng_key") == {} + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zrange_by_index(self, redis_client: TRedisClient): + key = get_random_string(10) + members_scores = {"one": 1, "two": 2, "three": 3} + assert await redis_client.zadd(key, members_scores=members_scores) == 3 + + assert await redis_client.zrange(key, RangeByIndex(start=0, stop=1)) == [ + "one", + "two", + ] + + assert ( + await redis_client.zrange_withscores(key, RangeByIndex(start=0, stop=-1)) + ) == {"one": 1.0, "two": 2.0, "three": 3.0} + + assert await redis_client.zrange( + key, RangeByIndex(start=0, stop=1), reverse=True + ) == [ + "three", + "two", + ] + + assert await redis_client.zrange(key, RangeByIndex(start=3, stop=1)) == [] + assert ( + await redis_client.zrange_withscores(key, RangeByIndex(start=3, stop=1)) + == {} + ) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zrange_byscore(self, redis_client: TRedisClient): + key = get_random_string(10) + members_scores = {"one": 1, "two": 2, "three": 3} + assert await redis_client.zadd(key, members_scores=members_scores) == 3 + + assert await redis_client.zrange( + key, + RangeByScore( + start=InfBound.NEG_INF, stop=ScoreBoundary(3, is_inclusive=False) + ), + ) == ["one", "two"] + + assert ( + await redis_client.zrange_withscores( + key, + RangeByScore(start=InfBound.NEG_INF, stop=InfBound.POS_INF), + ) + ) == {"one": 1.0, "two": 2.0, "three": 3.0} + + assert await redis_client.zrange( + key, + RangeByScore( + start=ScoreBoundary(3, is_inclusive=False), stop=InfBound.NEG_INF + ), + reverse=True, + ) == ["two", "one"] + + assert ( + await redis_client.zrange( + key, + RangeByScore( + start=InfBound.NEG_INF, + stop=InfBound.POS_INF, + limit=Limit(offset=1, count=2), + ), + ) + ) == ["two", "three"] + + assert ( + await redis_client.zrange( + key, + RangeByScore( + start=InfBound.NEG_INF, stop=ScoreBoundary(3, is_inclusive=False) + ), + reverse=True, + ) + == [] + ) # stop is greater than start with reverse set to True + + assert ( + await redis_client.zrange( + key, + RangeByScore( + start=InfBound.POS_INF, stop=ScoreBoundary(3, is_inclusive=False) + ), + ) + == [] + ) # start is greater than stop + + assert ( + await redis_client.zrange_withscores( + key, + RangeByScore( + start=InfBound.POS_INF, stop=ScoreBoundary(3, is_inclusive=False) + ), + ) + == {} + ) # start is greater than stop + + assert ( + await redis_client.zrange_withscores( + key, + RangeByScore( + start=InfBound.NEG_INF, stop=ScoreBoundary(3, is_inclusive=False) + ), + reverse=True, + ) + == {} + ) # stop is greater than start with reverse set to True + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zrange_bylex(self, redis_client: TRedisClient): + key = get_random_string(10) + members_scores = {"a": 1, "b": 2, "c": 3} + assert await redis_client.zadd(key, members_scores=members_scores) == 3 + + assert await redis_client.zrange( + key, + RangeByLex( + start=InfBound.NEG_INF, stop=LexBoundary("c", is_inclusive=False) + ), + ) == ["a", "b"] + + assert ( + await redis_client.zrange( + key, + RangeByLex( + start=InfBound.NEG_INF, + stop=InfBound.POS_INF, + limit=Limit(offset=1, count=2), + ), + ) + ) == ["b", "c"] + + assert await redis_client.zrange( + key, + RangeByLex( + start=LexBoundary("c", is_inclusive=False), stop=InfBound.NEG_INF + ), + reverse=True, + ) == ["b", "a"] + + assert ( + await redis_client.zrange( + key, + RangeByLex( + start=InfBound.NEG_INF, stop=LexBoundary("c", is_inclusive=False) + ), + reverse=True, + ) + == [] + ) # stop is greater than start with reverse set to True + + assert ( + await redis_client.zrange( + key, + RangeByLex( + start=InfBound.POS_INF, stop=LexBoundary("c", is_inclusive=False) + ), + ) + == [] + ) # start is greater than stop + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_zrange_different_types_of_keys(self, redis_client: TRedisClient): + key = get_random_string(10) + + assert ( + await redis_client.zrange("non_existing_key", RangeByIndex(start=0, stop=1)) + == [] + ) + + assert ( + await redis_client.zrange_withscores( + "non_existing_key", RangeByIndex(start=0, stop=-1) + ) + ) == {} + + assert await redis_client.set(key, "value") == OK + with pytest.raises(RequestError): + await redis_client.zrange(key, RangeByIndex(start=0, stop=1)) + + with pytest.raises(RequestError): + await redis_client.zrange_withscores(key, RangeByIndex(start=0, stop=1)) + @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_type(self, redis_client: TRedisClient): @@ -1403,6 +1657,44 @@ async def test_info_random_route(self, redis_client: RedisClusterClient): assert isinstance(info, str) assert "# Server" in info + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_cluster_route_by_address_reaches_correct_node( + self, redis_client: RedisClusterClient + ): + cluster_nodes = await redis_client.custom_command( + ["cluster", "nodes"], RandomNode() + ) + assert isinstance(cluster_nodes, str) + host = ( + [line for line in cluster_nodes.split("\n") if "myself" in line][0] + .split(" ")[1] + .split("@")[0] + ) + + second_result = await redis_client.custom_command( + ["cluster", "nodes"], ByAddressRoute(host) + ) + + assert cluster_nodes == second_result + + host, port = host.split(":") + port_as_int = int(port) + + third_result = await redis_client.custom_command( + ["cluster", "nodes"], ByAddressRoute(host, port_as_int) + ) + + assert cluster_nodes == third_result + + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_cluster_fail_routing_by_address_if_no_port_is_provided( + self, redis_client: RedisClusterClient + ): + with pytest.raises(RequestError) as e: + await redis_client.info(route=ByAddressRoute("foo")) + @pytest.mark.asyncio class TestExceptions: diff --git a/python/python/tests/test_transaction.py b/python/python/tests/test_transaction.py index 2e0bef1f74..b3e7838ede 100644 --- a/python/python/tests/test_transaction.py +++ b/python/python/tests/test_transaction.py @@ -5,7 +5,7 @@ import pytest from glide import RequestError -from glide.async_commands.core import InfBound, ScoreLimit +from glide.async_commands.sorted_set import InfBound, RangeByIndex, ScoreBoundary from glide.async_commands.transaction import ( BaseTransaction, ClusterTransaction, @@ -86,7 +86,8 @@ def transaction_test( args.append(value2) transaction.hlen(key4) args.append(2) - + transaction.hsetnx(key4, key, value) + args.append(False) transaction.hincrby(key4, key3, 5) args.append(5) transaction.hincrbyfloat(key4, key3, 5.5) @@ -134,6 +135,8 @@ def transaction_test( args.append({"bar"}) transaction.scard(key7) args.append(1) + transaction.sismember(key7, "bar") + args.append(True) transaction.zadd(key8, {"one": 1, "two": 2, "three": 3}) args.append(3) @@ -143,10 +146,14 @@ def transaction_test( args.append(1) transaction.zcard(key8) args.append(2) - transaction.zcount(key8, ScoreLimit(2, True), InfBound.POS_INF) + transaction.zcount(key8, ScoreBoundary(2, is_inclusive=True), InfBound.POS_INF) args.append(2) transaction.zscore(key8, "two") args.append(2.0) + transaction.zrange(key8, RangeByIndex(start=0, stop=-1)) + args.append(["two", "three"]) + transaction.zrange_withscores(key8, RangeByIndex(start=0, stop=-1)) + args.append({"two": 2, "three": 3}) transaction.zpopmin(key8) args.append({"two": 2.0}) transaction.zpopmax(key8) diff --git a/python/src/lib.rs b/python/src/lib.rs index fd05126c17..3b07eb93c3 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -139,7 +139,7 @@ fn glide(_py: Python, m: &PyModule) -> PyResult<()> { let set = PySet::new(py, set.iter())?; Ok(set.into_py(py)) } - Value::Double(double) => Ok(PyFloat::new(py, double.into()).into_py(py)), + Value::Double(double) => Ok(PyFloat::new(py, double).into_py(py)), Value::Boolean(boolean) => Ok(PyBool::new(py, boolean).into_py(py)), Value::VerbatimString { format: _, text } => Ok(text.into_py(py)), Value::BigNumber(bigint) => Ok(bigint.into_py(py)), diff --git a/submodules/redis-rs b/submodules/redis-rs index b974902c91..7f6e4fd68c 160000 --- a/submodules/redis-rs +++ b/submodules/redis-rs @@ -1 +1 @@ -Subproject commit b974902c9137a9d69a6db07f6f00412f4c4680b7 +Subproject commit 7f6e4fd68cf688b75e59e10e23e93433123f8da8