fix(deps): update module github.com/redis/go-redis/v9 to v9.8.0 (main) (#17529)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
pull/17532/head
renovate[bot] 3 weeks ago committed by GitHub
parent 5eb340b330
commit 300c8554fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      go.mod
  2. 4
      go.sum
  3. 6
      vendor/github.com/redis/go-redis/v9/.gitignore
  4. 31
      vendor/github.com/redis/go-redis/v9/.golangci.yml
  5. 27
      vendor/github.com/redis/go-redis/v9/CONTRIBUTING.md
  6. 39
      vendor/github.com/redis/go-redis/v9/Makefile
  7. 83
      vendor/github.com/redis/go-redis/v9/README.md
  8. 80
      vendor/github.com/redis/go-redis/v9/RELEASE-NOTES.md
  9. 54
      vendor/github.com/redis/go-redis/v9/acl_commands.go
  10. 7
      vendor/github.com/redis/go-redis/v9/cluster_commands.go
  11. 114
      vendor/github.com/redis/go-redis/v9/command.go
  12. 17
      vendor/github.com/redis/go-redis/v9/commands.go
  13. 106
      vendor/github.com/redis/go-redis/v9/docker-compose.yml
  14. 15
      vendor/github.com/redis/go-redis/v9/error.go
  15. 149
      vendor/github.com/redis/go-redis/v9/gears_commands.go
  16. 175
      vendor/github.com/redis/go-redis/v9/hash_commands.go
  17. 12
      vendor/github.com/redis/go-redis/v9/internal/pool/pool.go
  18. 53
      vendor/github.com/redis/go-redis/v9/internal/proto/writer.go
  19. 10
      vendor/github.com/redis/go-redis/v9/options.go
  20. 39
      vendor/github.com/redis/go-redis/v9/osscluster.go
  21. 72
      vendor/github.com/redis/go-redis/v9/probabilistic.go
  22. 5
      vendor/github.com/redis/go-redis/v9/pubsub.go
  23. 5
      vendor/github.com/redis/go-redis/v9/redis.go
  24. 11
      vendor/github.com/redis/go-redis/v9/ring.go
  25. 331
      vendor/github.com/redis/go-redis/v9/search_commands.go
  26. 87
      vendor/github.com/redis/go-redis/v9/sentinel.go
  27. 31
      vendor/github.com/redis/go-redis/v9/universal.go
  28. 2
      vendor/github.com/redis/go-redis/v9/version.go
  29. 2
      vendor/modules.txt

@ -87,7 +87,7 @@ require (
github.com/prometheus/client_model v0.6.2
github.com/prometheus/common v0.62.0
github.com/prometheus/prometheus v0.302.1
github.com/redis/go-redis/v9 v9.7.3
github.com/redis/go-redis/v9 v9.8.0
github.com/segmentio/fasthash v1.0.3
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c
github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92

@ -1127,8 +1127,8 @@ github.com/prometheus/sigv4 v0.1.2/go.mod h1:GF9fwrvLgkQwDdQ5BXeV9XUSCH/IPNqzvAo
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI=
github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/redis/rueidis v1.0.19 h1:s65oWtotzlIFN8eMPhyYwxlwLR1lUdhza2KtWprKYSo=
github.com/redis/rueidis v1.0.19/go.mod h1:8B+r5wdnjwK3lTFml5VtxjzGOQAC+5UmujoD12pDrEo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=

@ -3,4 +3,8 @@ testdata/*
.idea/
.DS_Store
*.tar.gz
*.dic
*.dic
redis8tests.sh
coverage.txt
**/coverage.txt
.vscode

@ -1,3 +1,34 @@
version: "2"
run:
timeout: 5m
tests: false
linters:
settings:
staticcheck:
checks:
- all
# Incorrect or missing package comment.
# https://staticcheck.dev/docs/checks/#ST1000
- -ST1000
# Omit embedded fields from selector expression.
# https://staticcheck.dev/docs/checks/#QF1008
- -QF1008
- -ST1003
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
formatters:
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$

@ -32,20 +32,33 @@ Here's how to get started with your code contribution:
1. Create your own fork of go-redis
2. Do the changes in your fork
3. If you need a development environment, run `make test`. Note: this clones and builds the latest release of [redis](https://redis.io). You also need a redis-stack-server docker, in order to run the capabilities tests. This can be started by running:
```docker run -p 6379:6379 -it redis/redis-stack-server:edge```
4. While developing, make sure the tests pass by running `make tests`
3. If you need a development environment, run `make docker.start`.
> Note: this clones and builds the docker containers specified in `docker-compose.yml`, to understand more about
> the infrastructure that will be started you can check the `docker-compose.yml`. You also have the possiblity
> to specify the redis image that will be pulled with the env variable `CLIENT_LIBS_TEST_IMAGE`.
> By default the docker image that will be pulled and started is `redislabs/client-libs-test:rs-7.4.0-v2`.
> If you want to test with newer Redis version, using a newer version of `redislabs/client-libs-test` should work out of the box.
4. While developing, make sure the tests pass by running `make test` (if you have the docker containers running, `make test.ci` may be sufficient).
> Note: `make test` will try to start all containers, run the tests with `make test.ci` and then stop all containers.
5. If you like the change and think the project could use it, send a
pull request
To see what else is part of the automation, run `invoke -l`
## Testing
Call `make test` to run all tests, including linters.
### Setting up Docker
To run the tests, you need to have Docker installed and running. If you are using a host OS that does not support
docker host networks out of the box (e.g. Windows, OSX), you need to set up a docker desktop and enable docker host networks.
### Running tests
Call `make test` to run all tests.
Continuous Integration uses these same wrappers to run all of these
tests against multiple versions of python. Feel free to test your
tests against multiple versions of redis. Feel free to test your
changes against all the go versions supported, as declared by the
[build.yml](./.github/workflows/build.yml) file.
@ -99,3 +112,7 @@ The core team regularly looks at pull requests. We will provide
feedback as soon as possible. After receiving our feedback, please respond
within two weeks. After that time, we may close your PR if it isn't
showing any activity.
## Support
Maintainers can provide limited support to contributors on discord: https://discord.gg/W4txy5AeKM

@ -1,42 +1,35 @@
GO_MOD_DIRS := $(shell find . -type f -name 'go.mod' -exec dirname {} \; | sort)
test: testdeps
$(eval GO_VERSION := $(shell go version | cut -d " " -f 3 | cut -d. -f2))
docker.start:
docker compose --profile all up -d --quiet-pull
docker.stop:
docker compose --profile all down
test:
$(MAKE) docker.start
$(MAKE) test.ci
$(MAKE) docker.stop
test.ci:
set -e; for dir in $(GO_MOD_DIRS); do \
if echo "$${dir}" | grep -q "./example" && [ "$(GO_VERSION)" = "19" ]; then \
echo "Skipping go test in $${dir} due to Go version 1.19 and dir contains ./example"; \
continue; \
fi; \
echo "go test in $${dir}"; \
(cd "$${dir}" && \
go mod tidy -compat=1.18 && \
go test && \
go test ./... -short -race && \
go test ./... -run=NONE -bench=. -benchmem && \
env GOOS=linux GOARCH=386 go test && \
go test -coverprofile=coverage.txt -covermode=atomic ./... && \
go vet); \
go vet && \
go test -v -coverprofile=coverage.txt -covermode=atomic ./... -race); \
done
cd internal/customvet && go build .
go vet -vettool ./internal/customvet/customvet
testdeps: testdata/redis/src/redis-server
bench: testdeps
bench:
go test ./... -test.run=NONE -test.bench=. -test.benchmem
.PHONY: all test testdeps bench fmt
.PHONY: all test bench fmt
build:
go build .
testdata/redis:
mkdir -p $@
wget -qO- https://download.redis.io/releases/redis-7.4-rc2.tar.gz | tar xvz --strip-components=1 -C $@
testdata/redis/src/redis-server: testdata/redis
cd $< && make all
fmt:
gofumpt -w ./
goimports -w -local github.com/redis/go-redis ./

@ -3,16 +3,30 @@
[![build workflow](https://github.com/redis/go-redis/actions/workflows/build.yml/badge.svg)](https://github.com/redis/go-redis/actions)
[![PkgGoDev](https://pkg.go.dev/badge/github.com/redis/go-redis/v9)](https://pkg.go.dev/github.com/redis/go-redis/v9?tab=doc)
[![Documentation](https://img.shields.io/badge/redis-documentation-informational)](https://redis.uptrace.dev/)
[![Go Report Card](https://goreportcard.com/badge/github.com/redis/go-redis/v9)](https://goreportcard.com/report/github.com/redis/go-redis/v9)
[![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.
[![Discord](https://img.shields.io/discord/697882427875393627.svg?style=social&logo=discord)](https://discord.gg/W4txy5AeKM)
[![Twitch](https://img.shields.io/twitch/status/redisinc?style=social)](https://www.twitch.tv/redisinc)
[![YouTube](https://img.shields.io/youtube/channel/views/UCD78lHSwYqMlyetR0_P4Vig?style=social)](https://www.youtube.com/redisinc)
[![Twitter](https://img.shields.io/twitter/follow/redisinc?style=social)](https://twitter.com/redisinc)
[![Stack Exchange questions](https://img.shields.io/stackexchange/stackoverflow/t/go-redis?style=social&logo=stackoverflow&label=Stackoverflow)](https://stackoverflow.com/questions/tagged/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
In `go-redis` we are aiming to support the last three releases of Redis. Currently, this means we do support:
- [Redis 7.2](https://raw.githubusercontent.com/redis/redis/7.2/00-RELEASENOTES) - using Redis Stack 7.2 for modules support
- [Redis 7.4](https://raw.githubusercontent.com/redis/redis/7.4/00-RELEASENOTES) - using Redis Stack 7.4 for modules support
- [Redis 8.0](https://raw.githubusercontent.com/redis/redis/8.0/00-RELEASENOTES) - using Redis CE 8.0 where modules are included
Although the `go.mod` states it requires at minimum `go 1.18`, our CI is configured to run the tests against all three
versions of Redis and latest two versions of Go ([1.23](https://go.dev/doc/devel/release#go1.23.0),
[1.24](https://go.dev/doc/devel/release#go1.24.0)). We observe that some modules related test may not pass with
Redis Stack 7.2 and some commands are changed with Redis CE 8.0.
Please do refer to the documentation and the tests if you experience any issues. We do plan to update the go version
in the `go.mod` to `go 1.24` in one of the next releases.
## How do I Redis?
@ -36,7 +50,7 @@
## Resources
- [Discussions](https://github.com/redis/go-redis/discussions)
- [Chat](https://discord.gg/rWtp5Aj)
- [Chat](https://discord.gg/W4txy5AeKM)
- [Reference](https://pkg.go.dev/github.com/redis/go-redis/v9)
- [Examples](https://pkg.go.dev/github.com/redis/go-redis/v9#pkg-examples)
@ -159,6 +173,24 @@ func ExampleClient() *redis.Client {
```
### Instrument with OpenTelemetry
```go
import (
"github.com/redis/go-redis/v9"
"github.com/redis/go-redis/extra/redisotel/v9"
"errors"
)
func main() {
...
rdb := redis.NewClient(&redis.Options{...})
if err := errors.Join(redisotel.InstrumentTracing(rdb), redisotel.InstrumentMetrics(rdb)); err != nil {
log.Fatal(err)
}
```
### Advanced Configuration
@ -203,9 +235,30 @@ res1, err := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptio
val1 := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptions{}).RawVal()
```
## Contributing
#### 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.
**Important**: Be aware that the query dialect may impact the results returned. If needed, you can revert to a different dialect version by passing the desired dialect in the arguments of the command you want to execute.
For example:
```
res2, err := rdb.FTSearchWithArgs(ctx,
"idx:bicycle",
"@pickup_zone:[CONTAINS $bike]",
&redis.FTSearchOptions{
Params: map[string]interface{}{
"bike": "POINT(-0.1278 51.5074)",
},
DialectVersion: 3,
},
).Result()
```
You can find further details in the [query dialect documentation](https://redis.io/docs/latest/develop/interact/search-and-query/advanced-concepts/dialects/).
Please see [out contributing guidelines](CONTRIBUTING.md) to help us improve this library!
## Contributing
We welcome contributions to the go-redis library! If you have a bug fix, feature request, or improvement, please open an issue or pull request on GitHub.
We appreciate your help in making go-redis better for everyone.
If you are interested in contributing to the go-redis library, please check out our [contributing guidelines](CONTRIBUTING.md) for more information on how to get started.
## Look and feel
@ -285,6 +338,14 @@ REDIS_PORT=9999 go test <your options>
## 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!
<a href="https://github.com/redis/go-redis/graphs/contributors">

@ -0,0 +1,80 @@
# Release Notes
# 9.8.0 (2025-04-30)
## 🚀 Highlights
- **Redis 8 Support**: Full compatibility with Redis 8.0, including testing and CI integration
- **Enhanced Hash Operations**: Added support for new hash commands (`HGETDEL`, `HGETEX`, `HSETEX`) and `HSTRLEN` command
- **Search Improvements**: Enabled Search DIALECT 2 by default and added `CountOnly` argument for `FT.Search`
## ✨ New Features
- Added support for new hash commands: `HGETDEL`, `HGETEX`, `HSETEX` ([#3305](https://github.com/redis/go-redis/pull/3305))
- Added `HSTRLEN` command for hash operations ([#2843](https://github.com/redis/go-redis/pull/2843))
- Added `Do` method for raw query by single connection from `pool.Conn()` ([#3182](https://github.com/redis/go-redis/pull/3182))
- Prevent false-positive marshaling by treating zero time.Time as empty in isEmptyValue ([#3273](https://github.com/redis/go-redis/pull/3273))
- Added FailoverClusterClient support for Universal client ([#2794](https://github.com/redis/go-redis/pull/2794))
- Added support for cluster mode with `IsClusterMode` config parameter ([#3255](https://github.com/redis/go-redis/pull/3255))
- Added client name support in `HELLO` RESP handshake ([#3294](https://github.com/redis/go-redis/pull/3294))
- **Enabled Search DIALECT 2 by default** ([#3213](https://github.com/redis/go-redis/pull/3213))
- Added read-only option for failover configurations ([#3281](https://github.com/redis/go-redis/pull/3281))
- Added `CountOnly` argument for `FT.Search` to use `LIMIT 0 0` ([#3338](https://github.com/redis/go-redis/pull/3338))
- Added `DB` option support in `NewFailoverClusterClient` ([#3342](https://github.com/redis/go-redis/pull/3342))
- Added `nil` check for the options when creating a client ([#3363](https://github.com/redis/go-redis/pull/3363))
## 🐛 Bug Fixes
- Fixed `PubSub` concurrency safety issues ([#3360](https://github.com/redis/go-redis/pull/3360))
- Fixed panic caused when argument is `nil` ([#3353](https://github.com/redis/go-redis/pull/3353))
- Improved error handling when fetching master node from sentinels ([#3349](https://github.com/redis/go-redis/pull/3349))
- Fixed connection pool timeout issues and increased retries ([#3298](https://github.com/redis/go-redis/pull/3298))
- Fixed context cancellation error leading to connection spikes on Primary instances ([#3190](https://github.com/redis/go-redis/pull/3190))
- Fixed RedisCluster client to consider `MASTERDOWN` a retriable error ([#3164](https://github.com/redis/go-redis/pull/3164))
- Fixed tracing to show complete commands instead of truncated versions ([#3290](https://github.com/redis/go-redis/pull/3290))
- Fixed OpenTelemetry instrumentation to prevent multiple span reporting ([#3168](https://github.com/redis/go-redis/pull/3168))
- Fixed `FT.Search` Limit argument and added `CountOnly` argument for limit 0 0 ([#3338](https://github.com/redis/go-redis/pull/3338))
- Fixed missing command in interface ([#3344](https://github.com/redis/go-redis/pull/3344))
- Fixed slot calculation for `COUNTKEYSINSLOT` command ([#3327](https://github.com/redis/go-redis/pull/3327))
- Updated PubSub implementation with correct context ([#3329](https://github.com/redis/go-redis/pull/3329))
## 📚 Documentation
- Added hash search examples ([#3357](https://github.com/redis/go-redis/pull/3357))
- Fixed documentation comments ([#3351](https://github.com/redis/go-redis/pull/3351))
- Added `CountOnly` search example ([#3345](https://github.com/redis/go-redis/pull/3345))
- Added examples for list commands: `LLEN`, `LPOP`, `LPUSH`, `LRANGE`, `RPOP`, `RPUSH` ([#3234](https://github.com/redis/go-redis/pull/3234))
- Added `SADD` and `SMEMBERS` command examples ([#3242](https://github.com/redis/go-redis/pull/3242))
- Updated `README.md` to use Redis Discord guild ([#3331](https://github.com/redis/go-redis/pull/3331))
- Updated `HExpire` command documentation ([#3355](https://github.com/redis/go-redis/pull/3355))
- Featured OpenTelemetry instrumentation more prominently ([#3316](https://github.com/redis/go-redis/pull/3316))
- Updated `README.md` with additional information ([#310ce55](https://github.com/redis/go-redis/commit/310ce55))
## ⚡ Performance and Reliability
- Bound connection pool background dials to configured dial timeout ([#3089](https://github.com/redis/go-redis/pull/3089))
- Ensured context isn't exhausted via concurrent query ([#3334](https://github.com/redis/go-redis/pull/3334))
## 🔧 Dependencies and Infrastructure
- Updated testing image to Redis 8.0-RC2 ([#3361](https://github.com/redis/go-redis/pull/3361))
- Enabled CI for Redis CE 8.0 ([#3274](https://github.com/redis/go-redis/pull/3274))
- Updated various dependencies:
- Bumped golangci/golangci-lint-action from 6.5.0 to 7.0.0 ([#3354](https://github.com/redis/go-redis/pull/3354))
- Bumped rojopolis/spellcheck-github-actions ([#3336](https://github.com/redis/go-redis/pull/3336))
- Bumped golang.org/x/net in example/otel ([#3308](https://github.com/redis/go-redis/pull/3308))
- Migrated golangci-lint configuration to v2 format ([#3354](https://github.com/redis/go-redis/pull/3354))
## ⚠ Breaking Changes
- **Enabled Search DIALECT 2 by default** ([#3213](https://github.com/redis/go-redis/pull/3213))
- Dropped RedisGears (Triggers and Functions) support ([#3321](https://github.com/redis/go-redis/pull/3321))
- Dropped FT.PROFILE command that was never enabled ([#3323](https://github.com/redis/go-redis/pull/3323))
## 🔒 Security
- Fixed network error handling on SETINFO (CVE-2025-29923) ([#3295](https://github.com/redis/go-redis/pull/3295))
## 🧪 Testing
- Added integration tests for Redis 8 behavior changes in Redis Search ([#3337](https://github.com/redis/go-redis/pull/3337))
- Added vector types INT8 and UINT8 tests ([#3299](https://github.com/redis/go-redis/pull/3299))
- Added test codes for search_commands.go ([#3285](https://github.com/redis/go-redis/pull/3285))
- Fixed example test sorting ([#3292](https://github.com/redis/go-redis/pull/3292))
## 👥 Contributors
We would like to thank all the contributors who made this release possible:
[@alexander-menshchikov](https://github.com/alexander-menshchikov), [@EXPEbdodla](https://github.com/EXPEbdodla), [@afti](https://github.com/afti), [@dmaier-redislabs](https://github.com/dmaier-redislabs), [@four_leaf_clover](https://github.com/four_leaf_clover), [@alohaglenn](https://github.com/alohaglenn), [@gh73962](https://github.com/gh73962), [@justinmir](https://github.com/justinmir), [@LINKIWI](https://github.com/LINKIWI), [@liushuangbill](https://github.com/liushuangbill), [@golang88](https://github.com/golang88), [@gnpaone](https://github.com/gnpaone), [@ndyakov](https://github.com/ndyakov), [@nikolaydubina](https://github.com/nikolaydubina), [@oleglacto](https://github.com/oleglacto), [@andy-stark-redis](https://github.com/andy-stark-redis), [@rodneyosodo](https://github.com/rodneyosodo), [@dependabot](https://github.com/dependabot), [@rfyiamcool](https://github.com/rfyiamcool), [@frankxjkuang](https://github.com/frankxjkuang), [@fukua95](https://github.com/fukua95), [@soleymani-milad](https://github.com/soleymani-milad), [@ofekshenawa](https://github.com/ofekshenawa), [@khasanovbi](https://github.com/khasanovbi)

@ -4,8 +4,20 @@ import "context"
type ACLCmdable interface {
ACLDryRun(ctx context.Context, username string, command ...interface{}) *StringCmd
ACLLog(ctx context.Context, count int64) *ACLLogCmd
ACLLogReset(ctx context.Context) *StatusCmd
ACLSetUser(ctx context.Context, username string, rules ...string) *StatusCmd
ACLDelUser(ctx context.Context, username string) *IntCmd
ACLList(ctx context.Context) *StringSliceCmd
ACLCat(ctx context.Context) *StringSliceCmd
ACLCatArgs(ctx context.Context, options *ACLCatArgs) *StringSliceCmd
}
type ACLCatArgs struct {
Category string
}
func (c cmdable) ACLDryRun(ctx context.Context, username string, command ...interface{}) *StringCmd {
@ -33,3 +45,45 @@ func (c cmdable) ACLLogReset(ctx context.Context) *StatusCmd {
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ACLDelUser(ctx context.Context, username string) *IntCmd {
cmd := NewIntCmd(ctx, "acl", "deluser", username)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ACLSetUser(ctx context.Context, username string, rules ...string) *StatusCmd {
args := make([]interface{}, 3+len(rules))
args[0] = "acl"
args[1] = "setuser"
args[2] = username
for i, rule := range rules {
args[i+3] = rule
}
cmd := NewStatusCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ACLList(ctx context.Context) *StringSliceCmd {
cmd := NewStringSliceCmd(ctx, "acl", "list")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ACLCat(ctx context.Context) *StringSliceCmd {
cmd := NewStringSliceCmd(ctx, "acl", "cat")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ACLCatArgs(ctx context.Context, options *ACLCatArgs) *StringSliceCmd {
// if there is a category passed, build new cmd, if there isn't - use the ACLCat method
if options != nil && options.Category != "" {
cmd := NewStringSliceCmd(ctx, "acl", "cat", options.Category)
_ = c(ctx, cmd)
return cmd
}
return c.ACLCat(ctx)
}

@ -4,6 +4,7 @@ import "context"
type ClusterCmdable interface {
ClusterMyShardID(ctx context.Context) *StringCmd
ClusterMyID(ctx context.Context) *StringCmd
ClusterSlots(ctx context.Context) *ClusterSlotsCmd
ClusterShards(ctx context.Context) *ClusterShardsCmd
ClusterLinks(ctx context.Context) *ClusterLinksCmd
@ -35,6 +36,12 @@ func (c cmdable) ClusterMyShardID(ctx context.Context) *StringCmd {
return cmd
}
func (c cmdable) ClusterMyID(ctx context.Context) *StringCmd {
cmd := NewStringCmd(ctx, "cluster", "myid")
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) ClusterSlots(ctx context.Context) *ClusterSlotsCmd {
cmd := NewClusterSlotsCmd(ctx, "cluster", "slots")
_ = c(ctx, cmd)

@ -1405,27 +1405,64 @@ func (cmd *MapStringSliceInterfaceCmd) Val() map[string][]interface{} {
}
func (cmd *MapStringSliceInterfaceCmd) readReply(rd *proto.Reader) (err error) {
n, err := rd.ReadMapLen()
readType, err := rd.PeekReplyType()
if err != nil {
return err
}
cmd.val = make(map[string][]interface{}, n)
for i := 0; i < n; i++ {
k, err := rd.ReadString()
cmd.val = make(map[string][]interface{})
switch readType {
case proto.RespMap:
n, err := rd.ReadMapLen()
if err != nil {
return err
}
nn, err := rd.ReadArrayLen()
for i := 0; i < n; i++ {
k, err := rd.ReadString()
if err != nil {
return err
}
nn, err := rd.ReadArrayLen()
if err != nil {
return err
}
cmd.val[k] = make([]interface{}, nn)
for j := 0; j < nn; j++ {
value, err := rd.ReadReply()
if err != nil {
return err
}
cmd.val[k][j] = value
}
}
case proto.RespArray:
// RESP2 response
n, err := rd.ReadArrayLen()
if err != nil {
return err
}
cmd.val[k] = make([]interface{}, nn)
for j := 0; j < nn; j++ {
value, err := rd.ReadReply()
for i := 0; i < n; i++ {
// Each entry in this array is itself an array with key details
itemLen, err := rd.ReadArrayLen()
if err != nil {
return err
}
cmd.val[k][j] = value
key, err := rd.ReadString()
if err != nil {
return err
}
cmd.val[key] = make([]interface{}, 0, itemLen-1)
for j := 1; j < itemLen; j++ {
// Read the inner array for timestamp-value pairs
data, err := rd.ReadReply()
if err != nil {
return err
}
cmd.val[key] = append(cmd.val[key], data)
}
}
}
@ -3795,7 +3832,8 @@ func (cmd *MapStringStringSliceCmd) readReply(rd *proto.Reader) error {
}
// -----------------------------------------------------------------------
// MapStringInterfaceCmd represents a command that returns a map of strings to interface{}.
// MapMapStringInterfaceCmd represents a command that returns a map of strings to interface{}.
type MapMapStringInterfaceCmd struct {
baseCmd
val map[string]interface{}
@ -3826,30 +3864,48 @@ func (cmd *MapMapStringInterfaceCmd) Val() map[string]interface{} {
return cmd.val
}
// readReply will try to parse the reply from the proto.Reader for both resp2 and resp3
func (cmd *MapMapStringInterfaceCmd) readReply(rd *proto.Reader) (err error) {
n, err := rd.ReadArrayLen()
data, err := rd.ReadReply()
if err != nil {
return err
}
resultMap := map[string]interface{}{}
data := make(map[string]interface{}, n/2)
for i := 0; i < n; i += 2 {
_, err := rd.ReadArrayLen()
if err != nil {
cmd.err = err
}
key, err := rd.ReadString()
if err != nil {
cmd.err = err
}
value, err := rd.ReadString()
if err != nil {
cmd.err = err
switch midResponse := data.(type) {
case map[interface{}]interface{}: // resp3 will return map
for k, v := range midResponse {
stringKey, ok := k.(string)
if !ok {
return fmt.Errorf("redis: invalid map key %#v", k)
}
resultMap[stringKey] = v
}
case []interface{}: // resp2 will return array of arrays
n := len(midResponse)
for i := 0; i < n; i++ {
finalArr, ok := midResponse[i].([]interface{}) // final array that we need to transform to map
if !ok {
return fmt.Errorf("redis: unexpected response %#v", data)
}
m := len(finalArr)
if m%2 != 0 { // since this should be map, keys should be even number
return fmt.Errorf("redis: unexpected response %#v", data)
}
for j := 0; j < m; j += 2 {
stringKey, ok := finalArr[j].(string) // the first one
if !ok {
return fmt.Errorf("redis: invalid map key %#v", finalArr[i])
}
resultMap[stringKey] = finalArr[j+1] // second one is value
}
}
data[key] = value
default:
return fmt.Errorf("redis: unexpected response %#v", data)
}
cmd.val = data
cmd.val = resultMap
return nil
}
@ -5078,6 +5134,7 @@ type ClientInfo struct {
OutputListLength int // oll, output list length (replies are queued in this list when the buffer is full)
OutputMemory int // omem, output buffer memory usage
TotalMemory int // tot-mem, total memory consumed by this client in its various buffers
IoThread int // io-thread id
Events string // file descriptor events (see below)
LastCmd string // cmd, last command played
User string // the authenticated username of the client
@ -5256,6 +5313,8 @@ func parseClientInfo(txt string) (info *ClientInfo, err error) {
info.LibName = val
case "lib-ver":
info.LibVer = val
case "io-thread":
info.IoThread, err = strconv.Atoi(val)
default:
return nil, fmt.Errorf("redis: unexpected client info key(%s)", key)
}
@ -5435,8 +5494,6 @@ func (cmd *InfoCmd) readReply(rd *proto.Reader) error {
section := ""
scanner := bufio.NewScanner(strings.NewReader(val))
moduleRe := regexp.MustCompile(`module:name=(.+?),(.+)$`)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "#") {
@ -5447,6 +5504,7 @@ func (cmd *InfoCmd) readReply(rd *proto.Reader) error {
cmd.val[section] = make(map[string]string)
} else if line != "" {
if section == "Modules" {
moduleRe := regexp.MustCompile(`module:name=(.+?),(.+)$`)
kv := moduleRe.FindStringSubmatch(line)
if len(kv) == 3 {
cmd.val[section][kv[1]] = kv[2]

@ -81,6 +81,8 @@ func appendArg(dst []interface{}, arg interface{}) []interface{} {
return dst
case time.Time, time.Duration, encoding.BinaryMarshaler, net.IP:
return append(dst, arg)
case nil:
return dst
default:
// scan struct field
v := reflect.ValueOf(arg)
@ -153,6 +155,12 @@ func isEmptyValue(v reflect.Value) bool {
return v.Float() == 0
case reflect.Interface, reflect.Pointer:
return v.IsNil()
case reflect.Struct:
if v.Type() == reflect.TypeOf(time.Time{}) {
return v.IsZero()
}
// Only supports the struct time.Time,
// subsequent iterations will follow the func Scan support decoder.
}
return false
}
@ -211,7 +219,6 @@ type Cmdable interface {
ACLCmdable
BitMapCmdable
ClusterCmdable
GearsCmdable
GenericCmdable
GeoCmdable
HashCmdable
@ -331,7 +338,7 @@ func (info LibraryInfo) Validate() error {
return nil
}
// Hello Set the resp protocol used.
// Hello sets the resp protocol used.
func (c statefulCmdable) Hello(ctx context.Context,
ver int, username, password, clientName string,
) *MapStringInterfaceCmd {
@ -423,6 +430,12 @@ func (c cmdable) Ping(ctx context.Context) *StatusCmd {
return cmd
}
func (c cmdable) Do(ctx context.Context, args ...interface{}) *Cmd {
cmd := NewCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) Quit(_ context.Context) *StatusCmd {
panic("not implemented")
}

@ -0,0 +1,106 @@
---
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
- REDIS_CLUSTER=no
- PORT=6379
- TLS_PORT=6666
command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save ""}
ports:
- 6379:6379
- 6666:6666 # TLS port
volumes:
- "./dockers/standalone:/redis/work"
profiles:
- standalone
- sentinel
- all-stack
- all
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
- PORT=16600
command: "--cluster-enabled yes"
ports:
- "16600-16605:16600-16605"
volumes:
- "./dockers/osscluster:/redis/work"
profiles:
- cluster
- all-stack
- all
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:
- NODES=3
- TLS_ENABLED=yes
- REDIS_CLUSTER=no
- PORT=9121
command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save ""}
#ports:
# - "9121-9123:9121-9123"
volumes:
- "./dockers/sentinel-cluster:/redis/work"
profiles:
- sentinel
- all-stack
- all
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
environment:
- NODES=3
- REDIS_CLUSTER=no
- PORT=26379
command: ${REDIS_EXTRA_ARGS:---sentinel}
network_mode: "host"
#ports:
# - 26379:26379
# - 26380:26380
# - 26381:26381
volumes:
- "./dockers/sentinel.conf:/redis/config-default/redis.conf"
- "./dockers/sentinel:/redis/work"
profiles:
- sentinel
- all-stack
- all
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
- TLS_ENABLED=yes
- REDIS_CLUSTER=no
- PORT=6390
command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save ""}
ports:
- 6390:6390
- 6391:6391
- 6392:6392
volumes:
- "./dockers/ring:/redis/work"
profiles:
- ring
- cluster
- all-stack
- all

@ -38,12 +38,24 @@ type Error interface {
var _ Error = proto.RedisError("")
func isContextError(err error) bool {
switch err {
case context.Canceled, context.DeadlineExceeded:
return true
default:
return false
}
}
func shouldRetry(err error, retryTimeout bool) bool {
switch err {
case io.EOF, io.ErrUnexpectedEOF:
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 {
@ -63,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
}

@ -1,149 +0,0 @@
package redis
import (
"context"
"fmt"
"strings"
)
type GearsCmdable interface {
TFunctionLoad(ctx context.Context, lib string) *StatusCmd
TFunctionLoadArgs(ctx context.Context, lib string, options *TFunctionLoadOptions) *StatusCmd
TFunctionDelete(ctx context.Context, libName string) *StatusCmd
TFunctionList(ctx context.Context) *MapStringInterfaceSliceCmd
TFunctionListArgs(ctx context.Context, options *TFunctionListOptions) *MapStringInterfaceSliceCmd
TFCall(ctx context.Context, libName string, funcName string, numKeys int) *Cmd
TFCallArgs(ctx context.Context, libName string, funcName string, numKeys int, options *TFCallOptions) *Cmd
TFCallASYNC(ctx context.Context, libName string, funcName string, numKeys int) *Cmd
TFCallASYNCArgs(ctx context.Context, libName string, funcName string, numKeys int, options *TFCallOptions) *Cmd
}
type TFunctionLoadOptions struct {
Replace bool
Config string
}
type TFunctionListOptions struct {
Withcode bool
Verbose int
Library string
}
type TFCallOptions struct {
Keys []string
Arguments []string
}
// TFunctionLoad - load a new JavaScript library into Redis.
// For more information - https://redis.io/commands/tfunction-load/
func (c cmdable) TFunctionLoad(ctx context.Context, lib string) *StatusCmd {
args := []interface{}{"TFUNCTION", "LOAD", lib}
cmd := NewStatusCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) TFunctionLoadArgs(ctx context.Context, lib string, options *TFunctionLoadOptions) *StatusCmd {
args := []interface{}{"TFUNCTION", "LOAD"}
if options != nil {
if options.Replace {
args = append(args, "REPLACE")
}
if options.Config != "" {
args = append(args, "CONFIG", options.Config)
}
}
args = append(args, lib)
cmd := NewStatusCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// TFunctionDelete - delete a JavaScript library from Redis.
// For more information - https://redis.io/commands/tfunction-delete/
func (c cmdable) TFunctionDelete(ctx context.Context, libName string) *StatusCmd {
args := []interface{}{"TFUNCTION", "DELETE", libName}
cmd := NewStatusCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// TFunctionList - list the functions with additional information about each function.
// For more information - https://redis.io/commands/tfunction-list/
func (c cmdable) TFunctionList(ctx context.Context) *MapStringInterfaceSliceCmd {
args := []interface{}{"TFUNCTION", "LIST"}
cmd := NewMapStringInterfaceSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) TFunctionListArgs(ctx context.Context, options *TFunctionListOptions) *MapStringInterfaceSliceCmd {
args := []interface{}{"TFUNCTION", "LIST"}
if options != nil {
if options.Withcode {
args = append(args, "WITHCODE")
}
if options.Verbose != 0 {
v := strings.Repeat("v", options.Verbose)
args = append(args, v)
}
if options.Library != "" {
args = append(args, "LIBRARY", options.Library)
}
}
cmd := NewMapStringInterfaceSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// TFCall - invoke a function.
// For more information - https://redis.io/commands/tfcall/
func (c cmdable) TFCall(ctx context.Context, libName string, funcName string, numKeys int) *Cmd {
lf := libName + "." + funcName
args := []interface{}{"TFCALL", lf, numKeys}
cmd := NewCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) TFCallArgs(ctx context.Context, libName string, funcName string, numKeys int, options *TFCallOptions) *Cmd {
lf := libName + "." + funcName
args := []interface{}{"TFCALL", lf, numKeys}
if options != nil {
for _, key := range options.Keys {
args = append(args, key)
}
for _, key := range options.Arguments {
args = append(args, key)
}
}
cmd := NewCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// TFCallASYNC - invoke an asynchronous JavaScript function (coroutine).
// For more information - https://redis.io/commands/TFCallASYNC/
func (c cmdable) TFCallASYNC(ctx context.Context, libName string, funcName string, numKeys int) *Cmd {
lf := fmt.Sprintf("%s.%s", libName, funcName)
args := []interface{}{"TFCALLASYNC", lf, numKeys}
cmd := NewCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) TFCallASYNCArgs(ctx context.Context, libName string, funcName string, numKeys int, options *TFCallOptions) *Cmd {
lf := fmt.Sprintf("%s.%s", libName, funcName)
args := []interface{}{"TFCALLASYNC", lf, numKeys}
if options != nil {
for _, key := range options.Keys {
args = append(args, key)
}
for _, key := range options.Arguments {
args = append(args, key)
}
}
cmd := NewCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}

@ -10,6 +10,9 @@ 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
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
HIncrBy(ctx context.Context, key, field string, incr int64) *IntCmd
HIncrByFloat(ctx context.Context, key, field string, incr float64) *FloatCmd
HKeys(ctx context.Context, key string) *StringSliceCmd
@ -17,12 +20,15 @@ type HashCmdable interface {
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
HVals(ctx context.Context, key string) *StringSliceCmd
HRandField(ctx context.Context, key string, count int) *StringSliceCmd
HRandFieldWithValues(ctx context.Context, key string, count int) *KeyValueSliceCmd
HStrLen(ctx context.Context, key, field string) *IntCmd
HExpire(ctx context.Context, key string, expiration time.Duration, fields ...string) *IntSliceCmd
HExpireWithArgs(ctx context.Context, key string, expiration time.Duration, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd
HPExpire(ctx context.Context, key string, expiration time.Duration, fields ...string) *IntSliceCmd
@ -190,6 +196,11 @@ func (c cmdable) HScan(ctx context.Context, key string, cursor uint64, match str
return cmd
}
func (c cmdable) HStrLen(ctx context.Context, key, field string) *IntCmd {
cmd := NewIntCmd(ctx, "hstrlen", key, field)
_ = c(ctx, cmd)
return cmd
}
func (c cmdable) HScanNoValues(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd {
args := []interface{}{"hscan", key, cursor}
if match != "" {
@ -213,7 +224,10 @@ type HExpireArgs struct {
// HExpire - Sets the expiration time for specified fields in a hash in seconds.
// The command constructs an argument list starting with "HEXPIRE", followed by the key, duration, any conditional flags, and the specified fields.
// For more information - https://redis.io/commands/hexpire/
// Available since Redis 7.4 CE.
// For more information refer to [HEXPIRE Documentation].
//
// [HEXPIRE Documentation]: https://redis.io/commands/hexpire/
func (c cmdable) HExpire(ctx context.Context, key string, expiration time.Duration, fields ...string) *IntSliceCmd {
args := []interface{}{"HEXPIRE", key, formatSec(ctx, expiration), "FIELDS", len(fields)}
@ -228,7 +242,10 @@ func (c cmdable) HExpire(ctx context.Context, key string, expiration time.Durati
// HExpireWithArgs - Sets the expiration time for specified fields in a hash in seconds.
// It requires a key, an expiration duration, a struct with boolean flags for conditional expiration settings (NX, XX, GT, LT), and a list of fields.
// The command constructs an argument list starting with "HEXPIRE", followed by the key, duration, any conditional flags, and the specified fields.
// For more information - https://redis.io/commands/hexpire/
// Available since Redis 7.4 CE.
// For more information refer to [HEXPIRE Documentation].
//
// [HEXPIRE Documentation]: https://redis.io/commands/hexpire/
func (c cmdable) HExpireWithArgs(ctx context.Context, key string, expiration time.Duration, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd {
args := []interface{}{"HEXPIRE", key, formatSec(ctx, expiration)}
@ -257,7 +274,10 @@ func (c cmdable) HExpireWithArgs(ctx context.Context, key string, expiration tim
// HPExpire - Sets the expiration time for specified fields in a hash in milliseconds.
// Similar to HExpire, it accepts a key, an expiration duration in milliseconds, a struct with expiration condition flags, and a list of fields.
// The command modifies the standard time.Duration to milliseconds for the Redis command.
// For more information - https://redis.io/commands/hpexpire/
// Available since Redis 7.4 CE.
// For more information refer to [HPEXPIRE Documentation].
//
// [HPEXPIRE Documentation]: https://redis.io/commands/hpexpire/
func (c cmdable) HPExpire(ctx context.Context, key string, expiration time.Duration, fields ...string) *IntSliceCmd {
args := []interface{}{"HPEXPIRE", key, formatMs(ctx, expiration), "FIELDS", len(fields)}
@ -269,6 +289,13 @@ func (c cmdable) HPExpire(ctx context.Context, key string, expiration time.Durat
return cmd
}
// HPExpireWithArgs - Sets the expiration time for specified fields in a hash in milliseconds.
// It requires a key, an expiration duration, a struct with boolean flags for conditional expiration settings (NX, XX, GT, LT), and a list of fields.
// The command constructs an argument list starting with "HPEXPIRE", followed by the key, duration, any conditional flags, and the specified fields.
// Available since Redis 7.4 CE.
// For more information refer to [HPEXPIRE Documentation].
//
// [HPEXPIRE Documentation]: https://redis.io/commands/hpexpire/
func (c cmdable) HPExpireWithArgs(ctx context.Context, key string, expiration time.Duration, expirationArgs HExpireArgs, fields ...string) *IntSliceCmd {
args := []interface{}{"HPEXPIRE", key, formatMs(ctx, expiration)}
@ -297,7 +324,10 @@ func (c cmdable) HPExpireWithArgs(ctx context.Context, key string, expiration ti
// HExpireAt - Sets the expiration time for specified fields in a hash to a UNIX timestamp in seconds.
// Takes a key, a UNIX timestamp, a struct of conditional flags, and a list of fields.
// The command sets absolute expiration times based on the UNIX timestamp provided.
// For more information - https://redis.io/commands/hexpireat/
// Available since Redis 7.4 CE.
// For more information refer to [HExpireAt Documentation].
//
// [HExpireAt Documentation]: https://redis.io/commands/hexpireat/
func (c cmdable) HExpireAt(ctx context.Context, key string, tm time.Time, fields ...string) *IntSliceCmd {
args := []interface{}{"HEXPIREAT", key, tm.Unix(), "FIELDS", len(fields)}
@ -337,7 +367,10 @@ func (c cmdable) HExpireAtWithArgs(ctx context.Context, key string, tm time.Time
// HPExpireAt - Sets the expiration time for specified fields in a hash to a UNIX timestamp in milliseconds.
// Similar to HExpireAt but for timestamps in milliseconds. It accepts the same parameters and adjusts the UNIX time to milliseconds.
// For more information - https://redis.io/commands/hpexpireat/
// Available since Redis 7.4 CE.
// For more information refer to [HExpireAt Documentation].
//
// [HExpireAt Documentation]: https://redis.io/commands/hexpireat/
func (c cmdable) HPExpireAt(ctx context.Context, key string, tm time.Time, fields ...string) *IntSliceCmd {
args := []interface{}{"HPEXPIREAT", key, tm.UnixNano() / int64(time.Millisecond), "FIELDS", len(fields)}
@ -377,7 +410,10 @@ func (c cmdable) HPExpireAtWithArgs(ctx context.Context, key string, tm time.Tim
// HPersist - Removes the expiration time from specified fields in a hash.
// Accepts a key and the fields themselves.
// This command ensures that each field specified will have its expiration removed if present.
// For more information - https://redis.io/commands/hpersist/
// Available since Redis 7.4 CE.
// For more information refer to [HPersist Documentation].
//
// [HPersist Documentation]: https://redis.io/commands/hpersist/
func (c cmdable) HPersist(ctx context.Context, key string, fields ...string) *IntSliceCmd {
args := []interface{}{"HPERSIST", key, "FIELDS", len(fields)}
@ -392,6 +428,10 @@ func (c cmdable) HPersist(ctx context.Context, key string, fields ...string) *In
// HExpireTime - Retrieves the expiration time for specified fields in a hash as a UNIX timestamp in seconds.
// Requires a key and the fields themselves to fetch their expiration timestamps.
// This command returns the expiration times for each field or error/status codes for each field as specified.
// Available since Redis 7.4 CE.
// For more information refer to [HExpireTime Documentation].
//
// [HExpireTime Documentation]: https://redis.io/commands/hexpiretime/
// For more information - https://redis.io/commands/hexpiretime/
func (c cmdable) HExpireTime(ctx context.Context, key string, fields ...string) *IntSliceCmd {
args := []interface{}{"HEXPIRETIME", key, "FIELDS", len(fields)}
@ -407,6 +447,10 @@ func (c cmdable) HExpireTime(ctx context.Context, key string, fields ...string)
// HPExpireTime - Retrieves the expiration time for specified fields in a hash as a UNIX timestamp in milliseconds.
// Similar to HExpireTime, adjusted for timestamps in milliseconds. It requires the same parameters.
// Provides the expiration timestamp for each field in milliseconds.
// Available since Redis 7.4 CE.
// For more information refer to [HExpireTime Documentation].
//
// [HExpireTime Documentation]: https://redis.io/commands/hexpiretime/
// For more information - https://redis.io/commands/hexpiretime/
func (c cmdable) HPExpireTime(ctx context.Context, key string, fields ...string) *IntSliceCmd {
args := []interface{}{"HPEXPIRETIME", key, "FIELDS", len(fields)}
@ -422,7 +466,10 @@ func (c cmdable) HPExpireTime(ctx context.Context, key string, fields ...string)
// HTTL - Retrieves the remaining time to live for specified fields in a hash in seconds.
// Requires a key and the fields themselves. It returns the TTL for each specified field.
// This command fetches the TTL in seconds for each field or returns error/status codes as appropriate.
// For more information - https://redis.io/commands/httl/
// Available since Redis 7.4 CE.
// For more information refer to [HTTL Documentation].
//
// [HTTL Documentation]: https://redis.io/commands/httl/
func (c cmdable) HTTL(ctx context.Context, key string, fields ...string) *IntSliceCmd {
args := []interface{}{"HTTL", key, "FIELDS", len(fields)}
@ -437,6 +484,10 @@ func (c cmdable) HTTL(ctx context.Context, key string, fields ...string) *IntSli
// HPTTL - Retrieves the remaining time to live for specified fields in a hash in milliseconds.
// Similar to HTTL, but returns the TTL in milliseconds. It requires a key and the specified fields.
// This command provides the TTL in milliseconds for each field or returns error/status codes as needed.
// Available since Redis 7.4 CE.
// For more information refer to [HPTTL Documentation].
//
// [HPTTL Documentation]: https://redis.io/commands/hpttl/
// For more information - https://redis.io/commands/hpttl/
func (c cmdable) HPTTL(ctx context.Context, key string, fields ...string) *IntSliceCmd {
args := []interface{}{"HPTTL", key, "FIELDS", len(fields)}
@ -448,3 +499,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
}
// HGetEXExpirationType 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
}

@ -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
}
}

@ -66,56 +66,95 @@ func (w *Writer) WriteArg(v interface{}) error {
case string:
return w.string(v)
case *string:
if v == nil {
return w.string("")
}
return w.string(*v)
case []byte:
return w.bytes(v)
case int:
return w.int(int64(v))
case *int:
if v == nil {
return w.int(0)
}
return w.int(int64(*v))
case int8:
return w.int(int64(v))
case *int8:
if v == nil {
return w.int(0)
}
return w.int(int64(*v))
case int16:
return w.int(int64(v))
case *int16:
if v == nil {
return w.int(0)
}
return w.int(int64(*v))
case int32:
return w.int(int64(v))
case *int32:
if v == nil {
return w.int(0)
}
return w.int(int64(*v))
case int64:
return w.int(v)
case *int64:
if v == nil {
return w.int(0)
}
return w.int(*v)
case uint:
return w.uint(uint64(v))
case *uint:
if v == nil {
return w.uint(0)
}
return w.uint(uint64(*v))
case uint8:
return w.uint(uint64(v))
case *uint8:
if v == nil {
return w.string("")
}
return w.uint(uint64(*v))
case uint16:
return w.uint(uint64(v))
case *uint16:
if v == nil {
return w.uint(0)
}
return w.uint(uint64(*v))
case uint32:
return w.uint(uint64(v))
case *uint32:
if v == nil {
return w.uint(0)
}
return w.uint(uint64(*v))
case uint64:
return w.uint(v)
case *uint64:
if v == nil {
return w.uint(0)
}
return w.uint(*v)
case float32:
return w.float(float64(v))
case *float32:
if v == nil {
return w.float(0)
}
return w.float(float64(*v))
case float64:
return w.float(v)
case *float64:
if v == nil {
return w.float(0)
}
return w.float(*v)
case bool:
if v {
@ -123,6 +162,9 @@ func (w *Writer) WriteArg(v interface{}) error {
}
return w.int(0)
case *bool:
if v == nil {
return w.int(0)
}
if *v {
return w.int(1)
}
@ -130,8 +172,19 @@ func (w *Writer) WriteArg(v interface{}) error {
case time.Time:
w.numBuf = v.AppendFormat(w.numBuf[:0], time.RFC3339Nano)
return w.bytes(w.numBuf)
case *time.Time:
if v == nil {
v = &time.Time{}
}
w.numBuf = v.AppendFormat(w.numBuf[:0], time.RFC3339Nano)
return w.bytes(w.numBuf)
case time.Duration:
return w.int(v.Nanoseconds())
case *time.Duration:
if v == nil {
return w.int(0)
}
return w.int(v.Nanoseconds())
case encoding.BinaryMarshaler:
b, err := v.MarshalBinary()
if err != nil {

@ -214,9 +214,10 @@ func (opt *Options) init() {
opt.ConnMaxIdleTime = 30 * time.Minute
}
if opt.MaxRetries == -1 {
switch opt.MaxRetries {
case -1:
opt.MaxRetries = 0
} else if opt.MaxRetries == 0 {
case 0:
opt.MaxRetries = 3
}
switch opt.MinRetryBackoff {
@ -276,6 +277,7 @@ func NewDialer(opt *Options) func(context.Context, string, string) (net.Conn, er
// URL attributes (scheme, host, userinfo, resp.), query parameters using these
// names will be treated as unknown parameters
// - unknown parameter names will result in an error
// - use "skip_verify=true" to ignore TLS certificate validation
//
// Examples:
//
@ -496,6 +498,9 @@ func setupConnParams(u *url.URL, o *Options) (*Options, error) {
if q.err != nil {
return nil, q.err
}
if o.TLSConfig != nil && q.has("skip_verify") {
o.TLSConfig.InsecureSkipVerify = q.bool("skip_verify")
}
// any parameters left?
if r := q.remaining(); len(r) > 0 {
@ -527,6 +532,7 @@ func newConnPool(
PoolFIFO: opt.PoolFIFO,
PoolSize: opt.PoolSize,
PoolTimeout: opt.PoolTimeout,
DialTimeout: opt.DialTimeout,
MinIdleConns: opt.MinIdleConns,
MaxIdleConns: opt.MaxIdleConns,
MaxActiveConns: opt.MaxActiveConns,

@ -21,6 +21,10 @@ import (
"github.com/redis/go-redis/v9/internal/rand"
)
const (
minLatencyMeasurementInterval = 10 * time.Second
)
var errClusterNoNodes = fmt.Errorf("redis: cluster has no nodes")
// ClusterOptions are used to configure a cluster client and should be
@ -107,9 +111,10 @@ type ClusterOptions struct {
}
func (opt *ClusterOptions) init() {
if opt.MaxRedirects == -1 {
switch opt.MaxRedirects {
case -1:
opt.MaxRedirects = 0
} else if opt.MaxRedirects == 0 {
case 0:
opt.MaxRedirects = 3
}
@ -332,6 +337,10 @@ type clusterNode struct {
latency uint32 // atomic
generation uint32 // atomic
failing uint32 // atomic
// last time the latency measurement was performed for the node, stored in nanoseconds
// from epoch
lastLatencyMeasurement int64 // atomic
}
func newClusterNode(clOpt *ClusterOptions, addr string) *clusterNode {
@ -384,6 +393,7 @@ func (n *clusterNode) updateLatency() {
latency = float64(dur) / float64(successes)
}
atomic.StoreUint32(&n.latency, uint32(latency+0.5))
n.SetLastLatencyMeasurement(time.Now())
}
func (n *clusterNode) Latency() time.Duration {
@ -413,6 +423,10 @@ func (n *clusterNode) Generation() uint32 {
return atomic.LoadUint32(&n.generation)
}
func (n *clusterNode) LastLatencyMeasurement() int64 {
return atomic.LoadInt64(&n.lastLatencyMeasurement)
}
func (n *clusterNode) SetGeneration(gen uint32) {
for {
v := atomic.LoadUint32(&n.generation)
@ -422,6 +436,15 @@ func (n *clusterNode) SetGeneration(gen uint32) {
}
}
func (n *clusterNode) SetLastLatencyMeasurement(t time.Time) {
for {
v := atomic.LoadInt64(&n.lastLatencyMeasurement)
if t.UnixNano() < v || atomic.CompareAndSwapInt64(&n.lastLatencyMeasurement, v, t.UnixNano()) {
break
}
}
}
//------------------------------------------------------------------------------
type clusterNodes struct {
@ -511,10 +534,11 @@ func (c *clusterNodes) GC(generation uint32) {
c.mu.Lock()
c.activeAddrs = c.activeAddrs[:0]
now := time.Now()
for addr, node := range c.nodes {
if node.Generation() >= generation {
c.activeAddrs = append(c.activeAddrs, addr)
if c.opt.RouteByLatency {
if c.opt.RouteByLatency && node.LastLatencyMeasurement() < now.Add(-minLatencyMeasurementInterval).UnixNano() {
go node.updateLatency()
}
continue
@ -900,6 +924,9 @@ type ClusterClient struct {
// NewClusterClient returns a Redis Cluster client as described in
// http://redis.io/topics/cluster-spec.
func NewClusterClient(opt *ClusterOptions) *ClusterClient {
if opt == nil {
panic("redis: NewClusterClient nil options")
}
opt.init()
c := &ClusterClient{
@ -1339,7 +1366,9 @@ func (c *ClusterClient) processPipelineNode(
_ = node.Client.withProcessPipelineHook(ctx, cmds, func(ctx context.Context, cmds []Cmder) error {
cn, err := node.Client.getConn(ctx)
if err != nil {
node.MarkAsFailing()
if !isContextError(err) {
node.MarkAsFailing()
}
_ = c.mapCmdsByNode(ctx, failedCmds, cmds)
setCmdsErr(cmds, err)
return err
@ -1831,7 +1860,7 @@ func (c *ClusterClient) cmdInfo(ctx context.Context, name string) *CommandInfo {
func (c *ClusterClient) cmdSlot(ctx context.Context, cmd Cmder) int {
args := cmd.Args()
if args[0] == "cluster" && args[1] == "getkeysinslot" {
if args[0] == "cluster" && (args[1] == "getkeysinslot" || args[1] == "countkeysinslot") {
return args[2].(int)
}

@ -319,37 +319,69 @@ func (cmd *BFInfoCmd) Result() (BFInfo, error) {
}
func (cmd *BFInfoCmd) readReply(rd *proto.Reader) (err error) {
n, err := rd.ReadMapLen()
result := BFInfo{}
// Create a mapping from key names to pointers of struct fields
respMapping := map[string]*int64{
"Capacity": &result.Capacity,
"CAPACITY": &result.Capacity,
"Size": &result.Size,
"SIZE": &result.Size,
"Number of filters": &result.Filters,
"FILTERS": &result.Filters,
"Number of items inserted": &result.ItemsInserted,
"ITEMS": &result.ItemsInserted,
"Expansion rate": &result.ExpansionRate,
"EXPANSION": &result.ExpansionRate,
}
// Helper function to read and assign a value based on the key
readAndAssignValue := func(key string) error {
fieldPtr, exists := respMapping[key]
if !exists {
return fmt.Errorf("redis: BLOOM.INFO unexpected key %s", key)
}
// Read the integer and assign to the field via pointer dereferencing
val, err := rd.ReadInt()
if err != nil {
return err
}
*fieldPtr = val
return nil
}
readType, err := rd.PeekReplyType()
if err != nil {
return err
}
var key string
var result BFInfo
for f := 0; f < n; f++ {
key, err = rd.ReadString()
if len(cmd.args) > 2 && readType == proto.RespArray {
n, err := rd.ReadArrayLen()
if err != nil {
return err
}
switch key {
case "Capacity":
result.Capacity, err = rd.ReadInt()
case "Size":
result.Size, err = rd.ReadInt()
case "Number of filters":
result.Filters, err = rd.ReadInt()
case "Number of items inserted":
result.ItemsInserted, err = rd.ReadInt()
case "Expansion rate":
result.ExpansionRate, err = rd.ReadInt()
default:
return fmt.Errorf("redis: BLOOM.INFO unexpected key %s", key)
if key, ok := cmd.args[2].(string); ok && n == 1 {
if err := readAndAssignValue(key); err != nil {
return err
}
} else {
return fmt.Errorf("redis: BLOOM.INFO invalid argument key type")
}
} else {
n, err := rd.ReadMapLen()
if err != nil {
return err
}
for i := 0; i < n; i++ {
key, err := rd.ReadString()
if err != nil {
return err
}
if err := readAndAssignValue(key); err != nil {
return err
}
}
}
cmd.val = result

@ -45,6 +45,9 @@ func (c *PubSub) init() {
}
func (c *PubSub) String() string {
c.mu.Lock()
defer c.mu.Unlock()
channels := mapKeys(c.channels)
channels = append(channels, mapKeys(c.patterns)...)
channels = append(channels, mapKeys(c.schannels)...)
@ -432,7 +435,7 @@ func (c *PubSub) ReceiveTimeout(ctx context.Context, timeout time.Duration) (int
return nil, err
}
err = cn.WithReader(context.Background(), timeout, func(rd *proto.Reader) error {
err = cn.WithReader(ctx, timeout, func(rd *proto.Reader) error {
return c.cmd.readReply(rd)
})

@ -310,7 +310,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 {
auth = true
} else if !isRedisError(err) {
// When the server responds with the RESP protocol and the result is not a normal
@ -661,6 +661,9 @@ type Client struct {
// NewClient returns a client to the Redis Server specified by Options.
func NewClient(opt *Options) *Client {
if opt == nil {
panic("redis: NewClient nil options")
}
opt.init()
c := Client{

@ -128,9 +128,10 @@ func (opt *RingOptions) init() {
opt.NewConsistentHash = newRendezvous
}
if opt.MaxRetries == -1 {
switch opt.MaxRetries {
case -1:
opt.MaxRetries = 0
} else if opt.MaxRetries == 0 {
case 0:
opt.MaxRetries = 3
}
switch opt.MinRetryBackoff {
@ -353,7 +354,8 @@ func (c *ringSharding) List() []*ringShard {
c.mu.RLock()
if !c.closed {
list = c.shards.list
list = make([]*ringShard, len(c.shards.list))
copy(list, c.shards.list)
}
c.mu.RUnlock()
@ -521,6 +523,9 @@ type Ring struct {
}
func NewRing(opt *RingOptions) *Ring {
if opt == nil {
panic("redis: NewRing nil options")
}
opt.init()
hbCtx, hbCancel := context.WithCancel(context.Background())

@ -114,6 +114,7 @@ type SpellCheckTerms struct {
}
type FTExplainOptions struct {
// Dialect 1,3 and 4 are deprecated since redis 8.0
Dialect string
}
@ -240,14 +241,19 @@ type FTAggregateWithCursor struct {
}
type FTAggregateOptions struct {
Verbatim bool
LoadAll bool
Load []FTAggregateLoad
Timeout int
GroupBy []FTAggregateGroupBy
SortBy []FTAggregateSortBy
SortByMax int
Scorer string
Verbatim bool
LoadAll bool
Load []FTAggregateLoad
Timeout int
GroupBy []FTAggregateGroupBy
SortBy []FTAggregateSortBy
SortByMax int
// Scorer is used to set scoring function, if not set passed, a default will be used.
// The default scorer depends on the Redis version:
// - `BM25` for Redis >= 8
// - `TFIDF` for Redis < 8
Scorer string
// AddScores is available in Redis CE 8
AddScores bool
Apply []FTAggregateApply
LimitOffset int
@ -256,7 +262,8 @@ type FTAggregateOptions struct {
WithCursor bool
WithCursorOptions *FTAggregateWithCursor
Params map[string]interface{}
DialectVersion int
// Dialect 1,3 and 4 are deprecated since redis 8.0
DialectVersion int
}
type FTSearchFilter struct {
@ -284,23 +291,30 @@ type FTSearchSortBy struct {
Desc bool
}
// FTSearchOptions hold options that can be passed to the FT.SEARCH command.
// More information about the options can be found
// in the documentation for FT.SEARCH https://redis.io/docs/latest/commands/ft.search/
type FTSearchOptions struct {
NoContent bool
Verbatim bool
NoStopWords bool
WithScores bool
WithPayloads bool
WithSortKeys bool
Filters []FTSearchFilter
GeoFilter []FTSearchGeoFilter
InKeys []interface{}
InFields []interface{}
Return []FTSearchReturn
Slop int
Timeout int
InOrder bool
Language string
Expander string
NoContent bool
Verbatim bool
NoStopWords bool
WithScores bool
WithPayloads bool
WithSortKeys bool
Filters []FTSearchFilter
GeoFilter []FTSearchGeoFilter
InKeys []interface{}
InFields []interface{}
Return []FTSearchReturn
Slop int
Timeout int
InOrder bool
Language string
Expander string
// Scorer is used to set scoring function, if not set passed, a default will be used.
// The default scorer depends on the Redis version:
// - `BM25` for Redis >= 8
// - `TFIDF` for Redis < 8
Scorer string
ExplainScore bool
Payload string
@ -308,8 +322,12 @@ type FTSearchOptions struct {
SortByWithCount bool
LimitOffset int
Limit int
Params map[string]interface{}
DialectVersion int
// CountOnly sets LIMIT 0 0 to get the count - number of documents in the result set without actually returning the result set.
// When using this option, the Limit and LimitOffset options are ignored.
CountOnly bool
Params map[string]interface{}
// Dialect 1,3 and 4 are deprecated since redis 8.0
DialectVersion int
}
type FTSynDumpResult struct {
@ -425,7 +443,8 @@ type IndexDefinition struct {
type FTSpellCheckOptions struct {
Distance int
Terms *FTSpellCheckTerms
Dialect int
// Dialect 1,3 and 4 are deprecated since redis 8.0
Dialect int
}
type FTSpellCheckTerms struct {
@ -592,6 +611,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
@ -789,6 +810,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)
}
}
@ -846,20 +869,32 @@ func (c cmdable) FTAlter(ctx context.Context, index string, skipInitialScan bool
return cmd
}
// FTConfigGet - Retrieves the value of a RediSearch configuration parameter.
// Retrieves the value of a RediSearch configuration parameter.
// The 'option' parameter specifies the configuration parameter to retrieve.
// For more information, please refer to the Redis documentation:
// [FT.CONFIG GET]: (https://redis.io/commands/ft.config-get/)
// For more information, please refer to the Redis [FT.CONFIG GET] documentation.
//
// Deprecated: FTConfigGet is deprecated in Redis 8.
// All configuration will be done with the CONFIG GET command.
// For more information check [Client.ConfigGet] and [CONFIG GET Documentation]
//
// [CONFIG GET Documentation]: https://redis.io/commands/config-get/
// [FT.CONFIG GET]: https://redis.io/commands/ft.config-get/
func (c cmdable) FTConfigGet(ctx context.Context, option string) *MapMapStringInterfaceCmd {
cmd := NewMapMapStringInterfaceCmd(ctx, "FT.CONFIG", "GET", option)
_ = c(ctx, cmd)
return cmd
}
// FTConfigSet - Sets the value of a RediSearch configuration parameter.
// Sets the value of a RediSearch configuration parameter.
// The 'option' parameter specifies the configuration parameter to set, and the 'value' parameter specifies the new value.
// For more information, please refer to the Redis documentation:
// [FT.CONFIG SET]: (https://redis.io/commands/ft.config-set/)
// For more information, please refer to the Redis [FT.CONFIG SET] documentation.
//
// Deprecated: FTConfigSet is deprecated in Redis 8.
// All configuration will be done with the CONFIG SET command.
// For more information check [Client.ConfigSet] and [CONFIG SET Documentation]
//
// [CONFIG SET Documentation]: https://redis.io/commands/config-set/
// [FT.CONFIG SET]: https://redis.io/commands/ft.config-set/
func (c cmdable) FTConfigSet(ctx context.Context, option string, value interface{}) *StatusCmd {
cmd := NewStatusCmd(ctx, "FT.CONFIG", "SET", option, value)
_ = c(ctx, cmd)
@ -1150,6 +1185,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)
@ -1447,6 +1484,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...)
@ -1816,6 +1855,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
@ -1920,8 +1961,12 @@ func (c cmdable) FTSearchWithArgs(ctx context.Context, index string, query strin
args = append(args, "WITHCOUNT")
}
}
if options.LimitOffset >= 0 && options.Limit > 0 {
args = append(args, "LIMIT", options.LimitOffset, options.Limit)
if options.CountOnly {
args = append(args, "LIMIT", 0, 0)
} else {
if options.LimitOffset >= 0 && options.Limit > 0 || options.LimitOffset > 0 && options.Limit == 0 {
args = append(args, "LIMIT", options.LimitOffset, options.Limit)
}
}
if options.Params != nil {
args = append(args, "PARAMS", len(options.Params)*2)
@ -1931,6 +1976,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...)
@ -2054,215 +2101,3 @@ func (c cmdable) FTTagVals(ctx context.Context, index string, field string) *Str
_ = c(ctx, cmd)
return cmd
}
// type FTProfileResult struct {
// Results []interface{}
// Profile ProfileDetails
// }
// type ProfileDetails struct {
// TotalProfileTime string
// ParsingTime string
// PipelineCreationTime string
// Warning string
// IteratorsProfile []IteratorProfile
// ResultProcessorsProfile []ResultProcessorProfile
// }
// type IteratorProfile struct {
// Type string
// QueryType string
// Time interface{}
// Counter int
// Term string
// Size int
// ChildIterators []IteratorProfile
// }
// type ResultProcessorProfile struct {
// Type string
// Time interface{}
// Counter int
// }
// func parseFTProfileResult(data []interface{}) (FTProfileResult, error) {
// var result FTProfileResult
// if len(data) < 2 {
// return result, fmt.Errorf("unexpected data length")
// }
// // Parse results
// result.Results = data[0].([]interface{})
// // Parse profile details
// profileData := data[1].([]interface{})
// profileDetails := ProfileDetails{}
// for i := 0; i < len(profileData); i += 2 {
// switch profileData[i].(string) {
// case "Total profile time":
// profileDetails.TotalProfileTime = profileData[i+1].(string)
// case "Parsing time":
// profileDetails.ParsingTime = profileData[i+1].(string)
// case "Pipeline creation time":
// profileDetails.PipelineCreationTime = profileData[i+1].(string)
// case "Warning":
// profileDetails.Warning = profileData[i+1].(string)
// case "Iterators profile":
// profileDetails.IteratorsProfile = parseIteratorsProfile(profileData[i+1].([]interface{}))
// case "Result processors profile":
// profileDetails.ResultProcessorsProfile = parseResultProcessorsProfile(profileData[i+1].([]interface{}))
// }
// }
// result.Profile = profileDetails
// return result, nil
// }
// func parseIteratorsProfile(data []interface{}) []IteratorProfile {
// var iterators []IteratorProfile
// for _, item := range data {
// profile := item.([]interface{})
// iterator := IteratorProfile{}
// for i := 0; i < len(profile); i += 2 {
// switch profile[i].(string) {
// case "Type":
// iterator.Type = profile[i+1].(string)
// case "Query type":
// iterator.QueryType = profile[i+1].(string)
// case "Time":
// iterator.Time = profile[i+1]
// case "Counter":
// iterator.Counter = int(profile[i+1].(int64))
// case "Term":
// iterator.Term = profile[i+1].(string)
// case "Size":
// iterator.Size = int(profile[i+1].(int64))
// case "Child iterators":
// iterator.ChildIterators = parseChildIteratorsProfile(profile[i+1].([]interface{}))
// }
// }
// iterators = append(iterators, iterator)
// }
// return iterators
// }
// func parseChildIteratorsProfile(data []interface{}) []IteratorProfile {
// var iterators []IteratorProfile
// for _, item := range data {
// profile := item.([]interface{})
// iterator := IteratorProfile{}
// for i := 0; i < len(profile); i += 2 {
// switch profile[i].(string) {
// case "Type":
// iterator.Type = profile[i+1].(string)
// case "Query type":
// iterator.QueryType = profile[i+1].(string)
// case "Time":
// iterator.Time = profile[i+1]
// case "Counter":
// iterator.Counter = int(profile[i+1].(int64))
// case "Term":
// iterator.Term = profile[i+1].(string)
// case "Size":
// iterator.Size = int(profile[i+1].(int64))
// }
// }
// iterators = append(iterators, iterator)
// }
// return iterators
// }
// func parseResultProcessorsProfile(data []interface{}) []ResultProcessorProfile {
// var processors []ResultProcessorProfile
// for _, item := range data {
// profile := item.([]interface{})
// processor := ResultProcessorProfile{}
// for i := 0; i < len(profile); i += 2 {
// switch profile[i].(string) {
// case "Type":
// processor.Type = profile[i+1].(string)
// case "Time":
// processor.Time = profile[i+1]
// case "Counter":
// processor.Counter = int(profile[i+1].(int64))
// }
// }
// processors = append(processors, processor)
// }
// return processors
// }
// func NewFTProfileCmd(ctx context.Context, args ...interface{}) *FTProfileCmd {
// return &FTProfileCmd{
// baseCmd: baseCmd{
// ctx: ctx,
// args: args,
// },
// }
// }
// type FTProfileCmd struct {
// baseCmd
// val FTProfileResult
// }
// func (cmd *FTProfileCmd) String() string {
// return cmdString(cmd, cmd.val)
// }
// func (cmd *FTProfileCmd) SetVal(val FTProfileResult) {
// cmd.val = val
// }
// func (cmd *FTProfileCmd) Result() (FTProfileResult, error) {
// return cmd.val, cmd.err
// }
// func (cmd *FTProfileCmd) Val() FTProfileResult {
// return cmd.val
// }
// func (cmd *FTProfileCmd) readReply(rd *proto.Reader) (err error) {
// data, err := rd.ReadSlice()
// if err != nil {
// return err
// }
// cmd.val, err = parseFTProfileResult(data)
// if err != nil {
// cmd.err = err
// }
// return nil
// }
// // FTProfile - Executes a search query and returns a profile of how the query was processed.
// // The 'index' parameter specifies the index to search, the 'limited' parameter specifies whether to limit the results,
// // and the 'query' parameter specifies the search / aggreagte query. Please notice that you must either pass a SearchQuery or an AggregateQuery.
// // For more information, please refer to the Redis documentation:
// // [FT.PROFILE]: (https://redis.io/commands/ft.profile/)
// func (c cmdable) FTProfile(ctx context.Context, index string, limited bool, query interface{}) *FTProfileCmd {
// queryType := ""
// var argsQuery []interface{}
// switch v := query.(type) {
// case AggregateQuery:
// queryType = "AGGREGATE"
// argsQuery = v
// case SearchQuery:
// queryType = "SEARCH"
// argsQuery = v
// default:
// panic("FT.PROFILE: query must be either AggregateQuery or SearchQuery")
// }
// args := []interface{}{"FT.PROFILE", index, queryType}
// if limited {
// args = append(args, "LIMITED")
// }
// args = append(args, "QUERY")
// args = append(args, argsQuery...)
// cmd := NewFTProfileCmd(ctx, args...)
// _ = c(ctx, cmd)
// return cmd
// }

@ -4,6 +4,7 @@ import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"strings"
"sync"
@ -223,6 +224,10 @@ func (opt *FailoverOptions) clusterOptions() *ClusterOptions {
// for automatic failover. It's safe for concurrent use by multiple
// goroutines.
func NewFailoverClient(failoverOpt *FailoverOptions) *Client {
if failoverOpt == nil {
panic("redis: NewFailoverClient nil options")
}
if failoverOpt.RouteByLatency {
panic("to route commands by latency, use NewFailoverClusterClient")
}
@ -312,6 +317,9 @@ type SentinelClient struct {
}
func NewSentinelClient(opt *Options) *SentinelClient {
if opt == nil {
panic("redis: NewSentinelClient nil options")
}
opt.init()
c := &SentinelClient{
baseClient: &baseClient{
@ -566,29 +574,50 @@ func (c *sentinelFailover) MasterAddr(ctx context.Context) (string, error) {
}
}
for i, sentinelAddr := range c.sentinelAddrs {
sentinel := NewSentinelClient(c.opt.sentinelOptions(sentinelAddr))
masterAddr, err := sentinel.GetMasterAddrByName(ctx, c.opt.MasterName).Result()
if err != nil {
_ = sentinel.Close()
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return "", err
}
internal.Logger.Printf(ctx, "sentinel: GetMasterAddrByName master=%q failed: %s",
c.opt.MasterName, err)
continue
}
var (
masterAddr string
wg sync.WaitGroup
once sync.Once
errCh = make(chan error, len(c.sentinelAddrs))
)
// Push working sentinel to the top.
c.sentinelAddrs[0], c.sentinelAddrs[i] = c.sentinelAddrs[i], c.sentinelAddrs[0]
c.setSentinel(ctx, sentinel)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
addr := net.JoinHostPort(masterAddr[0], masterAddr[1])
return addr, nil
for i, sentinelAddr := range c.sentinelAddrs {
wg.Add(1)
go func(i int, addr string) {
defer wg.Done()
sentinelCli := NewSentinelClient(c.opt.sentinelOptions(addr))
addrVal, err := sentinelCli.GetMasterAddrByName(ctx, c.opt.MasterName).Result()
if err != nil {
internal.Logger.Printf(ctx, "sentinel: GetMasterAddrByName addr=%s, master=%q failed: %s",
addr, c.opt.MasterName, err)
_ = sentinelCli.Close()
errCh <- err
return
}
once.Do(func() {
masterAddr = net.JoinHostPort(addrVal[0], addrVal[1])
// Push working sentinel to the top
c.sentinelAddrs[0], c.sentinelAddrs[i] = c.sentinelAddrs[i], c.sentinelAddrs[0]
c.setSentinel(ctx, sentinelCli)
internal.Logger.Printf(ctx, "sentinel: selected addr=%s masterAddr=%s", addr, masterAddr)
cancel()
})
}(i, sentinelAddr)
}
return "", errors.New("redis: all sentinels specified in configuration are unreachable")
wg.Wait()
close(errCh)
if masterAddr != "" {
return masterAddr, nil
}
errs := make([]error, 0, len(errCh))
for err := range errCh {
errs = append(errs, err)
}
return "", fmt.Errorf("redis: all sentinels specified in configuration are unreachable: %w", errors.Join(errs...))
}
func (c *sentinelFailover) replicaAddrs(ctx context.Context, useDisconnected bool) ([]string, error) {
@ -806,6 +835,10 @@ func contains(slice []string, str string) bool {
// NewFailoverClusterClient returns a client that supports routing read-only commands
// to a replica node.
func NewFailoverClusterClient(failoverOpt *FailoverOptions) *ClusterClient {
if failoverOpt == nil {
panic("redis: NewFailoverClusterClient nil options")
}
sentinelAddrs := make([]string, len(failoverOpt.SentinelAddrs))
copy(sentinelAddrs, failoverOpt.SentinelAddrs)
@ -815,6 +848,22 @@ func NewFailoverClusterClient(failoverOpt *FailoverOptions) *ClusterClient {
}
opt := failoverOpt.clusterOptions()
if failoverOpt.DB != 0 {
onConnect := opt.OnConnect
opt.OnConnect = func(ctx context.Context, cn *Conn) error {
if err := cn.Select(ctx, failoverOpt.DB).Err(); err != nil {
return err
}
if onConnect != nil {
return onConnect(ctx, cn)
}
return nil
}
}
opt.ClusterSlots = func(ctx context.Context) ([]ClusterSlot, error) {
masterAddr, err := failover.MasterAddr(ctx)
if err != nil {

@ -80,6 +80,8 @@ type UniversalOptions struct {
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
}
// Cluster returns cluster options created from the universal options.
@ -152,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,
@ -172,6 +177,8 @@ func (o *UniversalOptions) Failover() *FailoverOptions {
TLSConfig: o.TLSConfig,
ReplicaOnly: o.ReadOnly,
DisableIdentity: o.DisableIdentity,
DisableIndentity: o.DisableIndentity,
IdentitySuffix: o.IdentitySuffix,
@ -252,14 +259,26 @@ 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 != "" {
if opts == nil {
panic("redis: NewUniversalClient nil options")
}
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 {
case len(opts.Addrs) > 1 || opts.IsClusterMode:
return NewClusterClient(opts.Cluster())
default:
return NewClient(opts.Simple())
}
return NewClient(opts.Simple())
}

@ -2,5 +2,5 @@ package redis
// Version is the current release version.
func Version() string {
return "9.7.3"
return "9.8.0"
}

@ -1690,7 +1690,7 @@ github.com/prometheus/sigv4
# github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475
## explicit
github.com/rcrowley/go-metrics
# github.com/redis/go-redis/v9 v9.7.3
# github.com/redis/go-redis/v9 v9.8.0
## explicit; go 1.18
github.com/redis/go-redis/v9
github.com/redis/go-redis/v9/internal

Loading…
Cancel
Save