diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml index a1b96d88..def48baf 100644 --- a/.github/actions/run-tests/action.yml +++ b/.github/actions/run-tests/action.yml @@ -25,7 +25,7 @@ runs: # Mapping of redis version to redis testing containers declare -A redis_version_mapping=( - ["8.0-M03"]="8.0-M04-pre" + ["8.0-M05"]="8.0-M05-pre" ["7.4.2"]="rs-7.4.0-v2" ["7.2.7"]="rs-7.2.0-v14" ) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 592e4876..48bbdb75 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,7 @@ jobs: fail-fast: false matrix: redis-version: - - "8.0-M03" # 8.0 milestone 4 + - "8.0-M05" # 8.0 milestone 5 - "7.4.2" # should use redis stack 7.4 go-version: - "1.23.x" @@ -43,7 +43,7 @@ jobs: # Mapping of redis version to redis testing containers declare -A redis_version_mapping=( - ["8.0-M03"]="8.0-M04-pre" + ["8.0-M05"]="8.0-M05-pre" ["7.4.2"]="rs-7.4.0-v2" ) if [[ -v redis_version_mapping[$REDIS_VERSION] ]]; then @@ -72,7 +72,7 @@ jobs: fail-fast: false matrix: redis-version: - - "8.0-M03" # 8.0 milestone 4 + - "8.0-M05" # 8.0 milestone 5 - "7.4.2" # should use redis stack 7.4 - "7.2.7" # should redis stack 7.2 go-version: diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index d9e53f70..515750af 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -21,4 +21,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: golangci-lint - uses: golangci/golangci-lint-action@v6.5.0 + uses: golangci/golangci-lint-action@v6.5.2 + with: + verify: false # disable verifying the configuration since golangci is currently introducing breaking changes in the configuration + diff --git a/Makefile b/Makefile index d94676ad..fc175f5f 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ test.ci: (cd "$${dir}" && \ go mod tidy -compat=1.18 && \ go vet && \ - go test -coverprofile=coverage.txt -covermode=atomic ./... -race); \ + go test -v -coverprofile=coverage.txt -covermode=atomic ./... -race); \ done cd internal/customvet && go build . go vet -vettool ./internal/customvet/customvet diff --git a/README.md b/README.md index 3ab23ba6..335d32da 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,7 @@ [![codecov](https://codecov.io/github/redis/go-redis/graph/badge.svg?token=tsrCZKuSSw)](https://codecov.io/github/redis/go-redis) [![Chat](https://discordapp.com/api/guilds/752070105847955518/widget.png)](https://discord.gg/rWtp5Aj) -> go-redis is brought to you by :star: [**uptrace/uptrace**](https://github.com/uptrace/uptrace). -> Uptrace is an open-source APM tool that supports distributed tracing, metrics, and logs. You can -> use it to monitor applications and set up automatic alerts to receive notifications via email, -> Slack, Telegram, and others. -> -> See [OpenTelemetry](https://github.com/redis/go-redis/tree/master/example/otel) example which -> demonstrates how you can use Uptrace to monitor go-redis. +> go-redis is the official Redis client library for the Go programming language. It offers a straightforward interface for interacting with Redis servers. ## Supported versions @@ -184,16 +178,18 @@ By default, go-redis automatically sends the client library name and version dur #### Disabling Identity Verification -When connection identity verification is not required or needs to be explicitly disabled, a `DisableIndentity` configuration option exists. In V10 of this library, `DisableIndentity` will become `DisableIdentity` in order to fix the associated typo. +When connection identity verification is not required or needs to be explicitly disabled, a `DisableIdentity` configuration option exists. +Initially there was a typo and the option was named `DisableIndentity` instead of `DisableIdentity`. The misspelled option is marked as Deprecated and will be removed in V10 of this library. +Although both options will work at the moment, the correct option is `DisableIdentity`. The deprecated option will be removed in V10 of this library, so please use the correct option name to avoid any issues. -To disable verification, set the `DisableIndentity` option to `true` in the Redis client options: +To disable verification, set the `DisableIdentity` option to `true` in the Redis client options: ```go rdb := redis.NewClient(&redis.Options{ Addr: "localhost:6379", Password: "", DB: 0, - DisableIndentity: true, // Disable set-info on connect + DisableIdentity: true, // Disable set-info on connect }) ``` @@ -215,6 +211,10 @@ res1, err := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptio val1 := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptions{}).RawVal() ``` +#### Redis-Search Default Dialect + +In the Redis-Search module, **the default dialect is 2**. If needed, you can explicitly specify a different dialect using the appropriate configuration in your queries. + ## Contributing Please see [out contributing guidelines](CONTRIBUTING.md) to help us improve this library! @@ -297,6 +297,14 @@ REDIS_PORT=9999 go test ## Contributors +> The go-redis project was originally initiated by :star: [**uptrace/uptrace**](https://github.com/uptrace/uptrace). +> Uptrace is an open-source APM tool that supports distributed tracing, metrics, and logs. You can +> use it to monitor applications and set up automatic alerts to receive notifications via email, +> Slack, Telegram, and others. +> +> See [OpenTelemetry](https://github.com/redis/go-redis/tree/master/example/otel) example which +> demonstrates how you can use Uptrace to monitor go-redis. + Thanks to all the people who already contributed! diff --git a/bench_decode_test.go b/bench_decode_test.go index 16bdf2cd..d61a901a 100644 --- a/bench_decode_test.go +++ b/bench_decode_test.go @@ -30,7 +30,7 @@ func NewClientStub(resp []byte) *ClientStub { Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) { return stub.stubConn(initHello), nil }, - DisableIndentity: true, + DisableIdentity: true, }) return stub } @@ -46,7 +46,7 @@ func NewClusterClientStub(resp []byte) *ClientStub { Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) { return stub.stubConn(initHello), nil }, - DisableIndentity: true, + DisableIdentity: true, ClusterSlots: func(_ context.Context) ([]ClusterSlot, error) { return []ClusterSlot{ diff --git a/commands_test.go b/commands_test.go index 681fe470..a9e90fc9 100644 --- a/commands_test.go +++ b/commands_test.go @@ -2659,7 +2659,6 @@ var _ = Describe("Commands", func() { Expect(res).To(Equal([]int64{1, 1, -2})) }) - It("should HPExpire", Label("hash-expiration", "NonRedisEnterprise"), func() { SkipBeforeRedisVersion(7.4, "doesn't work with older redis stack images") res, err := client.HPExpire(ctx, "no_such_key", 10*time.Second, "field1", "field2", "field3").Result() @@ -2812,6 +2811,148 @@ var _ = Describe("Commands", func() { Expect(err).NotTo(HaveOccurred()) Expect(res[0]).To(BeNumerically("~", 10*time.Second.Milliseconds(), 1)) }) + + It("should HGETDEL", Label("hash", "HGETDEL"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + + err := client.HSet(ctx, "myhash", "f1", "val1", "f2", "val2", "f3", "val3").Err() + Expect(err).NotTo(HaveOccurred()) + + // Execute HGETDEL on fields f1 and f2. + res, err := client.HGetDel(ctx, "myhash", "f1", "f2").Result() + Expect(err).NotTo(HaveOccurred()) + // Expect the returned values for f1 and f2. + Expect(res).To(Equal([]string{"val1", "val2"})) + + // Verify that f1 and f2 have been deleted, while f3 remains. + remaining, err := client.HMGet(ctx, "myhash", "f1", "f2", "f3").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(remaining[0]).To(BeNil()) + Expect(remaining[1]).To(BeNil()) + Expect(remaining[2]).To(Equal("val3")) + }) + + It("should return nil responses for HGETDEL on non-existent key", Label("hash", "HGETDEL"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + // HGETDEL on a key that does not exist. + res, err := client.HGetDel(ctx, "nonexistent", "f1", "f2").Result() + Expect(err).To(BeNil()) + Expect(res).To(Equal([]string{"", ""})) + }) + + // ----------------------------- + // HGETEX with various TTL options + // ----------------------------- + It("should HGETEX with EX option", Label("hash", "HGETEX"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + + err := client.HSet(ctx, "myhash", "f1", "val1", "f2", "val2").Err() + Expect(err).NotTo(HaveOccurred()) + + // Call HGETEX with EX option and 60 seconds TTL. + opt := redis.HGetEXOptions{ + ExpirationType: redis.HGetEXExpirationEX, + ExpirationVal: 60, + } + res, err := client.HGetEXWithArgs(ctx, "myhash", &opt, "f1", "f2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]string{"val1", "val2"})) + }) + + It("should HGETEX with PERSIST option", Label("hash", "HGETEX"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + + err := client.HSet(ctx, "myhash", "f1", "val1", "f2", "val2").Err() + Expect(err).NotTo(HaveOccurred()) + + // Call HGETEX with PERSIST (no TTL value needed). + opt := redis.HGetEXOptions{ExpirationType: redis.HGetEXExpirationPERSIST} + res, err := client.HGetEXWithArgs(ctx, "myhash", &opt, "f1", "f2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]string{"val1", "val2"})) + }) + + It("should HGETEX with EXAT option", Label("hash", "HGETEX"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + + err := client.HSet(ctx, "myhash", "f1", "val1", "f2", "val2").Err() + Expect(err).NotTo(HaveOccurred()) + + // Set expiration at a specific Unix timestamp (60 seconds from now). + expireAt := time.Now().Add(60 * time.Second).Unix() + opt := redis.HGetEXOptions{ + ExpirationType: redis.HGetEXExpirationEXAT, + ExpirationVal: expireAt, + } + res, err := client.HGetEXWithArgs(ctx, "myhash", &opt, "f1", "f2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal([]string{"val1", "val2"})) + }) + + // ----------------------------- + // HSETEX with FNX/FXX options + // ----------------------------- + It("should HSETEX with FNX condition", Label("hash", "HSETEX"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + + opt := redis.HSetEXOptions{ + Condition: redis.HSetEXFNX, + ExpirationType: redis.HSetEXExpirationEX, + ExpirationVal: 60, + } + res, err := client.HSetEXWithArgs(ctx, "myhash", &opt, "f1", "val1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(int64(1))) + + opt = redis.HSetEXOptions{ + Condition: redis.HSetEXFNX, + ExpirationType: redis.HSetEXExpirationEX, + ExpirationVal: 60, + } + res, err = client.HSetEXWithArgs(ctx, "myhash", &opt, "f1", "val2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(int64(0))) + }) + + It("should HSETEX with FXX condition", Label("hash", "HSETEX"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + + err := client.HSet(ctx, "myhash", "f2", "val1").Err() + Expect(err).NotTo(HaveOccurred()) + + opt := redis.HSetEXOptions{ + Condition: redis.HSetEXFXX, + ExpirationType: redis.HSetEXExpirationEX, + ExpirationVal: 60, + } + res, err := client.HSetEXWithArgs(ctx, "myhash", &opt, "f2", "val2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(int64(1))) + opt = redis.HSetEXOptions{ + Condition: redis.HSetEXFXX, + ExpirationType: redis.HSetEXExpirationEX, + ExpirationVal: 60, + } + res, err = client.HSetEXWithArgs(ctx, "myhash", &opt, "f3", "val3").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(int64(0))) + }) + + It("should HSETEX with multiple field operations", Label("hash", "HSETEX"), func() { + SkipBeforeRedisVersion(7.9, "requires Redis 8.x") + + opt := redis.HSetEXOptions{ + ExpirationType: redis.HSetEXExpirationEX, + ExpirationVal: 60, + } + res, err := client.HSetEXWithArgs(ctx, "myhash", &opt, "f1", "val1", "f2", "val2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(int64(1))) + + values, err := client.HMGet(ctx, "myhash", "f1", "f2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(values).To(Equal([]interface{}{"val1", "val2"})) + }) }) Describe("hyperloglog", func() { diff --git a/docker-compose.yml b/docker-compose.yml index 5bf69f19..3d4347bf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,7 @@ services: redis: image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:rs-7.4.0-v2} + platform: linux/amd64 container_name: redis-standalone environment: - TLS_ENABLED=yes @@ -23,6 +24,7 @@ services: osscluster: image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:rs-7.4.0-v2} + platform: linux/amd64 container_name: redis-osscluster environment: - NODES=6 @@ -39,6 +41,7 @@ services: sentinel-cluster: image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:rs-7.4.0-v2} + platform: linux/amd64 container_name: redis-sentinel-cluster network_mode: "host" environment: @@ -58,6 +61,7 @@ services: sentinel: image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:rs-7.4.0-v2} + platform: linux/amd64 container_name: redis-sentinel depends_on: - sentinel-cluster @@ -81,6 +85,7 @@ services: ring-cluster: image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:rs-7.4.0-v2} + platform: linux/amd64 container_name: redis-ring-cluster environment: - NODES=3 diff --git a/doctests/cmds_set_test.go b/doctests/cmds_set_test.go new file mode 100644 index 00000000..fecddbb8 --- /dev/null +++ b/doctests/cmds_set_test.go @@ -0,0 +1,102 @@ +// EXAMPLE: cmds_set +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_sadd_cmd() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "myset") + // REMOVE_END + + // STEP_START sadd + sAddResult1, err := rdb.SAdd(ctx, "myset", "Hello").Result() + + if err != nil { + panic(err) + } + + fmt.Println(sAddResult1) // >>> 1 + + sAddResult2, err := rdb.SAdd(ctx, "myset", "World").Result() + + if err != nil { + panic(err) + } + + fmt.Println(sAddResult2) // >>> 1 + + sAddResult3, err := rdb.SAdd(ctx, "myset", "World").Result() + + if err != nil { + panic(err) + } + + fmt.Println(sAddResult3) // >>> 0 + + sMembersResult, err := rdb.SMembers(ctx, "myset").Result() + + if err != nil { + panic(err) + } + + fmt.Println(sMembersResult) // >>> [Hello World] + // STEP_END + + // Output: + // 1 + // 1 + // 0 + // [Hello World] +} + +func ExampleClient_smembers_cmd() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.Del(ctx, "myset") + // REMOVE_END + + // STEP_START smembers + sAddResult, err := rdb.SAdd(ctx, "myset", "Hello", "World").Result() + + if err != nil { + panic(err) + } + + fmt.Println(sAddResult) // >>> 2 + + sMembersResult, err := rdb.SMembers(ctx, "myset").Result() + + if err != nil { + panic(err) + } + + fmt.Println(sMembersResult) // >>> [Hello World] + // STEP_END + + // Output: + // 2 + // [Hello World] +} diff --git a/error.go b/error.go index a7bf159c..6f47f7cf 100644 --- a/error.go +++ b/error.go @@ -53,6 +53,9 @@ func shouldRetry(err error, retryTimeout bool) bool { return true case nil, context.Canceled, context.DeadlineExceeded: return false + case pool.ErrPoolTimeout: + // connection pool timeout, increase retries. #3289 + return true } if v, ok := err.(timeoutError); ok { @@ -72,6 +75,9 @@ func shouldRetry(err error, retryTimeout bool) bool { if strings.HasPrefix(s, "READONLY ") { return true } + if strings.HasPrefix(s, "MASTERDOWN ") { + return true + } if strings.HasPrefix(s, "CLUSTERDOWN ") { return true } diff --git a/error_test.go b/error_test.go new file mode 100644 index 00000000..da9a471a --- /dev/null +++ b/error_test.go @@ -0,0 +1,65 @@ +package redis_test + +import ( + "context" + "errors" + "io" + + . "github.com/bsm/ginkgo/v2" + . "github.com/bsm/gomega" + "github.com/redis/go-redis/v9" +) + +type testTimeout struct { + timeout bool +} + +func (t testTimeout) Timeout() bool { + return t.timeout +} + +func (t testTimeout) Error() string { + return "test timeout" +} + +var _ = Describe("error", func() { + BeforeEach(func() { + + }) + + AfterEach(func() { + + }) + + It("should retry", func() { + data := map[error]bool{ + io.EOF: true, + io.ErrUnexpectedEOF: true, + nil: false, + context.Canceled: false, + context.DeadlineExceeded: false, + redis.ErrPoolTimeout: true, + errors.New("ERR max number of clients reached"): true, + errors.New("LOADING Redis is loading the dataset in memory"): true, + errors.New("READONLY You can't write against a read only replica"): true, + errors.New("CLUSTERDOWN The cluster is down"): true, + errors.New("TRYAGAIN Command cannot be processed, please try again"): true, + errors.New("other"): false, + } + + for err, expected := range data { + Expect(redis.ShouldRetry(err, false)).To(Equal(expected)) + Expect(redis.ShouldRetry(err, true)).To(Equal(expected)) + } + }) + + It("should retry timeout", func() { + t1 := testTimeout{timeout: true} + Expect(redis.ShouldRetry(t1, true)).To(Equal(true)) + Expect(redis.ShouldRetry(t1, false)).To(Equal(false)) + + t2 := testTimeout{timeout: false} + Expect(redis.ShouldRetry(t2, true)).To(Equal(true)) + Expect(redis.ShouldRetry(t2, false)).To(Equal(true)) + }) +}) diff --git a/example/del-keys-without-ttl/go.mod b/example/del-keys-without-ttl/go.mod index 40ad6297..0699671c 100644 --- a/example/del-keys-without-ttl/go.mod +++ b/example/del-keys-without-ttl/go.mod @@ -5,7 +5,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. require ( - github.com/redis/go-redis/v9 v9.7.1 + github.com/redis/go-redis/v9 v9.7.3 go.uber.org/zap v1.24.0 ) diff --git a/example/hll/go.mod b/example/hll/go.mod index 14a8827f..72ab52aa 100644 --- a/example/hll/go.mod +++ b/example/hll/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.7.1 +require github.com/redis/go-redis/v9 v9.7.3 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/hset-struct/go.sum b/example/hset-struct/go.sum index 1602e702..5496d29e 100644 --- a/example/hset-struct/go.sum +++ b/example/hset-struct/go.sum @@ -1,7 +1,5 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= diff --git a/example/lua-scripting/go.mod b/example/lua-scripting/go.mod index 64f5c8af..e2bc161a 100644 --- a/example/lua-scripting/go.mod +++ b/example/lua-scripting/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.7.1 +require github.com/redis/go-redis/v9 v9.7.3 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/otel/go.mod b/example/otel/go.mod index 93b5d46c..299e4b02 100644 --- a/example/otel/go.mod +++ b/example/otel/go.mod @@ -1,6 +1,8 @@ module github.com/redis/go-redis/example/otel -go 1.19 +go 1.23.0 + +toolchain go1.24.1 replace github.com/redis/go-redis/v9 => ../.. @@ -9,8 +11,8 @@ replace github.com/redis/go-redis/extra/redisotel/v9 => ../../extra/redisotel replace github.com/redis/go-redis/extra/rediscmd/v9 => ../../extra/rediscmd require ( - github.com/redis/go-redis/extra/redisotel/v9 v9.7.1 - github.com/redis/go-redis/v9 v9.7.1 + github.com/redis/go-redis/extra/redisotel/v9 v9.7.3 + github.com/redis/go-redis/v9 v9.7.3 github.com/uptrace/uptrace-go v1.21.0 go.opentelemetry.io/otel v1.22.0 ) @@ -23,7 +25,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect - github.com/redis/go-redis/extra/rediscmd/v9 v9.7.1 // indirect + github.com/redis/go-redis/extra/rediscmd/v9 v9.7.3 // indirect go.opentelemetry.io/contrib/instrumentation/runtime v0.46.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect @@ -34,9 +36,9 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.21.0 // indirect go.opentelemetry.io/otel/trace v1.22.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect - golang.org/x/net v0.33.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/net v0.36.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 // indirect diff --git a/example/otel/go.sum b/example/otel/go.sum index 1a1729c6..fa94c15b 100644 --- a/example/otel/go.sum +++ b/example/otel/go.sum @@ -1,10 +1,13 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -17,10 +20,13 @@ github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/uptrace/uptrace-go v1.21.0 h1:oJoUjhiVT7aiuoG6B3ClVHtJozLn3cK9hQt8U5dQO1M= github.com/uptrace/uptrace-go v1.21.0/go.mod h1:/aXAFGKOqeAFBqWa1xtzLnGX2xJm1GScqz9NJ0TJjLM= go.opentelemetry.io/contrib/instrumentation/runtime v0.46.1 h1:m9ReioVPIffxjJlGNRd0d5poy+9oTro3D+YbiEzUDOc= @@ -46,12 +52,13 @@ go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40 go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= +golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1 h1:/IWabOtPziuXTEtI1KYCpM6Ss7vaAkeMxk+uXV/xvZs= google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k= @@ -66,3 +73,4 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/example/redis-bloom/go.mod b/example/redis-bloom/go.mod index a973cd17..04494565 100644 --- a/example/redis-bloom/go.mod +++ b/example/redis-bloom/go.mod @@ -4,7 +4,7 @@ go 1.18 replace github.com/redis/go-redis/v9 => ../.. -require github.com/redis/go-redis/v9 v9.7.1 +require github.com/redis/go-redis/v9 v9.7.3 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/example/scan-struct/go.mod b/example/scan-struct/go.mod index 21d7e527..f4453607 100644 --- a/example/scan-struct/go.mod +++ b/example/scan-struct/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/davecgh/go-spew v1.1.1 - github.com/redis/go-redis/v9 v9.7.1 + github.com/redis/go-redis/v9 v9.7.3 ) require ( diff --git a/export_test.go b/export_test.go index 3f92983d..10d8f23c 100644 --- a/export_test.go +++ b/export_test.go @@ -11,6 +11,8 @@ import ( "github.com/redis/go-redis/v9/internal/pool" ) +var ErrPoolTimeout = pool.ErrPoolTimeout + func (c *baseClient) Pool() pool.Pooler { return c.connPool } @@ -102,3 +104,7 @@ func (c *Ring) ShardByName(name string) *ringShard { func (c *ModuleLoadexConfig) ToArgs() []interface{} { return c.toArgs() } + +func ShouldRetry(err error, retryTimeout bool) bool { + return shouldRetry(err, retryTimeout) +} diff --git a/extra/rediscensus/go.mod b/extra/rediscensus/go.mod index cc0bd0fb..11230414 100644 --- a/extra/rediscensus/go.mod +++ b/extra/rediscensus/go.mod @@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../.. replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd require ( - github.com/redis/go-redis/extra/rediscmd/v9 v9.7.1 - github.com/redis/go-redis/v9 v9.7.1 + github.com/redis/go-redis/extra/rediscmd/v9 v9.7.3 + github.com/redis/go-redis/v9 v9.7.3 go.opencensus.io v0.24.0 ) @@ -18,4 +18,7 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect ) -retract v9.5.3 // This version was accidentally released. +retract ( + v9.5.3 // This version was accidentally released. + v9.7.2 // This version was accidentally released. +) diff --git a/extra/rediscmd/go.mod b/extra/rediscmd/go.mod index 0689fe90..d64ad570 100644 --- a/extra/rediscmd/go.mod +++ b/extra/rediscmd/go.mod @@ -7,7 +7,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/bsm/ginkgo/v2 v2.12.0 github.com/bsm/gomega v1.27.10 - github.com/redis/go-redis/v9 v9.7.1 + github.com/redis/go-redis/v9 v9.7.3 ) require ( @@ -15,4 +15,7 @@ require ( github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect ) -retract v9.5.3 // This version was accidentally released. +retract ( + v9.5.3 // This version was accidentally released. + v9.7.2 // This version was accidentally released. +) diff --git a/extra/rediscmd/rediscmd.go b/extra/rediscmd/rediscmd.go index c97689f9..6423b6ab 100644 --- a/extra/rediscmd/rediscmd.go +++ b/extra/rediscmd/rediscmd.go @@ -17,7 +17,6 @@ func CmdString(cmd redis.Cmder) string { } func CmdsString(cmds []redis.Cmder) (string, string) { - const numCmdLimit = 100 const numNameLimit = 10 seen := make(map[string]struct{}, numNameLimit) @@ -26,10 +25,6 @@ func CmdsString(cmds []redis.Cmder) (string, string) { b := make([]byte, 0, 32*len(cmds)) for i, cmd := range cmds { - if i > numCmdLimit { - break - } - if i > 0 { b = append(b, '\n') } @@ -51,12 +46,7 @@ func CmdsString(cmds []redis.Cmder) (string, string) { } func AppendCmd(b []byte, cmd redis.Cmder) []byte { - const numArgLimit = 32 - for i, arg := range cmd.Args() { - if i > numArgLimit { - break - } if i > 0 { b = append(b, ' ') } @@ -72,20 +62,12 @@ func AppendCmd(b []byte, cmd redis.Cmder) []byte { } func appendArg(b []byte, v interface{}) []byte { - const argLenLimit = 64 - switch v := v.(type) { case nil: return append(b, ""...) case string: - if len(v) > argLenLimit { - v = v[:argLenLimit] - } return appendUTF8String(b, Bytes(v)) case []byte: - if len(v) > argLenLimit { - v = v[:argLenLimit] - } return appendUTF8String(b, v) case int: return strconv.AppendInt(b, int64(v), 10) diff --git a/extra/redisotel/go.mod b/extra/redisotel/go.mod index ab6288de..13899eb5 100644 --- a/extra/redisotel/go.mod +++ b/extra/redisotel/go.mod @@ -7,8 +7,8 @@ replace github.com/redis/go-redis/v9 => ../.. replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd require ( - github.com/redis/go-redis/extra/rediscmd/v9 v9.7.1 - github.com/redis/go-redis/v9 v9.7.1 + github.com/redis/go-redis/extra/rediscmd/v9 v9.7.3 + github.com/redis/go-redis/v9 v9.7.3 go.opentelemetry.io/otel v1.22.0 go.opentelemetry.io/otel/metric v1.22.0 go.opentelemetry.io/otel/sdk v1.22.0 @@ -23,4 +23,7 @@ require ( golang.org/x/sys v0.16.0 // indirect ) -retract v9.5.3 // This version was accidentally released. +retract ( + v9.5.3 // This version was accidentally released. + v9.7.2 // This version was accidentally released. +) diff --git a/extra/redisotel/tracing_test.go b/extra/redisotel/tracing_test.go index bbe82814..e5ef86ed 100644 --- a/extra/redisotel/tracing_test.go +++ b/extra/redisotel/tracing_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net" + "strings" "testing" "go.opentelemetry.io/otel/attribute" @@ -222,6 +223,169 @@ func TestTracingHook_ProcessPipelineHook(t *testing.T) { } } +func TestTracingHook_ProcessHook_LongCommand(t *testing.T) { + imsb := tracetest.NewInMemoryExporter() + provider := sdktrace.NewTracerProvider(sdktrace.WithSyncer(imsb)) + hook := newTracingHook( + "redis://localhost:6379", + WithTracerProvider(provider), + ) + longValue := strings.Repeat("a", 102400) + + tests := []struct { + name string + cmd redis.Cmder + expected string + }{ + { + name: "short command", + cmd: redis.NewCmd(context.Background(), "SET", "key", "value"), + expected: "SET key value", + }, + { + name: "set command with long key", + cmd: redis.NewCmd(context.Background(), "SET", longValue, "value"), + expected: "SET " + longValue + " value", + }, + { + name: "set command with long value", + cmd: redis.NewCmd(context.Background(), "SET", "key", longValue), + expected: "SET key " + longValue, + }, + { + name: "set command with long key and value", + cmd: redis.NewCmd(context.Background(), "SET", longValue, longValue), + expected: "SET " + longValue + " " + longValue, + }, + { + name: "short command with many arguments", + cmd: redis.NewCmd(context.Background(), "MSET", "key1", "value1", "key2", "value2", "key3", "value3", "key4", "value4", "key5", "value5"), + expected: "MSET key1 value1 key2 value2 key3 value3 key4 value4 key5 value5", + }, + { + name: "long command", + cmd: redis.NewCmd(context.Background(), longValue, "key", "value"), + expected: longValue + " key value", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer imsb.Reset() + + processHook := hook.ProcessHook(func(ctx context.Context, cmd redis.Cmder) error { + return nil + }) + + if err := processHook(context.Background(), tt.cmd); err != nil { + t.Fatal(err) + } + + assertEqual(t, 1, len(imsb.GetSpans())) + + spanData := imsb.GetSpans()[0] + + var dbStatement string + for _, attr := range spanData.Attributes { + if attr.Key == semconv.DBStatementKey { + dbStatement = attr.Value.AsString() + break + } + } + + if dbStatement != tt.expected { + t.Errorf("Expected DB statement: %q\nGot: %q", tt.expected, dbStatement) + } + }) + } +} + +func TestTracingHook_ProcessPipelineHook_LongCommands(t *testing.T) { + imsb := tracetest.NewInMemoryExporter() + provider := sdktrace.NewTracerProvider(sdktrace.WithSyncer(imsb)) + hook := newTracingHook( + "redis://localhost:6379", + WithTracerProvider(provider), + ) + + tests := []struct { + name string + cmds []redis.Cmder + expected string + }{ + { + name: "multiple short commands", + cmds: []redis.Cmder{ + redis.NewCmd(context.Background(), "SET", "key1", "value1"), + redis.NewCmd(context.Background(), "SET", "key2", "value2"), + }, + expected: "SET key1 value1\nSET key2 value2", + }, + { + name: "multiple short commands with long key", + cmds: []redis.Cmder{ + redis.NewCmd(context.Background(), "SET", strings.Repeat("a", 102400), "value1"), + redis.NewCmd(context.Background(), "SET", strings.Repeat("b", 102400), "value2"), + }, + expected: "SET " + strings.Repeat("a", 102400) + " value1\nSET " + strings.Repeat("b", 102400) + " value2", + }, + { + name: "multiple short commands with long value", + cmds: []redis.Cmder{ + redis.NewCmd(context.Background(), "SET", "key1", strings.Repeat("a", 102400)), + redis.NewCmd(context.Background(), "SET", "key2", strings.Repeat("b", 102400)), + }, + expected: "SET key1 " + strings.Repeat("a", 102400) + "\nSET key2 " + strings.Repeat("b", 102400), + }, + { + name: "multiple short commands with long key and value", + cmds: []redis.Cmder{ + redis.NewCmd(context.Background(), "SET", strings.Repeat("a", 102400), strings.Repeat("b", 102400)), + redis.NewCmd(context.Background(), "SET", strings.Repeat("c", 102400), strings.Repeat("d", 102400)), + }, + expected: "SET " + strings.Repeat("a", 102400) + " " + strings.Repeat("b", 102400) + "\nSET " + strings.Repeat("c", 102400) + " " + strings.Repeat("d", 102400), + }, + { + name: "multiple long commands", + cmds: []redis.Cmder{ + redis.NewCmd(context.Background(), strings.Repeat("a", 102400), "key1", "value1"), + redis.NewCmd(context.Background(), strings.Repeat("a", 102400), "key2", "value2"), + }, + expected: strings.Repeat("a", 102400) + " key1 value1\n" + strings.Repeat("a", 102400) + " key2 value2", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer imsb.Reset() + + processHook := hook.ProcessPipelineHook(func(ctx context.Context, cmds []redis.Cmder) error { + return nil + }) + + if err := processHook(context.Background(), tt.cmds); err != nil { + t.Fatal(err) + } + + assertEqual(t, 1, len(imsb.GetSpans())) + + spanData := imsb.GetSpans()[0] + + var dbStatement string + for _, attr := range spanData.Attributes { + if attr.Key == semconv.DBStatementKey { + dbStatement = attr.Value.AsString() + break + } + } + + if dbStatement != tt.expected { + t.Errorf("Expected DB statement:\n%q\nGot:\n%q", tt.expected, dbStatement) + } + }) + } +} + func assertEqual(t *testing.T, expected, actual interface{}) { t.Helper() if expected != actual { diff --git a/extra/redisprometheus/go.mod b/extra/redisprometheus/go.mod index a1659bb0..fa3c43ae 100644 --- a/extra/redisprometheus/go.mod +++ b/extra/redisprometheus/go.mod @@ -6,7 +6,7 @@ replace github.com/redis/go-redis/v9 => ../.. require ( github.com/prometheus/client_golang v1.14.0 - github.com/redis/go-redis/v9 v9.7.1 + github.com/redis/go-redis/v9 v9.7.3 ) require ( @@ -22,4 +22,7 @@ require ( google.golang.org/protobuf v1.33.0 // indirect ) -retract v9.5.3 // This version was accidentally released. +retract ( + v9.5.3 // This version was accidentally released. + v9.7.2 // This version was accidentally released. +) diff --git a/go.mod b/go.mod index 1492d270..83e8fd3d 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( ) retract ( + v9.7.2 // This version was accidentally released. Please use version 9.7.3 instead. v9.5.4 // This version was accidentally released. Please use version 9.6.0 instead. v9.5.3 // This version was accidentally released. Please use version 9.6.0 instead. ) diff --git a/hash_commands.go b/hash_commands.go index 039d8e07..1f53f344 100644 --- a/hash_commands.go +++ b/hash_commands.go @@ -10,13 +10,17 @@ type HashCmdable interface { HExists(ctx context.Context, key, field string) *BoolCmd HGet(ctx context.Context, key, field string) *StringCmd HGetAll(ctx context.Context, key string) *MapStringStringCmd - HIncrBy(ctx context.Context, key, field string, incr int64) *IntCmd + HGetDel(ctx context.Context, key string, fields ...string) *StringSliceCmd + HGetEX(ctx context.Context, key string, fields ...string) *StringSliceCmd + HGetEXWithArgs(ctx context.Context, key string, options *HGetEXOptions, fields ...string) *StringSliceCmd HIncrByFloat(ctx context.Context, key, field string, incr float64) *FloatCmd HKeys(ctx context.Context, key string) *StringSliceCmd HLen(ctx context.Context, key string) *IntCmd HMGet(ctx context.Context, key string, fields ...string) *SliceCmd HSet(ctx context.Context, key string, values ...interface{}) *IntCmd HMSet(ctx context.Context, key string, values ...interface{}) *BoolCmd + HSetEX(ctx context.Context, key string, fieldsAndValues ...string) *IntCmd + HSetEXWithArgs(ctx context.Context, key string, options *HSetEXOptions, fieldsAndValues ...string) *IntCmd HSetNX(ctx context.Context, key, field string, value interface{}) *BoolCmd HScan(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd HScanNoValues(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd @@ -454,3 +458,113 @@ func (c cmdable) HPTTL(ctx context.Context, key string, fields ...string) *IntSl _ = c(ctx, cmd) return cmd } + +func (c cmdable) HGetDel(ctx context.Context, key string, fields ...string) *StringSliceCmd { + args := []interface{}{"HGETDEL", key, "FIELDS", len(fields)} + for _, field := range fields { + args = append(args, field) + } + cmd := NewStringSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) HGetEX(ctx context.Context, key string, fields ...string) *StringSliceCmd { + args := []interface{}{"HGETEX", key, "FIELDS", len(fields)} + for _, field := range fields { + args = append(args, field) + } + cmd := NewStringSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// ExpirationType represents an expiration option for the HGETEX command. +type HGetEXExpirationType string + +const ( + HGetEXExpirationEX HGetEXExpirationType = "EX" + HGetEXExpirationPX HGetEXExpirationType = "PX" + HGetEXExpirationEXAT HGetEXExpirationType = "EXAT" + HGetEXExpirationPXAT HGetEXExpirationType = "PXAT" + HGetEXExpirationPERSIST HGetEXExpirationType = "PERSIST" +) + +type HGetEXOptions struct { + ExpirationType HGetEXExpirationType + ExpirationVal int64 +} + +func (c cmdable) HGetEXWithArgs(ctx context.Context, key string, options *HGetEXOptions, fields ...string) *StringSliceCmd { + args := []interface{}{"HGETEX", key} + if options.ExpirationType != "" { + args = append(args, string(options.ExpirationType)) + if options.ExpirationType != HGetEXExpirationPERSIST { + args = append(args, options.ExpirationVal) + } + } + + args = append(args, "FIELDS", len(fields)) + for _, field := range fields { + args = append(args, field) + } + + cmd := NewStringSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +type HSetEXCondition string + +const ( + HSetEXFNX HSetEXCondition = "FNX" // Only set the fields if none of them already exist. + HSetEXFXX HSetEXCondition = "FXX" // Only set the fields if all already exist. +) + +type HSetEXExpirationType string + +const ( + HSetEXExpirationEX HSetEXExpirationType = "EX" + HSetEXExpirationPX HSetEXExpirationType = "PX" + HSetEXExpirationEXAT HSetEXExpirationType = "EXAT" + HSetEXExpirationPXAT HSetEXExpirationType = "PXAT" + HSetEXExpirationKEEPTTL HSetEXExpirationType = "KEEPTTL" +) + +type HSetEXOptions struct { + Condition HSetEXCondition + ExpirationType HSetEXExpirationType + ExpirationVal int64 +} + +func (c cmdable) HSetEX(ctx context.Context, key string, fieldsAndValues ...string) *IntCmd { + args := []interface{}{"HSETEX", key, "FIELDS", len(fieldsAndValues) / 2} + for _, field := range fieldsAndValues { + args = append(args, field) + } + + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) HSetEXWithArgs(ctx context.Context, key string, options *HSetEXOptions, fieldsAndValues ...string) *IntCmd { + args := []interface{}{"HSETEX", key} + if options.Condition != "" { + args = append(args, string(options.Condition)) + } + if options.ExpirationType != "" { + args = append(args, string(options.ExpirationType)) + if options.ExpirationType != HSetEXExpirationKEEPTTL { + args = append(args, options.ExpirationVal) + } + } + args = append(args, "FIELDS", len(fieldsAndValues)/2) + for _, field := range fieldsAndValues { + args = append(args, field) + } + + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} diff --git a/internal/pool/bench_test.go b/internal/pool/bench_test.go index 71049f48..72308e12 100644 --- a/internal/pool/bench_test.go +++ b/internal/pool/bench_test.go @@ -33,6 +33,7 @@ func BenchmarkPoolGetPut(b *testing.B) { Dialer: dummyDialer, PoolSize: bm.poolSize, PoolTimeout: time.Second, + DialTimeout: 1 * time.Second, ConnMaxIdleTime: time.Hour, }) @@ -76,6 +77,7 @@ func BenchmarkPoolGetRemove(b *testing.B) { Dialer: dummyDialer, PoolSize: bm.poolSize, PoolTimeout: time.Second, + DialTimeout: 1 * time.Second, ConnMaxIdleTime: time.Hour, }) diff --git a/internal/pool/pool.go b/internal/pool/pool.go index 2125f3e1..b69c75f4 100644 --- a/internal/pool/pool.go +++ b/internal/pool/pool.go @@ -62,6 +62,7 @@ type Options struct { PoolFIFO bool PoolSize int + DialTimeout time.Duration PoolTimeout time.Duration MinIdleConns int MaxIdleConns int @@ -140,7 +141,10 @@ func (p *ConnPool) checkMinIdleConns() { } func (p *ConnPool) addIdleConn() error { - cn, err := p.dialConn(context.TODO(), true) + ctx, cancel := context.WithTimeout(context.Background(), p.cfg.DialTimeout) + defer cancel() + + cn, err := p.dialConn(ctx, true) if err != nil { return err } @@ -230,15 +234,19 @@ func (p *ConnPool) tryDial() { return } - conn, err := p.cfg.Dialer(context.Background()) + ctx, cancel := context.WithTimeout(context.Background(), p.cfg.DialTimeout) + + conn, err := p.cfg.Dialer(ctx) if err != nil { p.setLastDialError(err) time.Sleep(time.Second) + cancel() continue } atomic.StoreUint32(&p.dialErrorsNum, 0) _ = conn.Close() + cancel() return } } diff --git a/internal/pool/pool_test.go b/internal/pool/pool_test.go index 4ccc4893..99f31bd7 100644 --- a/internal/pool/pool_test.go +++ b/internal/pool/pool_test.go @@ -22,6 +22,7 @@ var _ = Describe("ConnPool", func() { Dialer: dummyDialer, PoolSize: 10, PoolTimeout: time.Hour, + DialTimeout: 1 * time.Second, ConnMaxIdleTime: time.Millisecond, }) }) @@ -46,6 +47,7 @@ var _ = Describe("ConnPool", func() { }, PoolSize: 10, PoolTimeout: time.Hour, + DialTimeout: 1 * time.Second, ConnMaxIdleTime: time.Millisecond, MinIdleConns: minIdleConns, }) @@ -129,6 +131,7 @@ var _ = Describe("MinIdleConns", func() { PoolSize: poolSize, MinIdleConns: minIdleConns, PoolTimeout: 100 * time.Millisecond, + DialTimeout: 1 * time.Second, ConnMaxIdleTime: -1, }) Eventually(func() int { @@ -306,6 +309,7 @@ var _ = Describe("race", func() { Dialer: dummyDialer, PoolSize: 10, PoolTimeout: time.Minute, + DialTimeout: 1 * time.Second, ConnMaxIdleTime: time.Millisecond, }) @@ -336,6 +340,7 @@ var _ = Describe("race", func() { PoolSize: 1000, MinIdleConns: 50, PoolTimeout: 3 * time.Second, + DialTimeout: 1 * time.Second, } p := pool.NewConnPool(opt) diff --git a/options.go b/options.go index 3f0661d0..df361f5d 100644 --- a/options.go +++ b/options.go @@ -45,7 +45,7 @@ type Options struct { // Network and Addr options. Dialer func(ctx context.Context, network, addr string) (net.Conn, error) - // OnConnect Hook that is called when new connection is established. + // Hook that is called when new connection is established. OnConnect func(ctx context.Context, cn *Conn) error // Protocol 2 or 3. Use the version to negotiate RESP version with redis-server. @@ -188,9 +188,19 @@ type Options struct { // readOnly enables read only queries on slave/follower nodes. readOnly bool - // DisableIndentity set-lib on connect. Default is false. + // DisableIndentity - Disable set-lib on connect. + // + // default: false + // + // Deprecated: Use DisableIdentity instead. DisableIndentity bool + // DisableIdentity is used to disable CLIENT SETINFO command on connect. + // + // default: false + DisableIdentity bool + + // Add suffix to client name. Default is empty. // IdentitySuffix - add suffix to client name. IdentitySuffix string diff --git a/osscluster.go b/osscluster.go index 1e9ee7de..b018cc9e 100644 --- a/osscluster.go +++ b/osscluster.go @@ -90,8 +90,19 @@ type ClusterOptions struct { ConnMaxIdleTime time.Duration ConnMaxLifetime time.Duration - TLSConfig *tls.Config - DisableIndentity bool // Disable set-lib on connect. Default is false. + TLSConfig *tls.Config + + // DisableIndentity - Disable set-lib on connect. + // + // default: false + // + // Deprecated: Use DisableIdentity instead. + DisableIndentity bool + + // DisableIdentity is used to disable CLIENT SETINFO command on connect. + // + // default: false + DisableIdentity bool IdentitySuffix string // Add suffix to client name. Default is empty. @@ -303,7 +314,8 @@ func (opt *ClusterOptions) clientOptions() *Options { MaxActiveConns: opt.MaxActiveConns, ConnMaxIdleTime: opt.ConnMaxIdleTime, ConnMaxLifetime: opt.ConnMaxLifetime, - DisableIndentity: opt.DisableIndentity, + DisableIdentity: opt.DisableIdentity, + DisableIndentity: opt.DisableIdentity, IdentitySuffix: opt.IdentitySuffix, TLSConfig: opt.TLSConfig, // If ClusterSlots is populated, then we probably have an artificial diff --git a/redis.go b/redis.go index 6116d720..c2a18051 100644 --- a/redis.go +++ b/redis.go @@ -322,7 +322,7 @@ func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error { // for redis-server versions that do not support the HELLO command, // RESP2 will continue to be used. - if err = conn.Hello(ctx, protocol, username, password, "").Err(); err == nil { + if err = conn.Hello(ctx, protocol, username, password, c.opt.ClientName).Err(); err == nil { authenticated = true } else if !isRedisError(err) { // When the server responds with the RESP protocol and the result is not a normal @@ -360,7 +360,7 @@ func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error { return err } - if !c.opt.DisableIndentity { + if !c.opt.DisableIdentity && !c.opt.DisableIndentity { libName := "" libVer := Version() if c.opt.IdentitySuffix != "" { @@ -369,7 +369,11 @@ func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error { p := conn.Pipeline() p.ClientSetInfo(ctx, WithLibraryName(libName)) p.ClientSetInfo(ctx, WithLibraryVersion(libVer)) - _, _ = p.Exec(ctx) + // Handle network errors (e.g. timeouts) in CLIENT SETINFO to avoid + // out of order responses later on. + if _, err = p.Exec(ctx); err != nil && !isRedisError(err) { + return err + } } if c.opt.OnConnect != nil { diff --git a/redis_test.go b/redis_test.go index f8c91b4a..7d9bf1ce 100644 --- a/redis_test.go +++ b/redis_test.go @@ -186,6 +186,32 @@ var _ = Describe("Client", func() { Expect(val).Should(ContainSubstring("name=hi")) }) + It("should attempt to set client name in HELLO", func() { + opt := redisOptions() + opt.ClientName = "hi" + db := redis.NewClient(opt) + + defer func() { + Expect(db.Close()).NotTo(HaveOccurred()) + }() + + // Client name should be already set on any successfully initialized connection + name, err := db.ClientGetName(ctx).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(name).Should(Equal("hi")) + + // HELLO should be able to explicitly overwrite the client name + conn := db.Conn() + hello, err := conn.Hello(ctx, 3, "", "", "hi2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(hello["proto"]).Should(Equal(int64(3))) + name, err = conn.ClientGetName(ctx).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(name).Should(Equal("hi2")) + err = conn.Close() + Expect(err).NotTo(HaveOccurred()) + }) + It("should client PROTO 2", func() { opt := redisOptions() opt.Protocol = 2 @@ -370,6 +396,13 @@ var _ = Describe("Client timeout", func() { }) testTimeout := func() { + It("SETINFO timeouts", func() { + conn := client.Conn() + err := conn.Ping(ctx).Err() + Expect(err).To(HaveOccurred()) + Expect(err.(net.Error).Timeout()).To(BeTrue()) + }) + It("Ping timeouts", func() { err := client.Ping(ctx).Err() Expect(err).To(HaveOccurred()) diff --git a/ring.go b/ring.go index 06a26020..0ff3f75b 100644 --- a/ring.go +++ b/ring.go @@ -98,9 +98,19 @@ type RingOptions struct { TLSConfig *tls.Config Limiter Limiter + // DisableIndentity - Disable set-lib on connect. + // + // default: false + // + // Deprecated: Use DisableIdentity instead. DisableIndentity bool - IdentitySuffix string - UnstableResp3 bool + + // DisableIdentity is used to disable CLIENT SETINFO command on connect. + // + // default: false + DisableIdentity bool + IdentitySuffix string + UnstableResp3 bool } func (opt *RingOptions) init() { @@ -167,9 +177,11 @@ func (opt *RingOptions) clientOptions() *Options { TLSConfig: opt.TLSConfig, Limiter: opt.Limiter, + DisableIdentity: opt.DisableIdentity, DisableIndentity: opt.DisableIndentity, - IdentitySuffix: opt.IdentitySuffix, - UnstableResp3: opt.UnstableResp3, + + IdentitySuffix: opt.IdentitySuffix, + UnstableResp3: opt.UnstableResp3, } } diff --git a/search_commands.go b/search_commands.go index c50ac07f..8be39d2a 100644 --- a/search_commands.go +++ b/search_commands.go @@ -604,6 +604,8 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) AggregateQuery if options.DialectVersion > 0 { queryArgs = append(queryArgs, "DIALECT", options.DialectVersion) + } else { + queryArgs = append(queryArgs, "DIALECT", 2) } } return queryArgs @@ -801,6 +803,8 @@ func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query st } if options.DialectVersion > 0 { args = append(args, "DIALECT", options.DialectVersion) + } else { + args = append(args, "DIALECT", 2) } } @@ -1174,6 +1178,8 @@ func (c cmdable) FTExplainWithArgs(ctx context.Context, index string, query stri args := []interface{}{"FT.EXPLAIN", index, query} if options.Dialect != "" { args = append(args, "DIALECT", options.Dialect) + } else { + args = append(args, "DIALECT", 2) } cmd := NewStringCmd(ctx, args...) _ = c(ctx, cmd) @@ -1471,6 +1477,8 @@ func (c cmdable) FTSpellCheckWithArgs(ctx context.Context, index string, query s } if options.Dialect > 0 { args = append(args, "DIALECT", options.Dialect) + } else { + args = append(args, "DIALECT", 2) } } cmd := newFTSpellCheckCmd(ctx, args...) @@ -1840,6 +1848,8 @@ func FTSearchQuery(query string, options *FTSearchOptions) SearchQuery { } if options.DialectVersion > 0 { queryArgs = append(queryArgs, "DIALECT", options.DialectVersion) + } else { + queryArgs = append(queryArgs, "DIALECT", 2) } } return queryArgs @@ -1955,6 +1965,8 @@ func (c cmdable) FTSearchWithArgs(ctx context.Context, index string, query strin } if options.DialectVersion > 0 { args = append(args, "DIALECT", options.DialectVersion) + } else { + args = append(args, "DIALECT", 2) } } cmd := newFTSearchCmd(ctx, options, args...) diff --git a/search_test.go b/search_test.go index d309b1a8..296f5bd8 100644 --- a/search_test.go +++ b/search_test.go @@ -1143,6 +1143,55 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Expect(res.Docs[0].Fields["__v_score"]).To(BeEquivalentTo("0")) }) + It("should FTCreate VECTOR with dialect 1 ", Label("search", "ftcreate"), func() { + hnswOptions := &redis.FTHNSWOptions{Type: "FLOAT32", Dim: 2, DistanceMetric: "L2"} + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{HNSWOptions: hnswOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.HSet(ctx, "a", "v", "aaaaaaaa") + client.HSet(ctx, "b", "v", "aaaabaaa") + client.HSet(ctx, "c", "v", "aaaaabaa") + + searchOptions := &redis.FTSearchOptions{ + Return: []redis.FTSearchReturn{{FieldName: "v"}}, + SortBy: []redis.FTSearchSortBy{{FieldName: "v", Asc: true}}, + Limit: 10, + DialectVersion: 1, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Docs[0].ID).To(BeEquivalentTo("a")) + Expect(res.Docs[0].Fields["v"]).To(BeEquivalentTo("aaaaaaaa")) + }) + + It("should FTCreate VECTOR with default dialect", Label("search", "ftcreate"), func() { + hnswOptions := &redis.FTHNSWOptions{Type: "FLOAT32", Dim: 2, DistanceMetric: "L2"} + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "v", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{HNSWOptions: hnswOptions}}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + client.HSet(ctx, "a", "v", "aaaaaaaa") + client.HSet(ctx, "b", "v", "aaaabaaa") + client.HSet(ctx, "c", "v", "aaaaabaa") + + searchOptions := &redis.FTSearchOptions{ + Return: []redis.FTSearchReturn{{FieldName: "__v_score"}}, + SortBy: []redis.FTSearchSortBy{{FieldName: "__v_score", Asc: true}}, + Params: map[string]interface{}{"vec": "aaaaaaaa"}, + } + res, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 2 @v $vec]", searchOptions).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res.Docs[0].ID).To(BeEquivalentTo("a")) + Expect(res.Docs[0].Fields["__v_score"]).To(BeEquivalentTo("0")) + }) + It("should FTCreate and FTSearch text params", Label("search", "ftcreate", "ftsearch"), func() { val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "name", FieldType: redis.SearchFieldTypeText}).Result() Expect(err).NotTo(HaveOccurred()) @@ -1577,6 +1626,63 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Expect(res.Docs[0].ID).To(BeEquivalentTo("property:1")) Expect(res.Docs[1].ID).To(BeEquivalentTo("property:2")) }) + + It("should FTCreate VECTOR with int8 and uint8 types", Label("search", "ftcreate"), func() { + SkipBeforeRedisVersion(7.9, "doesn't work with older redis") + // Define INT8 vector field + hnswOptionsInt8 := &redis.FTHNSWOptions{ + Type: "INT8", + Dim: 2, + DistanceMetric: "L2", + } + + // Define UINT8 vector field + hnswOptionsUint8 := &redis.FTHNSWOptions{ + Type: "UINT8", + Dim: 2, + DistanceMetric: "L2", + } + + // Create index with INT8 and UINT8 vector fields + val, err := client.FTCreate(ctx, "idx1", + &redis.FTCreateOptions{}, + &redis.FieldSchema{FieldName: "int8_vector", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{HNSWOptions: hnswOptionsInt8}}, + &redis.FieldSchema{FieldName: "uint8_vector", FieldType: redis.SearchFieldTypeVector, VectorArgs: &redis.FTVectorArgs{HNSWOptions: hnswOptionsUint8}}, + ).Result() + + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "idx1") + + // Insert vectors in int8 and uint8 format + client.HSet(ctx, "doc1", "int8_vector", "\x01\x02", "uint8_vector", "\x01\x02") + client.HSet(ctx, "doc2", "int8_vector", "\x03\x04", "uint8_vector", "\x03\x04") + + // Perform KNN search on INT8 vector + searchOptionsInt8 := &redis.FTSearchOptions{ + Return: []redis.FTSearchReturn{{FieldName: "int8_vector"}}, + SortBy: []redis.FTSearchSortBy{{FieldName: "int8_vector", Asc: true}}, + DialectVersion: 2, + Params: map[string]interface{}{"vec": "\x01\x02"}, + } + + resInt8, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 1 @int8_vector $vec]", searchOptionsInt8).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resInt8.Docs[0].ID).To(BeEquivalentTo("doc1")) + + // Perform KNN search on UINT8 vector + searchOptionsUint8 := &redis.FTSearchOptions{ + Return: []redis.FTSearchReturn{{FieldName: "uint8_vector"}}, + SortBy: []redis.FTSearchSortBy{{FieldName: "uint8_vector", Asc: true}}, + DialectVersion: 2, + Params: map[string]interface{}{"vec": "\x01\x02"}, + } + + resUint8, err := client.FTSearchWithArgs(ctx, "idx1", "*=>[KNN 1 @uint8_vector $vec]", searchOptionsUint8).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resUint8.Docs[0].ID).To(BeEquivalentTo("doc1")) + }) + }) func _assert_geosearch_result(result *redis.FTSearchResult, expectedDocIDs []string) { diff --git a/sentinel.go b/sentinel.go index 31569554..a4c9f53c 100644 --- a/sentinel.go +++ b/sentinel.go @@ -80,9 +80,20 @@ type FailoverOptions struct { TLSConfig *tls.Config + // DisableIndentity - Disable set-lib on connect. + // + // default: false + // + // Deprecated: Use DisableIdentity instead. DisableIndentity bool - IdentitySuffix string - UnstableResp3 bool + + // DisableIdentity is used to disable CLIENT SETINFO command on connect. + // + // default: false + DisableIdentity bool + + IdentitySuffix string + UnstableResp3 bool } func (opt *FailoverOptions) clientOptions() *Options { @@ -118,9 +129,11 @@ func (opt *FailoverOptions) clientOptions() *Options { TLSConfig: opt.TLSConfig, + DisableIdentity: opt.DisableIdentity, DisableIndentity: opt.DisableIndentity, - IdentitySuffix: opt.IdentitySuffix, - UnstableResp3: opt.UnstableResp3, + + IdentitySuffix: opt.IdentitySuffix, + UnstableResp3: opt.UnstableResp3, } } @@ -156,9 +169,11 @@ func (opt *FailoverOptions) sentinelOptions(addr string) *Options { TLSConfig: opt.TLSConfig, + DisableIdentity: opt.DisableIdentity, DisableIndentity: opt.DisableIndentity, - IdentitySuffix: opt.IdentitySuffix, - UnstableResp3: opt.UnstableResp3, + + IdentitySuffix: opt.IdentitySuffix, + UnstableResp3: opt.UnstableResp3, } } @@ -197,8 +212,10 @@ func (opt *FailoverOptions) clusterOptions() *ClusterOptions { TLSConfig: opt.TLSConfig, + DisableIdentity: opt.DisableIdentity, DisableIndentity: opt.DisableIndentity, - IdentitySuffix: opt.IdentitySuffix, + + IdentitySuffix: opt.IdentitySuffix, } } diff --git a/universal.go b/universal.go index 3e3367e3..3d91dd49 100644 --- a/universal.go +++ b/universal.go @@ -61,14 +61,24 @@ type UniversalOptions struct { RouteByLatency bool RouteRandomly bool - // The sentinel master name. - // Only failover clients. - + // MasterName is the sentinel master name. + // Only for failover clients. MasterName string + // DisableIndentity - Disable set-lib on connect. + // + // default: false + // + // Deprecated: Use DisableIdentity instead. DisableIndentity bool - IdentitySuffix string - UnstableResp3 bool + + // DisableIdentity is used to disable CLIENT SETINFO command on connect. + // + // default: false + DisableIdentity bool + + IdentitySuffix string + UnstableResp3 bool // IsClusterMode can be used when only one Addrs is provided (e.g. Elasticache supports setting up cluster mode with configuration endpoint). IsClusterMode bool @@ -116,6 +126,7 @@ func (o *UniversalOptions) Cluster() *ClusterOptions { TLSConfig: o.TLSConfig, + DisableIdentity: o.DisableIdentity, DisableIndentity: o.DisableIndentity, IdentitySuffix: o.IdentitySuffix, UnstableResp3: o.UnstableResp3, @@ -143,6 +154,9 @@ func (o *UniversalOptions) Failover() *FailoverOptions { SentinelUsername: o.SentinelUsername, SentinelPassword: o.SentinelPassword, + RouteByLatency: o.RouteByLatency, + RouteRandomly: o.RouteRandomly, + MaxRetries: o.MaxRetries, MinRetryBackoff: o.MinRetryBackoff, MaxRetryBackoff: o.MaxRetryBackoff, @@ -163,8 +177,9 @@ func (o *UniversalOptions) Failover() *FailoverOptions { TLSConfig: o.TLSConfig, - ReplicaOnly: o.ReadOnly, + ReplicaOnly: o.ReadOnly, + DisableIdentity: o.DisableIdentity, DisableIndentity: o.DisableIndentity, IdentitySuffix: o.IdentitySuffix, UnstableResp3: o.UnstableResp3, @@ -209,6 +224,7 @@ func (o *UniversalOptions) Simple() *Options { TLSConfig: o.TLSConfig, + DisableIdentity: o.DisableIdentity, DisableIndentity: o.DisableIndentity, IdentitySuffix: o.IdentitySuffix, UnstableResp3: o.UnstableResp3, @@ -243,14 +259,22 @@ var ( // NewUniversalClient returns a new multi client. The type of the returned client depends // on the following conditions: // -// 1. If the MasterName option is specified, a sentinel-backed FailoverClient is returned. -// 2. if the number of Addrs is two or more, a ClusterClient is returned. -// 3. Otherwise, a single-node Client is returned. +// 1. If the MasterName option is specified with RouteByLatency, RouteRandomly or IsClusterMode, +// a FailoverClusterClient is returned. +// 2. If the MasterName option is specified without RouteByLatency, RouteRandomly or IsClusterMode, +// a sentinel-backed FailoverClient is returned. +// 3. If the number of Addrs is two or more, or IsClusterMode option is specified, +// a ClusterClient is returned. +// 4. Otherwise, a single-node Client is returned. func NewUniversalClient(opts *UniversalOptions) UniversalClient { - if opts.MasterName != "" { + switch { + case opts.MasterName != "" && (opts.RouteByLatency || opts.RouteRandomly || opts.IsClusterMode): + return NewFailoverClusterClient(opts.Failover()) + case opts.MasterName != "": return NewFailoverClient(opts.Failover()) - } else if len(opts.Addrs) > 1 || opts.IsClusterMode { + case len(opts.Addrs) > 1 || opts.IsClusterMode: return NewClusterClient(opts.Cluster()) + default: + return NewClient(opts.Simple()) } - return NewClient(opts.Simple()) } diff --git a/universal_test.go b/universal_test.go index e389fe4f..f965253f 100644 --- a/universal_test.go +++ b/universal_test.go @@ -24,6 +24,16 @@ var _ = Describe("UniversalClient", func() { Expect(client.Ping(ctx).Err()).NotTo(HaveOccurred()) }) + It("should connect to failover cluster", Label("NonRedisEnterprise"), func() { + client = redis.NewUniversalClient(&redis.UniversalOptions{ + MasterName: sentinelName, + RouteRandomly: true, + Addrs: sentinelAddrs, + }) + _, ok := client.(*redis.ClusterClient) + Expect(ok).To(BeTrue(), "expected a ClusterClient") + }) + It("should connect to simple servers", func() { client = redis.NewUniversalClient(&redis.UniversalOptions{ Addrs: []string{redisAddr}, @@ -79,6 +89,7 @@ var _ = Describe("UniversalClient", func() { err = client.Set(ctx, "somekey", "somevalue", 0).Err() Expect(err).To(HaveOccurred()) }) + It("should connect to clusters if IsClusterMode is set even if only a single address is provided", Label("NonRedisEnterprise"), func() { client = redis.NewUniversalClient(&redis.UniversalOptions{ Addrs: []string{cluster.addrs()[0]}, @@ -96,4 +107,3 @@ var _ = Describe("UniversalClient", func() { Expect(client.ClusterSlots(ctx).Val()).To(HaveLen(3)) }) }) - diff --git a/version.go b/version.go index a447a546..a4832fc1 100644 --- a/version.go +++ b/version.go @@ -2,5 +2,5 @@ package redis // Version is the current release version. func Version() string { - return "9.7.1" + return "9.7.3" }