From 07cd8b475d723ad8f52d8458fb690d082125e135 Mon Sep 17 00:00:00 2001 From: James Xin Date: Fri, 5 Sep 2025 12:25:25 -0700 Subject: [PATCH 01/22] Go: Add MUSL support (#4476) * Go: Switch to MUSL binary Signed-off-by: James Xin Signed-off-by: Alex Rehnby-Martin * rustup add musl targets Signed-off-by: James Xin Signed-off-by: Alex Rehnby-Martin * lint Signed-off-by: James Xin Signed-off-by: Alex Rehnby-Martin * rustup add musl targets in Makefile Signed-off-by: James Xin Signed-off-by: Alex Rehnby-Martin * Add pathing Signed-off-by: Alex Rehnby-Martin * Trigger CodeQL on main Signed-off-by: Alex Rehnby-Martin * Update CI Signed-off-by: Alex Rehnby-Martin * Update CI Signed-off-by: Alex Rehnby-Martin * Update CI Signed-off-by: Alex Rehnby-Martin * Update CI Signed-off-by: Alex Rehnby-Martin * Update CI Signed-off-by: Alex Rehnby-Martin * Update CI Signed-off-by: Alex Rehnby-Martin * Update CI Signed-off-by: Alex Rehnby-Martin * Update CI Signed-off-by: Alex Rehnby-Martin * Update CI Signed-off-by: Alex Rehnby-Martin * Update CI Signed-off-by: Alex Rehnby-Martin * Update CI Signed-off-by: Alex Rehnby-Martin * Update CI Signed-off-by: Alex Rehnby-Martin * Fix typo Signed-off-by: Alex Rehnby-Martin * Fix typo Signed-off-by: Alex Rehnby-Martin * Modify CD Signed-off-by: Alex Rehnby-Martin * Modify CD Signed-off-by: Alex Rehnby-Martin * Modify CD Signed-off-by: Alex Rehnby-Martin * Modify CD Signed-off-by: Alex Rehnby-Martin * Update docs Signed-off-by: Alex Rehnby-Martin * Format Signed-off-by: Alex Rehnby-Martin * Fix Signed-off-by: Alex Rehnby-Martin * Fix Signed-off-by: Alex Rehnby-Martin * Address feedback Signed-off-by: Alex Rehnby-Martin * Update doc Signed-off-by: Alex Rehnby-Martin --------- Signed-off-by: James Xin Signed-off-by: Alex Rehnby-Martin Co-authored-by: Alex Rehnby-Martin Co-authored-by: Alexey Temnikov --- .github/json_matrices/build-matrix.json | 6 ++-- .github/workflows/go-cd.yml | 10 ++++-- .github/workflows/go.yml | 28 +++++++++++++--- .github/workflows/install-zig/action.yml | 6 +++- CONTRIBUTING.md | 1 + go/DEVELOPER.md | 10 +----- go/Makefile | 42 +++++++++++++++++++++--- go/README.md | 7 ++++ go/base_client.go | 6 ++-- 9 files changed, 89 insertions(+), 27 deletions(-) diff --git a/.github/json_matrices/build-matrix.json b/.github/json_matrices/build-matrix.json index 995029d9d0..5383db24e5 100644 --- a/.github/json_matrices/build-matrix.json +++ b/.github/json_matrices/build-matrix.json @@ -49,7 +49,7 @@ "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 +61,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/go-cd.yml b/.github/workflows/go-cd.yml index 01173d1ff4..acf891f1b5 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,17 +124,21 @@ 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') }} run: | mkdir -p $GITHUB_WORKSPACE/ffi/target/release cp ffi/target/*/release/libglide_ffi.a $GITHUB_WORKSPACE/ffi/target/release/ + - name: Set artifact name + id: set-artifact-name + run: | + echo "ARTIFACT_NAME=${{ matrix.host.TARGET }}" >> $GITHUB_OUTPUT - name: Upload artifacts continue-on-error: true uses: actions/upload-artifact@v4 with: - name: ${{ matrix.host.TARGET }} + name: ${{ steps.set-artifact-name.outputs.artifact-name }} path: | ffi/target/release/libglide_ffi.a go/lib.h diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 988086d241..ae69a8f2cd 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -255,10 +255,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 +285,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/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/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" From 976e2aa69678cc977fdc7f704d4304e7e2c28e73 Mon Sep 17 00:00:00 2001 From: prateek-kumar-improving Date: Mon, 8 Sep 2025 00:47:40 -0700 Subject: [PATCH 02/22] Java: Migration guide for Jedis compatibility layer (#4672) (#4681) * Java: Migration guide for Jedis compatibility layer Signed-off-by: Prateek Kumar --- .../compatibility-layer-migration-guide.md | 242 ++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 java/client/src/main/java/redis/clients/jedis/compatibility-layer-migration-guide.md diff --git a/java/client/src/main/java/redis/clients/jedis/compatibility-layer-migration-guide.md b/java/client/src/main/java/redis/clients/jedis/compatibility-layer-migration-guide.md new file mode 100644 index 0000000000..cedd19d0b4 --- /dev/null +++ b/java/client/src/main/java/redis/clients/jedis/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 From d2731ec8832b31c7d09a32529082af8eb20785e9 Mon Sep 17 00:00:00 2001 From: affonsov <67347924+affonsov@users.noreply.github.com> Date: Mon, 8 Sep 2025 15:52:21 -0700 Subject: [PATCH 03/22] [Backport to 2.1] Python: Add Multi-Database Support for Cluster Mode Valkey 9.0 (#4694) Python: Add Multi-Database Support for Cluster Mode Valkey 9.0 (#4659) * - Implement database selection for cluster clients when database_id != 0 - Send SELECT command to all cluster nodes using MultiNode routing - Add comprehensive error handling with clear error messages - Include test for database selection error handling in cluster mode - Support backward compatibility with servers that don't support multi-db cluster mode - Enable cluster-databases=16 for Valkey 9.0+ in cluster manager This enables cluster clients to work with non-default databases on Valkey 9.0+ servers configured with cluster-databases support, while gracefully handling older servers that don't support this feature. * update valkey9 multi db tests * fix lint * fixing valkey 9 cluster tests * add to route to all nodes in the core * rust lint fix * implement database selection in cluster mode via connection parameters - Add database_id field to ClusterParams and BuilderParams structs - Add database() method to ClusterClientBuilder for setting database ID - Pass database_id through connection info instead of post-connection SELECT - Remove SELECT command from cluster routing - Remove post-connection SELECT command logic from create_cluster_client - Add comprehensive tests for database isolation and reconnection behavior - Verify database ID persistence across node reconnections - Test multi-database cluster support with proper isolation verification This change moves database selection from a post-connection SELECT command to a connection parameter, which is more reliable for cluster mode and handles reconnections automatically. The approach works with servers that support multi-database cluster configurations (like Valkey 9.0+). * add valkey9 constraint in the core tests * fix core test not skipping test when version was lower than valkey 9 * Fix version check * refactored test test_set_database_id_after_reconnection to be similar from standalone removed is_valkey_9_or_higher * removed tests and cleanup * renamed builder.database to builder.database_id * python: Add multi-database support for cluster and standalone modes - Add database_id parameter to BaseClientConfiguration, GlideClientConfiguration, and GlideClusterClientConfiguration - Implement SELECT command for both cluster and standalone modes with comprehensive documentation - Add validation for database_id parameter (0-15 range, integer type) - Refactor configuration protobuf request creation with helper methods for better maintainability - Add extensive test coverage for database_id configuration and validation - Include production warnings and recommended approaches in SELECT command documentation - Support routing configuration for cluster mode SELECT operations - Ensure database_id persists across reconnections when set in client configuration Documentation: - Added comprehensive docstrings with warnings about SELECT command limitations - Included examples showing recommended database_id configuration approach - Documented reconnection behavior and cluster-specific routing requirements * fixing database_id restriction * fixed test test_standalone_client_with_very_high_database_ids * fix python tests with higher databases id * fix lint error * refactoring out the route option from the select command * Remove SELECT command from cluster mode and enable cluster tests - Remove select() method from ClusterCommands in both async and sync clients - Enable cluster mode testing for database_id tests (parametrize True, False) - Delete database_id integration test file The SELECT command is not recommended for cluster mode due to reconnection behavior where nodes revert to configured database_id, not the selected one. * removed database_id validation| renamed tests to be cluster/standalone agnostic clean up comments * added changelog * lint fix * fix test database_id should be skipped for versions lower than 9.0.0 * fix test * added select rounting refactored extract_client_id to be in utilities * fix database_id documentation added custom command test using select --------- Signed-off-by: affonsov <67347924+affonsov@users.noreply.github.com> --- CHANGELOG.md | 1 + glide-core/redis-rs/redis/src/cluster.rs | 2 +- .../redis-rs/redis/src/cluster_client.rs | 12 +++ .../redis-rs/redis/src/cluster_routing.rs | 5 +- glide-core/src/client/mod.rs | 2 +- glide-core/tests/test_cluster_client.rs | 102 ++++++++++++++++++ glide-core/tests/test_standalone_client.rs | 8 -- glide-core/tests/utilities/mod.rs | 82 ++++++++++++++ .../async_commands/standalone_commands.py | 22 ++++ python/glide-shared/glide_shared/config.py | 82 +++++++++----- python/glide-sync/glide_sync/config.py | 6 +- .../sync_commands/standalone_commands.py | 22 ++++ python/tests/async_tests/test_async_client.py | 32 +++++- python/tests/sync_tests/test_sync_client.py | 32 +++++- python/tests/test_config.py | 82 ++++++++++++++ python/tests/utils/utils.py | 4 +- utils/cluster_manager.py | 3 + 17 files changed, 453 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index afb8a011c2..d2df163cc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * 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)) +* 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/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/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-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/standalone_commands.py b/python/glide-sync/glide_sync/sync_commands/standalone_commands.py index 1f3643850b..5514b5dc29 100644 --- a/python/glide-sync/glide_sync/sync_commands/standalone_commands.py +++ b/python/glide-sync/glide_sync/sync_commands/standalone_commands.py @@ -165,6 +165,28 @@ 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 = 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/tests/async_tests/test_async_client.py b/python/tests/async_tests/test_async_client.py index 9d9354702d..492b180a64 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): diff --git a/python/tests/sync_tests/test_sync_client.py b/python/tests/sync_tests/test_sync_client.py index 77328cd72a..69c0888a12 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): 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..df0a92ca44 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, # Add database_id parameter 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, # Add database_id parameter 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( From fe35eeb772322a04630d152cf0800f8de29c6cd2 Mon Sep 17 00:00:00 2001 From: affonsov <67347924+affonsov@users.noreply.github.com> Date: Mon, 8 Sep 2025 16:23:01 -0700 Subject: [PATCH 04/22] [Backport 2.1] Go: Add Multi-Database Support for Cluster Mode Valkey 9.0 (#4696) Go: Add Multi-Database Support for Cluster Mode Valkey 9.0 (#4660) * - Implement database selection for cluster clients when database_id != 0 - Send SELECT command to all cluster nodes using MultiNode routing - Add comprehensive error handling with clear error messages - Include test for database selection error handling in cluster mode - Support backward compatibility with servers that don't support multi-db cluster mode - Enable cluster-databases=16 for Valkey 9.0+ in cluster manager This enables cluster clients to work with non-default databases on Valkey 9.0+ servers configured with cluster-databases support, while gracefully handling older servers that don't support this feature. * update valkey9 multi db tests * fix lint * fixing valkey 9 cluster tests * add to route to all nodes in the core * rust lint fix * implement database selection in cluster mode via connection parameters - Add database_id field to ClusterParams and BuilderParams structs - Add database() method to ClusterClientBuilder for setting database ID - Pass database_id through connection info instead of post-connection SELECT - Remove SELECT command from cluster routing - Remove post-connection SELECT command logic from create_cluster_client - Add comprehensive tests for database isolation and reconnection behavior - Verify database ID persistence across node reconnections - Test multi-database cluster support with proper isolation verification This change moves database selection from a post-connection SELECT command to a connection parameter, which is more reliable for cluster mode and handles reconnections automatically. The approach works with servers that support multi-database cluster configurations (like Valkey 9.0+). * add valkey9 constraint in the core tests * fix core test not skipping test when version was lower than valkey 9 * Fix version check * refactored test test_set_database_id_after_reconnection to be similar from standalone removed is_valkey_9_or_higher * removed tests and cleanup * renamed builder.database to builder.database_id * go: Add multi-database support for cluster mode clients - Add DatabaseId field to baseClientConfiguration for both standalone and cluster clients - Implement WithDatabaseId() method for ClusterClientConfiguration - Add database ID validation with proper error handling for negative values - Refactor standalone client to use shared database ID logic from base configuration - Add Select() and SelectWithOptions() methods for cluster clients with routing support - Include comprehensive test coverage for database isolation, reconnection persistence, and error handling - Add backward compatibility support - clients default to database 0 when no database_id specified - Add server version compatibility checks (cluster multi-database requires Valkey 9.0+) - Update documentation with warnings about SELECT command limitations and recommended configuration approach This enables cluster mode clients to connect to specific databases at initialization time, with the database selection persisting across reconnections, unlike the SELECT command which resets on reconnection. * fixed go tests - batch tests - database id tests * removed selectWithOptions * go: Remove Select command from cluster clients - Remove Select method from ClusterClient and related interfaces - Delete comprehensive database_id integration tests - Remove Select command from batch operations - Remove Select examples and tests - Add example for cluster client with database_id configuration The Select command is being removed in favor of using the database_id configuration parameter, which provides persistent database selection across reconnections. * remove config validation simplified tests * added changelog * removing batch test, and readding standalone select and removing batch select --------- Signed-off-by: affonsov <67347924+affonsov@users.noreply.github.com> --- CHANGELOG.md | 1 + go/config/config.go | 17 ++++++++++---- go/config/config_test.go | 51 ++++++++++++++++++++++++++++++++++++++++ go/create_client_test.go | 24 +++++++++++++++++++ go/glide_client.go | 14 +++++++++++ 5 files changed, 102 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2df163cc9..5ae930e043 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * 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)) +* 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/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: From 8342c16f08ba4b9d8a07f7993ef5c55a468ffd3b Mon Sep 17 00:00:00 2001 From: affonsov <67347924+affonsov@users.noreply.github.com> Date: Mon, 8 Sep 2025 16:46:14 -0700 Subject: [PATCH 05/22] [Backport 2.1] Java: Multi-Database Support for Cluster Mode Valkey 9.0 (#4697) Java: Multi-Database Support for Cluster Mode Valkey 9.0 (#4658) * - Implement database selection for cluster clients when database_id != 0 - Send SELECT command to all cluster nodes using MultiNode routing - Add comprehensive error handling with clear error messages - Include test for database selection error handling in cluster mode - Support backward compatibility with servers that don't support multi-db cluster mode - Enable cluster-databases=16 for Valkey 9.0+ in cluster manager This enables cluster clients to work with non-default databases on Valkey 9.0+ servers configured with cluster-databases support, while gracefully handling older servers that don't support this feature. * update valkey9 multi db tests * fix lint * fixing valkey 9 cluster tests * add to route to all nodes in the core * rust lint fix * implement database selection in cluster mode via connection parameters - Add database_id field to ClusterParams and BuilderParams structs - Add database() method to ClusterClientBuilder for setting database ID - Pass database_id through connection info instead of post-connection SELECT - Remove SELECT command from cluster routing - Remove post-connection SELECT command logic from create_cluster_client - Add comprehensive tests for database isolation and reconnection behavior - Verify database ID persistence across node reconnections - Test multi-database cluster support with proper isolation verification This change moves database selection from a post-connection SELECT command to a connection parameter, which is more reliable for cluster mode and handles reconnections automatically. The approach works with servers that support multi-database cluster configurations (like Valkey 9.0+). * add valkey9 constraint in the core tests * fix core test not skipping test when version was lower than valkey 9 * Fix version check * refactored test test_set_database_id_after_reconnection to be similar from standalone removed is_valkey_9_or_higher * removed tests and cleanup * renamed builder.database to builder.database_id * Java: implement multi-database support for cluster mode - Move databaseId from GlideClientConfiguration to BaseClientConfiguration - Add databaseId validation and protobuf integration - Update SELECT command with AllNodes routing and reconnection warnings - Add connection management for database selection after auth - Add comprehensive tests with Valkey 9+ version guards Completes Java client implementation for Valkey 9 cluster multi-database support. * fixing database id restriction * fixed java test error select_command_invalid_database_standalone * fixed lint errors * Refactored select command to route to all nodes per default Removed the option to provide a route * java lint fix * java: Remove SELECT command from cluster client - Remove select() method from GlideClusterClient and ConnectionManagementClusterCommands interface - Delete MultiDatabaseTests.java which tested SELECT command functionality - Remove SELECT command tests from SharedCommandTests.java - Add focused test for cluster database_id configuration in ClusterClientTests - Remove SELECT import from GlideClusterClient The SELECT command is being removed from the cluster client API as database selection should be handled through client configuration (databaseId parameter) rather than runtime commands, which provides better consistency and avoids reconnection issues. * remove database_id validatation and simplified tests * added changelog * fix documentation * fix databaseId documentation --------- Signed-off-by: affonsov <67347924+affonsov@users.noreply.github.com> --- CHANGELOG.md | 1 + .../ConnectionManagementCommands.java | 17 ++++++ .../BaseClientConfiguration.java | 6 ++ .../GlideClientConfiguration.java | 3 - .../glide/managers/ConnectionManager.java | 8 +-- .../BaseClientConfigurationTest.java | 59 +++++++++++++++++++ .../glide/cluster/ClusterClientTests.java | 18 ++++++ 7 files changed, 105 insertions(+), 7 deletions(-) create mode 100644 java/client/src/test/java/glide/api/models/configuration/BaseClientConfigurationTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ae930e043..abbf89bfd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * 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)) +* 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)) 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/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/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() { From 62dac03649458e5c5ed1af88820d0631800cee78 Mon Sep 17 00:00:00 2001 From: affonsov <67347924+affonsov@users.noreply.github.com> Date: Mon, 8 Sep 2025 17:42:11 -0700 Subject: [PATCH 06/22] [Backport 2.1] Node: Add Multi-Database Support for Cluster Mode (Valkey 9.0) (#4698) Node: Add Multi-Database Support for Cluster Mode (Valkey 9.0) (#4657) * - Implement database selection for cluster clients when database_id != 0 - Send SELECT command to all cluster nodes using MultiNode routing - Add comprehensive error handling with clear error messages - Include test for database selection error handling in cluster mode - Support backward compatibility with servers that don't support multi-db cluster mode - Enable cluster-databases=16 for Valkey 9.0+ in cluster manager This enables cluster clients to work with non-default databases on Valkey 9.0+ servers configured with cluster-databases support, while gracefully handling older servers that don't support this feature. * update valkey9 multi db tests * fix lint * fixing valkey 9 cluster tests * add to route to all nodes in the core * rust lint fix * implement database selection in cluster mode via connection parameters - Add database_id field to ClusterParams and BuilderParams structs - Add database() method to ClusterClientBuilder for setting database ID - Pass database_id through connection info instead of post-connection SELECT - Remove SELECT command from cluster routing - Remove post-connection SELECT command logic from create_cluster_client - Add comprehensive tests for database isolation and reconnection behavior - Verify database ID persistence across node reconnections - Test multi-database cluster support with proper isolation verification This change moves database selection from a post-connection SELECT command to a connection parameter, which is more reliable for cluster mode and handles reconnections automatically. The approach works with servers that support multi-database cluster configurations (like Valkey 9.0+). * add valkey9 constraint in the core tests * fix core test not skipping test when version was lower than valkey 9 * Fix version check * refactored test test_set_database_id_after_reconnection to be similar from standalone removed is_valkey_9_or_higher * removed tests and cleanup * renamed builder.database to builder.database_id * node changes * move tests to sharedtests.ts * reafactoring to select to all nodes being in the core * node: Remove SELECT command support from cluster client - Remove select() method from GlideClusterClient - Remove createSelect import from cluster client - Remove extensive test coverage for SELECT functionality - Remove database ID validation tests from client internals - Add minimal database ID test for cluster client configuration - Clean up ConfigurationError import that's no longer needed This change removes the SELECT command implementation that was added for Valkey 9.0+ cluster support, likely due to reliability concerns with database switching in cluster mode or to simplify the API. * added changelog * fix test, and removed comment --------- Signed-off-by: affonsov <67347924+affonsov@users.noreply.github.com> --- CHANGELOG.md | 1 + node/src/BaseClient.ts | 40 +++++++++++++++++++++++++-- node/src/Batch.ts | 8 ++++++ node/src/GlideClient.ts | 29 +++++++++++++------ node/src/GlideClusterClient.ts | 11 ++++++-- node/tests/GlideClusterClient.test.ts | 22 +++++++++++++++ 6 files changed, 97 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index abbf89bfd1..af88184404 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * 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)) 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, + ); }); From fcb569e11ad548e7ea07c9f906d75123dbb90551 Mon Sep 17 00:00:00 2001 From: Lior Sventitzky Date: Wed, 10 Sep 2025 15:27:57 +0300 Subject: [PATCH 07/22] [Backport 2.1] Python sync: removed select command (#4705) Python sync: removed select command (#4684) removed sync select command Signed-off-by: Lior Sventitzky --- .../sync_commands/standalone_commands.py | 36 ------------ python/tests/sync_tests/test_sync_client.py | 57 +++++++------------ python/tests/test_api_consistency.py | 2 + python/tests/utils/utils.py | 4 +- 4 files changed, 26 insertions(+), 73 deletions(-) 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 5514b5dc29..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,42 +161,6 @@ def exec( timeout=timeout, ) - 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 = 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: - 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/tests/sync_tests/test_sync_client.py b/python/tests/sync_tests/test_sync_client.py index 69c0888a12..772d7398e2 100644 --- a/python/tests/sync_tests/test_sync_client.py +++ b/python/tests/sync_tests/test_sync_client.py @@ -613,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 @@ -640,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): @@ -652,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() @@ -660,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]) @@ -5210,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]) @@ -8996,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 @@ -9010,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 @@ -9096,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 ( @@ -9115,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 @@ -9133,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]) @@ -9240,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 @@ -9253,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..3bea180a71 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,7 @@ "async_only": [ "test_inflight_request_limit", "test_statistics", + "test_select", ], "sync_only": ["test_sync_fork"], } diff --git a/python/tests/utils/utils.py b/python/tests/utils/utils.py index df0a92ca44..eda894ccbe 100644 --- a/python/tests/utils/utils.py +++ b/python/tests/utils/utils.py @@ -558,7 +558,7 @@ def create_client_config( addresses=seed_nodes if addresses is None else addresses, use_tls=use_tls, credentials=credentials, - database_id=database_id, # Add database_id parameter + database_id=database_id, client_name=client_name, protocol=protocol, request_timeout=timeout, @@ -626,7 +626,7 @@ def create_sync_client_config( addresses=seed_nodes if addresses is None else addresses, use_tls=use_tls, credentials=credentials, - database_id=database_id, # Add database_id parameter + database_id=database_id, client_name=client_name, protocol=protocol, request_timeout=timeout, From 31990dea116550e7169a3f62c901a1bbe394ce8e Mon Sep 17 00:00:00 2001 From: Lior Sventitzky Date: Wed, 10 Sep 2025 15:28:22 +0300 Subject: [PATCH 08/22] [Backport 2.1] CI/CD: Added self hosted macOS ARM runners (#4706) * CI: Added self hosted macOS runners (#4683) added self hosted runners for macos Signed-off-by: Lior Sventitzky * CD: removed self hosted mac arm runners from required CD platforms #4700 (#4702) removed self hosted mac from cd, reverted npm-cd Signed-off-by: Lior Sventitzky --------- Signed-off-by: Lior Sventitzky --- .github/json_matrices/build-matrix.json | 8 ++++++++ .../workflows/create-test-matrices/action.yml | 18 ++++++++++++------ .github/workflows/full-matrix-tests.yml | 8 ++++++-- .github/workflows/go.yml | 12 ++++++++---- .github/workflows/java.yml | 10 +++++++--- .github/workflows/node.yml | 10 +++++++--- .github/workflows/npm-cd.yml | 4 ++-- .github/workflows/python.yml | 12 ++++++++---- .github/workflows/rust.yml | 10 +++++++--- 9 files changed, 65 insertions(+), 27 deletions(-) diff --git a/.github/json_matrices/build-matrix.json b/.github/json_matrices/build-matrix.json index 5383db24e5..ad338e545d 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", 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.yml b/.github/workflows/go.yml index ae69a8f2cd..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 }} diff --git a/.github/workflows/java.yml b/.github/workflows/java.yml index eb1c4e63b4..41df841862 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 }} 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..5be343b73d 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 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 }} From 236d9677e1daa49e50cc8712c9e587acd700a320 Mon Sep 17 00:00:00 2001 From: Lior Sventitzky Date: Wed, 10 Sep 2025 16:39:03 +0300 Subject: [PATCH 09/22] [backport 2.1] Python: fixed pypi-cd workflow, fixed release tests (#4708) Python: fixed pypi-cd workflow, fixed release tests (#4703) fixed pypi upload, fixed release tests Signed-off-by: Lior Sventitzky --- .github/workflows/pypi-cd.yml | 4 +- .../python/async_rc_test.py | 33 +++++++++++---- .../python/sync_rc_test.py | 35 ++++++++++++---- .../release-candidate-testing/python/utils.py | 40 ++----------------- 4 files changed, 59 insertions(+), 53 deletions(-) diff --git a/.github/workflows/pypi-cd.yml b/.github/workflows/pypi-cd.yml index 7a3c8f135b..9561f500e2 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: @@ -422,6 +422,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 +460,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/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) From 7ec113192925ff504ae240cd195c4b819cca2c8e Mon Sep 17 00:00:00 2001 From: Jeremy Parr-Pearson Date: Wed, 10 Sep 2025 09:56:59 -0700 Subject: [PATCH 10/22] Fix FFI tests for non-alpine distros (#4711) Signed-off-by: Jeremy Parr-Pearson --- java/client/build.gradle | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/java/client/build.gradle b/java/client/build.gradle index b9888fff86..b2ed85dfc7 100644 --- a/java/client/build.gradle +++ b/java/client/build.gradle @@ -189,6 +189,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 +297,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" From 5d2de8d1613a3ed5cf277f2577437954f375d8b6 Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Wed, 10 Sep 2025 10:33:02 -0700 Subject: [PATCH 11/22] MUSL Java Build Fixes (#4712) * Use zigbuild for easier cross-compilation Signed-off-by: Alex Rehnby-Martin * Fix Signed-off-by: Alex Rehnby-Martin * Use zigbuild for easier cross-compilation Signed-off-by: Alex Rehnby-Martin * Use zigbuild for easier cross-compilation Signed-off-by: Alex Rehnby-Martin * Fix musl classifier / detection Signed-off-by: Alex Rehnby-Martin * Fix musl classifier / detection Signed-off-by: Alex Rehnby-Martin * Fix musl classifier / detection Signed-off-by: Alex Rehnby-Martin * Update docs Signed-off-by: Alex Rehnby-Martin * Exclude arm musl test Signed-off-by: Alex Rehnby-Martin * Update arm runner Signed-off-by: Alex Rehnby-Martin * Update go exclusion Signed-off-by: Alex Rehnby-Martin * Format Signed-off-by: Alex Rehnby-Martin --------- Signed-off-by: Alex Rehnby-Martin --- .github/json_matrices/build-matrix.json | 2 +- .github/workflows/go-cd.yml | 6 ++++ .github/workflows/java-cd.yml | 3 ++ .github/workflows/java.yml | 5 ++++ java/README.md | 38 +++++++++++++++++++++++-- java/benchmarks/build.gradle | 10 ++++++- java/client/build.gradle | 8 +++--- 7 files changed, 64 insertions(+), 8 deletions(-) diff --git a/.github/json_matrices/build-matrix.json b/.github/json_matrices/build-matrix.json index ad338e545d..59fe5dcab5 100644 --- a/.github/json_matrices/build-matrix.json +++ b/.github/json_matrices/build-matrix.json @@ -53,7 +53,7 @@ "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", diff --git a/.github/workflows/go-cd.yml b/.github/workflows/go-cd.yml index acf891f1b5..ae98888c7c 100644 --- a/.github/workflows/go-cd.yml +++ b/.github/workflows/go-cd.yml @@ -191,6 +191,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/java-cd.yml b/.github/workflows/java-cd.yml index 5dc9661bd3..81228d3457 100644 --- a/.github/workflows/java-cd.yml +++ b/.github/workflows/java-cd.yml @@ -253,6 +253,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 41df841862..a9cc7706b3 100644 --- a/.github/workflows/java.yml +++ b/.github/workflows/java.yml @@ -331,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/java/README.md b/java/README.md index 62427fddd4..42aa89ad99 100644 --- a/java/README.md +++ b/java/README.md @@ -57,12 +57,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 +91,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 +146,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 +195,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 b2ed85dfc7..58ae266d98 100644 --- a/java/client/build.gradle +++ b/java/client/build.gradle @@ -119,7 +119,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 +147,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' @@ -313,7 +313,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 } @@ -369,7 +369,7 @@ 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 } From 60a3b36aad2b97d15b44f42953e997ffbb5a298f Mon Sep 17 00:00:00 2001 From: Alex Rehnby-Martin Date: Wed, 10 Sep 2025 12:09:28 -0700 Subject: [PATCH 12/22] Revert go upload step change (#4716) Signed-off-by: Alex Rehnby-Martin --- .github/workflows/go-cd.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/go-cd.yml b/.github/workflows/go-cd.yml index ae98888c7c..a2dd56d1a6 100644 --- a/.github/workflows/go-cd.yml +++ b/.github/workflows/go-cd.yml @@ -130,15 +130,11 @@ jobs: run: | mkdir -p $GITHUB_WORKSPACE/ffi/target/release cp ffi/target/*/release/libglide_ffi.a $GITHUB_WORKSPACE/ffi/target/release/ - - name: Set artifact name - id: set-artifact-name - run: | - echo "ARTIFACT_NAME=${{ matrix.host.TARGET }}" >> $GITHUB_OUTPUT - name: Upload artifacts continue-on-error: true uses: actions/upload-artifact@v4 with: - name: ${{ steps.set-artifact-name.outputs.artifact-name }} + name: ${{ matrix.host.TARGET }} path: | ffi/target/release/libglide_ffi.a go/lib.h From a6c64bb42578008fa08e762893b406c9712ce4dd Mon Sep 17 00:00:00 2001 From: James Xin Date: Wed, 10 Sep 2025 13:19:59 -0700 Subject: [PATCH 13/22] CD: Fix Node CD delete artifact issue (#4717) Signed-off-by: James Xin --- .github/workflows/npm-cd.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/npm-cd.yml b/.github/workflows/npm-cd.yml index 5be343b73d..4ed30046bf 100644 --- a/.github/workflows/npm-cd.yml +++ b/.github/workflows/npm-cd.yml @@ -447,7 +447,6 @@ jobs: uses: geekyeggo/delete-artifact@v2 with: name: | - npm-packages-${{ github.run_id }} artifacts-* js-files-${{ github.run_id }} @@ -550,7 +549,6 @@ jobs: uses: geekyeggo/delete-artifact@v2 with: name: | - npm-packages-${{ github.run_id }} bindings-* js-files-${{ github.run_id }} From 2c009b1710b389528bec87a2e4f7683daa0a08ff Mon Sep 17 00:00:00 2001 From: James Xin Date: Wed, 10 Sep 2025 15:06:12 -0700 Subject: [PATCH 14/22] CD: Fix Go musl CD issue (#4719) Signed-off-by: James Xin --- .github/workflows/go-cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/go-cd.yml b/.github/workflows/go-cd.yml index a2dd56d1a6..4c6c8d81a7 100644 --- a/.github/workflows/go-cd.yml +++ b/.github/workflows/go-cd.yml @@ -126,7 +126,7 @@ jobs: make install-build-tools 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/ From 33538baccf56ff4c142fce2f30a1d46ad0ec84be Mon Sep 17 00:00:00 2001 From: James Xin Date: Wed, 10 Sep 2025 15:41:21 -0700 Subject: [PATCH 15/22] CD: Fix Node CD delete artifact issue 2nd try (#4718) * CD: Fix Node CD delete artifact issue 2nd try Signed-off-by: James Xin * Linter Signed-off-by: James Xin * remove temporary testing Signed-off-by: James Xin --------- Signed-off-by: James Xin --- .github/workflows/npm-cd.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/npm-cd.yml b/.github/workflows/npm-cd.yml index 4ed30046bf..f55787c32b 100644 --- a/.github/workflows/npm-cd.yml +++ b/.github/workflows/npm-cd.yml @@ -449,6 +449,7 @@ jobs: name: | artifacts-* js-files-${{ github.run_id }} + failOnError: false test-published-release: needs: [get-build-parameters, prepare-and-publish] @@ -551,6 +552,7 @@ jobs: name: | bindings-* js-files-${{ github.run_id }} + failOnError: false deprecate-on-failure: needs: [test-published-release, get-build-parameters] From 24fed43107a82efb07890e39b8d22265a96355b1 Mon Sep 17 00:00:00 2001 From: affonsov <67347924+affonsov@users.noreply.github.com> Date: Wed, 10 Sep 2025 15:57:13 -0700 Subject: [PATCH 16/22] Python: Update ExpiryType enum references in hash field expiration docs (#4715) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit python: Update ExpiryType enum references in hash field expiration docs Replace deprecated ExpiryType enum values with new names in docstrings: - EX → SEC - PX → MILLSEC - EXAT → UNIX_SEC - PXAT → UNIX_MILLSEC - KEEPTTL → KEEP_TTL Updates documentation for hash field expiration commands in both async and sync clients. Signed-off-by: affonsov <67347924+affonsov@users.noreply.github.com> --- .../python/glide/async_commands/core.py | 42 +++++++++---------- .../glide_sync/sync_commands/core.py | 42 +++++++++---------- 2 files changed, 42 insertions(+), 42 deletions(-) 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-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 From a1ff1953ab25c390643037662660cf2143a9ef88 Mon Sep 17 00:00:00 2001 From: Lior Sventitzky Date: Thu, 11 Sep 2025 15:58:22 +0300 Subject: [PATCH 17/22] [Backport 2.1] Python sync/CD: added description to pypi package of the sync client (#4720) Python sync/CD: added description to pypi package of the sync client (#4714) added description to sync client Signed-off-by: Lior Sventitzky --- .github/workflows/pypi-cd.yml | 40 ++++++++++++++++++++++++++++++-- python/glide-sync/pyproject.toml | 5 ++-- python/glide-sync/setup.py | 5 ++++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pypi-cd.yml b/.github/workflows/pypi-cd.yml index 9561f500e2..e43a708999 100644 --- a/.github/workflows/pypi-cd.yml +++ b/.github/workflows/pypi-cd.yml @@ -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 diff --git a/python/glide-sync/pyproject.toml b/python/glide-sync/pyproject.toml index 06bd0e0143..f626548207 100644 --- a/python/glide-sync/pyproject.toml +++ b/python/glide-sync/pyproject.toml @@ -1,7 +1,8 @@ [project] -dynamic = ["version"] +dynamic = ["version", "readme"] name = "valkey-glide-sync" -license = { text = "Apache-2.0" } +description = "Valkey GLIDE Sync client. Supports Valkey and Redis OSS." +license = "Apache-2.0" dependencies = [ # ⚠️ Note: If you add a dependency here, make sure to also add it to glide-sync/requirements.txt # Once issue https://github.com/aboutcode-org/python-inspector/issues/197 is resolved, the requirements.txt file can be removed. 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", ) From f0c981865ad6e759645320a3e7102a55f6d1eca5 Mon Sep 17 00:00:00 2001 From: prateek-kumar-improving Date: Thu, 11 Sep 2025 07:31:26 -0700 Subject: [PATCH 18/22] Java: Compatibility layer module (#4692) * Java: Compatibility layer module Signed-off-by: Prateek Kumar * Java: fix integration tests Signed-off-by: Prateek Kumar * Java: Fix codeql for java code Signed-off-by: Prateek Kumar * Java: Fix codeql java build Signed-off-by: Prateek Kumar * Java: Fix codeql java build Signed-off-by: Prateek Kumar * Java: Fix codeql java build Signed-off-by: Prateek Kumar * Java: Fix codeql java build Signed-off-by: Prateek Kumar * Java: Fix codeql java build Signed-off-by: Prateek Kumar * Java: Fix codeql java build Signed-off-by: Prateek Kumar * Java: Fix codeql java build Signed-off-by: Prateek Kumar * Java: Update java cd Signed-off-by: Prateek Kumar * Java: lint issues fixed Signed-off-by: Prateek Kumar * Java: Add signing configuration to jedis-compatibility build.gradle Signed-off-by: Prateek Kumar * Java: update dependency Signed-off-by: Prateek Kumar * Java: Update valkey-glide-jedis-compatibility file path Signed-off-by: Prateek Kumar * Java: Fix build.gradle publishing code Signed-off-by: Prateek Kumar --------- Signed-off-by: Prateek Kumar --- .github/workflows/java-cd.yml | 24 ++- java/README.md | 5 +- java/client/build.gradle | 4 - java/client/src/main/java/module-info.java | 12 -- java/integTest/build.gradle | 17 +- java/integTest/src/test/java/module-info.java | 17 -- java/jedis-compatibility/README.md | 94 ++++++++++ java/jedis-compatibility/build.gradle | 171 ++++++++++++++++++ .../compatibility-layer-migration-guide.md | 0 .../java/redis/clients/jedis/Builder.java | 0 .../redis/clients/jedis/BuilderFactory.java | 0 .../clients/jedis/ClientSetInfoConfig.java | 0 .../jedis/ClusterConfigurationMapper.java | 0 .../jedis/ClusterConnectionProvider.java | 0 .../clients/jedis/ConfigurationMapper.java | 0 .../java/redis/clients/jedis/Connection.java | 0 .../redis/clients/jedis/ConnectionPool.java | 0 .../clients/jedis/ConnectionPoolConfig.java | 0 .../clients/jedis/ConnectionProvider.java | 0 .../jedis/DefaultJedisClientConfig.java | 0 .../jedis/DefaultRedisCredentials.java | 0 .../DefaultRedisCredentialsProvider.java | 0 .../redis/clients/jedis/GeoCoordinate.java | 0 .../clients/jedis/GlideJedisFactory.java | 0 .../java/redis/clients/jedis/HostAndPort.java | 0 .../clients/jedis/HostAndPortMapper.java | 0 .../main/java/redis/clients/jedis/Jedis.java | 0 .../clients/jedis/JedisClientConfig.java | 0 .../redis/clients/jedis/JedisCluster.java | 0 .../clients/jedis/JedisClusterInfoCache.java | 0 .../java/redis/clients/jedis/JedisPool.java | 0 .../java/redis/clients/jedis/JedisPooled.java | 0 .../main/java/redis/clients/jedis/Module.java | 0 .../java/redis/clients/jedis/Protocol.java | 0 .../redis/clients/jedis/RedisCredentials.java | 0 .../jedis/RedisCredentialsProvider.java | 0 .../redis/clients/jedis/RedisProtocol.java | 0 .../jedis/ResourceLifecycleManager.java | 0 .../clients/jedis/SSLParametersUtils.java | 0 .../jedis/SingleConnectionPoolConfig.java | 0 .../java/redis/clients/jedis/SslOptions.java | 0 .../redis/clients/jedis/SslVerifyMode.java | 0 .../redis/clients/jedis/StreamEntryID.java | 0 .../redis/clients/jedis/UnifiedJedis.java | 0 .../clients/jedis/args/BitCountOption.java | 0 .../java/redis/clients/jedis/args/BitOP.java | 0 .../clients/jedis/args/ExpiryOption.java | 0 .../clients/jedis/args/ListDirection.java | 0 .../clients/jedis/args/ListPosition.java | 0 .../redis/clients/jedis/args/Rawable.java | 0 .../jedis/authentication/AuthXManager.java | 0 .../jedis/bloom/RedisBloomProtocol.java | 0 .../jedis/commands/ProtocolCommand.java | 0 .../exceptions/JedisConnectionException.java | 0 .../jedis/exceptions/JedisDataException.java | 0 .../jedis/exceptions/JedisException.java | 0 .../exceptions/JedisValidationException.java | 0 .../jedis/json/DefaultGsonObjectMapper.java | 0 .../clients/jedis/json/JsonObjectMapper.java | 0 .../clients/jedis/json/JsonProtocol.java | 0 .../clients/jedis/params/BitPosParams.java | 0 .../clients/jedis/params/GetExParams.java | 0 .../clients/jedis/params/HGetExParams.java | 0 .../clients/jedis/params/HSetExParams.java | 0 .../clients/jedis/params/LPosParams.java | 0 .../clients/jedis/params/MigrateParams.java | 0 .../clients/jedis/params/RestoreParams.java | 0 .../clients/jedis/params/ScanParams.java | 0 .../redis/clients/jedis/params/SetParams.java | 0 .../clients/jedis/params/SortingParams.java | 0 .../jedis/resps/AccessControlLogEntry.java | 0 .../jedis/resps/AccessControlUser.java | 0 .../clients/jedis/resps/CommandDocument.java | 0 .../clients/jedis/resps/CommandInfo.java | 0 .../clients/jedis/resps/FunctionStats.java | 0 .../jedis/resps/GeoRadiusResponse.java | 0 .../redis/clients/jedis/resps/KeyValue.java | 0 .../clients/jedis/resps/KeyedListElement.java | 0 .../clients/jedis/resps/KeyedZSetElement.java | 0 .../clients/jedis/resps/LCSMatchResult.java | 0 .../clients/jedis/resps/LibraryInfo.java | 0 .../redis/clients/jedis/resps/ScanResult.java | 0 .../redis/clients/jedis/resps/Slowlog.java | 0 .../jedis/resps/StreamConsumerFullInfo.java | 0 .../jedis/resps/StreamConsumerInfo.java | 0 .../jedis/resps/StreamConsumersInfo.java | 0 .../clients/jedis/resps/StreamEntry.java | 0 .../clients/jedis/resps/StreamFullInfo.java | 0 .../jedis/resps/StreamGroupFullInfo.java | 0 .../clients/jedis/resps/StreamGroupInfo.java | 0 .../redis/clients/jedis/resps/StreamInfo.java | 0 .../jedis/resps/StreamPendingEntry.java | 0 .../jedis/resps/StreamPendingSummary.java | 0 .../java/redis/clients/jedis/resps/Tuple.java | 0 .../redis/clients/jedis/search/Document.java | 0 .../jedis/search/SearchBuilderFactory.java | 0 .../clients/jedis/search/SearchProtocol.java | 0 .../clients/jedis/search/SearchResult.java | 0 .../jedis/search/aggr/AggregationResult.java | 0 .../redis/clients/jedis/search/aggr/Row.java | 0 .../jedis/timeseries/AggregationType.java | 0 .../jedis/timeseries/DuplicatePolicy.java | 0 .../clients/jedis/timeseries/TSElement.java | 0 .../clients/jedis/timeseries/TSInfo.java | 0 .../clients/jedis/timeseries/TSKeyValue.java | 0 .../jedis/timeseries/TSKeyedElements.java | 0 .../timeseries/TimeSeriesBuilderFactory.java | 0 .../jedis/timeseries/TimeSeriesProtocol.java | 0 .../clients/jedis/util/DoublePrecision.java | 0 .../jedis/util/JedisClusterHashTag.java | 0 .../clients/jedis/util/JedisURIHelper.java | 0 .../redis/clients/jedis/util/KeyValue.java | 0 .../java/redis/clients/jedis/util/Pool.java | 0 .../redis/clients/jedis/util/SafeEncoder.java | 0 .../clients/jedis/BasicCompatibilityTest.java | 0 .../clients/jedis/ConfigurationTest.java | 0 .../clients/jedis/JedisCompatibilityTest.java | 40 ++++ .../redis/clients/jedis/JedisMethodsTest.java | 0 .../redis/clients/jedis/JedisWrapperTest.java | 0 .../redis/clients/jedis/PoolImportTest.java | 0 java/settings.gradle | 1 + 121 files changed, 345 insertions(+), 40 deletions(-) delete mode 100644 java/integTest/src/test/java/module-info.java create mode 100644 java/jedis-compatibility/README.md create mode 100644 java/jedis-compatibility/build.gradle rename java/{client/src/main/java/redis/clients/jedis => jedis-compatibility}/compatibility-layer-migration-guide.md (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/Builder.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/BuilderFactory.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/ClientSetInfoConfig.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/ClusterConfigurationMapper.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/ClusterConnectionProvider.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/ConfigurationMapper.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/Connection.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/ConnectionPool.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/ConnectionPoolConfig.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/ConnectionProvider.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/DefaultRedisCredentials.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/DefaultRedisCredentialsProvider.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/GeoCoordinate.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/GlideJedisFactory.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/HostAndPort.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/HostAndPortMapper.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/Jedis.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/JedisClientConfig.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/JedisCluster.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/JedisClusterInfoCache.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/JedisPool.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/JedisPooled.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/Module.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/Protocol.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/RedisCredentials.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/RedisCredentialsProvider.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/RedisProtocol.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/ResourceLifecycleManager.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/SSLParametersUtils.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/SingleConnectionPoolConfig.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/SslOptions.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/SslVerifyMode.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/StreamEntryID.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/UnifiedJedis.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/args/BitCountOption.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/args/BitOP.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/args/ExpiryOption.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/args/ListDirection.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/args/ListPosition.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/args/Rawable.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/authentication/AuthXManager.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/bloom/RedisBloomProtocol.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/commands/ProtocolCommand.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/exceptions/JedisConnectionException.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/exceptions/JedisDataException.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/exceptions/JedisException.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/exceptions/JedisValidationException.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/json/DefaultGsonObjectMapper.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/json/JsonObjectMapper.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/json/JsonProtocol.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/params/BitPosParams.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/params/GetExParams.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/params/HGetExParams.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/params/HSetExParams.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/params/LPosParams.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/params/MigrateParams.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/params/RestoreParams.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/params/ScanParams.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/params/SetParams.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/params/SortingParams.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/resps/AccessControlLogEntry.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/resps/AccessControlUser.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/resps/CommandDocument.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/resps/CommandInfo.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/resps/FunctionStats.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/resps/GeoRadiusResponse.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/resps/KeyValue.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/resps/KeyedListElement.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/resps/KeyedZSetElement.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/resps/LCSMatchResult.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/resps/LibraryInfo.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/resps/ScanResult.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/resps/Slowlog.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/resps/StreamConsumerFullInfo.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/resps/StreamConsumerInfo.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/resps/StreamConsumersInfo.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/resps/StreamEntry.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/resps/StreamFullInfo.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/resps/StreamGroupFullInfo.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/resps/StreamGroupInfo.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/resps/StreamInfo.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/resps/StreamPendingEntry.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/resps/StreamPendingSummary.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/resps/Tuple.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/search/Document.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/search/SearchBuilderFactory.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/search/SearchProtocol.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/search/SearchResult.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/search/aggr/AggregationResult.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/search/aggr/Row.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/timeseries/AggregationType.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/timeseries/DuplicatePolicy.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/timeseries/TSElement.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/timeseries/TSInfo.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/timeseries/TSKeyValue.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/timeseries/TSKeyedElements.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/timeseries/TimeSeriesBuilderFactory.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/timeseries/TimeSeriesProtocol.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/util/DoublePrecision.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/util/JedisClusterHashTag.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/util/JedisURIHelper.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/util/KeyValue.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/util/Pool.java (100%) rename java/{client => jedis-compatibility}/src/main/java/redis/clients/jedis/util/SafeEncoder.java (100%) rename java/{client => jedis-compatibility}/src/test/java/redis/clients/jedis/BasicCompatibilityTest.java (100%) rename java/{client => jedis-compatibility}/src/test/java/redis/clients/jedis/ConfigurationTest.java (100%) create mode 100644 java/jedis-compatibility/src/test/java/redis/clients/jedis/JedisCompatibilityTest.java rename java/{client => jedis-compatibility}/src/test/java/redis/clients/jedis/JedisMethodsTest.java (100%) rename java/{client => jedis-compatibility}/src/test/java/redis/clients/jedis/JedisWrapperTest.java (100%) rename java/{client => jedis-compatibility}/src/test/java/redis/clients/jedis/PoolImportTest.java (100%) diff --git a/.github/workflows/java-cd.yml b/.github/workflows/java-cd.yml index 81228d3457..792faec13f 100644 --- a/.github/workflows/java-cd.yml +++ b/.github/workflows/java-cd.yml @@ -141,11 +141,11 @@ jobs: working-directory: java run: | if [[ "${{ matrix.host.TARGET }}" == *"musl"* ]]; then - ./gradlew --build-cache :client:publishToMavenLocal -Psigning.secretKeyRingFile=secring.gpg \ + ./gradlew --build-cache :client:publishToMavenLocal :jedis-compatibility:publishToMavenLocal -Psigning.secretKeyRingFile=secring.gpg \ -Psigning.password="${{ secrets.GPG_PASSWORD }}" -Psigning.keyId=${{ secrets.GPG_KEY_ID }} \ -Ptarget=${{ matrix.host.TARGET }} else - ./gradlew --build-cache :client:publishToMavenLocal -Psigning.secretKeyRingFile=secring.gpg \ + ./gradlew --build-cache :client:publishToMavenLocal :jedis-compatibility:publishToMavenLocal -Psigning.secretKeyRingFile=secring.gpg \ -Psigning.password="${{ secrets.GPG_PASSWORD }}" -Psigning.keyId=${{ secrets.GPG_KEY_ID }} fi env: @@ -154,6 +154,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 +162,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 +177,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 +206,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 diff --git a/java/README.md b/java/README.md index 42aa89ad99..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. diff --git a/java/client/build.gradle b/java/client/build.gradle index 58ae266d98..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 @@ -376,7 +373,6 @@ shadowJar { 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/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/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/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..093aa3fead --- /dev/null +++ b/java/jedis-compatibility/build.gradle @@ -0,0 +1,171 @@ +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 { + // Depend on the main GLIDE client + implementation project(':client') + + // 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') +copyNativeLib.dependsOn(':client:buildRust') +compileJava.dependsOn(':client:shadowJar', ':client:jar') +compileTestJava.dependsOn('copyNativeLib') +delombok.dependsOn('compileJava') + +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/client/src/main/java/redis/clients/jedis/compatibility-layer-migration-guide.md b/java/jedis-compatibility/compatibility-layer-migration-guide.md similarity index 100% rename from java/client/src/main/java/redis/clients/jedis/compatibility-layer-migration-guide.md rename to java/jedis-compatibility/compatibility-layer-migration-guide.md 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' From 0d60e3a8e2422884286688d0af8af1b1bc8b801f Mon Sep 17 00:00:00 2001 From: Joseph Brinkman Date: Thu, 11 Sep 2025 13:10:25 -0400 Subject: [PATCH 19/22] Fix: Copy GPG secring to jedis-compatibility in Java CD workflow (#4723) fix(java-cd): copy secring.gpg to jedis-compatibility for signing Signed-off-by: jbrinkman --- .github/workflows/java-cd.yml | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/workflows/java-cd.yml b/.github/workflows/java-cd.yml index 792faec13f..e4bab7a551 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 }} From 6b71f6442837c6381716fd1aaf1de96c36ed1045 Mon Sep 17 00:00:00 2001 From: Lior Sventitzky Date: Sun, 14 Sep 2025 14:51:14 +0300 Subject: [PATCH 20/22] [Backport 2.1] Python Sync: revert license format (#4732) Python Sync: revert license format (#4731) revert license format Signed-off-by: Lior Sventitzky --- python/glide-sync/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/glide-sync/pyproject.toml b/python/glide-sync/pyproject.toml index f626548207..b46fd572a1 100644 --- a/python/glide-sync/pyproject.toml +++ b/python/glide-sync/pyproject.toml @@ -2,7 +2,7 @@ dynamic = ["version", "readme"] name = "valkey-glide-sync" description = "Valkey GLIDE Sync client. Supports Valkey and Redis OSS." -license = "Apache-2.0" +license = { text = "Apache-2.0" } dependencies = [ # ⚠️ Note: If you add a dependency here, make sure to also add it to glide-sync/requirements.txt # Once issue https://github.com/aboutcode-org/python-inspector/issues/197 is resolved, the requirements.txt file can be removed. From 743f87ab3e6fdffab9417ebfc8fa130dcf0e97cf Mon Sep 17 00:00:00 2001 From: prateek-kumar-improving Date: Mon, 15 Sep 2025 14:34:15 -0700 Subject: [PATCH 21/22] Java: Fix Jedis compatibility layer valkey-glide dependency (#4737) * Java: Fix Jedis compatibility layer valkey-glide dependency Signed-off-by: Prateek Kumar --- .github/workflows/java-cd.yml | 15 +++++++++++++-- java/jedis-compatibility/build.gradle | 8 +++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/.github/workflows/java-cd.yml b/.github/workflows/java-cd.yml index e4bab7a551..efb9da41d5 100644 --- a/.github/workflows/java-cd.yml +++ b/.github/workflows/java-cd.yml @@ -152,11 +152,22 @@ jobs: working-directory: java run: | if [[ "${{ matrix.host.TARGET }}" == *"musl"* ]]; then - ./gradlew --build-cache :client:publishToMavenLocal :jedis-compatibility:publishToMavenLocal -Psigning.secretKeyRingFile=secring.gpg \ + # 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 - ./gradlew --build-cache :client:publishToMavenLocal :jedis-compatibility:publishToMavenLocal -Psigning.secretKeyRingFile=secring.gpg \ + # 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: diff --git a/java/jedis-compatibility/build.gradle b/java/jedis-compatibility/build.gradle index 093aa3fead..6ddfcbb687 100644 --- a/java/jedis-compatibility/build.gradle +++ b/java/jedis-compatibility/build.gradle @@ -21,9 +21,11 @@ ext { } dependencies { - // Depend on the main GLIDE client 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' @@ -74,10 +76,10 @@ tasks.register('copyNativeLib', Copy) { jar.dependsOn('copyNativeLib') shadowJar.dependsOn('copyNativeLib') javadoc.dependsOn('copyNativeLib') -copyNativeLib.dependsOn(':client:buildRust') -compileJava.dependsOn(':client:shadowJar', ':client:jar') compileTestJava.dependsOn('copyNativeLib') delombok.dependsOn('compileJava') +copyNativeLib.dependsOn(':client:buildRust') +compileJava.dependsOn(':client:shadowJar', ':client:jar') tasks.withType(Test) { useJUnitPlatform() From 25f509c1f9e318c67876b56f408cc287dc934a6e Mon Sep 17 00:00:00 2001 From: Joseph Brinkman Date: Thu, 18 Sep 2025 13:02:50 -0400 Subject: [PATCH 22/22] [Backport 2.1] Python: improve UDS socket error handling (#4733) (#4755) Python: improve UDS socket error handling (#4733) fixed uds error handling Signed-off-by: Lior Sventitzky Co-authored-by: Lior Sventitzky --- .../glide-async/python/glide/glide_client.py | 7 ++++-- python/tests/async_tests/test_async_client.py | 23 +++++++++++++++++++ python/tests/test_api_consistency.py | 1 + 3 files changed, 29 insertions(+), 2 deletions(-) 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/tests/async_tests/test_async_client.py b/python/tests/async_tests/test_async_client.py index 492b180a64..c5c59e2cca 100644 --- a/python/tests/async_tests/test_async_client.py +++ b/python/tests/async_tests/test_async_client.py @@ -412,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/test_api_consistency.py b/python/tests/test_api_consistency.py index 3bea180a71..4fd3fc26b5 100644 --- a/python/tests/test_api_consistency.py +++ b/python/tests/test_api_consistency.py @@ -69,6 +69,7 @@ "test_inflight_request_limit", "test_statistics", "test_select", + "test_UDS_socket_connection_failure", ], "sync_only": ["test_sync_fork"], }