diff --git a/.github/json_matrices/build-matrix.json b/.github/json_matrices/build-matrix.json index 995029d9d0..59fe5dcab5 100644 --- a/.github/json_matrices/build-matrix.json +++ b/.github/json_matrices/build-matrix.json @@ -19,6 +19,14 @@ "PACKAGE_MANAGERS": ["pypi", "npm", "maven", "pkg_go_dev"], "languages": ["python", "node", "java", "go"] }, + { + "OS": "macos", + "NAMED_OS": "darwin", + "RUNNER": ["self-hosted", "macOS", "ARM64", "ephemeral"], + "ARCH": "arm64", + "TARGET": "aarch64-apple-darwin", + "languages": ["python", "node", "java", "go"] + }, { "OS": "macos", "NAMED_OS": "darwin", @@ -45,11 +53,11 @@ "ARCH": "arm64", "TARGET": "aarch64-unknown-linux-musl", "CD_TARGET": "aarch64-unknown-linux-gnu", - "RUNNER": "ubuntu-latest-arm", + "RUNNER": "ubuntu-24.04-arm", "CD_RUNNER": "ubuntu-24.04-arm", "IMAGE": "node:lts-alpine", "CONTAINER_OPTIONS": "--user root --privileged --rm", - "PACKAGE_MANAGERS": ["npm", "maven"], + "PACKAGE_MANAGERS": ["npm", "maven", "pkg_go_dev"], "languages": [] }, { @@ -61,8 +69,8 @@ "RUNNER": "ubuntu-latest", "IMAGE": "node:lts-alpine", "CONTAINER_OPTIONS": "--user root --privileged", - "PACKAGE_MANAGERS": ["npm", "maven"], - "languages": ["node", "java"] + "PACKAGE_MANAGERS": ["npm", "maven", "pkg_go_dev"], + "languages": ["node", "java", "go"] }, { "OS": "amazon-linux", diff --git a/.github/workflows/create-test-matrices/action.yml b/.github/workflows/create-test-matrices/action.yml index 6a8c67ef34..b08dea10a4 100644 --- a/.github/workflows/create-test-matrices/action.yml +++ b/.github/workflows/create-test-matrices/action.yml @@ -14,8 +14,11 @@ inputs: required: true type: boolean run-with-macos: - description: "Run with macos included" - type: boolean + type: choice + options: + - false + - use-self-hosted + - use-github default: false containers: description: "Run in containers" @@ -83,10 +86,13 @@ runs: fi # Add macOS runners if specified - if [[ "$RUN_WITH_MACOS" == "true" ]]; then - echo "Including macOS runners separately" - MAC_RUNNERS=$(jq --arg lang "$LANGUAGE_NAME" -c '[.[] | select(.languages? and any(.languages[] == $lang; .) and '"$CONDITION"' and .TARGET == "aarch64-apple-darwin")]' < .github/json_matrices/build-matrix.json) - + if [[ "$RUN_WITH_MACOS" == "use-self-hosted" ]]; then + echo "Including only self-hosted macOS runners" + MAC_RUNNERS=$(jq --arg lang "$LANGUAGE_NAME" -c '[.[] | select(.languages? and any(.languages[] == $lang; .) and '"$CONDITION"' and .TARGET == "aarch64-apple-darwin" and (.RUNNER == ["self-hosted","macOS","ARM64","ephemeral"]))]' < .github/json_matrices/build-matrix.json) + FINAL_MATRIX=$(echo "$BASE_MATRIX" "$MAC_RUNNERS" | jq -sc 'add') + elif [[ "$RUN_WITH_MACOS" == "use-github" ]]; then + echo "Including only GitHub-hosted macOS runners" + MAC_RUNNERS=$(jq --arg lang "$LANGUAGE_NAME" -c '[.[] | select(.languages? and any(.languages[] == $lang; .) and '"$CONDITION"' and .TARGET == "aarch64-apple-darwin" and .RUNNER == "macos-15")]' < .github/json_matrices/build-matrix.json) FINAL_MATRIX=$(echo "$BASE_MATRIX" "$MAC_RUNNERS" | jq -sc 'add') else FINAL_MATRIX="$BASE_MATRIX" diff --git a/.github/workflows/full-matrix-tests.yml b/.github/workflows/full-matrix-tests.yml index 610cfad7b3..e79ce2b026 100644 --- a/.github/workflows/full-matrix-tests.yml +++ b/.github/workflows/full-matrix-tests.yml @@ -14,8 +14,12 @@ on: default: true run-with-macos: - description: "Run with macos included (only when necessary)" - type: boolean + description: "Run with macos inclauded (only when necessary)" + type: choice + options: + - false + - use-self-hosted + - use-github default: false run-modules-tests: diff --git a/.github/workflows/go-cd.yml b/.github/workflows/go-cd.yml index 01173d1ff4..4c6c8d81a7 100644 --- a/.github/workflows/go-cd.yml +++ b/.github/workflows/go-cd.yml @@ -116,7 +116,7 @@ jobs: uses: ./.github/workflows/install-shared-dependencies with: os: ${{ matrix.host.OS }} - target: ${{ matrix.host.TARGET }} + target: ${{ matrix.host.CD_TARGET || matrix.host.TARGET }} github-token: ${{ secrets.GITHUB_TOKEN }} - name: Build Go client working-directory: go @@ -124,9 +124,9 @@ jobs: RELEASE_VERSION: ${{ needs.validate-release-version.outputs.RELEASE_VERSION }} run: | make install-build-tools - make build GLIDE_VERSION="${RELEASE_VERSION}" + make build GLIDE_VERSION="${RELEASE_VERSION}" TARGET_TRIPLET="${{ matrix.host.TARGET }}" - name: Move FFI artifacts on linux - if: ${{ contains(matrix.host.TARGET, 'linux-gnu') }} + if: ${{ contains(matrix.host.TARGET, 'linux') }} run: | mkdir -p $GITHUB_WORKSPACE/ffi/target/release cp ffi/target/*/release/libglide_ffi.a $GITHUB_WORKSPACE/ffi/target/release/ @@ -187,6 +187,12 @@ jobs: fail-fast: false matrix: host: ${{ fromJson(needs.load-platform-matrix.outputs.PLATFORM_MATRIX) }} + # Excluding musl targets for testing for now as there are issues with the runners + exclude: + - host: + TARGET: aarch64-unknown-linux-musl + - host: + TARGET: x86_64-unknown-linux-musl runs-on: ${{ matrix.host.RUNNER }} steps: - name: Setup self-hosted runner access diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 988086d241..452e3b55f0 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -43,7 +43,11 @@ on: default: false run-with-macos: description: "Run with macos included (only when needed)" - type: boolean + type: choice + options: + - false + - use-self-hosted + - use-github default: false rc-version: required: false @@ -61,8 +65,8 @@ on: workflow_call: inputs: run-with-macos: - description: "Run with macos included (only when needed)" - type: boolean + description: "Run with macos included (only when necessary)" + type: string default: false concurrency: @@ -92,7 +96,7 @@ jobs: language-name: go # Run full test matrix if job started by cron or it was explictly specified by a person who triggered the workflow run-full-matrix: ${{ github.event.inputs.full-matrix == 'true' || github.event_name == 'schedule' }} - run-with-macos: ${{ github.event.inputs.run-with-macos == 'true' }} + run-with-macos: ${{ github.event.inputs.run-with-macos }} test-go: name: Go Tests - ${{ matrix.go }}, EngineVersion - ${{ matrix.engine.version }}, Target - ${{ matrix.host.TARGET }} @@ -255,10 +259,17 @@ jobs: steps: - name: Install git run: | - yum update - yum install -y git tar - git config --global --add safe.directory "$GITHUB_WORKSPACE" - echo IMAGE=amazonlinux:latest | sed -r 's/:/-/g' >> $GITHUB_ENV + if [[ "${{ matrix.host.OS }}" == "amazon-linux" ]]; then + yum update + yum install -y git tar + git config --global --add safe.directory "$GITHUB_WORKSPACE" + echo IMAGE=amazonlinux:latest | sed -r 's/:/-/g' >> $GITHUB_ENV + elif [[ "${{ matrix.host.TARGET }}" == *"musl"* ]]; then + apk update + apk add git bash tar + git config --global --add safe.directory "$GITHUB_WORKSPACE" + fi + # Replace `:` in the variable otherwise it can't be used in `upload-artifact` - uses: actions/checkout@v4 with: @@ -278,6 +289,19 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} engine-version: ${{ matrix.engine.version }} + - name: Install Musl Dependencies + if: ${{ contains(matrix.host.TARGET, 'musl') }} + run: | + apk add clang lld compiler-rt + # Ensure Rust is in PATH for container environments + echo "$HOME/.cargo/bin:$PATH" >> $GITHUB_PATH + + - name: Install zig + if: ${{ contains(matrix.host.TARGET, 'musl') }} + uses: ./.github/workflows/install-zig + with: + target: ${{ matrix.host.TARGET }} + - uses: actions/cache@v4 with: path: | diff --git a/.github/workflows/install-zig/action.yml b/.github/workflows/install-zig/action.yml index 2783a759a1..844ef0f16e 100644 --- a/.github/workflows/install-zig/action.yml +++ b/.github/workflows/install-zig/action.yml @@ -17,10 +17,14 @@ runs: run: | if [[ `cat /etc/os-release | grep "Amazon Linux"` ]]; then yum install -y python3-pip + pip3 install ziglang + elif [[ `cat /etc/os-release | grep "Alpine"` ]]; then + apk add py3-pip + pip3 install ziglang --break-system-packages else sudo apt install -y python3-pip + pip3 install ziglang fi - pip3 install ziglang cargo install --locked cargo-zigbuild # Set environment variable to prevent cargo-zigbuild from auto-detecting malformed targets diff --git a/.github/workflows/java-cd.yml b/.github/workflows/java-cd.yml index 5dc9661bd3..efb9da41d5 100644 --- a/.github/workflows/java-cd.yml +++ b/.github/workflows/java-cd.yml @@ -39,9 +39,9 @@ jobs: export PLATFORM_MATRIX=$(jq 'map( select(.PACKAGE_MANAGERS != null and (.PACKAGE_MANAGERS | contains(["maven"]))) | .RUNNER = ( - if (.RUNNER | type == "array") - then (.RUNNER | map(if . == "ephemeral" then "persistent" else . end)) - else (if .RUNNER == "ephemeral" then "persistent" else .RUNNER end) + if (.RUNNER | type == "array") + then (.RUNNER | map(if . == "ephemeral" then "persistent" else . end)) + else (if .RUNNER == "ephemeral" then "persistent" else .RUNNER end) end ) )' < .github/json_matrices/build-matrix.json | jq -c .) @@ -129,11 +129,22 @@ jobs: ${{ runner.os }}-gradle-cd- ${{ runner.os }}-gradle- - - name: Create secret key ring file + - name: Create secret key ring file for all Java submodules working-directory: java/client run: | + # Decode the provided base64 GPG key into the client module echo "$SECRING_GPG" | base64 --decode > ./secring.gpg - ls -ltr + + # Copy the key ring file into the jedis-compatibility module which also performs signing + if [ -d ../jedis-compatibility ]; then + cp ./secring.gpg ../jedis-compatibility/secring.gpg + else + echo "jedis-compatibility module directory not found" >&2 + exit 1 + fi + + echo "Listing key ring files to verify presence:" + ls -l ./secring.gpg ../jedis-compatibility/secring.gpg env: SECRING_GPG: ${{ secrets.SECRING_GPG }} @@ -141,12 +152,23 @@ jobs: working-directory: java run: | if [[ "${{ matrix.host.TARGET }}" == *"musl"* ]]; then + # Build and publish client first ./gradlew --build-cache :client:publishToMavenLocal -Psigning.secretKeyRingFile=secring.gpg \ -Psigning.password="${{ secrets.GPG_PASSWORD }}" -Psigning.keyId=${{ secrets.GPG_KEY_ID }} \ -Ptarget=${{ matrix.host.TARGET }} + + # Then build jedis-compatibility + ./gradlew --build-cache :jedis-compatibility:publishToMavenLocal -Psigning.secretKeyRingFile=secring.gpg \ + -Psigning.password="${{ secrets.GPG_PASSWORD }}" -Psigning.keyId=${{ secrets.GPG_KEY_ID }} \ + -Ptarget=${{ matrix.host.TARGET }} else + # Build and publish client first ./gradlew --build-cache :client:publishToMavenLocal -Psigning.secretKeyRingFile=secring.gpg \ -Psigning.password="${{ secrets.GPG_PASSWORD }}" -Psigning.keyId=${{ secrets.GPG_KEY_ID }} + + # Then build jedis-compatibility + ./gradlew --build-cache :jedis-compatibility:publishToMavenLocal -Psigning.secretKeyRingFile=secring.gpg \ + -Psigning.password="${{ secrets.GPG_PASSWORD }}" -Psigning.keyId=${{ secrets.GPG_KEY_ID }} fi env: GLIDE_RELEASE_VERSION: ${{ env.RELEASE_VERSION }} @@ -154,6 +176,7 @@ jobs: - name: Bundle JAR working-directory: java run: | + # Bundle client JAR src_folder=~/.m2/repository/io/valkey/valkey-glide/${{ env.RELEASE_VERSION }} cd $src_folder jar -cvf bundle.jar * @@ -161,6 +184,14 @@ jobs: cd - cp $src_folder/bundle.jar bundle-${{ matrix.host.TARGET }}.jar + # Bundle jedis-compatibility JAR + jedis_src_folder=~/.m2/repository/io/valkey/valkey-glide-jedis-compatibility/${{ env.RELEASE_VERSION }} + cd $jedis_src_folder + jar -cvf jedis-bundle.jar * + ls -ltr + cd - + cp $jedis_src_folder/jedis-bundle.jar jedis-bundle-${{ matrix.host.TARGET }}.jar + - name: Upload artifacts to publish continue-on-error: true uses: actions/upload-artifact@v4 @@ -168,6 +199,7 @@ jobs: name: java-${{ matrix.host.TARGET }} path: | java/bundle*.jar + java/jedis-bundle*.jar publish-to-maven-central-deployment: if: ${{ inputs.maven_publish == true || github.event_name == 'push' }} @@ -196,8 +228,16 @@ jobs: - name: Move files to the correct directory tree run: | mkdir -p build/io/valkey/valkey-glide/${{ env.RELEASE_VERSION }} - cp -a maven-files/* build/io/valkey/valkey-glide/${{ env.RELEASE_VERSION }} + mkdir -p build/io/valkey/valkey-glide-jedis-compatibility/${{ env.RELEASE_VERSION }} + + # Move jedis-compatibility files first + cp -a maven-files/valkey-glide-jedis-compatibility* build/io/valkey/valkey-glide-jedis-compatibility/${{ env.RELEASE_VERSION }} + + # Move client files (exclude jedis-compatibility files) + find maven-files -name "valkey-glide-*" ! -name "*jedis-compatibility*" -exec cp {} build/io/valkey/valkey-glide/${{ env.RELEASE_VERSION }}/ \; + rm -rf build/io/valkey/valkey-glide/${{ env.RELEASE_VERSION }}/META-INF + rm -rf build/io/valkey/valkey-glide-jedis-compatibility/${{ env.RELEASE_VERSION }}/META-INF cd build zip -r ../build io @@ -253,6 +293,9 @@ jobs: fail-fast: false matrix: host: ${{ fromJson(needs.load-platform-matrix.outputs.PLATFORM_MATRIX) }} + exclude: + - host: + TARGET: aarch64-unknown-linux-musl runs-on: ${{ matrix.host.test-runner || matrix.host.runner }} container: image: ${{ matrix.host.IMAGE || ''}} diff --git a/.github/workflows/java.yml b/.github/workflows/java.yml index eb1c4e63b4..a9cc7706b3 100644 --- a/.github/workflows/java.yml +++ b/.github/workflows/java.yml @@ -44,7 +44,11 @@ on: default: false run-with-macos: description: "Run with macos included (only when necessary)" - type: boolean + type: choice + options: + - false + - use-self-hosted + - use-github default: false name: required: false @@ -59,7 +63,7 @@ on: inputs: run-with-macos: description: "Run with macos included (only when necessary)" - type: boolean + type: string default: false concurrency: @@ -85,7 +89,7 @@ jobs: language-name: java # Run full test matrix if job started by cron or it was explictly specified by a person who triggered the workflow run-full-matrix: ${{ github.event.inputs.full-matrix == 'true' || github.event_name == 'schedule' }} - run-with-macos: ${{ (github.event.inputs.run-with-macos == 'true') }} + run-with-macos: ${{ (github.event.inputs.run-with-macos) }} test-java: name: Java Tests - ${{ matrix.java }}, EngineVersion - ${{ matrix.engine.version }}, Target - ${{ matrix.host.TARGET }} @@ -327,8 +331,13 @@ jobs: - name: Setup Rust Build if: ${{ contains(matrix.host.TARGET, 'musl') }} run: | + export PATH="$HOME/.cargo/bin:$PATH" echo "PATH=$HOME/.cargo/bin:$PATH" >> $GITHUB_ENV + # Install ziglang and zigbuild + pip3 install ziglang --break-system-packages + cargo install --locked cargo-zigbuild + - uses: actions/cache@v4 with: path: | diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml index 437819f2e6..61dc2f02fd 100644 --- a/.github/workflows/node.yml +++ b/.github/workflows/node.yml @@ -42,7 +42,11 @@ on: default: false run-with-macos: description: "Run with macos included (only when necessary)" - type: boolean + type: choice + options: + - false + - use-self-hosted + - use-github default: false name: required: false @@ -57,7 +61,7 @@ on: inputs: run-with-macos: description: "Run with macos included (only when necessary)" - type: boolean + type: string default: false concurrency: @@ -86,7 +90,7 @@ jobs: with: language-name: node run-full-matrix: ${{ github.event.inputs.full-matrix == 'true' || github.event_name == 'schedule' }} - run-with-macos: ${{ github.event.run-with-macos == 'true' }} + run-with-macos: ${{ github.event.run-with-macos }} test-node: name: Node Tests - ${{ matrix.node }}, EngineVersion - ${{ matrix.engine.version }}, Target - ${{ matrix.host.TARGET }} diff --git a/.github/workflows/npm-cd.yml b/.github/workflows/npm-cd.yml index fc0bd63342..f55787c32b 100644 --- a/.github/workflows/npm-cd.yml +++ b/.github/workflows/npm-cd.yml @@ -149,7 +149,7 @@ jobs: id: load-platform-matrix shell: bash run: | - # Filter entries with npm in PACKAGE_MANAGERS and use CD_RUNNER if available + # Filter entries with npm in PACKAGE_MANAGERS and use CD_RUNNER if available, replace "ephemeral" with "persistent" in RUNNER export PLATFORM_MATRIX=$(jq 'map( select(.PACKAGE_MANAGERS != null and (.PACKAGE_MANAGERS | contains(["npm"]))) | .runner = ( @@ -271,7 +271,7 @@ jobs: - name: Upload Native Modules uses: actions/upload-artifact@v4 with: - name: bindings-${{ matrix.TARGET }} + name: bindings-${{ matrix.TARGET }}-${{ matrix.RUNNER }} path: ./node/${{ matrix.TARGET }} retention-days: 1 if-no-files-found: error @@ -447,9 +447,9 @@ jobs: uses: geekyeggo/delete-artifact@v2 with: name: | - npm-packages-${{ github.run_id }} artifacts-* js-files-${{ github.run_id }} + failOnError: false test-published-release: needs: [get-build-parameters, prepare-and-publish] @@ -550,9 +550,9 @@ jobs: uses: geekyeggo/delete-artifact@v2 with: name: | - npm-packages-${{ github.run_id }} bindings-* js-files-${{ github.run_id }} + failOnError: false deprecate-on-failure: needs: [test-published-release, get-build-parameters] diff --git a/.github/workflows/pypi-cd.yml b/.github/workflows/pypi-cd.yml index 7a3c8f135b..e43a708999 100644 --- a/.github/workflows/pypi-cd.yml +++ b/.github/workflows/pypi-cd.yml @@ -85,7 +85,7 @@ jobs: if: github.repository_owner == 'valkey-io' name: Publish packages to PyPi runs-on: ${{ matrix.build.RUNNER }} - timeout-minutes: 60 + timeout-minutes: 120 strategy: fail-fast: false matrix: @@ -134,7 +134,22 @@ jobs: # List of packages to update for package in glide-async glide-sync glide-shared; do echo "Patching version in $package/pyproject.toml" - sed -i $SED_FOR_MACOS "s/^dynamic = \[\s*\"version\"\s*\]/version = \"${{ env.RELEASE_VERSION }}\"/" "./$package/pyproject.toml" + # Remove "version" from dynamic array (handles all positions and comma combinations) + sed -i $SED_FOR_MACOS '/^dynamic = /s/"version",\s*//g' "./$package/pyproject.toml" # Remove "version", + sed -i $SED_FOR_MACOS '/^dynamic = /s/,\s*"version"//g' "./$package/pyproject.toml" # Remove ,"version" + sed -i $SED_FOR_MACOS '/^dynamic = /s/"version"//g' "./$package/pyproject.toml" # Remove "version" + + # Add version field after [project] line (handle macOS vs Linux differently) + if [[ "${{ matrix.build.OS }}" =~ .*"macos".* ]]; then + # macOS requires backslash and newline + sed -i '' '/^\[project\]/a\ + version = "${{ env.RELEASE_VERSION }}" + ' "./$package/pyproject.toml" + else + # Linux syntax + sed -i '/^\[project\]/a version = "${{ env.RELEASE_VERSION }}"' "./$package/pyproject.toml" + fi + # Log the updated file cat "./$package/pyproject.toml" done @@ -405,11 +420,32 @@ jobs: # List of packages to update for package in glide-async glide-sync glide-shared; do echo "Patching version in $package/pyproject.toml" - sed -i $SED_FOR_MACOS "s/^dynamic = \[\s*\"version\"\s*\]/version = \"${{ env.RELEASE_VERSION }}\"/" "./$package/pyproject.toml" + # Remove "version" from dynamic array (handles all positions and comma combinations) + sed -i $SED_FOR_MACOS '/^dynamic = /s/"version",\s*//g' "./$package/pyproject.toml" # Remove "version", + sed -i $SED_FOR_MACOS '/^dynamic = /s/,\s*"version"//g' "./$package/pyproject.toml" # Remove ,"version" + sed -i $SED_FOR_MACOS '/^dynamic = /s/"version"//g' "./$package/pyproject.toml" # Remove "version" + + # Add version field after [project] line (handle macOS vs Linux differently) + if [[ "${{ matrix.build.OS }}" =~ .*"macos".* ]]; then + # macOS requires backslash and newline + sed -i '' '/^\[project\]/a\ + version = "${{ env.RELEASE_VERSION }}" + ' "./$package/pyproject.toml" + else + # Linux syntax + sed -i '/^\[project\]/a version = "${{ env.RELEASE_VERSION }}"' "./$package/pyproject.toml" + fi + # Log the updated file cat "./$package/pyproject.toml" done + - name: Copy README file into package directories + working-directory: ./python + run: | + cp README.md glide-sync/README.md + cp README.md glide-async/README.md + ### ASYNC CLIENT ### - name: Async client - Build source distributions @@ -422,6 +458,7 @@ jobs: - name: Async client - Download binaries uses: actions/download-artifact@v4 with: + pattern: wheels-*-async path: python/glide-async/wheels merge-multiple: true @@ -459,6 +496,7 @@ jobs: - name: Sync client - Download binaries uses: actions/download-artifact@v4 with: + pattern: wheels-*-sync path: python/glide-sync/wheels merge-multiple: true diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index c20321dc92..23f01ca7f8 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -45,7 +45,11 @@ on: default: false run-with-macos: description: "Run with macos included (only when necessary)" - type: boolean + type: choice + options: + - false + - use-self-hosted + - use-github default: false name: required: false @@ -70,7 +74,7 @@ on: inputs: run-with-macos: description: "Run with macos included (only when necessary)" - type: boolean + type: string default: false concurrency: @@ -89,7 +93,7 @@ run-name: env: # Run full test matrix if job started by cron or it was explictly specified by a person who triggered the workflow RUN_FULL_MATRIX: ${{ (github.event.inputs.full-matrix == 'true' || github.event_name == 'schedule') }} - RUN_WITH_MACOS: ${{ (github.event.inputs.run-with-macos == 'true') }} + RUN_WITH_MACOS: ${{ (github.event.inputs.run-with-macos) }} RUN_SYNC_ONLY: ${{ (github.event.inputs.run_sync_tests == 'true' && github.event.inputs.run_async_tests == 'false') }} RUN_ASYNC_ONLY: ${{ (github.event.inputs.run_async_tests == 'true' && github.event.inputs.run_sync_tests == 'false') }} @@ -107,7 +111,7 @@ jobs: with: language-name: python run-full-matrix: ${{ env.RUN_FULL_MATRIX == 'true' }} - run-with-macos: ${{ env.RUN_WITH_MACOS == 'true' }} + run-with-macos: ${{ env.RUN_WITH_MACOS }} test-python: name: Python Tests - ${{ matrix.python }}, EngineVersion - ${{ matrix.engine.version }}, Target - ${{ matrix.host.TARGET }} diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8476770fc9..599f116421 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -44,7 +44,11 @@ on: default: false run-with-macos: description: "Run with macos included (only when necessary)" - type: boolean + type: choice + options: + - false + - use-self-hosted + - use-github default: false name: required: false @@ -55,7 +59,7 @@ on: inputs: run-with-macos: description: "Run with macos included (only when necessary)" - type: boolean + type: string default: false concurrency: @@ -87,7 +91,7 @@ jobs: language-name: rust # Run full test matrix if job started by cron or it was explictly specified by a person who triggered the workflow run-full-matrix: ${{ github.event.inputs.full-matrix == 'true' || github.event_name == 'schedule' }} - run-with-macos: ${{ github.event.inputs.run-with-macos == 'true' }} + run-with-macos: ${{ github.event.inputs.run-with-macos }} tests: runs-on: ${{ matrix.host.RUNNER }} diff --git a/CHANGELOG.md b/CHANGELOG.md index afb8a011c2..af88184404 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ * JAVA: Valkey 9 new commands HASH items expiration ([#4556](https://github.com/valkey-io/valkey-glide/pull/4556)) * NODE: Valkey 9 support - Add Hash Field Expiration Commands Support ([#4598](https://github.com/valkey-io/valkey-glide/pull/4598)) * Python: Valkey 9 new hash field expiration commands ([#4610](https://github.com/valkey-io/valkey-glide/pull/4610)) +* Node: Add Multi-Database Support for Cluster Mode (Valkey 9.0) ([#4657](https://github.com/valkey-io/valkey-glide/pull/4657)) +* Java: Multi-Database Support for Cluster Mode Valkey 9.0 ([#4658](https://github.com/valkey-io/valkey-glide/pull/4658)) +* Go: Add Multi-Database Support for Cluster Mode Valkey 9.0 ([#4660](https://github.com/valkey-io/valkey-glide/pull/4660)) +* Python: Add Multi-Database Support for Cluster Mode Valkey 9.0 ([#4659](https://github.com/valkey-io/valkey-glide/pull/4659)) #### Fixes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bd319c9339..017c4153e0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,6 +41,7 @@ Looking at the existing issues is a great way to find something to contribute on - [Java](./java/DEVELOPER.md) - [Node](./node/DEVELOPER.md) - [Python](./python/DEVELOPER.md) +- [Go](./go/DEVELOPER.md) ## Code of Conduct This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). diff --git a/glide-core/redis-rs/redis/src/cluster.rs b/glide-core/redis-rs/redis/src/cluster.rs index 23856efafb..49f5438f6a 100644 --- a/glide-core/redis-rs/redis/src/cluster.rs +++ b/glide-core/redis-rs/redis/src/cluster.rs @@ -992,7 +992,7 @@ pub(crate) fn get_connection_info( username: cluster_params.username, client_name: cluster_params.client_name, protocol: cluster_params.protocol, - db: 0, + db: cluster_params.database_id, pubsub_subscriptions: cluster_params.pubsub_subscriptions, }, }) diff --git a/glide-core/redis-rs/redis/src/cluster_client.rs b/glide-core/redis-rs/redis/src/cluster_client.rs index dbd990b70a..7ae479d240 100644 --- a/glide-core/redis-rs/redis/src/cluster_client.rs +++ b/glide-core/redis-rs/redis/src/cluster_client.rs @@ -44,6 +44,7 @@ struct BuilderParams { protocol: ProtocolVersion, pubsub_subscriptions: Option, reconnect_retry_strategy: Option, + database_id: i64, } #[derive(Clone)] @@ -144,6 +145,7 @@ pub struct ClusterParams { pub(crate) protocol: ProtocolVersion, pub(crate) pubsub_subscriptions: Option, pub(crate) reconnect_retry_strategy: Option, + pub(crate) database_id: i64, } impl ClusterParams { @@ -173,6 +175,7 @@ impl ClusterParams { protocol: value.protocol, pubsub_subscriptions: value.pubsub_subscriptions, reconnect_retry_strategy: value.reconnect_retry_strategy, + database_id: value.database_id, }) } } @@ -489,6 +492,15 @@ impl ClusterClientBuilder { self } + /// Sets the database ID for the new ClusterClient. + /// + /// Note: Database selection in cluster mode requires server support for multiple databases. + /// Most cluster configurations only support database 0. + pub fn database_id(mut self, database_id: i64) -> ClusterClientBuilder { + self.builder_params.database_id = database_id; + self + } + /// Use `build()`. #[deprecated(since = "0.22.0", note = "Use build()")] pub fn open(self) -> RedisResult { diff --git a/glide-core/redis-rs/redis/src/cluster_routing.rs b/glide-core/redis-rs/redis/src/cluster_routing.rs index 11f3fc90b0..282eb2452c 100644 --- a/glide-core/redis-rs/redis/src/cluster_routing.rs +++ b/glide-core/redis-rs/redis/src/cluster_routing.rs @@ -545,8 +545,8 @@ impl ResponsePolicy { | b"CLIENT SETINFO" | b"CONFIG SET" | b"CONFIG RESETSTAT" | b"CONFIG REWRITE" | b"FLUSHALL" | b"FLUSHDB" | b"FUNCTION DELETE" | b"FUNCTION FLUSH" | b"FUNCTION LOAD" | b"FUNCTION RESTORE" | b"MEMORY PURGE" | b"MSET" | b"JSON.MSET" - | b"PING" | b"SCRIPT FLUSH" | b"SCRIPT LOAD" | b"SLOWLOG RESET" | b"UNWATCH" - | b"WATCH" => Some(ResponsePolicy::AllSucceeded), + | b"PING" | b"SCRIPT FLUSH" | b"SCRIPT LOAD" | b"SELECT" | b"SLOWLOG RESET" + | b"UNWATCH" | b"WATCH" => Some(ResponsePolicy::AllSucceeded), b"KEYS" | b"FT._ALIASLIST" @@ -601,6 +601,7 @@ fn base_routing(cmd: &[u8]) -> RouteBy { | b"ACL SAVE" | b"CLIENT SETNAME" | b"CLIENT SETINFO" + | b"SELECT" | b"SLOWLOG GET" | b"SLOWLOG LEN" | b"SLOWLOG RESET" diff --git a/glide-core/src/client/mod.rs b/glide-core/src/client/mod.rs index 9349180c04..226bab03d9 100644 --- a/glide-core/src/client/mod.rs +++ b/glide-core/src/client/mod.rs @@ -902,6 +902,7 @@ async fn create_cluster_client( builder = builder.periodic_topology_checks(interval_duration); } builder = builder.use_protocol(request.protocol.unwrap_or_default()); + builder = builder.database_id(redis_connection_info.db); if let Some(client_name) = redis_connection_info.client_name { builder = builder.client_name(client_name); } @@ -979,7 +980,6 @@ async fn create_cluster_client( } } } - Ok(con) } diff --git a/glide-core/tests/test_cluster_client.rs b/glide-core/tests/test_cluster_client.rs index c69f5009f3..29f152dba7 100644 --- a/glide-core/tests/test_cluster_client.rs +++ b/glide-core/tests/test_cluster_client.rs @@ -365,6 +365,108 @@ mod cluster_client_tests { total_clients } + #[rstest] + #[serial_test::serial] + #[timeout(SHORT_CLUSTER_TEST_TIMEOUT)] + /// Test that verifies the client maintains the correct database ID after an automatic reconnection. + /// This test: + /// 1. Creates a client connected to database 4 + /// 2. Verifies the initial connection is to the correct database + /// 3. Simulates a connection drop by killing the connection + /// 4. Sends another command which either: + /// - Fails due to the dropped connection, then retries and verifies reconnection to db=4 + /// - Succeeds with a new client ID (indicating reconnection) and verifies still on db=4 + /// This ensures that database selection persists across reconnections. + fn test_set_database_id_after_reconnection() { + let mut client_info_cmd = redis::cmd("CLIENT"); + client_info_cmd.arg("INFO"); + block_on_all(async { + // First create a basic client to check server version + let mut version_check_basics = setup_test_basics_internal(TestConfiguration { + cluster_mode: ClusterMode::Enabled, + shared_server: true, + ..Default::default() + }) + .await; + + // Skip test if server version is less than 9.0 (database isolation not supported) + if !utilities::version_greater_or_equal(&mut version_check_basics.client, "9.0.0").await + { + return; + } + let mut test_basics = setup_test_basics_internal(TestConfiguration { + cluster_mode: ClusterMode::Enabled, + shared_server: true, + database_id: 4, // Set a specific database ID + ..Default::default() + }) + .await; + + let client_info = test_basics + .client + .send_command(&client_info_cmd, None) + .await + .unwrap(); + let client_info_str = match client_info { + redis::Value::BulkString(bytes) => String::from_utf8_lossy(&bytes).to_string(), + redis::Value::VerbatimString { text, .. } => text, + _ => panic!("Unexpected CLIENT INFO response type: {:?}", client_info), + }; + assert!(client_info_str.contains("db=4")); + + // Extract initial client ID + let initial_client_id = + extract_client_id(&client_info_str).expect("Failed to extract initial client ID"); + + kill_connection(&mut test_basics.client).await; + + let res = test_basics + .client + .send_command(&client_info_cmd, None) + .await; + match res { + Err(err) => { + // Connection was dropped as expected + assert!( + err.is_connection_dropped() || err.is_timeout(), + "Expected connection dropped or timeout error, got: {err:?}", + ); + let client_info = repeat_try_create(|| async { + let mut client = test_basics.client.clone(); + let response = client.send_command(&client_info_cmd, None).await.ok()?; + match response { + redis::Value::BulkString(bytes) => { + Some(String::from_utf8_lossy(&bytes).to_string()) + } + redis::Value::VerbatimString { text, .. } => Some(text), + _ => None, + } + }) + .await; + assert!(client_info.contains("db=4")); + } + Ok(response) => { + // Command succeeded, extract new client ID and compare + let new_client_info = match response { + redis::Value::BulkString(bytes) => { + String::from_utf8_lossy(&bytes).to_string() + } + redis::Value::VerbatimString { text, .. } => text, + _ => panic!("Unexpected CLIENT INFO response type: {:?}", response), + }; + let new_client_id = extract_client_id(&new_client_info) + .expect("Failed to extract new client ID"); + assert_ne!( + initial_client_id, new_client_id, + "Client ID should change after reconnection if command succeeds" + ); + // Check that the database ID is still 4 + assert!(new_client_info.contains("db=4")); + } + } + }); + } + #[rstest] #[serial_test::serial] #[timeout(LONG_CLUSTER_TEST_TIMEOUT)] diff --git a/glide-core/tests/test_standalone_client.rs b/glide-core/tests/test_standalone_client.rs index 2d0ab3eb1a..35849810c9 100644 --- a/glide-core/tests/test_standalone_client.rs +++ b/glide-core/tests/test_standalone_client.rs @@ -524,14 +524,6 @@ mod standalone_client_tests { }); } - fn extract_client_id(client_info: &str) -> Option { - client_info - .split_whitespace() - .find(|part| part.starts_with("id=")) - .and_then(|id_part| id_part.strip_prefix("id=")) - .map(|id| id.to_string()) - } - #[rstest] #[serial_test::serial] #[timeout(LONG_STANDALONE_TEST_TIMEOUT)] diff --git a/glide-core/tests/utilities/mod.rs b/glide-core/tests/utilities/mod.rs index 26d1b8470a..20ab97e98f 100644 --- a/glide-core/tests/utilities/mod.rs +++ b/glide-core/tests/utilities/mod.rs @@ -19,6 +19,7 @@ use std::{ }; use tempfile::TempDir; use tokio::sync::mpsc; +use versions::Versioning; pub mod cluster; pub mod mocks; @@ -769,3 +770,84 @@ pub enum BackingServer { Standalone(Option), Cluster(Option), } + +/// Get the server version from a client connection +pub async fn get_server_version( + client: &mut impl glide_core::client::GlideClientForTests, +) -> (u16, u16, u16) { + let mut info_cmd = redis::cmd("INFO"); + info_cmd.arg("SERVER"); + + let info_result = client.send_command(&info_cmd, None).await.unwrap(); + let info_string = match info_result { + Value::BulkString(bytes) => String::from_utf8_lossy(&bytes).to_string(), + Value::VerbatimString { text, .. } => text, + Value::Map(node_results) => { + // In cluster mode, INFO returns a map of node -> info string + // We just need to get the version from any node (they should all be the same) + if let Some((_, node_info)) = node_results.first() { + match node_info { + Value::VerbatimString { text, .. } => text.clone(), + Value::BulkString(bytes) => String::from_utf8_lossy(bytes).to_string(), + _ => panic!( + "Unexpected node info type in cluster INFO response: {:?}", + node_info + ), + } + } else { + panic!("Empty cluster INFO response"); + } + } + _ => panic!("Unexpected INFO response type: {:?}", info_result), + }; + + // Parse the INFO response to extract version + // First try to find valkey_version, then fall back to redis_version + for line in info_string.lines() { + if let Some(version_str) = line.strip_prefix("valkey_version:") { + return parse_version_string(version_str); + } + } + + // If no valkey_version found, look for redis_version + for line in info_string.lines() { + if let Some(version_str) = line.strip_prefix("redis_version:") { + return parse_version_string(version_str); + } + } + + panic!("Could not find version information in INFO response"); +} + +/// Parse a version string like "8.1.3" into (8, 1, 3) +fn parse_version_string(version_str: &str) -> (u16, u16, u16) { + let parts: Vec<&str> = version_str.split('.').collect(); + if parts.len() >= 3 { + let major = parts[0].parse().unwrap_or(0); + let minor = parts[1].parse().unwrap_or(0); + let patch = parts[2].parse().unwrap_or(0); + (major, minor, patch) + } else { + panic!("Invalid version format: {}", version_str); + } +} + +/// Check if the server version is greater than or equal to the specified version +pub async fn version_greater_or_equal( + client: &mut impl glide_core::client::GlideClientForTests, + version: &str, +) -> bool { + let (major, minor, patch) = get_server_version(client).await; + let server_version = Versioning::new(format!("{major}.{minor}.{patch}")).unwrap(); + let compared_version = Versioning::new(version).unwrap(); + server_version >= compared_version +} + +/// Extract client ID from CLIENT INFO response string +pub fn extract_client_id(client_info: &str) -> Option { + client_info + .split_whitespace() + .find(|part| part.starts_with("id=")) + .and_then(|id_part| id_part.strip_prefix("id=")) + .map(|id| id.to_string()) +} diff --git a/go/DEVELOPER.md b/go/DEVELOPER.md index 71b1e7caf2..3ba35edce3 100644 --- a/go/DEVELOPER.md +++ b/go/DEVELOPER.md @@ -141,15 +141,7 @@ Before starting this step, make sure you've installed all software requirements. make build ``` -4. Run tests: - 1. Ensure that you have installed valkey-server and valkey-cli on your host. You can find the Valkey installation guide at the following link: [Valkey Installation Guide](https://valkey.io/topics/installation/). - 2. Execute the following command from the go folder: - - ```bash - go test -race ./... - ``` - -5. Install Go development tools with: +4. Install Go development tools with: ```bash make install-dev-tools diff --git a/go/Makefile b/go/Makefile index 1e4cba1a0f..03c3b51dc6 100644 --- a/go/Makefile +++ b/go/Makefile @@ -10,6 +10,7 @@ TARGET_DIR := $(GLIDE_FFI_PATH)/target # Determine the target folder based on OS and architecture UNAME := $(shell uname) ARCH := $(shell uname -m) +IS_ALPINE := $(shell test -f /etc/alpine-release && echo true || echo false) # Set default values for GLIDE_NAME and GLIDE_VERSION # GLIDE_VERSION is automatically set during the deployment workflow based on the value defined in go-cd.yml. @@ -17,20 +18,31 @@ ARCH := $(shell uname -m) GLIDE_NAME = GlideGo GLIDE_VERSION ?= unknown +ifndef TARGET_TRIPLET ifeq ($(UNAME), Darwin) TARGET_TRIPLET := $(if $(filter arm64,$(ARCH)),aarch64-apple-darwin,x86_64-apple-darwin) +else ifeq ($(UNAME), Linux) + ifeq ($(IS_ALPINE), true) + TARGET_TRIPLET := $(if $(filter arm64 aarch64,$(ARCH)),aarch64-unknown-linux-musl,x86_64-unknown-linux-musl) + else + TARGET_TRIPLET := $(if $(filter arm64 aarch64,$(ARCH)),aarch64-unknown-linux-gnu,x86_64-unknown-linux-gnu) + endif +else + $(error Unsupported platform: $(UNAME) $(ARCH)) +endif +endif + +ifeq ($(UNAME), Darwin) # https://github.com/rust-lang/rust/issues/51009#issuecomment-2274649980 BUILD_CMD := rustc --crate-type staticlib CARGO_FIX_CMD := : # no-op CARGO_POSTFIX_CMD := : # no-op STRIP_CMD := strip -x else ifeq ($(UNAME), Linux) - # TODO: musl - TARGET_TRIPLET := $(if $(filter arm64 aarch64,$(ARCH)),aarch64-unknown-linux-gnu,x86_64-unknown-linux-gnu) # zigbuild https://github.com/rust-cross/cargo-zigbuild - BUILD_CMD := zigbuild --target $(TARGET_TRIPLET).2.17 + BUILD_CMD := zigbuild --target $(TARGET_TRIPLET) # workaround for https://github.com/rust-cross/cargo-zigbuild/issues/337 - CARGO_FIX_CMD := sed -i 's/crate-type.*/crate-type = ["staticlib"]/g' Cargo.toml + CARGO_FIX_CMD := sed -i 's/crate-type.*/crate-type = ["staticlib"]/g' Cargo.toml && rustup target add $(TARGET_TRIPLET) CARGO_POSTFIX_CMD := git restore Cargo.toml STRIP_CMD := strip --strip-unneeded TARGET_DIR := $(TARGET_DIR)/$(TARGET_TRIPLET) @@ -41,6 +53,15 @@ endif # Path where compiled binary is copied to and therefore used by go DEST_PATH := rustbin/$(TARGET_TRIPLET) +# Determine if we're using musl +IS_MUSL := $(if $(filter true,$(IS_ALPINE)),true,$(if $(findstring musl,$(TARGET_TRIPLET)),true,false)) + +# Set GOFLAGS for musl to use musl tag +ifeq ($(IS_MUSL), true) + export GOFLAGS := -tags=musl +endif + + install-build-tools: go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.33.0 cargo install cbindgen @@ -171,10 +192,21 @@ long-timeout-test: __it opentelemetry-test: export TEST_FILTER = -skip "TestGlideTestSuite/TestModule" -otel-test $(if $(test-filter), -testify.m $(test-filter), -testify.m TestOpenTelemetry) opentelemetry-test: __it +# Test command for integration tests +ifeq ($(IS_MUSL), true) +# musl gcc does not support -fsanitize=address, so we use clang instead +# TODO - Investigate musl + address sanitizer issue - https://github.com/valkey-io/valkey-glide/issues/4671 +# Disabled for now +# MUSL_TEST_CMD := CC=clang go test -asan -v ./integTest/... + TEST_CMD := CC="gcc" go test -v ./integTest/... +else + TEST_CMD := CC="gcc -fsanitize=address" go test -v ./integTest/... +endif + __it: mkdir -p reports set -o pipefail; \ - CC="gcc -fsanitize=address" go test -v ./integTest/... \ + $(TEST_CMD) \ $(TEST_FILTER) \ $(if $(filter true, $(tls)), --tls,) \ $(if $(standalone-endpoints), --standalone-endpoints=$(standalone-endpoints)) \ diff --git a/go/README.md b/go/README.md index e84a7640de..8b0541c5fe 100644 --- a/go/README.md +++ b/go/README.md @@ -50,6 +50,13 @@ To install Valkey GLIDE in your Go project, follow these steps: ``` 3. After installation, you can start up a Valkey server and run one of the examples in [Basic Examples](#basic-examples). +### Alpine Linux / MUSL + +If you are running on Alpine Linux or otherwise require a MUSL-based build, you must add the 'musl' tag to your build. + +``` +export GOFLAGS := -tags=musl +``` ## Basic Examples diff --git a/go/base_client.go b/go/base_client.go index f44e26afd2..87f8ce3162 100644 --- a/go/base_client.go +++ b/go/base_client.go @@ -6,8 +6,10 @@ package glide // #cgo !windows LDFLAGS: -lm // #cgo darwin LDFLAGS: -framework Security // #cgo darwin,amd64 LDFLAGS: -framework CoreFoundation -// #cgo linux,amd64 LDFLAGS: -L${SRCDIR}/rustbin/x86_64-unknown-linux-gnu -// #cgo linux,arm64 LDFLAGS: -L${SRCDIR}/rustbin/aarch64-unknown-linux-gnu +// #cgo linux,amd64,!musl LDFLAGS: -L${SRCDIR}/rustbin/x86_64-unknown-linux-gnu +// #cgo linux,amd64,musl LDFLAGS: -L${SRCDIR}/rustbin/x86_64-unknown-linux-musl +// #cgo linux,arm64,!musl LDFLAGS: -L${SRCDIR}/rustbin/aarch64-unknown-linux-gnu +// #cgo linux,arm64,musl LDFLAGS: -L${SRCDIR}/rustbin/aarch64-unknown-linux-musl // #cgo darwin,arm64 LDFLAGS: -L${SRCDIR}/rustbin/aarch64-apple-darwin // #cgo darwin,amd64 LDFLAGS: -L${SRCDIR}/rustbin/x86_64-apple-darwin // #include "lib.h" diff --git a/go/config/config.go b/go/config/config.go index 45782b7d04..5b8020c599 100644 --- a/go/config/config.go +++ b/go/config/config.go @@ -102,6 +102,7 @@ type baseClientConfiguration struct { clientAZ string reconnectStrategy *BackoffStrategy lazyConnect bool + DatabaseId *int `json:"database_id,omitempty"` } func (config *baseClientConfiguration) toProtobuf() (*protobuf.ConnectionRequest, error) { @@ -152,6 +153,10 @@ func (config *baseClientConfiguration) toProtobuf() (*protobuf.ConnectionRequest request.LazyConnect = config.lazyConnect } + if config.DatabaseId != nil { + request.DatabaseId = uint32(*config.DatabaseId) + } + return &request, nil } @@ -214,7 +219,6 @@ func (strategy *BackoffStrategy) toProtobuf() *protobuf.ConnectionRetryStrategy // ClientConfiguration represents the configuration settings for a Standalone client. type ClientConfiguration struct { baseClientConfiguration - databaseId int subscriptionConfig *StandaloneSubscriptionConfig AdvancedClientConfiguration } @@ -232,9 +236,6 @@ func (config *ClientConfiguration) ToProtobuf() (*protobuf.ConnectionRequest, er } request.ClusterModeEnabled = false - if config.databaseId != 0 { - request.DatabaseId = uint32(config.databaseId) - } if config.subscriptionConfig != nil && len(config.subscriptionConfig.subscriptions) > 0 { request.PubsubSubscriptions = config.subscriptionConfig.toProtobuf() } @@ -330,7 +331,7 @@ func (config *ClientConfiguration) WithReconnectStrategy(strategy *BackoffStrate // WithDatabaseId sets the index of the logical database to connect to. func (config *ClientConfiguration) WithDatabaseId(id int) *ClientConfiguration { - config.databaseId = id + config.DatabaseId = &id return config } @@ -481,6 +482,12 @@ func (config *ClusterClientConfiguration) WithReconnectStrategy( return config } +// WithDatabaseId sets the index of the logical database to connect to. +func (config *ClusterClientConfiguration) WithDatabaseId(id int) *ClusterClientConfiguration { + config.DatabaseId = &id + return config +} + // WithAdvancedConfiguration sets the advanced configuration settings for the client. func (config *ClusterClientConfiguration) WithAdvancedConfiguration( advancedConfig *AdvancedClusterClientConfiguration, diff --git a/go/config/config_test.go b/go/config/config_test.go index b598a6de55..bf003aa2fe 100644 --- a/go/config/config_test.go +++ b/go/config/config_test.go @@ -340,3 +340,54 @@ func TestConfig_LazyConnect(t *testing.T) { assert.False(t, defaultClusterResult.LazyConnect) } + +func TestConfig_DatabaseId(t *testing.T) { + // Test standalone client with database ID + standaloneConfig := NewClientConfiguration().WithDatabaseId(5) + standaloneResult, err := standaloneConfig.ToProtobuf() + if err != nil { + t.Fatalf("Failed to convert standalone config to protobuf: %v", err) + } + assert.Equal(t, uint32(5), standaloneResult.DatabaseId) + + // Test cluster client with database ID + clusterConfig := NewClusterClientConfiguration().WithDatabaseId(3) + clusterResult, err := clusterConfig.ToProtobuf() + if err != nil { + t.Fatalf("Failed to convert cluster config to protobuf: %v", err) + } + assert.Equal(t, uint32(3), clusterResult.DatabaseId) + + // Test default behavior (no database ID set) + defaultStandaloneConfig := NewClientConfiguration() + defaultStandaloneResult, err := defaultStandaloneConfig.ToProtobuf() + if err != nil { + t.Fatalf("Failed to convert default standalone config to protobuf: %v", err) + } + assert.Equal(t, uint32(0), defaultStandaloneResult.DatabaseId) + + defaultClusterConfig := NewClusterClientConfiguration() + defaultClusterResult, err := defaultClusterConfig.ToProtobuf() + if err != nil { + t.Fatalf("Failed to convert default cluster config to protobuf: %v", err) + } + assert.Equal(t, uint32(0), defaultClusterResult.DatabaseId) +} + +func TestConfig_DatabaseId_BaseConfiguration(t *testing.T) { + // Test that database_id is properly handled in base configuration for both client types + + // Test standalone client inherits database_id from base configuration + standaloneConfig := NewClientConfiguration().WithDatabaseId(5) + standaloneResult, err := standaloneConfig.ToProtobuf() + assert.NoError(t, err) + assert.Equal(t, uint32(5), standaloneResult.DatabaseId) + assert.False(t, standaloneResult.ClusterModeEnabled) + + // Test cluster client inherits database_id from base configuration + clusterConfig := NewClusterClientConfiguration().WithDatabaseId(3) + clusterResult, err := clusterConfig.ToProtobuf() + assert.NoError(t, err) + assert.Equal(t, uint32(3), clusterResult.DatabaseId) + assert.True(t, clusterResult.ClusterModeEnabled) +} diff --git a/go/create_client_test.go b/go/create_client_test.go index 821ed209f4..9d1df8505a 100644 --- a/go/create_client_test.go +++ b/go/create_client_test.go @@ -54,3 +54,27 @@ func ExampleNewClusterClient() { // Output: // Client created and connected: *glide.ClusterClient } + +func ExampleNewClusterClient_withDatabaseId() { + // This WithDatabaseId for cluster requires Valkey 9.0+ + clientConf := config.NewClusterClientConfiguration(). + WithAddress(&getClusterAddresses()[0]). + WithRequestTimeout(5 * time.Second). + WithUseTLS(false). + WithDatabaseId(1). + WithSubscriptionConfig( + config.NewClusterSubscriptionConfig(). + WithSubscription(config.PatternClusterChannelMode, "news.*"). + WithCallback(func(message *models.PubSubMessage, ctx any) { + fmt.Printf("Received message on '%s': %s", message.Channel, message.Message) + }, nil), + ) + client, err := NewClusterClient(clientConf) + if err != nil { + fmt.Println("Failed to create a client and connect: ", err) + } + fmt.Printf("Client created and connected: %T", client) + + // Output: + // Client created and connected: *glide.ClusterClient +} diff --git a/go/glide_client.go b/go/glide_client.go index 924075fad4..fb412e7e1b 100644 --- a/go/glide_client.go +++ b/go/glide_client.go @@ -210,6 +210,20 @@ func (client *Client) ConfigGet(ctx context.Context, args []string) (map[string] // Select changes the currently selected database. // +// WARNING: This command is NOT RECOMMENDED for production use. +// Upon reconnection, the client will revert to the database_id specified +// in the client configuration (default: 0), NOT the database selected +// via this command. +// +// RECOMMENDED APPROACH: Use the database_id parameter in client +// configuration instead: +// +// config := &config.ClientConfiguration{ +// Addresses: []config.NodeAddress{{Host: "localhost", Port: 6379}}, +// DatabaseId: &databaseId, // Recommended: persists across reconnections +// } +// client, err := NewClient(config) +// // See [valkey.io] for details. // // Parameters: diff --git a/java/README.md b/java/README.md index 62427fddd4..4397cf32da 100644 --- a/java/README.md +++ b/java/README.md @@ -25,8 +25,9 @@ The Java client contains the following parts: 1. `src`: Rust dynamic library FFI to integrate with [GLIDE core library](../glide-core/). 2. `client`: A Java-wrapper around the GLIDE core rust library and unit tests for it. -3. `benchmark`: A dedicated benchmarking tool designed to evaluate and compare the performance of Valkey GLIDE and other Java clients. -4. `integTest`: An integration test sub-project for API and E2E testing. +3. `jedis-compatibility`: A Jedis-compatible API layer that provides drop-in replacement for existing Jedis applications. +4. `benchmark`: A dedicated benchmarking tool designed to evaluate and compare the performance of Valkey GLIDE and other Java clients. +5. `integTest`: An integration test sub-project for API and E2E testing. An example app (called glide.examples.ExamplesApp) is also available under [examples app](../examples/java), to sanity check the project. @@ -57,12 +58,14 @@ Once set up, you can run the basic examples. Additionally, consider installing the Gradle plugin, [OS Detector](https://github.com/google/osdetector-gradle-plugin) to help you determine what classifier to use. ## Classifiers -There are 4 types of classifiers for Valkey GLIDE which are +There are 6 types of classifiers for Valkey GLIDE which are ``` osx-aarch_64 osx-x86_64 linux-aarch_64 linux-x86_64 +linux_musl-aarch_64 +linux_musl-x86_64 ``` Gradle: @@ -89,7 +92,17 @@ dependencies { implementation group: 'io.valkey', name: 'valkey-glide', version: '1.+', classifier: 'linux-x86_64' } -// with osdetector +// linux_musl-aarch_64 +dependencies { + implementation group: 'io.valkey', name: 'valkey-glide', version: '1.+', classifier: 'linux_musl-aarch_64' +} + +// linux_musl-x86_64 +dependencies { + implementation group: 'io.valkey', name: 'valkey-glide', version: '1.+', classifier: 'linux_musl-x86_64' +} + +// with osdetector - does not work for musl plugins { id "com.google.osdetector" version "1.7.3" } @@ -134,6 +147,22 @@ Maven: [1.0.0,) + + + io.valkey + valkey-glide + linux_musl-aarch_64 + [1.0.0,) + + + + + io.valkey + valkey-glide + linux_musl-x86_64 + [1.0.0,) + + @@ -167,6 +196,12 @@ libraryDependencies += "io.valkey" % "valkey-glide" % "1.+" classifier "linux-aa // linux-x86_64 libraryDependencies += "io.valkey" % "valkey-glide" % "1.+" classifier "linux-x86_64" + +// linux_musl-aarch_64 +libraryDependencies += "io.valkey" % "valkey-glide" % "1.+" classifier "linux_musl-aarch_64" + +// linux_musl-x86_64 +libraryDependencies += "io.valkey" % "valkey-glide" % "1.+" classifier "linux_musl-x86_64" ``` ## Setting up the Java module diff --git a/java/benchmarks/build.gradle b/java/benchmarks/build.gradle index a6baeb9d79..b93844aaaa 100644 --- a/java/benchmarks/build.gradle +++ b/java/benchmarks/build.gradle @@ -17,7 +17,15 @@ tasks.withType(JavaCompile) { dependencies { version = System.getenv("GLIDE_RELEASE_VERSION") ?: project.ext.defaultReleaseVersion - implementation "io.valkey:valkey-glide:${version}:${osdetector.classifier}" + def classifier + if (osdetector.os == 'linux' && osdetector.release.id == 'alpine') { + classifier = "linux_musl-${osdetector.arch}" + } + else { + classifier = osdetector.classifier + } + implementation "io.valkey:valkey-glide:${version}:${classifier}" + // This dependency is used internally, and not exposed to consumers on their own compile classpath. implementation 'com.google.guava:guava:33.4.8-jre' diff --git a/java/client/build.gradle b/java/client/build.gradle index b9888fff86..cb9399d365 100644 --- a/java/client/build.gradle +++ b/java/client/build.gradle @@ -22,9 +22,6 @@ dependencies { implementation group: 'com.google.protobuf', name: 'protobuf-java', version: '4.29.1' shadow group: 'org.apache.commons', name: 'commons-lang3', version: '3.13.0' - // Apache Commons Pool 2 for Jedis compatibility layer - implementation group: 'org.apache.commons', name: 'commons-pool2', version: '2.12.0' - implementation group: 'io.netty', name: 'netty-handler', version: '4.1.121.Final' // https://github.com/netty/netty/wiki/Native-transports // At the moment, Windows is not supported @@ -119,7 +116,7 @@ tasks.register('buildRust', Exec) { if (target.contains("linux") && !target.contains("musl")) { commandLine 'cargo', 'zigbuild', '--target', "$target", '--release' } else if (target.contains("musl")) { - commandLine 'cargo', 'build', '--target', "$target", '--release' + commandLine 'cargo', 'zigbuild', '--target', "$target", '--release' environment RUSTFLAGS: '-C target-feature=-crt-static' } else { commandLine 'cargo', 'build', '--release' @@ -147,7 +144,7 @@ tasks.register('buildRustFfi', Exec) { commandLine 'cargo', 'zigbuild', '--target', "$target", '--release' environment RUSTFLAGS: '--cfg ffi_test' } else if (target.contains("musl")) { - commandLine 'cargo', 'build', '--target', "$target", '--release' + commandLine 'cargo', 'zigbuild', '--target', "$target", '--release' environment RUSTFLAGS: '--cfg ffi_test -C target-feature=-crt-static' } else { commandLine 'cargo', 'build', '--release' @@ -189,6 +186,8 @@ tasks.register('copyNativeLib', Copy) { from "${projectDir}/../target/${arch}-unknown-linux-gnu/release/" } else if (osdetector.os == 'linux' && osdetector.release.id == 'alpine') { from "${projectDir}/../target/${arch}-unknown-linux-musl/release/" + } else { + from "${projectDir}/../target/release/" } include "*.dylib", "*.so" into sourceSets.main.output.resourcesDir @@ -295,7 +294,12 @@ tasks.withType(Test) { showStandardStreams true } // This is needed for the FFI tests - if (osdetector.os == 'linux' && osdetector.release.id == 'alpine') { + if (project.hasProperty('target')) { + target = project.target + jvmArgs "-Djava.library.path=${projectDir}/../target/${target}/release" + } else if (osdetector.os == 'linux' && osdetector.release.id != 'alpine') { + jvmArgs "-Djava.library.path=${projectDir}/../target/${arch}-unknown-linux-gnu/release" + } else if (osdetector.os == 'linux' && osdetector.release.id == 'alpine') { jvmArgs "-Djava.library.path=${projectDir}/../target/${arch}-unknown-linux-musl/release" } else { jvmArgs "-Djava.library.path=${projectDir}/../target/release" @@ -306,7 +310,7 @@ tasks.withType(Test) { jar { if (project.hasProperty('target') && project.target.contains("musl")) { - archiveClassifier = "linux_musl_${arch}" + archiveClassifier = "linux_musl-${osdetector.arch}" } else { archiveClassifier = osdetector.classifier } @@ -362,14 +366,13 @@ class NettyResourceTransformer implements Transformer { shadowJar { dependsOn('copyNativeLib') if (project.hasProperty('target') && project.target.contains("musl")) { - archiveClassifier = "linux_musl_${arch}" + archiveClassifier = "linux_musl-${osdetector.arch}" } else { archiveClassifier = osdetector.classifier } excludes.remove("module-info.class") relocate('io.netty', 'glide.io.netty') relocate('com.google.protobuf', 'glide.com.google.protobuf') - relocate('org.apache.commons.pool2', 'glide.org.apache.commons.pool2') mergeServiceFiles() relocate 'META-INF/native/libnetty', 'META-INF/native/libglide_netty' 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 56bd75d872..27f51b3490 100644 --- a/java/client/src/main/java/glide/api/commands/ConnectionManagementCommands.java +++ b/java/client/src/main/java/glide/api/commands/ConnectionManagementCommands.java @@ -110,6 +110,23 @@ public interface ConnectionManagementCommands { /** * Changes the currently selected database. * + *

WARNING: This command is NOT RECOMMENDED for production use. Upon + * reconnection, the client will revert to the database_id specified in the client configuration + * (default: 0), NOT the database selected via this command. + * + *

RECOMMENDED APPROACH: Use the database_id parameter in client configuration instead: + * + *

RECOMMENDED EXAMPLE: + * + *

{@code
+     * GlideClient client = GlideClient.createClient(
+     *     GlideClientConfiguration.builder()
+     *         .address(NodeAddress.builder().host("localhost").port(6379).build())
+     *         .databaseId(5)  // Recommended: persists across reconnections
+     *         .build()
+     * ).get();
+     * }
+ * * @see valkey.io for details. * @param index The index of the database to select. * @return A simple OK response. 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 6bb56ac777..d24672ebcb 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 @@ -85,6 +85,12 @@ public abstract class BaseClientConfiguration { /** Strategy used to determine how and when to reconnect, in case of connection failures. */ private final BackoffStrategy reconnectStrategy; + /** + * Index of the logical database to connect to. Must be non-negative and within the range + * supported by the server configuration. If not specified, defaults to database 0. + */ + private final Integer databaseId; + /** * Enables lazy connection mode, where physical connections to the server(s) are deferred until * the first command is sent. This can reduce startup latency and allow for client creation in diff --git a/java/client/src/main/java/glide/api/models/configuration/GlideClientConfiguration.java b/java/client/src/main/java/glide/api/models/configuration/GlideClientConfiguration.java index e3a13ccfc5..adcb18e530 100644 --- a/java/client/src/main/java/glide/api/models/configuration/GlideClientConfiguration.java +++ b/java/client/src/main/java/glide/api/models/configuration/GlideClientConfiguration.java @@ -34,9 +34,6 @@ @ToString public class GlideClientConfiguration extends BaseClientConfiguration { - /** Index of the logical database to connect to. */ - private final Integer databaseId; - /** Subscription configuration for the current client. */ private final StandaloneSubscriptionConfiguration subscriptionConfiguration; diff --git a/java/client/src/main/java/glide/managers/ConnectionManager.java b/java/client/src/main/java/glide/managers/ConnectionManager.java index 39c9558649..7c0b4efb6b 100644 --- a/java/client/src/main/java/glide/managers/ConnectionManager.java +++ b/java/client/src/main/java/glide/managers/ConnectionManager.java @@ -162,6 +162,10 @@ private ConnectionRequest.Builder setupConnectionRequestBuilderBaseConfiguration connectionRequestBuilder.setLazyConnect(configuration.isLazyConnect()); } + if (configuration.getDatabaseId() != null) { + connectionRequestBuilder.setDatabaseId(configuration.getDatabaseId()); + } + return connectionRequestBuilder; } @@ -176,10 +180,6 @@ private ConnectionRequest.Builder setupConnectionRequestBuilderGlideClient( setupConnectionRequestBuilderBaseConfiguration(configuration); connectionRequestBuilder.setClusterModeEnabled(false); - if (configuration.getDatabaseId() != null) { - connectionRequestBuilder.setDatabaseId(configuration.getDatabaseId()); - } - if (configuration.getSubscriptionConfiguration() != null) { if (configuration.getProtocol() == ProtocolVersion.RESP2) { throw new ConfigurationError( diff --git a/java/client/src/main/java/module-info.java b/java/client/src/main/java/module-info.java index d44c521256..c1cf8540ec 100644 --- a/java/client/src/main/java/module-info.java +++ b/java/client/src/main/java/module-info.java @@ -15,20 +15,8 @@ exports glide.api.models.configuration; exports glide.api.models.exceptions; exports glide.api.commands.servermodules; - // Export Jedis compatibility layer for drop-in replacement - exports redis.clients.jedis; - exports redis.clients.jedis.params; - exports redis.clients.jedis.args; - exports redis.clients.jedis.resps; - exports redis.clients.jedis.util; - - // Open Jedis compatibility layer for reflection access in tests - opens redis.clients.jedis to - glide.integTest; requires java.logging; // required by shadowed protobuf - requires java.management; // required by Apache Commons Pool for JMX requires static lombok; requires org.apache.commons.lang3; - requires org.apache.commons.pool2; // Required for Jedis compatibility layer } diff --git a/java/client/src/test/java/glide/api/models/configuration/BaseClientConfigurationTest.java b/java/client/src/test/java/glide/api/models/configuration/BaseClientConfigurationTest.java new file mode 100644 index 0000000000..2adcb900ed --- /dev/null +++ b/java/client/src/test/java/glide/api/models/configuration/BaseClientConfigurationTest.java @@ -0,0 +1,59 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.configuration; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +public class BaseClientConfigurationTest { + + /** Test implementation of BaseClientConfiguration for testing purposes */ + private static class TestClientConfiguration extends BaseClientConfiguration { + private TestClientConfiguration(TestClientConfigurationBuilder builder) { + super(builder); + } + + public static TestClientConfigurationBuilder builder() { + return new TestClientConfigurationBuilder(); + } + + @Override + public BaseSubscriptionConfiguration getSubscriptionConfiguration() { + return null; + } + + public static class TestClientConfigurationBuilder + extends BaseClientConfigurationBuilder< + TestClientConfiguration, TestClientConfigurationBuilder> { + @Override + protected TestClientConfigurationBuilder self() { + return this; + } + + @Override + public TestClientConfiguration build() { + return new TestClientConfiguration(this); + } + } + } + + @Test + public void testDatabaseIdDefault() { + // Test that databaseId defaults to null when not specified + TestClientConfiguration config = TestClientConfiguration.builder().build(); + assertNull(config.getDatabaseId()); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 5, 10, 15, 50, 100, 1000}) + public void testDatabaseIdValidRange(int databaseId) { + // Test that non-negative database IDs are accepted (server-side validation will handle range + // checks) + TestClientConfiguration config = + TestClientConfiguration.builder().databaseId(databaseId).build(); + assertEquals(databaseId, config.getDatabaseId()); + } +} diff --git a/java/integTest/build.gradle b/java/integTest/build.gradle index e157823648..4dc9711638 100644 --- a/java/integTest/build.gradle +++ b/java/integTest/build.gradle @@ -13,8 +13,11 @@ tasks.withType(JavaCompile) { } dependencies { - // client + // Use published GLIDE artifact implementation group: 'io.valkey', name: 'valkey-glide', version: project.ext.defaultReleaseVersion, classifier: osdetector.classifier + + // Use published jedis-compatibility artifact + testImplementation group: 'io.valkey', name: 'valkey-glide-jedis-compatibility', version: project.ext.defaultReleaseVersion, classifier: osdetector.classifier implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.13.0' implementation 'com.google.code.gson:gson:2.10.1' @@ -136,9 +139,10 @@ clearDirs.finalizedBy 'startStandalone' clearDirs.finalizedBy 'startCluster' clearDirs.finalizedBy 'startClusterForAz' afterTests.finalizedBy 'stopAllAfterTests' -compileTestJava.dependsOn ':client:publishToMavenLocal' +compileTestJava.dependsOn ':client:publishToMavenLocal', ':jedis-compatibility:publishToMavenLocal' tasks.withType(Test) { + dependsOn ':client:publishToMavenLocal', ':jedis-compatibility:publishToMavenLocal' useJUnitPlatform() if (!project.gradle.startParameter.taskNames.contains(':integTest:modulesTest')) { dependsOn 'beforeTests' @@ -160,6 +164,15 @@ tasks.withType(Test) { minHeapSize = "2048m" // Initial heap size. Needed for max size tests. maxHeapSize = "2048m" // Maximum heap size. Needed for max size tests. + + // Native library path for GLIDE FFI - needed for Jedis compatibility tests + jvmArgs "-Djava.library.path=${project.rootDir}/../target/release" + + // Disable modularity for jedis compatibility tests + if (name.contains('jedis') || filter.includePatterns.any { it.contains('jedis') }) { + jvmArgs += "--add-opens=java.base/java.lang=ALL-UNNAMED" + jvmArgs += "--add-opens=java.base/java.util=ALL-UNNAMED" + } afterTest { desc, result -> logger.quiet "${desc.className}.${desc.name}: ${result.resultType} ${(result.getEndTime() - result.getStartTime())/1000}s" diff --git a/java/integTest/src/test/java/glide/cluster/ClusterClientTests.java b/java/integTest/src/test/java/glide/cluster/ClusterClientTests.java index 16fc6491c6..3139685aa0 100644 --- a/java/integTest/src/test/java/glide/cluster/ClusterClientTests.java +++ b/java/integTest/src/test/java/glide/cluster/ClusterClientTests.java @@ -154,6 +154,24 @@ public void client_name() { client.close(); } + @SneakyThrows + @Test + public void select_cluster_database_id() { + String minVersion = "9.0.0"; + assumeTrue( + SERVER_VERSION.isGreaterThanOrEqualTo(minVersion), + "Valkey version required >= " + minVersion); + + GlideClusterClient client = + GlideClusterClient.createClient(commonClusterClientConfig().databaseId(4).build()).get(); + + String clientInfo = + (String) client.customCommand(new String[] {"CLIENT", "INFO"}).get().getSingleValue(); + assertTrue(clientInfo.contains("db=4")); + + client.close(); + } + @Test @SneakyThrows public void closed_client_throws_ExecutionException_with_ClosingException_as_cause() { diff --git a/java/integTest/src/test/java/module-info.java b/java/integTest/src/test/java/module-info.java deleted file mode 100644 index a81aa1a6c3..0000000000 --- a/java/integTest/src/test/java/module-info.java +++ /dev/null @@ -1,17 +0,0 @@ -module glide.integTest { - opens glide; - opens glide.cluster; - opens glide.modules; - opens glide.standalone; - opens compatibility.jedis; - - requires glide.api; - requires com.google.gson; - requires static lombok; - requires net.bytebuddy; - requires org.apache.commons.lang3; - requires org.apache.commons.pool2; - requires org.junit.jupiter.api; // Added: Required for JUnit tests - requires org.junit.jupiter.params; - requires org.semver4j; -} diff --git a/java/jedis-compatibility/README.md b/java/jedis-compatibility/README.md new file mode 100644 index 0000000000..07753a7dc6 --- /dev/null +++ b/java/jedis-compatibility/README.md @@ -0,0 +1,94 @@ +# Jedis Compatibility Layer + +This sub-module provides a Jedis-compatible API layer for Valkey GLIDE, allowing existing Jedis applications to migrate to GLIDE with minimal code changes. + +## Architecture + +The Jedis compatibility layer is implemented as a separate Gradle sub-module that: + +- **Depends on**: The main `client` module containing GLIDE core functionality +- **Provides**: Jedis-compatible classes and interfaces +- **Enables**: Drop-in replacement for Jedis in existing applications + +## Key Components + +### Core Classes +- `Jedis` - Main client class compatible with Jedis API +- `JedisCluster` - Cluster client compatible with Jedis cluster API +- `UnifiedJedis` - Unified interface for both standalone and cluster operations +- `JedisPool` / `JedisPooled` - Connection pooling implementations + +### Configuration +- `JedisClientConfig` - Client configuration interface +- `DefaultJedisClientConfig` - Default configuration implementation +- `ConfigurationMapper` - Maps Jedis config to GLIDE config +- `ClusterConfigurationMapper` - Maps Jedis cluster config to GLIDE cluster config + +### Protocol Support +- `Protocol` - Redis protocol constants and commands +- `RedisProtocol` - Protocol version handling +- Various parameter classes for command options + +## Usage + +### Gradle Dependency + +```gradle +dependencies { + implementation project(':jedis-compatibility') +} +``` + +### Maven Dependency + +```xml + + io.valkey + valkey-glide-jedis-compatibility + ${valkey-glide.version} + +``` + +### Basic Example + +```java +import redis.clients.jedis.Jedis; + +// Drop-in replacement for Jedis +try (Jedis jedis = new Jedis("localhost", 6379)) { + jedis.set("key", "value"); + String value = jedis.get("key"); + System.out.println(value); // prints: value +} +``` + +## Migration Guide + +See the [compatibility layer migration guide](src/main/java/redis/clients/jedis/compatibility-layer-migration-guide.md) for detailed migration instructions. + +## Build Commands + +```bash +# Compile the compatibility layer +./gradlew :jedis-compatibility:compileJava + +# Run tests +./gradlew :jedis-compatibility:test + +# Build JAR +./gradlew :jedis-compatibility:jar + +# Publish to local repository +./gradlew :jedis-compatibility:publishToMavenLocal +``` + +## Module Dependencies + +``` +jedis-compatibility +├── client (GLIDE core client) +│ ├── protobuf-java +│ ├── netty-handler +│ └── native libraries (Rust FFI) +└── commons-pool2 (connection pooling) +``` diff --git a/java/jedis-compatibility/build.gradle b/java/jedis-compatibility/build.gradle new file mode 100644 index 0000000000..6ddfcbb687 --- /dev/null +++ b/java/jedis-compatibility/build.gradle @@ -0,0 +1,173 @@ +plugins { + id 'java-library' + id 'maven-publish' + id 'signing' + id 'io.freefair.lombok' version '8.6' + id 'com.google.osdetector' version '1.7.3' + id 'com.gradleup.shadow' version '8.3.8' +} + +repositories { + mavenCentral() +} + +configurations { + testImplementation { extendsFrom shadow } +} + +ext { + // osdetector returns 'aarch_64', but rust triplet has 'aarch64' + arch = osdetector.arch == 'aarch_64' ? 'aarch64' : osdetector.arch; +} + +dependencies { + implementation project(':client') + + // Transitive dependencies needed by GLIDE client + implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.13.0' + + // Explicit protobuf version to match client + shadow group: 'com.google.protobuf', name: 'protobuf-java', version: '4.29.3' + + // Apache Commons Pool 2 for connection pooling - part of public API + implementation group: 'org.apache.commons', name: 'commons-pool2', version: '2.12.0' + + // Test dependencies + testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter', version: '5.10.2' + testImplementation group: 'org.mockito', name: 'mockito-inline', version: '5.2.0' + testImplementation group: 'org.mockito', name: 'mockito-junit-jupiter', version: '5.2.0' + + // Lombok + compileOnly 'org.projectlombok:lombok:1.18.38' + annotationProcessor 'org.projectlombok:lombok:1.18.38' + testCompileOnly 'org.projectlombok:lombok:1.18.38' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.38' +} + +java { + modularity.inferModulePath = false // Disable module system for compatibility + withSourcesJar() + withJavadocJar() +} + +javadoc { + dependsOn delombok + source = delombok.outputs + options.tags = [ "example:a:Example:", "apiNote:a:API Note:" ] + failOnError = false // Disable javadoc errors for compatibility layer +} + +tasks.register('copyNativeLib', Copy) { + def target + if (project.hasProperty('target')) { + target = project.target + from "${projectDir}/../target/${target}/release/" + } else if (osdetector.os == 'linux' && osdetector.release.id != 'alpine') { + from "${projectDir}/../target/${arch}-unknown-linux-gnu/release/" + } else if (osdetector.os == 'linux' && osdetector.release.id == 'alpine') { + from "${projectDir}/../target/${arch}-unknown-linux-musl/release/" + } else { + from "${projectDir}/../target/release/" + } + include "*.dylib", "*.so" + into sourceSets.main.output.resourcesDir +} + +jar.dependsOn('copyNativeLib') +shadowJar.dependsOn('copyNativeLib') +javadoc.dependsOn('copyNativeLib') +compileTestJava.dependsOn('copyNativeLib') +delombok.dependsOn('compileJava') +copyNativeLib.dependsOn(':client:buildRust') +compileJava.dependsOn(':client:shadowJar', ':client:jar') + +tasks.withType(Test) { + useJUnitPlatform() + testLogging { + exceptionFormat "full" + events "started", "skipped", "passed", "failed" + showStandardStreams true + } + // Native library path for tests + if (osdetector.os == 'linux' && osdetector.release.id == 'alpine') { + jvmArgs "-Djava.library.path=${projectDir}/../target/${arch}-unknown-linux-musl/release" + } else { + jvmArgs "-Djava.library.path=${projectDir}/../target/release" + } +} + +jar { + if (project.hasProperty('target') && project.target.contains("musl")) { + archiveClassifier = "linux_musl_${arch}" + } else { + archiveClassifier = osdetector.classifier + } +} + +shadowJar { + if (project.hasProperty('target') && project.target.contains("musl")) { + archiveClassifier = "linux_musl_${arch}" + } else { + archiveClassifier = osdetector.classifier + } + excludes.remove("module-info.class") + relocate('com.google.protobuf', 'glide.com.google.protobuf') + mergeServiceFiles() +} + +publishing { + publications { + mavenJava(MavenPublication) { + from components.shadow + artifact javadocJar + artifact sourcesJar + groupId = 'io.valkey' + artifactId = 'valkey-glide-jedis-compatibility' + version = System.getenv("GLIDE_RELEASE_VERSION") ?: project.ext.defaultReleaseVersion + pom { + name = 'valkey-glide-jedis-compatibility' + description = 'Jedis compatibility layer for Valkey GLIDE' + url = 'https://github.com/valkey-io/valkey-glide.git' + inceptionYear = '2024' + scm { + url = 'https://github.com/valkey-io/valkey-glide' + connection = 'scm:git:ssh://git@github.com/valkey-io/valkey-glide.git' + developerConnection = 'scm:git:ssh://git@github.com/valkey-io/valkey-glide.git' + } + licenses { + license { + name = 'The Apache License, Version 2.0' + url = 'https://www.apache.org/licenses/LICENSE-2.0.txt' + distribution = 'repo' + } + } + developers { + developer { + name = 'Valkey GLIDE Maintainers' + url = 'https://github.com/valkey-io/valkey-glide.git' + } + } + } + } + } + repositories { + mavenLocal() + } +} + +// Fix publishing task dependencies +tasks.withType(GenerateModuleMetadata) { + dependsOn jar, shadowJar +} + +publishMavenJavaPublicationToMavenLocal.dependsOn jar, shadowJar + +tasks.withType(Sign) { + def releaseVersion = System.getenv("GLIDE_RELEASE_VERSION") ?: project.ext.defaultReleaseVersion; + def isReleaseVersion = !releaseVersion.endsWith("SNAPSHOT") && releaseVersion != project.ext.defaultReleaseVersion; + onlyIf("isReleaseVersion is set") { isReleaseVersion } +} + +signing { + sign publishing.publications +} diff --git a/java/jedis-compatibility/compatibility-layer-migration-guide.md b/java/jedis-compatibility/compatibility-layer-migration-guide.md new file mode 100644 index 0000000000..cedd19d0b4 --- /dev/null +++ b/java/jedis-compatibility/compatibility-layer-migration-guide.md @@ -0,0 +1,242 @@ +# Valkey GLIDE Compatibility Layer Migration Guide + +## Overview + +The Valkey GLIDE compatibility layer enables seamless migration from Jedis to Valkey GLIDE with minimal code changes. This guide covers supported features, migration steps, and current limitations. + +## Quick Migration + +### Step 1: Update Dependencies + +Replace your Jedis dependency with Valkey GLIDE: + +**Before (Jedis):** +```gradle +dependencies { + implementation 'redis.clients:jedis:5.1.5' +} +``` + +**After (Valkey GLIDE):** +```gradle +dependencies { + implementation group: 'io.valkey', name: 'valkey-glide', version: '1.+', classifier: 'osx-aarch_64' +} +``` + +### Step 2: No Code Changes Required + +Your existing Jedis code works without modification: + +```java +import redis.clients.jedis.Jedis; + +public class JedisExample { + public static void main(String[] args) { + Jedis jedis = new Jedis(); + + // Basic operations work unchanged + String setResult = jedis.set("user:1001:name", "John Doe"); + String getValue = jedis.get("user:1001:name"); + } +} +``` + +### How to switch without a recompile? +Change the application's classpath such that it does not have the Jedis JAR and instead has Glide + the Jedis compatibility layer. + +## Supported input parameters + +### Configuration Mapping Overview + +The compatibility layer provides varying levels of support for Jedis configuration parameters, based on detailed analysis of `DefaultJedisClientConfig` fields: + +#### ✅ Successfully Mapped +- `user` → `ServerCredentials.username` +- `password` → `ServerCredentials.password` +- `clientName` → `BaseClientConfiguration.clientName` +- `ssl` → `BaseClientConfiguration.useTLS` +- `redisProtocol` → `BaseClientConfiguration.protocol` +- `connectionTimeoutMillis` → `AdvancedBaseClientConfiguration.connectionTimeout` +- `socketTimeoutMillis` → `BaseClientConfiguration.requestTimeout` +- `database` → Handled via SELECT command after connection + +#### 🔶 Partially Mapped +- `sslSocketFactory` → Requires SSL/TLS migration to system certificate store +- `sslParameters` → Limited mapping; custom protocols/ciphers not supported +- `hostnameVerifier` → Standard verification works; custom verifiers require `useInsecureTLS` + +#### ❌ Not Mapped +- `blockingSocketTimeoutMillis` → No equivalent (GLIDE uses async I/O model) + +### SSL/TLS Configuration Complexity + +#### Internal SSL Fields Analysis (21 sub-fields total): +- **SSLParameters**: 3/9 fields partially mapped +- **SSLSocketFactory**: 1/8 fields directly mapped +- **HostnameVerifier**: 2/4 verification types mapped + +#### Migration Requirements by Complexity: + +**Low Complexity** +- Direct parameter mapping +- No code changes required +- Examples: Basic auth, timeouts, protocol selection + +**Medium Complexity** +- SSL/TLS certificate migration required +- System certificate store installation needed +- Custom SSL configurations → GLIDE secure defaults + +**High Complexity** +- No GLIDE equivalent +- Architectural differences (async vs blocking I/O) +- Requires application redesign + +### Overall Migration Success Rate + +**Including SSL/TLS Internal Fields:** +- **Total analyzable fields**: 33 (12 main + 21 SSL internal) +- **Successfully mapped**: 9/33 +- **Partially mapped with migration**: 11/33 +- **Not mappable**: 13/33 + +### Key Migration Insights + +1. **GLIDE Architecture Shift**: From application-managed SSL to system-managed SSL with secure defaults +2. **Certificate Management**: Custom keystores/truststores require migration to system certificate store +3. **Protocol Selection**: GLIDE auto-selects TLS 1.2+ and secure cipher suites +4. **Client Authentication**: Client certificates not supported; use username/password authentication + +## Supported Features + +### Core Commands +- ✅ Basic string operations (GET, SET, MGET, MSET) +- ✅ Hash operations (HGET, HSET, HMGET, HMSET) +- ✅ List operations (LPUSH, RPUSH, LPOP, RPOP) +- ⚠️ Set operations (SADD, SREM, SMEMBERS) - **Available via `sendCommand()` only** +- ⚠️ Sorted set operations (ZADD, ZREM, ZRANGE) - **Available via `sendCommand()` only** +- ✅ Key operations (DEL, EXISTS, EXPIRE, TTL) +- ✅ Connection commands (PING, SELECT) +- ✅ Generic commands via `sendCommand()` (Protocol.Command types only) + +### Client Types +- ✅ Basic Jedis client +- ✅ Simple connection configurations +- ⚠️ JedisPool (limited support) +- ⚠️ JedisPooled (limited support) + +### Configuration +- ✅ Host and port configuration +- ✅ Basic authentication +- ✅ Database selection +- ✅ Connection timeout +- ⚠️ SSL/TLS (partial support) + +## Drawbacks and Unsupported Features + +### Connection Management +- ❌ **JedisPool advanced configurations**: Complex pool settings not fully supported +- ❌ **JedisPooled**: Advanced pooled connection features unavailable +- ❌ **Connection pooling**: Native Jedis pooling mechanisms not implemented +- ❌ **Failover configurations**: Jedis-specific failover logic not supported + +### Advanced Features +- ❌ **Transactions**: MULTI/EXEC transaction blocks not supported +- ❌ **Pipelining**: Jedis pipelining functionality unavailable +- ❌ **Pub/Sub**: Redis publish/subscribe not implemented +- ❌ **Lua scripting**: EVAL/EVALSHA commands not supported +- ❌ **Modules**: Redis module commands not available +- ⚠️ **Typed set/sorted set methods**: No dedicated methods like `sadd()`, `zadd()` - use `sendCommand()` instead + +### Configuration Limitations +- ❌ **Complex SSL configurations**: Jedis `JedisClientConfig` SSL parameters cannot be mapped to Valkey GLIDE `GlideClientConfiguration` +- ❌ **Custom trust stores**: SSL trust store configurations require manual migration +- ❌ **Client certificates**: SSL client certificate authentication not supported in compatibility layer +- ❌ **SSL protocols and cipher suites**: Advanced SSL protocol settings cannot be automatically converted +- ❌ **Custom serializers**: Jedis serialization options not supported +- ❌ **Connection validation**: Jedis connection health checks unavailable +- ❌ **Retry mechanisms**: Jedis-specific retry logic not implemented + +### Cluster Support +- ❌ **JedisCluster**: Cluster client not supported in compatibility layer +- ❌ **Cluster failover**: Automatic cluster failover not available +- ❌ **Hash slot management**: Manual slot management not supported + +### Performance Features +- ❌ **Async operations**: Jedis async methods not implemented +- ❌ **Batch operations**: Bulk operation optimizations unavailable +- ❌ **Custom protocols**: Protocol customization not supported + +## Migration Considerations + +### Before Migration +1. **Audit your codebase** for unsupported features listed above +2. **Test thoroughly** in a development environment +3. **Review connection configurations** for compatibility +4. **Plan for feature gaps** that may require code changes + +### Recommended Approach +1. Start with simple applications using basic commands +2. Gradually migrate complex features to native Valkey GLIDE APIs +3. Consider hybrid approach for applications with unsupported features +4. Monitor performance and behavior differences + +### Alternative Migration Path +For applications heavily using unsupported features, consider migrating directly to native Valkey GLIDE APIs: + +```java +import glide.api.GlideClient; +import glide.api.models.configuration.GlideClientConfiguration; +import glide.api.models.configuration.NodeAddress; + +GlideClientConfiguration config = GlideClientConfiguration.builder() + .address(NodeAddress.builder().host("localhost").port(6379).build()) + .build(); + +try (GlideClient client = GlideClient.createClient(config).get()) { + client.set(gs("key"), gs("value")).get(); +} +``` + +## Getting Help + +- Review the [main README](https://github.com/valkey-io/valkey-glide/blob/main/README.md) for native Valkey GLIDE usage +- Check [integration tests](./integTest/src/test/java/glide) for examples +- Report compatibility issues through the project's issue tracker + +## Known Challenges and Limitations + +### Version Compatibility Issues +- ❌ **Jedis version incompatibility**: The compatibility layer targets latest Jedis versions, but many projects use older versions (e.g., 4.4.3) +- ❌ **Backward compatibility**: Jedis itself is not backward compatible across major versions +- ⚠️ **Multiple version support**: No clear strategy for supporting multiple Jedis versions simultaneously + +### Implementation Gaps +- ⚠️ **Generic command support**: `sendCommand()` is implemented but only supports `Protocol.Command` types +- ❌ **Stub implementations**: Many classes exist but lack full functionality, creating false expectations +- ❌ **Runtime failures**: Build-time success doesn't guarantee runtime compatibility + +## Migration Warnings + +### Before You Start +1. **Check your Jedis version**: Compatibility layer may not support older Jedis versions +2. **Verify command types**: `sendCommand()` only supports `Protocol.Command` types, not custom `ProtocolCommand` implementations +3. **Test thoroughly**: Classes may exist but lack implementation +4. **Expect runtime failures**: Successful compilation doesn't guarantee runtime success +5. **Review SSL/TLS configurations**: Advanced SSL settings require manual migration to native Valkey GLIDE APIs + +### Recommended Testing Strategy +1. **Start with simple operations** to verify basic compatibility +2. **Test all code paths** - don't rely on successful compilation +3. **Monitor for runtime exceptions** from stub implementations +4. **Have rollback plan** ready for incompatible features + +## Next Steps + +The compatibility layer is under active development. Priority improvements include: +- Multi-version Jedis support strategy +- Enhanced JedisPool support +- Complete `sendCommand()` implementation +- Runtime compatibility validation +- Clear documentation of stub vs. implemented features diff --git a/java/client/src/main/java/redis/clients/jedis/Builder.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/Builder.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/Builder.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/Builder.java diff --git a/java/client/src/main/java/redis/clients/jedis/BuilderFactory.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/BuilderFactory.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/BuilderFactory.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/BuilderFactory.java diff --git a/java/client/src/main/java/redis/clients/jedis/ClientSetInfoConfig.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/ClientSetInfoConfig.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/ClientSetInfoConfig.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/ClientSetInfoConfig.java diff --git a/java/client/src/main/java/redis/clients/jedis/ClusterConfigurationMapper.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/ClusterConfigurationMapper.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/ClusterConfigurationMapper.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/ClusterConfigurationMapper.java diff --git a/java/client/src/main/java/redis/clients/jedis/ClusterConnectionProvider.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/ClusterConnectionProvider.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/ClusterConnectionProvider.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/ClusterConnectionProvider.java diff --git a/java/client/src/main/java/redis/clients/jedis/ConfigurationMapper.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/ConfigurationMapper.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/ConfigurationMapper.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/ConfigurationMapper.java diff --git a/java/client/src/main/java/redis/clients/jedis/Connection.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/Connection.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/Connection.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/Connection.java diff --git a/java/client/src/main/java/redis/clients/jedis/ConnectionPool.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/ConnectionPool.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/ConnectionPool.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/ConnectionPool.java diff --git a/java/client/src/main/java/redis/clients/jedis/ConnectionPoolConfig.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/ConnectionPoolConfig.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/ConnectionPoolConfig.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/ConnectionPoolConfig.java diff --git a/java/client/src/main/java/redis/clients/jedis/ConnectionProvider.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/ConnectionProvider.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/ConnectionProvider.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/ConnectionProvider.java diff --git a/java/client/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java diff --git a/java/client/src/main/java/redis/clients/jedis/DefaultRedisCredentials.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/DefaultRedisCredentials.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/DefaultRedisCredentials.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/DefaultRedisCredentials.java diff --git a/java/client/src/main/java/redis/clients/jedis/DefaultRedisCredentialsProvider.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/DefaultRedisCredentialsProvider.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/DefaultRedisCredentialsProvider.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/DefaultRedisCredentialsProvider.java diff --git a/java/client/src/main/java/redis/clients/jedis/GeoCoordinate.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/GeoCoordinate.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/GeoCoordinate.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/GeoCoordinate.java diff --git a/java/client/src/main/java/redis/clients/jedis/GlideJedisFactory.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/GlideJedisFactory.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/GlideJedisFactory.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/GlideJedisFactory.java diff --git a/java/client/src/main/java/redis/clients/jedis/HostAndPort.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/HostAndPort.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/HostAndPort.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/HostAndPort.java diff --git a/java/client/src/main/java/redis/clients/jedis/HostAndPortMapper.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/HostAndPortMapper.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/HostAndPortMapper.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/HostAndPortMapper.java diff --git a/java/client/src/main/java/redis/clients/jedis/Jedis.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/Jedis.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/Jedis.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/Jedis.java diff --git a/java/client/src/main/java/redis/clients/jedis/JedisClientConfig.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/JedisClientConfig.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/JedisClientConfig.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/JedisClientConfig.java diff --git a/java/client/src/main/java/redis/clients/jedis/JedisCluster.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/JedisCluster.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/JedisCluster.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/JedisCluster.java diff --git a/java/client/src/main/java/redis/clients/jedis/JedisClusterInfoCache.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/JedisClusterInfoCache.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/JedisClusterInfoCache.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/JedisClusterInfoCache.java diff --git a/java/client/src/main/java/redis/clients/jedis/JedisPool.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/JedisPool.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/JedisPool.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/JedisPool.java diff --git a/java/client/src/main/java/redis/clients/jedis/JedisPooled.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/JedisPooled.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/JedisPooled.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/JedisPooled.java diff --git a/java/client/src/main/java/redis/clients/jedis/Module.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/Module.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/Module.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/Module.java diff --git a/java/client/src/main/java/redis/clients/jedis/Protocol.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/Protocol.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/Protocol.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/Protocol.java diff --git a/java/client/src/main/java/redis/clients/jedis/RedisCredentials.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/RedisCredentials.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/RedisCredentials.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/RedisCredentials.java diff --git a/java/client/src/main/java/redis/clients/jedis/RedisCredentialsProvider.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/RedisCredentialsProvider.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/RedisCredentialsProvider.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/RedisCredentialsProvider.java diff --git a/java/client/src/main/java/redis/clients/jedis/RedisProtocol.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/RedisProtocol.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/RedisProtocol.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/RedisProtocol.java diff --git a/java/client/src/main/java/redis/clients/jedis/ResourceLifecycleManager.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/ResourceLifecycleManager.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/ResourceLifecycleManager.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/ResourceLifecycleManager.java diff --git a/java/client/src/main/java/redis/clients/jedis/SSLParametersUtils.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/SSLParametersUtils.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/SSLParametersUtils.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/SSLParametersUtils.java diff --git a/java/client/src/main/java/redis/clients/jedis/SingleConnectionPoolConfig.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/SingleConnectionPoolConfig.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/SingleConnectionPoolConfig.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/SingleConnectionPoolConfig.java diff --git a/java/client/src/main/java/redis/clients/jedis/SslOptions.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/SslOptions.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/SslOptions.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/SslOptions.java diff --git a/java/client/src/main/java/redis/clients/jedis/SslVerifyMode.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/SslVerifyMode.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/SslVerifyMode.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/SslVerifyMode.java diff --git a/java/client/src/main/java/redis/clients/jedis/StreamEntryID.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/StreamEntryID.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/StreamEntryID.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/StreamEntryID.java diff --git a/java/client/src/main/java/redis/clients/jedis/UnifiedJedis.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/UnifiedJedis.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/UnifiedJedis.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/UnifiedJedis.java diff --git a/java/client/src/main/java/redis/clients/jedis/args/BitCountOption.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/args/BitCountOption.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/args/BitCountOption.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/args/BitCountOption.java diff --git a/java/client/src/main/java/redis/clients/jedis/args/BitOP.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/args/BitOP.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/args/BitOP.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/args/BitOP.java diff --git a/java/client/src/main/java/redis/clients/jedis/args/ExpiryOption.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/args/ExpiryOption.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/args/ExpiryOption.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/args/ExpiryOption.java diff --git a/java/client/src/main/java/redis/clients/jedis/args/ListDirection.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/args/ListDirection.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/args/ListDirection.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/args/ListDirection.java diff --git a/java/client/src/main/java/redis/clients/jedis/args/ListPosition.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/args/ListPosition.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/args/ListPosition.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/args/ListPosition.java diff --git a/java/client/src/main/java/redis/clients/jedis/args/Rawable.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/args/Rawable.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/args/Rawable.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/args/Rawable.java diff --git a/java/client/src/main/java/redis/clients/jedis/authentication/AuthXManager.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/authentication/AuthXManager.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/authentication/AuthXManager.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/authentication/AuthXManager.java diff --git a/java/client/src/main/java/redis/clients/jedis/bloom/RedisBloomProtocol.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/bloom/RedisBloomProtocol.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/bloom/RedisBloomProtocol.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/bloom/RedisBloomProtocol.java diff --git a/java/client/src/main/java/redis/clients/jedis/commands/ProtocolCommand.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/commands/ProtocolCommand.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/commands/ProtocolCommand.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/commands/ProtocolCommand.java diff --git a/java/client/src/main/java/redis/clients/jedis/exceptions/JedisConnectionException.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/exceptions/JedisConnectionException.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/exceptions/JedisConnectionException.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/exceptions/JedisConnectionException.java diff --git a/java/client/src/main/java/redis/clients/jedis/exceptions/JedisDataException.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/exceptions/JedisDataException.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/exceptions/JedisDataException.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/exceptions/JedisDataException.java diff --git a/java/client/src/main/java/redis/clients/jedis/exceptions/JedisException.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/exceptions/JedisException.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/exceptions/JedisException.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/exceptions/JedisException.java diff --git a/java/client/src/main/java/redis/clients/jedis/exceptions/JedisValidationException.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/exceptions/JedisValidationException.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/exceptions/JedisValidationException.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/exceptions/JedisValidationException.java diff --git a/java/client/src/main/java/redis/clients/jedis/json/DefaultGsonObjectMapper.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/json/DefaultGsonObjectMapper.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/json/DefaultGsonObjectMapper.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/json/DefaultGsonObjectMapper.java diff --git a/java/client/src/main/java/redis/clients/jedis/json/JsonObjectMapper.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/json/JsonObjectMapper.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/json/JsonObjectMapper.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/json/JsonObjectMapper.java diff --git a/java/client/src/main/java/redis/clients/jedis/json/JsonProtocol.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/json/JsonProtocol.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/json/JsonProtocol.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/json/JsonProtocol.java diff --git a/java/client/src/main/java/redis/clients/jedis/params/BitPosParams.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/params/BitPosParams.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/params/BitPosParams.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/params/BitPosParams.java diff --git a/java/client/src/main/java/redis/clients/jedis/params/GetExParams.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/params/GetExParams.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/params/GetExParams.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/params/GetExParams.java diff --git a/java/client/src/main/java/redis/clients/jedis/params/HGetExParams.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/params/HGetExParams.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/params/HGetExParams.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/params/HGetExParams.java diff --git a/java/client/src/main/java/redis/clients/jedis/params/HSetExParams.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/params/HSetExParams.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/params/HSetExParams.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/params/HSetExParams.java diff --git a/java/client/src/main/java/redis/clients/jedis/params/LPosParams.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/params/LPosParams.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/params/LPosParams.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/params/LPosParams.java diff --git a/java/client/src/main/java/redis/clients/jedis/params/MigrateParams.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/params/MigrateParams.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/params/MigrateParams.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/params/MigrateParams.java diff --git a/java/client/src/main/java/redis/clients/jedis/params/RestoreParams.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/params/RestoreParams.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/params/RestoreParams.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/params/RestoreParams.java diff --git a/java/client/src/main/java/redis/clients/jedis/params/ScanParams.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/params/ScanParams.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/params/ScanParams.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/params/ScanParams.java diff --git a/java/client/src/main/java/redis/clients/jedis/params/SetParams.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/params/SetParams.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/params/SetParams.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/params/SetParams.java diff --git a/java/client/src/main/java/redis/clients/jedis/params/SortingParams.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/params/SortingParams.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/params/SortingParams.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/params/SortingParams.java diff --git a/java/client/src/main/java/redis/clients/jedis/resps/AccessControlLogEntry.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/AccessControlLogEntry.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/resps/AccessControlLogEntry.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/AccessControlLogEntry.java diff --git a/java/client/src/main/java/redis/clients/jedis/resps/AccessControlUser.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/AccessControlUser.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/resps/AccessControlUser.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/AccessControlUser.java diff --git a/java/client/src/main/java/redis/clients/jedis/resps/CommandDocument.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/CommandDocument.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/resps/CommandDocument.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/CommandDocument.java diff --git a/java/client/src/main/java/redis/clients/jedis/resps/CommandInfo.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/CommandInfo.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/resps/CommandInfo.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/CommandInfo.java diff --git a/java/client/src/main/java/redis/clients/jedis/resps/FunctionStats.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/FunctionStats.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/resps/FunctionStats.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/FunctionStats.java diff --git a/java/client/src/main/java/redis/clients/jedis/resps/GeoRadiusResponse.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/GeoRadiusResponse.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/resps/GeoRadiusResponse.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/GeoRadiusResponse.java diff --git a/java/client/src/main/java/redis/clients/jedis/resps/KeyValue.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/KeyValue.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/resps/KeyValue.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/KeyValue.java diff --git a/java/client/src/main/java/redis/clients/jedis/resps/KeyedListElement.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/KeyedListElement.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/resps/KeyedListElement.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/KeyedListElement.java diff --git a/java/client/src/main/java/redis/clients/jedis/resps/KeyedZSetElement.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/KeyedZSetElement.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/resps/KeyedZSetElement.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/KeyedZSetElement.java diff --git a/java/client/src/main/java/redis/clients/jedis/resps/LCSMatchResult.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/LCSMatchResult.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/resps/LCSMatchResult.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/LCSMatchResult.java diff --git a/java/client/src/main/java/redis/clients/jedis/resps/LibraryInfo.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/LibraryInfo.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/resps/LibraryInfo.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/LibraryInfo.java diff --git a/java/client/src/main/java/redis/clients/jedis/resps/ScanResult.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/ScanResult.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/resps/ScanResult.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/ScanResult.java diff --git a/java/client/src/main/java/redis/clients/jedis/resps/Slowlog.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/Slowlog.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/resps/Slowlog.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/Slowlog.java diff --git a/java/client/src/main/java/redis/clients/jedis/resps/StreamConsumerFullInfo.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/StreamConsumerFullInfo.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/resps/StreamConsumerFullInfo.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/StreamConsumerFullInfo.java diff --git a/java/client/src/main/java/redis/clients/jedis/resps/StreamConsumerInfo.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/StreamConsumerInfo.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/resps/StreamConsumerInfo.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/StreamConsumerInfo.java diff --git a/java/client/src/main/java/redis/clients/jedis/resps/StreamConsumersInfo.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/StreamConsumersInfo.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/resps/StreamConsumersInfo.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/StreamConsumersInfo.java diff --git a/java/client/src/main/java/redis/clients/jedis/resps/StreamEntry.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/StreamEntry.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/resps/StreamEntry.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/StreamEntry.java diff --git a/java/client/src/main/java/redis/clients/jedis/resps/StreamFullInfo.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/StreamFullInfo.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/resps/StreamFullInfo.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/StreamFullInfo.java diff --git a/java/client/src/main/java/redis/clients/jedis/resps/StreamGroupFullInfo.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/StreamGroupFullInfo.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/resps/StreamGroupFullInfo.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/StreamGroupFullInfo.java diff --git a/java/client/src/main/java/redis/clients/jedis/resps/StreamGroupInfo.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/StreamGroupInfo.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/resps/StreamGroupInfo.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/StreamGroupInfo.java diff --git a/java/client/src/main/java/redis/clients/jedis/resps/StreamInfo.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/StreamInfo.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/resps/StreamInfo.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/StreamInfo.java diff --git a/java/client/src/main/java/redis/clients/jedis/resps/StreamPendingEntry.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/StreamPendingEntry.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/resps/StreamPendingEntry.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/StreamPendingEntry.java diff --git a/java/client/src/main/java/redis/clients/jedis/resps/StreamPendingSummary.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/StreamPendingSummary.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/resps/StreamPendingSummary.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/StreamPendingSummary.java diff --git a/java/client/src/main/java/redis/clients/jedis/resps/Tuple.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/Tuple.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/resps/Tuple.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/resps/Tuple.java diff --git a/java/client/src/main/java/redis/clients/jedis/search/Document.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/search/Document.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/search/Document.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/search/Document.java diff --git a/java/client/src/main/java/redis/clients/jedis/search/SearchBuilderFactory.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/search/SearchBuilderFactory.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/search/SearchBuilderFactory.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/search/SearchBuilderFactory.java diff --git a/java/client/src/main/java/redis/clients/jedis/search/SearchProtocol.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/search/SearchProtocol.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/search/SearchProtocol.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/search/SearchProtocol.java diff --git a/java/client/src/main/java/redis/clients/jedis/search/SearchResult.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/search/SearchResult.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/search/SearchResult.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/search/SearchResult.java diff --git a/java/client/src/main/java/redis/clients/jedis/search/aggr/AggregationResult.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/search/aggr/AggregationResult.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/search/aggr/AggregationResult.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/search/aggr/AggregationResult.java diff --git a/java/client/src/main/java/redis/clients/jedis/search/aggr/Row.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/search/aggr/Row.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/search/aggr/Row.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/search/aggr/Row.java diff --git a/java/client/src/main/java/redis/clients/jedis/timeseries/AggregationType.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/timeseries/AggregationType.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/timeseries/AggregationType.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/timeseries/AggregationType.java diff --git a/java/client/src/main/java/redis/clients/jedis/timeseries/DuplicatePolicy.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/timeseries/DuplicatePolicy.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/timeseries/DuplicatePolicy.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/timeseries/DuplicatePolicy.java diff --git a/java/client/src/main/java/redis/clients/jedis/timeseries/TSElement.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/timeseries/TSElement.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/timeseries/TSElement.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/timeseries/TSElement.java diff --git a/java/client/src/main/java/redis/clients/jedis/timeseries/TSInfo.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/timeseries/TSInfo.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/timeseries/TSInfo.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/timeseries/TSInfo.java diff --git a/java/client/src/main/java/redis/clients/jedis/timeseries/TSKeyValue.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/timeseries/TSKeyValue.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/timeseries/TSKeyValue.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/timeseries/TSKeyValue.java diff --git a/java/client/src/main/java/redis/clients/jedis/timeseries/TSKeyedElements.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/timeseries/TSKeyedElements.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/timeseries/TSKeyedElements.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/timeseries/TSKeyedElements.java diff --git a/java/client/src/main/java/redis/clients/jedis/timeseries/TimeSeriesBuilderFactory.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/timeseries/TimeSeriesBuilderFactory.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/timeseries/TimeSeriesBuilderFactory.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/timeseries/TimeSeriesBuilderFactory.java diff --git a/java/client/src/main/java/redis/clients/jedis/timeseries/TimeSeriesProtocol.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/timeseries/TimeSeriesProtocol.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/timeseries/TimeSeriesProtocol.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/timeseries/TimeSeriesProtocol.java diff --git a/java/client/src/main/java/redis/clients/jedis/util/DoublePrecision.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/util/DoublePrecision.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/util/DoublePrecision.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/util/DoublePrecision.java diff --git a/java/client/src/main/java/redis/clients/jedis/util/JedisClusterHashTag.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/util/JedisClusterHashTag.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/util/JedisClusterHashTag.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/util/JedisClusterHashTag.java diff --git a/java/client/src/main/java/redis/clients/jedis/util/JedisURIHelper.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/util/JedisURIHelper.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/util/JedisURIHelper.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/util/JedisURIHelper.java diff --git a/java/client/src/main/java/redis/clients/jedis/util/KeyValue.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/util/KeyValue.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/util/KeyValue.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/util/KeyValue.java diff --git a/java/client/src/main/java/redis/clients/jedis/util/Pool.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/util/Pool.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/util/Pool.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/util/Pool.java diff --git a/java/client/src/main/java/redis/clients/jedis/util/SafeEncoder.java b/java/jedis-compatibility/src/main/java/redis/clients/jedis/util/SafeEncoder.java similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/util/SafeEncoder.java rename to java/jedis-compatibility/src/main/java/redis/clients/jedis/util/SafeEncoder.java diff --git a/java/client/src/test/java/redis/clients/jedis/BasicCompatibilityTest.java b/java/jedis-compatibility/src/test/java/redis/clients/jedis/BasicCompatibilityTest.java similarity index 100% rename from java/client/src/test/java/redis/clients/jedis/BasicCompatibilityTest.java rename to java/jedis-compatibility/src/test/java/redis/clients/jedis/BasicCompatibilityTest.java diff --git a/java/client/src/test/java/redis/clients/jedis/ConfigurationTest.java b/java/jedis-compatibility/src/test/java/redis/clients/jedis/ConfigurationTest.java similarity index 100% rename from java/client/src/test/java/redis/clients/jedis/ConfigurationTest.java rename to java/jedis-compatibility/src/test/java/redis/clients/jedis/ConfigurationTest.java diff --git a/java/jedis-compatibility/src/test/java/redis/clients/jedis/JedisCompatibilityTest.java b/java/jedis-compatibility/src/test/java/redis/clients/jedis/JedisCompatibilityTest.java new file mode 100644 index 0000000000..3fc2d9ffb0 --- /dev/null +++ b/java/jedis-compatibility/src/test/java/redis/clients/jedis/JedisCompatibilityTest.java @@ -0,0 +1,40 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package redis.clients.jedis; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +public class JedisCompatibilityTest { + + @Test + public void testProtocolCommandExists() { + // Test that Protocol.Command enum is accessible + assertNotNull(Protocol.Command.SET); + assertNotNull(Protocol.Command.GET); + assertEquals("SET", Protocol.Command.SET.name()); + assertEquals("GET", Protocol.Command.GET.name()); + } + + @Test + public void testHostAndPortCreation() { + HostAndPort hostAndPort = new HostAndPort("localhost", 6379); + assertEquals("localhost", hostAndPort.getHost()); + assertEquals(6379, hostAndPort.getPort()); + } + + @Test + public void testDefaultJedisClientConfigBuilder() { + JedisClientConfig config = + DefaultJedisClientConfig.builder() + .connectionTimeoutMillis(5000) + .socketTimeoutMillis(5000) + .database(0) + .build(); + + assertNotNull(config); + assertEquals(5000, config.getConnectionTimeoutMillis()); + assertEquals(5000, config.getSocketTimeoutMillis()); + assertEquals(0, config.getDatabase()); + } +} diff --git a/java/client/src/test/java/redis/clients/jedis/JedisMethodsTest.java b/java/jedis-compatibility/src/test/java/redis/clients/jedis/JedisMethodsTest.java similarity index 100% rename from java/client/src/test/java/redis/clients/jedis/JedisMethodsTest.java rename to java/jedis-compatibility/src/test/java/redis/clients/jedis/JedisMethodsTest.java diff --git a/java/client/src/test/java/redis/clients/jedis/JedisWrapperTest.java b/java/jedis-compatibility/src/test/java/redis/clients/jedis/JedisWrapperTest.java similarity index 100% rename from java/client/src/test/java/redis/clients/jedis/JedisWrapperTest.java rename to java/jedis-compatibility/src/test/java/redis/clients/jedis/JedisWrapperTest.java diff --git a/java/client/src/test/java/redis/clients/jedis/PoolImportTest.java b/java/jedis-compatibility/src/test/java/redis/clients/jedis/PoolImportTest.java similarity index 100% rename from java/client/src/test/java/redis/clients/jedis/PoolImportTest.java rename to java/jedis-compatibility/src/test/java/redis/clients/jedis/PoolImportTest.java diff --git a/java/settings.gradle b/java/settings.gradle index de441fff98..65de27712e 100644 --- a/java/settings.gradle +++ b/java/settings.gradle @@ -6,5 +6,6 @@ plugins { rootProject.name = 'glide' include 'client' +include 'jedis-compatibility' include 'integTest' include 'benchmarks' diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 181a952543..9f3a07f299 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -563,6 +563,13 @@ export type ReadFrom = * - **Standalone Mode**: In standalone mode, only the provided nodes will be used. * - **Lazy Connect**: Set `lazyConnect` to `true` to defer connection establishment until the first command is sent. * + * ### Database Selection + * + * - **Database ID**: Use `databaseId` to specify which logical database to connect to (0-15 by default). + * - **Cluster Mode**: Requires Valkey 9.0+ with multi-database cluster mode enabled. + * - **Standalone Mode**: Works with all Valkey versions. + * - **Reconnection**: Database selection persists across reconnections. + * * ### Security Settings * * - **TLS**: Enable secure communication using `useTLS`. @@ -610,6 +617,7 @@ export type ReadFrom = * { host: 'redis-node-1.example.com', port: 6379 }, * { host: 'redis-node-2.example.com' }, // Defaults to port 6379 * ], + * databaseId: 5, // Connect to database 5 * useTLS: true, * credentials: { * username: 'myUser', @@ -655,6 +663,33 @@ export interface BaseClientConfiguration { */ port?: number; }[]; + /** + * Index of the logical database to connect to. + * + * @remarks + * - **Standalone Mode**: Works with all Valkey versions. + * - **Cluster Mode**: Requires Valkey 9.0+ with multi-database cluster mode enabled. + * - **Reconnection**: Database selection persists across reconnections. + * - **Default**: If not specified, defaults to database 0. + * - **Range**: Must be non-negative. The server will validate the upper limit based on its configuration. + * - **Server Validation**: The server determines the maximum database ID based on its `databases` configuration (standalone) or `cluster-databases` configuration (cluster mode). + * + * @example + * ```typescript + * // Connect to database 5 + * const config: BaseClientConfiguration = { + * addresses: [{ host: 'localhost', port: 6379 }], + * databaseId: 5 + * }; + * + * // Connect to a higher database ID (server will validate the limit) + * const configHighDb: BaseClientConfiguration = { + * addresses: [{ host: 'localhost', port: 6379 }], + * databaseId: 100 + * }; + * ``` + */ + databaseId?: number; /** * True if communication with the cluster should use Transport Level Security. * Should match the TLS configuration of the server/cluster, @@ -1164,15 +1199,13 @@ export class BaseClient { } } - /** - * @internal - */ protected constructor( socket: net.Socket, options?: BaseClientConfiguration, ) { // if logger has been initialized by the external-user on info level this log will be shown Logger.log("info", "Client lifetime", `construct client`); + this.config = options; this.requestTimeout = options?.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT_IN_MILLISECONDS; @@ -8952,6 +8985,7 @@ export class BaseClient { clusterModeEnabled: false, readFrom, authenticationInfo, + databaseId: options.databaseId, inflightRequestsLimit: options.inflightRequestsLimit, clientAz: options.clientAz ?? null, connectionRetryStrategy: options.connectionBackoff, diff --git a/node/src/Batch.ts b/node/src/Batch.ts index 47cbc5f8b1..dfc1c17831 100644 --- a/node/src/Batch.ts +++ b/node/src/Batch.ts @@ -4384,6 +4384,14 @@ export class Batch extends BaseBatch { /** * Change the currently selected database. * + * **WARNING**: This command is NOT RECOMMENDED for production use. + * Upon reconnection, the client will revert to the database_id specified + * in the client configuration (default: 0), NOT the database selected + * via this command. + * + * **RECOMMENDED APPROACH**: Use the `databaseId` parameter in client + * configuration instead of using SELECT in batch operations. + * * @see {@link https://valkey.io/commands/select/|valkey.io} for details. * * @param index - The index of the database to select. diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index ec982c6dc8..23006a43dc 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -102,19 +102,19 @@ export namespace GlideClientConfiguration { /** * Configuration options for creating a {@link GlideClient | GlideClient}. * - * Extends `BaseClientConfiguration` with properties specific to `GlideClient`, such as database selection, + * Extends `BaseClientConfiguration` with properties specific to `GlideClient`, such as * reconnection strategies, and Pub/Sub subscription settings. * * @remarks * This configuration allows you to tailor the client's behavior when connecting to a standalone Valkey Glide server. * - * - **Database Selection**: Use `databaseId` to specify which logical database to connect to. + * - **Database Selection**: Use `databaseId` (inherited from BaseClientConfiguration) to specify which logical database to connect to. * - **Pub/Sub Subscriptions**: Predefine Pub/Sub channels and patterns to subscribe to upon connection establishment. * * @example * ```typescript * const config: GlideClientConfiguration = { - * databaseId: 1, + * databaseId: 1, // Inherited from BaseClientConfiguration * pubsubSubscriptions: { * channelsAndPatterns: { * [GlideClientConfiguration.PubSubChannelModes.Pattern]: new Set(['news.*']), @@ -127,10 +127,6 @@ export namespace GlideClientConfiguration { * ``` */ export type GlideClientConfiguration = BaseClientConfiguration & { - /** - * index of the logical database to connect to. - */ - databaseId?: number; /** * PubSub subscriptions to be used for the client. * Will be applied via SUBSCRIBE/PSUBSCRIBE commands during connection establishment. @@ -173,7 +169,6 @@ export class GlideClient extends BaseClient { options: GlideClientConfiguration, ): connection_request.IConnectionRequest { const configuration = super.createClientRequest(options); - configuration.databaseId = options.databaseId; this.configurePubsub(options, configuration); @@ -409,6 +404,21 @@ export class GlideClient extends BaseClient { /** * Changes the currently selected database. * + * **WARNING**: This command is NOT RECOMMENDED for production use. + * Upon reconnection, the client will revert to the database_id specified + * in the client configuration (default: 0), NOT the database selected + * via this command. + * + * **RECOMMENDED APPROACH**: Use the `databaseId` parameter in client + * configuration instead: + * + * ```typescript + * const client = await GlideClient.createClient({ + * addresses: [{ host: "localhost", port: 6379 }], + * databaseId: 5 // Recommended: persists across reconnections + * }); + * ``` + * * @see {@link https://valkey.io/commands/select/|valkey.io} for details. * * @param index - The index of the database to select. @@ -416,9 +426,10 @@ export class GlideClient extends BaseClient { * * @example * ```typescript - * // Example usage of select method + * // Example usage of select method (NOT RECOMMENDED) * const result = await client.select(2); * console.log(result); // Output: 'OK' + * // Note: Database selection will be lost on reconnection! * ``` */ public async select(index: number): Promise<"OK"> { diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index b07335a0e1..75754d9f6d 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -166,6 +166,11 @@ export namespace GlideClusterClientConfiguration { * @example * ```typescript * const config: GlideClusterClientConfiguration = { + * addresses: [ + * { host: 'cluster-node-1.example.com', port: 6379 }, + * { host: 'cluster-node-2.example.com', port: 6379 }, + * ], + * databaseId: 5, // Connect to database 5 (requires Valkey 9.0+ with multi-database cluster mode) * periodicChecks: { * duration_in_sec: 30, // Perform periodic checks every 30 seconds * }, @@ -544,12 +549,12 @@ export class GlideClusterClient extends BaseClient { /** * Creates a new `GlideClusterClient` instance and establishes connections to a Valkey Cluster. * - * @param options - The configuration options for the client, including cluster addresses, authentication credentials, TLS settings, periodic checks, and Pub/Sub subscriptions. + * @param options - The configuration options for the client, including cluster addresses, database selection, authentication credentials, TLS settings, periodic checks, and Pub/Sub subscriptions. * @returns A promise that resolves to a connected `GlideClusterClient` instance. * * @remarks * Use this static method to create and connect a `GlideClusterClient` to a Valkey Cluster. - * The client will automatically handle connection establishment, including cluster topology discovery and handling of authentication and TLS configurations. + * The client will automatically handle connection establishment, including cluster topology discovery, database selection, and handling of authentication and TLS configurations. * * @example * ```typescript @@ -561,6 +566,7 @@ export class GlideClusterClient extends BaseClient { * { host: 'address1.example.com', port: 6379 }, * { host: 'address2.example.com', port: 6379 }, * ], + * databaseId: 5, // Connect to database 5 (requires Valkey 9.0+) * credentials: { * username: 'user1', * password: 'passwordA', @@ -589,6 +595,7 @@ export class GlideClusterClient extends BaseClient { * * @remarks * - **Cluster Topology Discovery**: The client will automatically discover the cluster topology based on the seed addresses provided. + * - **Database Selection**: Use `databaseId` to specify which logical database to connect to. Requires Valkey 9.0+ with multi-database cluster mode enabled. * - **Authentication**: If `credentials` are provided, the client will attempt to authenticate using the specified username and password. * - **TLS**: If `useTLS` is set to `true`, the client will establish secure connections using TLS. * Should match the TLS configuration of the server/cluster, otherwise the connection attempt will fail. diff --git a/node/tests/GlideClusterClient.test.ts b/node/tests/GlideClusterClient.test.ts index 1e2be97aa7..216a34fa64 100644 --- a/node/tests/GlideClusterClient.test.ts +++ b/node/tests/GlideClusterClient.test.ts @@ -2868,4 +2868,26 @@ describe("GlideClusterClient", () => { }, TIMEOUT, ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "should pass database id for cluster client_%p", + async (protocol) => { + // Skip test if version is below 9.0.0 (Valkey 9) + if (cluster.checkIfServerVersionLessThan("9.0.0")) return; + + const client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol, { + databaseId: 1, + }), + ); + + try { + // Simple test to verify the client works with the database ID + expect(await client.ping()).toEqual("PONG"); + } finally { + client.close(); + } + }, + TIMEOUT, + ); }); diff --git a/python/glide-async/python/glide/async_commands/core.py b/python/glide-async/python/glide/async_commands/core.py index 87c315de62..05ec0ac06d 100644 --- a/python/glide-async/python/glide/async_commands/core.py +++ b/python/glide-async/python/glide/async_commands/core.py @@ -1120,7 +1120,7 @@ async def httl(self, key: TEncodable, fields: List[TEncodable]) -> List[int]: - `-2`: field does not exist or key does not exist Examples: - >>> await client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.EX, 10)) + >>> await client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.SEC, 10)) >>> await client.httl("my_hash", ["field1", "field2", "non_existent_field"]) [9, 9, -2] # field1 and field2 have ~9 seconds left, non_existent_field doesn't exist @@ -1150,7 +1150,7 @@ async def hpttl(self, key: TEncodable, fields: List[TEncodable]) -> List[int]: - `-2`: field does not exist or key does not exist Examples: - >>> await client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.PX, 10000)) + >>> await client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.MILLSEC, 10000)) >>> await client.hpttl("my_hash", ["field1", "field2", "non_existent_field"]) [9500, 9500, -2] # field1 and field2 have ~9500 milliseconds left, non_existent_field doesn't exist @@ -1182,7 +1182,7 @@ async def hexpiretime(self, key: TEncodable, fields: List[TEncodable]) -> List[i Examples: >>> import time >>> future_timestamp = int(time.time()) + 60 # 60 seconds from now - >>> await client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.EXAT, future_timestamp)) + >>> await client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.UNIX_SEC, future_timestamp)) >>> await client.hexpiretime("my_hash", ["field1", "field2", "non_existent_field"]) [future_timestamp, future_timestamp, -2] # field1 and field2 expire at future_timestamp, non_existent_field doesn't exist @@ -1216,7 +1216,7 @@ async def hpexpiretime( Examples: >>> import time >>> future_timestamp_ms = int(time.time() * 1000) + 60000 # 60 seconds from now in milliseconds - >>> await client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.PXAT, future_timestamp_ms)) + >>> await client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.UNIX_MILLSEC, future_timestamp_ms)) >>> await client.hpexpiretime("my_hash", ["field1", "field2", "non_existent_field"]) [future_timestamp_ms, future_timestamp_ms, -2] # field1 and field2 expire at future_timestamp_ms, non_existent_field doesn't exist @@ -1249,17 +1249,17 @@ async def hsetex( - ONLY_IF_ALL_EXIST (FXX): Only set fields if all of them already exist. - ONLY_IF_NONE_EXIST (FNX): Only set fields if none of them already exist. expiry (Optional[ExpirySet]): Expiration options for the fields: - - EX: Expiration time in seconds. - - PX: Expiration time in milliseconds. - - EXAT: Absolute expiration time in seconds (Unix timestamp). - - PXAT: Absolute expiration time in milliseconds (Unix timestamp). - - KEEPTTL: Retain existing TTL. + - SEC (EX): Expiration time in seconds. + - MILLSEC (PX): Expiration time in milliseconds. + - UNIX_SEC (EXAT): Absolute expiration time in seconds (Unix timestamp). + - UNIX_MILLSEC (PXAT): Absolute expiration time in milliseconds (Unix timestamp). + - KEEP_TTL (KEEPTTL): Retain existing TTL. Returns: int: 1 if all fields were set successfully, 0 if none were set due to conditional constraints. Examples: - >>> await client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.EX, 10)) + >>> await client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.SEC, 10)) 1 # All fields set with 10 second expiration >>> await client.hsetex("my_hash", {"field3": "value3"}, field_conditional_change=HashFieldConditionalChange.ONLY_IF_ALL_EXIST) 1 # Field set because field already exists @@ -1305,10 +1305,10 @@ async def hgetex( key (TEncodable): The key of the hash. fields (List[TEncodable]): The list of fields to retrieve from the hash. expiry (Optional[ExpiryGetEx]): Expiration options for the retrieved fields: - - EX: Expiration time in seconds. - - PX: Expiration time in milliseconds. - - EXAT: Absolute expiration time in seconds (Unix timestamp). - - PXAT: Absolute expiration time in milliseconds (Unix timestamp). + - SEC (EX): Expiration time in seconds. + - MILLSEC (PX): Expiration time in milliseconds. + - UNIX_SEC (EXAT): Absolute expiration time in seconds (Unix timestamp). + - UNIX_MILLSEC (PXAT): Absolute expiration time in milliseconds (Unix timestamp). - PERSIST: Remove expiration from the fields. Returns: @@ -1317,10 +1317,10 @@ async def hgetex( If `key` does not exist, it is treated as an empty hash, and the function returns a list of null values. Examples: - >>> await client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.EX, 10)) + >>> await client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.SEC, 10)) >>> await client.hgetex("my_hash", ["field1", "field2"]) [b"value1", b"value2"] - >>> await client.hgetex("my_hash", ["field1"], expiry=ExpiryGetEx(ExpiryTypeGetEx.EX, 20)) + >>> await client.hgetex("my_hash", ["field1"], expiry=ExpiryGetEx(ExpiryTypeGetEx.SEC, 20)) [b"value1"] # field1 now has 20 second expiration >>> await client.hgetex("my_hash", ["field1"], expiry=ExpiryGetEx(ExpiryTypeGetEx.PERSIST, None)) [b"value1"] # field1 expiration removed @@ -1374,7 +1374,7 @@ async def hexpire( - `2`: Field was deleted immediately (when seconds is 0 or timestamp is in the past). Examples: - >>> await client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.EX, 10)) + >>> await client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.SEC, 10)) >>> await client.hexpire("my_hash", 20, ["field1", "field2"]) [1, 1] # Both fields' expiration set to 20 seconds >>> await client.hexpire("my_hash", 30, ["field1"], option=ExpireOptions.NewExpiryGreaterThanCurrent) @@ -1420,7 +1420,7 @@ async def hpersist(self, key: TEncodable, fields: List[TEncodable]) -> List[int] - `-2`: Field does not exist or key does not exist. Examples: - >>> await client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.EX, 10)) + >>> await client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.SEC, 10)) >>> await client.hpersist("my_hash", ["field1", "field2"]) [1, 1] # Both fields made persistent >>> await client.hpersist("my_hash", ["field1"]) @@ -1467,7 +1467,7 @@ async def hpexpire( - `2`: Field was deleted immediately (when milliseconds is 0 or timestamp is in the past). Examples: - >>> await client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.PX, 10000)) + >>> await client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.MILLSEC, 10000)) >>> await client.hpexpire("my_hash", 20000, ["field1", "field2"]) [1, 1] # Both fields' expiration set to 20000 milliseconds >>> await client.hpexpire("my_hash", 30000, ["field1"], option=ExpireOptions.NewExpiryGreaterThanCurrent) @@ -1528,7 +1528,7 @@ async def hexpireat( Examples: >>> import time >>> future_timestamp = int(time.time()) + 60 # 60 seconds from now - >>> await client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.EX, 10)) + >>> await client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.SEC, 10)) >>> await client.hexpireat("my_hash", future_timestamp, ["field1", "field2"]) [1, 1] # Both fields' expiration set to future_timestamp >>> past_timestamp = int(time.time()) - 60 # 60 seconds ago @@ -1588,7 +1588,7 @@ async def hpexpireat( Examples: >>> import time >>> future_timestamp_ms = int(time.time() * 1000) + 60000 # 60 seconds from now in milliseconds - >>> await client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.PX, 10000)) + >>> await client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.MILLSEC, 10000)) >>> await client.hpexpireat("my_hash", future_timestamp_ms, ["field1", "field2"]) [1, 1] # Both fields' expiration set to future_timestamp_ms >>> past_timestamp_ms = int(time.time() * 1000) - 60000 # 60 seconds ago in milliseconds diff --git a/python/glide-async/python/glide/async_commands/standalone_commands.py b/python/glide-async/python/glide/async_commands/standalone_commands.py index af61d47438..ede2f4a3ee 100644 --- a/python/glide-async/python/glide/async_commands/standalone_commands.py +++ b/python/glide-async/python/glide/async_commands/standalone_commands.py @@ -166,6 +166,28 @@ async def select(self, index: int) -> TOK: """ Change the currently selected database. + **WARNING**: This command is NOT RECOMMENDED for production use. + Upon reconnection, the client will revert to the database_id specified + in the client configuration (default: 0), NOT the database selected + via this command. + + **RECOMMENDED APPROACH**: Use the database_id parameter in client + configuration instead: + + ```python + client = await GlideClient.create_client( + GlideClientConfiguration( + addresses=[NodeAddress("localhost", 6379)], + database_id=5 # Recommended: persists across reconnections + ) + ) + ``` + + **RECONNECTION BEHAVIOR**: After any reconnection (due to network issues, + timeouts, etc.), the client will automatically revert to the database_id + specified during client creation, losing any database selection made via + this SELECT command. + See [valkey.io](https://valkey.io/commands/select/) for details. Args: diff --git a/python/glide-async/python/glide/glide_client.py b/python/glide-async/python/glide/glide_client.py index 0b17fa9665..5e513a7d8c 100644 --- a/python/glide-async/python/glide/glide_client.py +++ b/python/glide-async/python/glide/glide_client.py @@ -338,7 +338,7 @@ async def _write_or_buffer_request(self, request: TRequest): request.callback_idx if isinstance(request, CommandRequest) else 0 ) res_future = self._available_futures.pop(callback_idx, None) - if res_future: + if res_future and not res_future.done(): res_future.set_exception(e) else: ClientLogger.log( @@ -355,7 +355,10 @@ async def _write_buffered_requests_to_socket(self) -> None: b_arr = bytearray() for request in requests: ProtobufCodec.encode_delimited(b_arr, request) - await self._stream.send(b_arr) + try: + await self._stream.send(b_arr) + except (anyio.ClosedResourceError, anyio.EndOfStream): + raise ClosingError("The communication layer was unexpectedly closed.") def _encode_arg(self, arg: TEncodable) -> bytes: """ diff --git a/python/glide-shared/glide_shared/config.py b/python/glide-shared/glide_shared/config.py index 271ae04154..8aec77cb58 100644 --- a/python/glide-shared/glide_shared/config.py +++ b/python/glide-shared/glide_shared/config.py @@ -249,6 +249,8 @@ class BaseClientConfiguration: reconnect_strategy (Optional[BackoffStrategy]): Strategy used to determine how and when to reconnect, in case of connection failures. If not set, a default backoff strategy will be used. + database_id (Optional[int]): Index of the logical database to connect to. + Must be a non-negative integer.If not set, the client will connect to database 0. client_name (Optional[str]): Client name to be used for the client. Will be used with CLIENT SETNAME command during connection establishment. protocol (ProtocolVersion): Serialization protocol to be used. If not set, `RESP3` will be used. @@ -292,6 +294,7 @@ def __init__( read_from: ReadFrom = ReadFrom.PRIMARY, request_timeout: Optional[int] = None, reconnect_strategy: Optional[BackoffStrategy] = None, + database_id: Optional[int] = None, client_name: Optional[str] = None, protocol: ProtocolVersion = ProtocolVersion.RESP3, inflight_requests_limit: Optional[int] = None, @@ -305,6 +308,7 @@ def __init__( self.read_from = read_from self.request_timeout = request_timeout self.reconnect_strategy = reconnect_strategy + self.database_id = database_id self.client_name = client_name self.protocol = protocol self.inflight_requests_limit = inflight_requests_limit @@ -322,6 +326,39 @@ def __init__( "client_az must be set when read_from is set to AZ_AFFINITY_REPLICAS_AND_PRIMARY" ) + def _set_addresses_in_request(self, request: ConnectionRequest) -> None: + """Set addresses in the protobuf request.""" + for address in self.addresses: + address_info = request.addresses.add() + address_info.host = address.host + address_info.port = address.port + + def _set_reconnect_strategy_in_request(self, request: ConnectionRequest) -> None: + """Set reconnect strategy in the protobuf request.""" + if not self.reconnect_strategy: + return + + request.connection_retry_strategy.number_of_retries = ( + self.reconnect_strategy.num_of_retries + ) + request.connection_retry_strategy.factor = self.reconnect_strategy.factor + request.connection_retry_strategy.exponent_base = ( + self.reconnect_strategy.exponent_base + ) + if self.reconnect_strategy.jitter_percent is not None: + request.connection_retry_strategy.jitter_percent = ( + self.reconnect_strategy.jitter_percent + ) + + def _set_credentials_in_request(self, request: ConnectionRequest) -> None: + """Set credentials in the protobuf request.""" + if not self.credentials: + return + + if self.credentials.username: + request.authentication_info.username = self.credentials.username + request.authentication_info.password = self.credentials.password + def _create_a_protobuf_conn_request( self, cluster_mode: bool = False ) -> ConnectionRequest: @@ -335,44 +372,34 @@ def _create_a_protobuf_conn_request( ConnectionRequest: Protobuf ConnectionRequest. """ request = ConnectionRequest() - for address in self.addresses: - address_info = request.addresses.add() - address_info.host = address.host - address_info.port = address.port + + # Set basic configuration + self._set_addresses_in_request(request) request.tls_mode = TlsMode.SecureTls if self.use_tls else TlsMode.NoTls request.read_from = self.read_from.value + request.cluster_mode_enabled = cluster_mode + request.protocol = self.protocol.value + + # Set optional configuration if self.request_timeout: request.request_timeout = self.request_timeout - if self.reconnect_strategy: - request.connection_retry_strategy.number_of_retries = ( - self.reconnect_strategy.num_of_retries - ) - request.connection_retry_strategy.factor = self.reconnect_strategy.factor - request.connection_retry_strategy.exponent_base = ( - self.reconnect_strategy.exponent_base - ) - if self.reconnect_strategy.jitter_percent is not None: - request.connection_retry_strategy.jitter_percent = ( - self.reconnect_strategy.jitter_percent - ) - request.cluster_mode_enabled = True if cluster_mode else False - if self.credentials: - if self.credentials.username: - request.authentication_info.username = self.credentials.username - request.authentication_info.password = self.credentials.password + self._set_reconnect_strategy_in_request(request) + self._set_credentials_in_request(request) + if self.client_name: request.client_name = self.client_name - request.protocol = self.protocol.value if self.inflight_requests_limit: request.inflight_requests_limit = self.inflight_requests_limit if self.client_az: request.client_az = self.client_az + if self.database_id is not None: + request.database_id = self.database_id if self.advanced_config: self.advanced_config._create_a_protobuf_conn_request(request) - if self.lazy_connect is not None: request.lazy_connect = self.lazy_connect + return request def _is_pubsub_configured(self) -> bool: @@ -425,7 +452,7 @@ class GlideClientConfiguration(BaseClientConfiguration): reconnect_strategy (Optional[BackoffStrategy]): Strategy used to determine how and when to reconnect, in case of connection failures. If not set, a default backoff strategy will be used. - database_id (Optional[int]): index of the logical database to connect to. + database_id (Optional[int]): Index of the logical database to connect to. client_name (Optional[str]): Client name to be used for the client. Will be used with CLIENT SETNAME command during connection establishment. protocol (ProtocolVersion): The version of the RESP protocol to communicate with the server. @@ -500,6 +527,7 @@ def __init__( read_from=read_from, request_timeout=request_timeout, reconnect_strategy=reconnect_strategy, + database_id=database_id, client_name=client_name, protocol=protocol, inflight_requests_limit=inflight_requests_limit, @@ -507,7 +535,6 @@ def __init__( advanced_config=advanced_config, lazy_connect=lazy_connect, ) - self.database_id = database_id self.pubsub_subscriptions = pubsub_subscriptions def _create_a_protobuf_conn_request( @@ -515,8 +542,6 @@ def _create_a_protobuf_conn_request( ) -> ConnectionRequest: assert cluster_mode is False request = super()._create_a_protobuf_conn_request(cluster_mode) - if self.database_id: - request.database_id = self.database_id if self.pubsub_subscriptions: if self.protocol == ProtocolVersion.RESP2: @@ -592,6 +617,7 @@ class GlideClusterClientConfiguration(BaseClientConfiguration): reconnect_strategy (Optional[BackoffStrategy]): Strategy used to determine how and when to reconnect, in case of connection failures. If not set, a default backoff strategy will be used. + database_id (Optional[int]): Index of the logical database to connect to. client_name (Optional[str]): Client name to be used for the client. Will be used with CLIENT SETNAME command during connection establishment. protocol (ProtocolVersion): The version of the RESP protocol to communicate with the server. @@ -661,6 +687,7 @@ def __init__( read_from: ReadFrom = ReadFrom.PRIMARY, request_timeout: Optional[int] = None, reconnect_strategy: Optional[BackoffStrategy] = None, + database_id: Optional[int] = None, client_name: Optional[str] = None, protocol: ProtocolVersion = ProtocolVersion.RESP3, periodic_checks: Union[ @@ -679,6 +706,7 @@ def __init__( read_from=read_from, request_timeout=request_timeout, reconnect_strategy=reconnect_strategy, + database_id=database_id, client_name=client_name, protocol=protocol, inflight_requests_limit=inflight_requests_limit, diff --git a/python/glide-sync/glide_sync/config.py b/python/glide-sync/glide_sync/config.py index a99e846cfe..fe4f66ea3a 100644 --- a/python/glide-sync/glide_sync/config.py +++ b/python/glide-sync/glide_sync/config.py @@ -123,6 +123,8 @@ class GlideClusterClientConfiguration(SharedGlideClusterClientConfiguration): reconnect_strategy (Optional[BackoffStrategy]): Strategy used to determine how and when to reconnect, in case of connection failures. If not set, a default backoff strategy will be used. + database_id (Optional[int]): Index of the logical database to connect to. + If not set, the client will connect to database 0. client_name (Optional[str]): Client name to be used for the client. Will be used with CLIENT SETNAME command during connection establishment. protocol (ProtocolVersion): The version of the RESP protocol to communicate with the server. @@ -153,6 +155,7 @@ def __init__( read_from: ReadFrom = ReadFrom.PRIMARY, request_timeout: Optional[int] = None, reconnect_strategy: Optional[BackoffStrategy] = None, + database_id: Optional[int] = None, client_name: Optional[str] = None, protocol: ProtocolVersion = ProtocolVersion.RESP3, periodic_checks: Union[ @@ -168,9 +171,10 @@ def __init__( credentials=credentials, read_from=read_from, request_timeout=request_timeout, + reconnect_strategy=reconnect_strategy, + database_id=database_id, periodic_checks=periodic_checks, pubsub_subscriptions=None, - reconnect_strategy=reconnect_strategy, client_name=client_name, protocol=protocol, inflight_requests_limit=None, diff --git a/python/glide-sync/glide_sync/sync_commands/core.py b/python/glide-sync/glide_sync/sync_commands/core.py index 5a609f4480..444d24f110 100644 --- a/python/glide-sync/glide_sync/sync_commands/core.py +++ b/python/glide-sync/glide_sync/sync_commands/core.py @@ -1085,7 +1085,7 @@ def httl(self, key: TEncodable, fields: List[TEncodable]) -> List[int]: - `-2`: field does not exist or key does not exist Examples: - >>> client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.EX, 10)) + >>> client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.SEC, 10)) >>> client.httl("my_hash", ["field1", "field2", "non_existent_field"]) [9, 9, -2] # field1 and field2 have ~9 seconds left, non_existent_field doesn't exist @@ -1115,7 +1115,7 @@ def hpttl(self, key: TEncodable, fields: List[TEncodable]) -> List[int]: - `-2`: field does not exist or key does not exist Examples: - >>> client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.PX, 10000)) + >>> client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.MILLSEC, 10000)) >>> client.hpttl("my_hash", ["field1", "field2", "non_existent_field"]) [9500, 9500, -2] # field1 and field2 have ~9500 milliseconds left, non_existent_field doesn't exist @@ -1147,7 +1147,7 @@ def hexpiretime(self, key: TEncodable, fields: List[TEncodable]) -> List[int]: Examples: >>> import time >>> future_timestamp = int(time.time()) + 60 # 60 seconds from now - >>> client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.EXAT, future_timestamp)) + >>> client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.UNIX_SEC, future_timestamp)) >>> client.hexpiretime("my_hash", ["field1", "field2", "non_existent_field"]) [future_timestamp, future_timestamp, -2] # field1 and field2 expire at future_timestamp, non_existent_field doesn't exist @@ -1179,7 +1179,7 @@ def hpexpiretime(self, key: TEncodable, fields: List[TEncodable]) -> List[int]: Examples: >>> import time >>> future_timestamp_ms = int(time.time() * 1000) + 60000 # 60 seconds from now in milliseconds - >>> client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.PXAT, future_timestamp_ms)) + >>> client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.UNIX_MILLSEC, future_timestamp_ms)) >>> client.hpexpiretime("my_hash", ["field1", "field2", "non_existent_field"]) [future_timestamp_ms, future_timestamp_ms, -2] # field1 and field2 expire at future_timestamp_ms, non_existent_field doesn't exist @@ -1212,17 +1212,17 @@ def hsetex( - ONLY_IF_ALL_EXIST (FXX): Only set fields if all of them already exist. - ONLY_IF_NONE_EXIST (FNX): Only set fields if none of them already exist. expiry (Optional[ExpirySet]): Expiration options for the fields: - - EX: Expiration time in seconds. - - PX: Expiration time in milliseconds. - - EXAT: Absolute expiration time in seconds (Unix timestamp). - - PXAT: Absolute expiration time in milliseconds (Unix timestamp). - - KEEPTTL: Retain existing TTL. + - SEC (EX): Expiration time in seconds. + - MILLSEC (PX): Expiration time in milliseconds. + - UNIX_SEC (EXAT): Absolute expiration time in seconds (Unix timestamp). + - UNIX_MILLSEC (PXAT): Absolute expiration time in milliseconds (Unix timestamp). + - KEEP_TTL (KEEPTTL): Retain existing TTL. Returns: int: 1 if all fields were set successfully, 0 if none were set due to conditional constraints. Examples: - >>> client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.EX, 10)) + >>> client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.SEC, 10)) 1 # All fields set with 10 second expiration >>> client.hsetex("my_hash", {"field3": "value3"}, field_conditional_change=HashFieldConditionalChange.ONLY_IF_ALL_EXIST) 1 # Field set because field already exists @@ -1268,10 +1268,10 @@ def hgetex( key (TEncodable): The key of the hash. fields (List[TEncodable]): The list of fields to retrieve from the hash. expiry (Optional[ExpiryGetEx]): Expiration options for the retrieved fields: - - EX: Expiration time in seconds. - - PX: Expiration time in milliseconds. - - EXAT: Absolute expiration time in seconds (Unix timestamp). - - PXAT: Absolute expiration time in milliseconds (Unix timestamp). + - SEC (EX): Expiration time in seconds. + - MILLSEC (PX): Expiration time in milliseconds. + - UNIX_SEC (EXAT): Absolute expiration time in seconds (Unix timestamp). + - UNIX_MILLSEC (PXAT): Absolute expiration time in milliseconds (Unix timestamp). - PERSIST: Remove expiration from the fields. Returns: @@ -1280,10 +1280,10 @@ def hgetex( If `key` does not exist, it is treated as an empty hash, and the function returns a list of null values. Examples: - >>> client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.EX, 10)) + >>> client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.SEC, 10)) >>> client.hgetex("my_hash", ["field1", "field2"]) [b"value1", b"value2"] - >>> client.hgetex("my_hash", ["field1"], expiry=ExpiryGetEx(ExpiryTypeGetEx.EX, 20)) + >>> client.hgetex("my_hash", ["field1"], expiry=ExpiryGetEx(ExpiryTypeGetEx.SEC, 20)) [b"value1"] # field1 now has 20 second expiration >>> client.hgetex("my_hash", ["field1"], expiry=ExpiryGetEx(ExpiryTypeGetEx.PERSIST, None)) [b"value1"] # field1 expiration removed @@ -1337,7 +1337,7 @@ def hexpire( - `2`: Field was deleted immediately (when seconds is 0 or timestamp is in the past). Examples: - >>> client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.EX, 10)) + >>> client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.SEC, 10)) >>> client.hexpire("my_hash", 20, ["field1", "field2"]) [1, 1] # Both fields' expiration set to 20 seconds >>> client.hexpire("my_hash", 30, ["field1"], option=ExpireOptions.NewExpiryGreaterThanCurrent) @@ -1383,7 +1383,7 @@ def hpersist(self, key: TEncodable, fields: List[TEncodable]) -> List[int]: - `-2`: Field does not exist or key does not exist. Examples: - >>> client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.EX, 10)) + >>> client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.SEC, 10)) >>> client.hpersist("my_hash", ["field1", "field2"]) [1, 1] # Both fields made persistent >>> client.hpersist("my_hash", ["field1"]) @@ -1430,7 +1430,7 @@ def hpexpire( - `2`: Field was deleted immediately (when milliseconds is 0 or timestamp is in the past). Examples: - >>> client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.PX, 10000)) + >>> client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.MILLSEC, 10000)) >>> client.hpexpire("my_hash", 20000, ["field1", "field2"]) [1, 1] # Both fields' expiration set to 20000 milliseconds >>> client.hpexpire("my_hash", 30000, ["field1"], option=ExpireOptions.NewExpiryGreaterThanCurrent) @@ -1491,7 +1491,7 @@ def hexpireat( Examples: >>> import time >>> future_timestamp = int(time.time()) + 60 # 60 seconds from now - >>> client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.EX, 10)) + >>> client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.SEC, 10)) >>> client.hexpireat("my_hash", future_timestamp, ["field1", "field2"]) [1, 1] # Both fields' expiration set to future_timestamp >>> past_timestamp = int(time.time()) - 60 # 60 seconds ago @@ -1551,7 +1551,7 @@ def hpexpireat( Examples: >>> import time >>> future_timestamp_ms = int(time.time() * 1000) + 60000 # 60 seconds from now in milliseconds - >>> client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.PX, 10000)) + >>> client.hsetex("my_hash", {"field1": "value1", "field2": "value2"}, expiry=ExpirySet(ExpiryType.MILLSEC, 10000)) >>> client.hpexpireat("my_hash", future_timestamp_ms, ["field1", "field2"]) [1, 1] # Both fields' expiration set to future_timestamp_ms >>> past_timestamp_ms = int(time.time() * 1000) - 60000 # 60 seconds ago in milliseconds diff --git a/python/glide-sync/glide_sync/sync_commands/standalone_commands.py b/python/glide-sync/glide_sync/sync_commands/standalone_commands.py index 1f3643850b..34659417b7 100644 --- a/python/glide-sync/glide_sync/sync_commands/standalone_commands.py +++ b/python/glide-sync/glide_sync/sync_commands/standalone_commands.py @@ -161,20 +161,6 @@ def exec( timeout=timeout, ) - def select(self, index: int) -> TOK: - """ - Change the currently selected database. - - See [valkey.io](https://valkey.io/commands/select/) for details. - - Args: - index (int): The index of the database to select. - - Returns: - A simple OK response. - """ - return cast(TOK, self._execute_command(RequestType.Select, [str(index)])) - def config_resetstat(self) -> TOK: """ Resets the statistics reported by the server using the INFO and LATENCY HISTOGRAM commands. diff --git a/python/glide-sync/pyproject.toml b/python/glide-sync/pyproject.toml index 06bd0e0143..b46fd572a1 100644 --- a/python/glide-sync/pyproject.toml +++ b/python/glide-sync/pyproject.toml @@ -1,6 +1,7 @@ [project] -dynamic = ["version"] +dynamic = ["version", "readme"] name = "valkey-glide-sync" +description = "Valkey GLIDE Sync client. Supports Valkey and Redis OSS." license = { text = "Apache-2.0" } dependencies = [ # ⚠️ Note: If you add a dependency here, make sure to also add it to glide-sync/requirements.txt diff --git a/python/glide-sync/setup.py b/python/glide-sync/setup.py index 0611c75c0a..d58445a4fe 100644 --- a/python/glide-sync/setup.py +++ b/python/glide-sync/setup.py @@ -47,6 +47,9 @@ class VendorFolder: ROOT = Path(__file__).resolve().parent # glide-sync/ + +long_description = (ROOT.parent / "README.md").read_text(encoding="utf-8") + VENDORED_DEPENDENCIES = { "glide_shared": VendorFolder( source=ROOT.parent / "glide-shared" / "glide_shared", @@ -245,4 +248,6 @@ def run(self): "sdist": sdist, "clean": CleanCommand, # type: ignore }, + long_description=long_description, + long_description_content_type="text/markdown", ) diff --git a/python/tests/async_tests/test_async_client.py b/python/tests/async_tests/test_async_client.py index 9d9354702d..c5c59e2cca 100644 --- a/python/tests/async_tests/test_async_client.py +++ b/python/tests/async_tests/test_async_client.py @@ -258,8 +258,18 @@ async def test_can_connect_with_auth_acl( # Delete this user await glide_client.custom_command(["ACL", "DELUSER", username]) - @pytest.mark.parametrize("cluster_mode", [False]) - async def test_select_standalone_database_id(self, request, cluster_mode): + @pytest.mark.parametrize("cluster_mode", [True, False]) + async def test_select_database_id(self, request, cluster_mode): + if cluster_mode: + # Check version using a temporary standalone client + temp_client = await create_client(request, cluster_mode=False) + if await check_if_server_version_lt(temp_client, "9.0.0"): + await temp_client.close() + return pytest.mark.skip( + reason="Database ID selection in cluster mode requires Valkey >= 9.0.0" + ) + await temp_client.close() + glide_client = await create_client( request, cluster_mode=cluster_mode, database_id=4 ) @@ -267,6 +277,24 @@ async def test_select_standalone_database_id(self, request, cluster_mode): assert b"db=4" in client_info await glide_client.close() + @pytest.mark.parametrize("cluster_mode", [True]) + async def test_select_database_id_custom_command(self, request, cluster_mode): + if cluster_mode: + # Check version using a temporary standalone client + temp_client = await create_client(request, cluster_mode=False) + if await check_if_server_version_lt(temp_client, "9.0.0"): + await temp_client.close() + return pytest.mark.skip( + reason="Database ID selection in cluster mode requires Valkey >= 9.0.0" + ) + await temp_client.close() + + glide_client = await create_client(request, cluster_mode=cluster_mode) + assert await glide_client.custom_command(["SELECT", "4"]) == OK + client_info = await glide_client.custom_command(["CLIENT", "INFO"]) + assert b"db=4" in client_info + await glide_client.close() + @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_client_name(self, request, cluster_mode, protocol): @@ -384,6 +412,29 @@ async def connect_to_client(): # Clean up the main client await client.close() + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_UDS_socket_connection_failure(self, glide_client: TGlideClient): + """Test that the client's error handling during UDS socket connection failure""" + assert await glide_client.set("test_key", "test_value") == OK + assert await glide_client.get("test_key") == b"test_value" + + # Force close the UDS connection to simulate socket failure + await glide_client._stream.aclose() + + # Verify a ClosingError is raised + with pytest.raises( + ClosingError, match="The communication layer was unexpectedly closed" + ): + await glide_client.get("test_key") + + # Verify the client is closed + with pytest.raises( + ClosingError, + match="Unable to execute requests; the client is closed. Please create a new client.", + ): + await glide_client.get("test_key") + @pytest.mark.anyio class TestCommands: diff --git a/python/tests/sync_tests/test_sync_client.py b/python/tests/sync_tests/test_sync_client.py index 77328cd72a..772d7398e2 100644 --- a/python/tests/sync_tests/test_sync_client.py +++ b/python/tests/sync_tests/test_sync_client.py @@ -269,8 +269,18 @@ def test_sync_can_connect_with_auth_acl( # Delete this user glide_sync_client.custom_command(["ACL", "DELUSER", username]) - @pytest.mark.parametrize("cluster_mode", [False]) - def test_sync_select_standalone_database_id(self, request, cluster_mode): + @pytest.mark.parametrize("cluster_mode", [True, False]) + def test_sync_select_database_id(self, request, cluster_mode): + if cluster_mode: + # Check version using a temporary standalone client + temp_client = create_sync_client(request, cluster_mode=False) + if sync_check_if_server_version_lt(temp_client, "9.0.0"): + temp_client.close() + pytest.skip( + reason="Database ID selection in cluster mode requires Valkey >= 9.0.0" + ) + temp_client.close() + glide_sync_client = create_sync_client( request, cluster_mode=cluster_mode, database_id=4 ) @@ -278,6 +288,24 @@ def test_sync_select_standalone_database_id(self, request, cluster_mode): assert b"db=4" in client_info glide_sync_client.close() + @pytest.mark.parametrize("cluster_mode", [True]) + def test_sync_select_database_id_custom_command(self, request, cluster_mode): + if cluster_mode: + # Check version using a temporary standalone client + temp_client = create_sync_client(request, cluster_mode=False) + if sync_check_if_server_version_lt(temp_client, "9.0.0"): + temp_client.close() + return pytest.skip( + reason="Database ID selection in cluster mode requires Valkey >= 9.0.0" + ) + temp_client.close() + + glide_sync_client = create_sync_client(request, cluster_mode=cluster_mode) + assert glide_sync_client.custom_command(["SELECT", "4"]) == OK + client_info = glide_sync_client.custom_command(["CLIENT", "INFO"]) + assert b"db=4" in client_info + glide_sync_client.close() + @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) def test_sync_client_name(self, request, cluster_mode, protocol): @@ -585,26 +613,13 @@ def test_sync_info_default(self, glide_sync_client: TGlideClient): info_result = get_first_result(info_result) assert b"# Memory" in info_result - @pytest.mark.parametrize("cluster_mode", [False]) - @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - def test_sync_select(self, glide_sync_client: GlideClient): - assert glide_sync_client.select(0) == OK - key = get_random_string(10) - value = get_random_string(10) - assert glide_sync_client.set(key, value) == OK - assert glide_sync_client.get(key) == value.encode() - assert glide_sync_client.select(1) == OK - assert glide_sync_client.get(key) is None - assert glide_sync_client.select(0) == OK - assert glide_sync_client.get(key) == value.encode() - @pytest.mark.parametrize("cluster_mode", [False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) def test_sync_move(self, glide_sync_client: GlideClient): key = get_random_string(10) value = get_random_string(10) - assert glide_sync_client.select(0) == OK + assert glide_sync_client.custom_command(["SELECT", "0"]) == OK assert glide_sync_client.move(key, 1) is False assert glide_sync_client.set(key, value) == OK @@ -612,7 +627,7 @@ def test_sync_move(self, glide_sync_client: GlideClient): assert glide_sync_client.move(key, 1) is True assert glide_sync_client.get(key) is None - assert glide_sync_client.select(1) == OK + assert glide_sync_client.custom_command(["SELECT", "1"]) == OK assert glide_sync_client.get(key) == value.encode() with pytest.raises(RequestError): @@ -624,7 +639,7 @@ def test_sync_move_with_bytes(self, glide_sync_client: GlideClient): key = get_random_string(10) value = get_random_string(10) - assert glide_sync_client.select(0) == OK + assert glide_sync_client.custom_command(["SELECT", "0"]) == OK assert glide_sync_client.set(key, value) == OK assert glide_sync_client.get(key.encode()) == value.encode() @@ -632,7 +647,7 @@ def test_sync_move_with_bytes(self, glide_sync_client: GlideClient): assert glide_sync_client.move(key.encode(), 1) is True assert glide_sync_client.get(key) is None assert glide_sync_client.get(key.encode()) is None - assert glide_sync_client.select(1) == OK + assert glide_sync_client.custom_command(["SELECT", "1"]) == OK assert glide_sync_client.get(key) == value.encode() @pytest.mark.parametrize("cluster_mode", [True, False]) @@ -5182,7 +5197,7 @@ def test_sync_dbsize(self, glide_sync_client: TGlideClient): assert glide_sync_client.set(key, value) == OK assert glide_sync_client.dbsize(SlotKeyRoute(SlotType.PRIMARY, key)) == 1 else: - assert glide_sync_client.select(1) == OK + assert glide_sync_client.custom_command(["SELECT", "1"]) == OK assert glide_sync_client.dbsize() == 0 @pytest.mark.parametrize("cluster_mode", [True, False]) @@ -8968,12 +8983,12 @@ def test_sync_standalone_flushdb(self, glide_sync_client: GlideClient): value = get_random_string(5) # fill DB 0 and check size non-empty - assert glide_sync_client.select(0) == OK + assert glide_sync_client.custom_command(["SELECT", "0"]) == OK glide_sync_client.set(key1, value) assert glide_sync_client.dbsize() > 0 # fill DB 1 and check size non-empty - assert glide_sync_client.select(1) == OK + assert glide_sync_client.custom_command(["SELECT", "1"]) == OK glide_sync_client.set(key2, value) assert glide_sync_client.dbsize() > 0 @@ -8982,7 +8997,7 @@ def test_sync_standalone_flushdb(self, glide_sync_client: GlideClient): assert glide_sync_client.dbsize() == 0 # swith to DB 0, flush, and check - assert glide_sync_client.select(0) == OK + assert glide_sync_client.custom_command(["SELECT", "0"]) == OK assert glide_sync_client.dbsize() > 0 assert glide_sync_client.flushdb(FlushMode.ASYNC) == OK assert glide_sync_client.dbsize() == 0 @@ -9068,12 +9083,12 @@ def test_sync_copy_database(self, glide_sync_client: GlideClient): value2 = get_random_string(5) value1_encoded = value1.encode() value2_encoded = value2.encode() - index0 = 0 - index1 = 1 - index2 = 2 + index0 = "0" + index1 = "1" + index2 = "2" try: - assert glide_sync_client.select(index0) == OK + assert glide_sync_client.custom_command(["SELECT", index0]) == OK # neither key exists assert ( @@ -9087,11 +9102,11 @@ def test_sync_copy_database(self, glide_sync_client: GlideClient): glide_sync_client.copy(source, destination, index1, replace=False) is True ) - assert glide_sync_client.select(index1) == OK + assert glide_sync_client.custom_command(["SELECT", index1]) == OK assert glide_sync_client.get(destination) == value1_encoded # new value for source key - assert glide_sync_client.select(index0) == OK + assert glide_sync_client.custom_command(["SELECT", index0]) == OK glide_sync_client.set(source, value2) # no REPLACE, copying to existing key on DB 0 & 1, non-existing key on DB 2 @@ -9105,25 +9120,25 @@ def test_sync_copy_database(self, glide_sync_client: GlideClient): ) # new value only gets copied to DB 2 - assert glide_sync_client.select(index1) == OK + assert glide_sync_client.custom_command(["SELECT", index1]) == OK assert glide_sync_client.get(destination) == value1_encoded - assert glide_sync_client.select(index2) == OK + assert glide_sync_client.custom_command(["SELECT", index2]) == OK assert glide_sync_client.get(destination) == value2_encoded # both exists, with REPLACE, when value isn't the same, source always get copied to destination - assert glide_sync_client.select(index0) == OK + assert glide_sync_client.custom_command(["SELECT", index0]) == OK assert ( glide_sync_client.copy(source, destination, index1, replace=True) is True ) - assert glide_sync_client.select(index1) == OK + assert glide_sync_client.custom_command(["SELECT", index1]) == OK assert glide_sync_client.get(destination) == value2_encoded # invalid DB index with pytest.raises(RequestError): glide_sync_client.copy(source, destination, -1, replace=True) finally: - assert glide_sync_client.select(0) == OK + assert glide_sync_client.custom_command(["SELECT", "0"]) == OK @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) @@ -9212,9 +9227,9 @@ def test_sync_standalone_client_random_key(self, glide_sync_client: GlideClient) key = get_random_string(10) # setup: delete all keys in DB 0 and DB 1 - assert glide_sync_client.select(0) == OK + assert glide_sync_client.custom_command(["SELECT", "0"]) == OK assert glide_sync_client.flushdb(FlushMode.SYNC) == OK - assert glide_sync_client.select(1) == OK + assert glide_sync_client.custom_command(["SELECT", "1"]) == OK assert glide_sync_client.flushdb(FlushMode.SYNC) == OK # no keys exist so random_key returns None @@ -9225,7 +9240,7 @@ def test_sync_standalone_client_random_key(self, glide_sync_client: GlideClient) assert glide_sync_client.random_key() == key.encode() # switch back to DB 0 - assert glide_sync_client.select(0) == OK + assert glide_sync_client.custom_command(["SELECT", "0"]) == OK # DB 0 should still have no keys, so random_key should still return None assert glide_sync_client.random_key() is None diff --git a/python/tests/test_api_consistency.py b/python/tests/test_api_consistency.py index f376786681..4fd3fc26b5 100644 --- a/python/tests/test_api_consistency.py +++ b/python/tests/test_api_consistency.py @@ -52,6 +52,7 @@ "create_leaked_value", "start_socket_listener_external", "value_from_pointer", + "select", ], "sync_only": [], } @@ -67,6 +68,8 @@ "async_only": [ "test_inflight_request_limit", "test_statistics", + "test_select", + "test_UDS_socket_connection_failure", ], "sync_only": ["test_sync_fork"], } diff --git a/python/tests/test_config.py b/python/tests/test_config.py index 123ea08326..5b71aab68f 100644 --- a/python/tests/test_config.py +++ b/python/tests/test_config.py @@ -195,3 +195,85 @@ def test_tls_insecure_in_protobuf_request(): assert isinstance(request, ConnectionRequest) assert request.tls_mode is TlsMode.InsecureTls + + +# Database ID configuration tests +def test_database_id_validation_in_base_config(): + """Test database_id validation in BaseClientConfiguration.""" + # Valid database_id values + config = BaseClientConfiguration([NodeAddress("127.0.0.1")], database_id=0) + assert config.database_id == 0 + + config = BaseClientConfiguration([NodeAddress("127.0.0.1")], database_id=5) + assert config.database_id == 5 + + config = BaseClientConfiguration([NodeAddress("127.0.0.1")], database_id=15) + assert config.database_id == 15 + + # Test broader range of database IDs + config = BaseClientConfiguration([NodeAddress("127.0.0.1")], database_id=100) + assert config.database_id == 100 + + config = BaseClientConfiguration([NodeAddress("127.0.0.1")], database_id=1000) + assert config.database_id == 1000 + + # None should be allowed (defaults to 0) + config = BaseClientConfiguration([NodeAddress("127.0.0.1")], database_id=None) + assert config.database_id is None + + +def test_database_id_in_standalone_config(): + """Test database_id configuration in GlideClientConfiguration.""" + config = GlideClientConfiguration([NodeAddress("127.0.0.1")], database_id=5) + assert config.database_id == 5 + + request = config._create_a_protobuf_conn_request() + assert request.database_id == 5 + assert request.cluster_mode_enabled is False + + +def test_database_id_in_cluster_config(): + """Test database_id configuration in GlideClusterClientConfiguration.""" + config = GlideClusterClientConfiguration([NodeAddress("127.0.0.1")], database_id=3) + assert config.database_id == 3 + + request = config._create_a_protobuf_conn_request(cluster_mode=True) + assert request.database_id == 3 + assert request.cluster_mode_enabled is True + + +def test_database_id_default_behavior(): + """Test default database_id behavior (None/0).""" + # Standalone config without database_id + config = GlideClientConfiguration([NodeAddress("127.0.0.1")]) + assert config.database_id is None + + request = config._create_a_protobuf_conn_request() + # When database_id is None, it should be 0 in protobuf (default value) + assert request.database_id == 0 + + # Cluster config without database_id + config = GlideClusterClientConfiguration([NodeAddress("127.0.0.1")]) + assert config.database_id is None + + request = config._create_a_protobuf_conn_request(cluster_mode=True) + # When database_id is None, it should be 0 in protobuf (default value) + assert request.database_id == 0 + + +def test_database_id_protobuf_inclusion(): + """Test that database_id is properly included in protobuf when set.""" + # Test with database_id = 0 (should be included) + config = GlideClientConfiguration([NodeAddress("127.0.0.1")], database_id=0) + request = config._create_a_protobuf_conn_request() + assert request.database_id == 0 + + # Test with database_id = 5 (should be included) + config = GlideClientConfiguration([NodeAddress("127.0.0.1")], database_id=5) + request = config._create_a_protobuf_conn_request() + assert request.database_id == 5 + + # Test with database_id = None (should default to 0) + config = GlideClientConfiguration([NodeAddress("127.0.0.1")]) + request = config._create_a_protobuf_conn_request() + assert request.database_id == 0 diff --git a/python/tests/utils/utils.py b/python/tests/utils/utils.py index a90ea8e035..eda894ccbe 100644 --- a/python/tests/utils/utils.py +++ b/python/tests/utils/utils.py @@ -552,13 +552,13 @@ def create_client_config( if cluster_mode: valkey_cluster = valkey_cluster or pytest.valkey_cluster # type: ignore assert type(valkey_cluster) is ValkeyCluster - assert database_id == 0 k = min(3, len(valkey_cluster.nodes_addr)) seed_nodes = random.sample(valkey_cluster.nodes_addr, k=k) return GlideClusterClientConfiguration( addresses=seed_nodes if addresses is None else addresses, use_tls=use_tls, credentials=credentials, + database_id=database_id, client_name=client_name, protocol=protocol, request_timeout=timeout, @@ -620,13 +620,13 @@ def create_sync_client_config( if cluster_mode: valkey_cluster = valkey_cluster or pytest.valkey_cluster # type: ignore assert type(valkey_cluster) is ValkeyCluster - assert database_id == 0 k = min(3, len(valkey_cluster.nodes_addr)) seed_nodes = random.sample(valkey_cluster.nodes_addr, k=k) return SyncGlideClusterClientConfiguration( addresses=seed_nodes if addresses is None else addresses, use_tls=use_tls, credentials=credentials, + database_id=database_id, client_name=client_name, protocol=protocol, request_timeout=timeout, diff --git a/utils/cluster_manager.py b/utils/cluster_manager.py index 9be6731c43..d005fff70b 100755 --- a/utils/cluster_manager.py +++ b/utils/cluster_manager.py @@ -397,6 +397,9 @@ def get_server_version(server_name): ] if server_version >= (7, 0, 0): cmd_args.extend(["--enable-debug-command", "yes"]) + # Enable multi-database support in cluster mode for Valkey 9.0+ + if cluster_mode and server_version >= (9, 0, 0): + cmd_args.extend(["--cluster-databases", "16"]) if load_module: if len(load_module) == 0: raise ValueError( diff --git a/utils/release-candidate-testing/python/async_rc_test.py b/utils/release-candidate-testing/python/async_rc_test.py index eb9bb155e4..4932d0eaea 100644 --- a/utils/release-candidate-testing/python/async_rc_test.py +++ b/utils/release-candidate-testing/python/async_rc_test.py @@ -1,13 +1,30 @@ # Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 import asyncio -from typing import List -from utils import start_servers, stop_servers, parse_cluster_script_start_output, create_client +from typing import List, Union -from glide import ( - GlideClient, - NodeAddress, -) +from glide import (GlideClient, GlideClientConfiguration, GlideClusterClient, + GlideClusterClientConfiguration, NodeAddress) + +from utils import (parse_cluster_script_start_output, start_servers, + stop_servers) + + +def create_client( + nodes_list: List[NodeAddress] = [("localhost", 6379)], is_cluster: bool = False +) -> Union[GlideClusterClient, GlideClient]: + addresses: List[NodeAddress] = nodes_list + if is_cluster: + config_class = GlideClusterClientConfiguration + client_class = GlideClusterClient + else: + config_class = GlideClientConfiguration + client_class = GlideClient + config = config_class( + addresses=addresses, + client_name=f"test_{'cluster' if is_cluster else 'standalone'}_client", + ) + return client_class.create(config) async def run_commands(client: GlideClient) -> None: @@ -87,7 +104,7 @@ async def test_standalone_client() -> None: output = start_servers(False, 1, 1) servers, folder = parse_cluster_script_start_output(output) servers = [NodeAddress(hp.host, hp.port) for hp in servers] - standalone_client = await create_client(servers, is_cluster=False, is_sync=False) + standalone_client = await create_client(servers, is_cluster=False) await run_commands(standalone_client) stop_servers(folder) print("Async Standalone Client test completed") @@ -98,7 +115,7 @@ async def test_cluster_client() -> None: output = start_servers(True, 3, 1) servers, folder = parse_cluster_script_start_output(output) servers = [NodeAddress(hp.host, hp.port) for hp in servers] - cluster_client = await create_client(servers, is_cluster=True, is_sync=False) + cluster_client = await create_client(servers, is_cluster=True) await run_commands(cluster_client) stop_servers(folder) print("Async Cluster Client test completed") diff --git a/utils/release-candidate-testing/python/sync_rc_test.py b/utils/release-candidate-testing/python/sync_rc_test.py index 6e05b177c0..699ac77e3d 100644 --- a/utils/release-candidate-testing/python/sync_rc_test.py +++ b/utils/release-candidate-testing/python/sync_rc_test.py @@ -1,11 +1,30 @@ -from typing import List -from utils import start_servers, stop_servers, parse_cluster_script_start_output, create_client +from typing import List, Union + +from glide_sync import (GlideClient, GlideClientConfiguration, + GlideClusterClient, GlideClusterClientConfiguration, + NodeAddress) + +from utils import (parse_cluster_script_start_output, start_servers, + stop_servers) + + +def create_client( + nodes_list: List[NodeAddress] = [("localhost", 6379)], is_cluster: bool = False +) -> Union[GlideClusterClient, GlideClient]: + addresses: List[NodeAddress] = nodes_list + if is_cluster: + config_class = GlideClusterClientConfiguration + client_class = GlideClusterClient + else: + config_class = GlideClientConfiguration + client_class = GlideClient + config = config_class( + addresses=addresses, + client_name=f"test_{'cluster' if is_cluster else 'standalone'}_client", + ) + return client_class.create(config) -from glide_sync import ( - GlideClient, - NodeAddress, -) def run_commands(client: GlideClient) -> None: print("Executing commands") @@ -84,7 +103,7 @@ def test_standalone_client() -> None: output = start_servers(False, 1, 1) servers, folder = parse_cluster_script_start_output(output) servers = [NodeAddress(hp.host, hp.port) for hp in servers] - standalone_client = create_client(servers, is_cluster=False, is_sync=True) + standalone_client = create_client(servers, is_cluster=False) run_commands(standalone_client) stop_servers(folder) print("Standalone client test completed") @@ -95,7 +114,7 @@ def test_cluster_client() -> None: output = start_servers(True, 3, 1) servers, folder = parse_cluster_script_start_output(output) servers = [NodeAddress(hp.host, hp.port) for hp in servers] - cluster_client = create_client(servers, is_cluster=True, is_sync=True) + cluster_client = create_client(servers, is_cluster=True) run_commands(cluster_client) stop_servers(folder) print("Cluster client test completed") diff --git a/utils/release-candidate-testing/python/utils.py b/utils/release-candidate-testing/python/utils.py index 4c3d1b4696..6c99d16efe 100644 --- a/utils/release-candidate-testing/python/utils.py +++ b/utils/release-candidate-testing/python/utils.py @@ -1,24 +1,8 @@ -from dataclasses import dataclass import os import subprocess import sys -from typing import List, Tuple, Union - -from glide import ( - GlideClient, - GlideClientConfiguration, - GlideClusterClient, - GlideClusterClientConfiguration, - NodeAddress, -) - -from glide_sync import ( - GlideClient as SyncGlideClient, - GlideClientConfiguration as SyncGlideClientConfiguration, - GlideClusterClient as SyncGlideClusterClient, - GlideClusterClientConfiguration as SyncGlideClusterClientConfiguration, - NodeAddress as SyncNodeAddress, -) +from dataclasses import dataclass +from typing import List, Tuple SCRIPT_FILE = os.path.abspath(f"{__file__}/../../../cluster_manager.py") @@ -27,7 +11,8 @@ class HostPort: host: str port: int - + + def start_servers(cluster_mode: bool, shard_count: int, replica_count: int) -> str: args_list: List[str] = [sys.executable, SCRIPT_FILE] args_list.append("start") @@ -86,20 +71,3 @@ def stop_servers(folder: str) -> str: raise Exception(f"Failed to stop the cluster. Executed: {p}:\n{err}") print("Servers stopped successfully") return output - - -def create_client( - nodes_list: List[Union[NodeAddress, SyncNodeAddress]] = [("localhost", 6379)], is_cluster: bool = False, is_sync: bool = False -) -> GlideClusterClient: - addresses: List[Union[NodeAddress, SyncNodeAddress]] = nodes_list - if is_cluster: - config_class = SyncGlideClusterClientConfiguration if is_sync else GlideClusterClientConfiguration - client_class = SyncGlideClusterClient if is_sync else GlideClusterClient - else: - config_class = SyncGlideClientConfiguration if is_sync else GlideClientConfiguration - client_class = SyncGlideClient if is_sync else GlideClient - config = config_class( - addresses=addresses, - client_name=f"test_{'cluster' if is_cluster else 'standalone'}_client", - ) - return client_class.create(config)