├── internal ├── customvet │ ├── .gitignore │ ├── go.mod │ ├── main.go │ ├── checks │ │ └── setval │ │ │ ├── setval_test.go │ │ │ ├── testdata │ │ │ └── src │ │ │ │ └── a │ │ │ │ └── a.go │ │ │ └── setval.go │ └── go.sum ├── redis.go ├── util │ ├── type.go │ ├── safe.go │ ├── math.go │ ├── unsafe.go │ ├── strconv.go │ ├── convert_test.go │ ├── convert.go │ ├── strconv_test.go │ └── atomic_min.go ├── proto │ ├── proto_test.go │ └── scan_test.go ├── internal_test.go ├── pool │ ├── export_test.go │ ├── conn_check_dummy.go │ ├── conn_check_test.go │ ├── conn_check.go │ ├── want_conn.go │ ├── pubsub.go │ ├── bench_test.go │ ├── main_test.go │ ├── conn_relaxed_timeout_test.go │ └── pool_single.go ├── internal.go ├── routing │ └── shard_picker.go ├── rand │ └── rand.go ├── arg.go ├── interfaces │ └── interfaces.go ├── once.go ├── log.go ├── util.go ├── hashtag │ └── hashtag_test.go ├── util_test.go └── auth │ └── streaming │ └── cred_listeners.go ├── .github ├── CODEOWNERS ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ └── bug_report.md ├── dependabot.yml ├── workflows │ ├── spellcheck.yml │ ├── golangci-lint.yml │ ├── release-drafter.yml │ ├── doctests.yaml │ ├── test-redis-enterprise.yml │ └── codeql-analysis.yml ├── spellcheck-settings.yml ├── wordlist.txt ├── release-drafter-config.yml └── actions │ └── run-tests │ └── action.yml ├── doc.go ├── .prettierrc.yml ├── dockers ├── .gitignore └── sentinel.conf ├── example ├── throughput │ └── throughput ├── otel │ ├── image │ │ ├── metrics.png │ │ └── redis-trace.png │ ├── config │ │ ├── vector.toml │ │ └── otel-collector.yaml │ ├── client.go │ ├── go.mod │ └── docker-compose.yml ├── hset-struct │ ├── README.md │ ├── go.mod │ └── go.sum ├── lua-scripting │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── main.go ├── hll │ ├── README.md │ ├── go.mod │ ├── main.go │ └── go.sum ├── scan-struct │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── main.go ├── del-keys-without-ttl │ ├── README.md │ ├── go.mod │ └── go.sum ├── cluster-mget │ ├── go.mod │ ├── go.sum │ └── main.go ├── redis-bloom │ ├── README.md │ ├── go.mod │ └── go.sum ├── maintnotifiations-pubsub │ ├── go.mod │ └── go.sum ├── disable-maintnotifications │ ├── go.mod │ └── go.sum └── digest-optimistic-locking │ ├── go.mod │ └── go.sum ├── version.go ├── extra ├── rediscmd │ ├── safe.go │ ├── unsafe.go │ ├── go.mod │ ├── rediscmd_test.go │ └── go.sum ├── redisotel │ ├── README.md │ ├── go.mod │ ├── metrics_test.go │ └── go.sum ├── rediscensus │ ├── go.mod │ └── rediscensus.go └── redisprometheus │ ├── go.mod │ └── README.md ├── .gitignore ├── helper └── helper.go ├── scripts ├── bump_deps.sh ├── release.sh └── tag.sh ├── doctests ├── main_test.go ├── Makefile ├── README.md ├── set_get_test.go ├── lpush_lrange_test.go ├── cmds_string_test.go ├── hll_tutorial_test.go ├── topk_tutorial_test.go ├── cuckoo_tutorial_test.go ├── bitfield_tutorial_test.go ├── cmds_servermgmt_test.go ├── bf_tutorial_test.go ├── cms_tutorial_test.go ├── bitmap_tutorial_test.go └── cmds_set_test.go ├── RELEASING.md ├── push ├── push.go ├── handler.go ├── registry.go └── handler_context.go ├── maintnotifications ├── e2e │ ├── .gitignore │ ├── main_test.go │ ├── doc.go │ └── utils_test.go ├── state.go ├── hooks.go └── README.md ├── unit_test.go ├── go.mod ├── .golangci.yml ├── push_notifications.go ├── fuzz └── fuzz.go ├── hyperloglog_commands.go ├── LICENSE ├── go.sum ├── iterator.go ├── logging ├── logging_test.go └── logging.go ├── auth ├── reauth_credentials_listener.go └── auth.go ├── error_test.go ├── command_recorder_test.go ├── pubsub_commands.go ├── bitmap_commands_test.go ├── command_test.go ├── script.go ├── osscluster_commands.go ├── export_test.go ├── Makefile └── command_digest_test.go /internal/customvet/.gitignore: -------------------------------------------------------------------------------- 1 | /customvet 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | doctests/* @dmaier-redislabs 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ['https://uptrace.dev/sponsor'] 2 | -------------------------------------------------------------------------------- /internal/redis.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | const RedisNull = "" 4 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package redis implements a Redis client. 3 | */ 4 | package redis 5 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | semi: false 2 | singleQuote: true 3 | proseWrap: always 4 | printWidth: 100 5 | -------------------------------------------------------------------------------- /internal/util/type.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | func ToPtr[T any](v T) *T { 4 | return &v 5 | } 6 | -------------------------------------------------------------------------------- /dockers/.gitignore: -------------------------------------------------------------------------------- 1 | osscluster/ 2 | ring/ 3 | standalone/ 4 | sentinel-cluster/ 5 | sentinel/ 6 | 7 | -------------------------------------------------------------------------------- /example/throughput/throughput: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis/go-redis/HEAD/example/throughput/throughput -------------------------------------------------------------------------------- /example/otel/image/metrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis/go-redis/HEAD/example/otel/image/metrics.png -------------------------------------------------------------------------------- /example/otel/image/redis-trace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis/go-redis/HEAD/example/otel/image/redis-trace.png -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | // Version is the current release version. 4 | func Version() string { 5 | return "9.18.0-beta.2" 6 | } 7 | -------------------------------------------------------------------------------- /example/hset-struct/README.md: -------------------------------------------------------------------------------- 1 | # Example for setting struct fields as hash fields 2 | 3 | To run this example: 4 | 5 | ```shell 6 | go run . 7 | ``` 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Discussions 4 | url: https://github.com/go-redis/redis/discussions 5 | about: Ask a question via GitHub Discussions 6 | -------------------------------------------------------------------------------- /internal/util/safe.go: -------------------------------------------------------------------------------- 1 | //go:build appengine 2 | 3 | package util 4 | 5 | func BytesToString(b []byte) string { 6 | return string(b) 7 | } 8 | 9 | func StringToBytes(s string) []byte { 10 | return []byte(s) 11 | } 12 | -------------------------------------------------------------------------------- /example/lua-scripting/README.md: -------------------------------------------------------------------------------- 1 | # Redis Lua scripting example 2 | 3 | This is an example for [Redis Lua scripting](https://redis.uptrace.dev/guide/lua-scripting.html) 4 | article. To run it: 5 | 6 | ```shell 7 | go run . 8 | ``` 9 | -------------------------------------------------------------------------------- /extra/rediscmd/safe.go: -------------------------------------------------------------------------------- 1 | //go:build appengine 2 | // +build appengine 3 | 4 | package rediscmd 5 | 6 | func String(b []byte) string { 7 | return string(b) 8 | } 9 | 10 | func Bytes(s string) []byte { 11 | return []byte(s) 12 | } 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | - package-ecosystem: github-actions 8 | directory: / 9 | schedule: 10 | interval: weekly 11 | -------------------------------------------------------------------------------- /dockers/sentinel.conf: -------------------------------------------------------------------------------- 1 | sentinel resolve-hostnames yes 2 | sentinel monitor go-redis-test 127.0.0.1 9121 2 3 | sentinel down-after-milliseconds go-redis-test 5000 4 | sentinel failover-timeout go-redis-test 60000 5 | sentinel parallel-syncs go-redis-test 1 -------------------------------------------------------------------------------- /example/hll/README.md: -------------------------------------------------------------------------------- 1 | # Redis HyperLogLog example 2 | 3 | To run this example: 4 | 5 | ```shell 6 | go run . 7 | ``` 8 | 9 | See [Using HyperLogLog command with go-redis](https://redis.uptrace.dev/guide/go-redis-hll.html) for 10 | details. 11 | -------------------------------------------------------------------------------- /internal/customvet/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/internal/customvet 2 | 3 | go 1.21 4 | 5 | require golang.org/x/tools v0.5.0 6 | 7 | require ( 8 | golang.org/x/mod v0.7.0 // indirect 9 | golang.org/x/sys v0.4.0 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /internal/proto/proto_test.go: -------------------------------------------------------------------------------- 1 | package proto_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/bsm/ginkgo/v2" 7 | . "github.com/bsm/gomega" 8 | ) 9 | 10 | func TestGinkgoSuite(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "proto") 13 | } 14 | -------------------------------------------------------------------------------- /internal/customvet/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "golang.org/x/tools/go/analysis/multichecker" 5 | 6 | "github.com/redis/go-redis/internal/customvet/checks/setval" 7 | ) 8 | 9 | func main() { 10 | multichecker.Main( 11 | setval.Analyzer, 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.rdb 2 | testdata/* 3 | .idea/ 4 | .DS_Store 5 | *.tar.gz 6 | *.dic 7 | redis8tests.sh 8 | coverage.txt 9 | **/coverage.txt 10 | .vscode 11 | tmp/* 12 | *.test 13 | 14 | # maintenanceNotifications upgrade documentation (temporary) 15 | maintenanceNotifications/docs/ 16 | -------------------------------------------------------------------------------- /helper/helper.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import "github.com/redis/go-redis/v9/internal/util" 4 | 5 | func ParseFloat(s string) (float64, error) { 6 | return util.ParseStringToFloat(s) 7 | } 8 | 9 | func MustParseFloat(s string) float64 { 10 | return util.MustParseFloat(s) 11 | } 12 | -------------------------------------------------------------------------------- /example/scan-struct/README.md: -------------------------------------------------------------------------------- 1 | # Example for scanning hash fields into a struct 2 | 3 | To run this example: 4 | 5 | ```shell 6 | go run . 7 | ``` 8 | 9 | See 10 | [Redis: Scanning hash fields into a struct](https://redis.uptrace.dev/guide/scanning-hash-fields.html) 11 | for details. 12 | -------------------------------------------------------------------------------- /scripts/bump_deps.sh: -------------------------------------------------------------------------------- 1 | PACKAGE_DIRS=$(find . -mindepth 2 -type f -name 'go.mod' -exec dirname {} \; \ 2 | | sed 's/^\.\///' \ 3 | | sort) 4 | 5 | for dir in $PACKAGE_DIRS 6 | do 7 | printf "${dir}: go get -d && go mod tidy\n" 8 | (cd ./${dir} && go get -d && go mod tidy) 9 | done 10 | -------------------------------------------------------------------------------- /internal/util/math.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | // Max returns the maximum of two integers 4 | func Max(a, b int) int { 5 | if a > b { 6 | return a 7 | } 8 | return b 9 | } 10 | 11 | // Min returns the minimum of two integers 12 | func Min(a, b int) int { 13 | if a < b { 14 | return a 15 | } 16 | return b 17 | } 18 | -------------------------------------------------------------------------------- /example/del-keys-without-ttl/README.md: -------------------------------------------------------------------------------- 1 | # Delete keys without a ttl 2 | 3 | This example demonstrates how to use `SCAN` and pipelines to efficiently delete keys without a TTL. 4 | 5 | To run this example: 6 | 7 | ```shell 8 | go run . 9 | ``` 10 | 11 | See [documentation](https://redis.uptrace.dev/guide/get-all-keys.html) for more details. 12 | -------------------------------------------------------------------------------- /example/cluster-mget/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/example/cluster-mget 2 | 3 | go 1.21 4 | 5 | replace github.com/redis/go-redis/v9 => ../.. 6 | 7 | require github.com/redis/go-redis/v9 v9.16.0 8 | 9 | require ( 10 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 11 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /internal/customvet/checks/setval/setval_test.go: -------------------------------------------------------------------------------- 1 | package setval_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "golang.org/x/tools/go/analysis/analysistest" 7 | 8 | "github.com/redis/go-redis/internal/customvet/checks/setval" 9 | ) 10 | 11 | func Test(t *testing.T) { 12 | testdata := analysistest.TestData() 13 | analysistest.Run(t, testdata, setval.Analyzer, "a") 14 | } 15 | -------------------------------------------------------------------------------- /doctests/main_test.go: -------------------------------------------------------------------------------- 1 | package example_commands_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | var RedisVersion float64 11 | 12 | func init() { 13 | // read REDIS_VERSION from env 14 | RedisVersion, _ = strconv.ParseFloat(strings.Trim(os.Getenv("REDIS_VERSION"), "\""), 64) 15 | fmt.Printf("REDIS_VERSION: %.1f\n", RedisVersion) 16 | } 17 | -------------------------------------------------------------------------------- /example/redis-bloom/README.md: -------------------------------------------------------------------------------- 1 | # RedisBloom example for go-redis 2 | 3 | This is an example for 4 | [Bloom, Cuckoo, Count-Min, Top-K](https://redis.uptrace.dev/guide/bloom-cuckoo-count-min-top-k.html) 5 | article. 6 | 7 | To run it, you need to compile and install 8 | [RedisBloom](https://oss.redis.com/redisbloom/Quick_Start/#building) module: 9 | 10 | ```shell 11 | go run . 12 | ``` 13 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | 1. Run `release.sh` script which updates versions in go.mod files and pushes a new branch to GitHub: 4 | 5 | ```shell 6 | TAG=v1.0.0 ./scripts/release.sh 7 | ``` 8 | 9 | 2. Open a pull request and wait for the build to finish. 10 | 11 | 3. Merge the pull request and run `tag.sh` to create tags for packages: 12 | 13 | ```shell 14 | TAG=v1.0.0 ./scripts/tag.sh 15 | ``` 16 | -------------------------------------------------------------------------------- /example/hll/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/example/hll 2 | 3 | go 1.21 4 | 5 | replace github.com/redis/go-redis/v9 => ../.. 6 | 7 | require github.com/redis/go-redis/v9 v9.18.0-beta.2 8 | 9 | require ( 10 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 11 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 12 | go.uber.org/atomic v1.11.0 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /example/maintnotifiations-pubsub/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/example/pubsub 2 | 3 | go 1.21 4 | 5 | replace github.com/redis/go-redis/v9 => ../.. 6 | 7 | require github.com/redis/go-redis/v9 v9.11.0 8 | 9 | require ( 10 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 11 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 12 | go.uber.org/atomic v1.11.0 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /example/redis-bloom/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/example/redis-bloom 2 | 3 | go 1.21 4 | 5 | replace github.com/redis/go-redis/v9 => ../.. 6 | 7 | require github.com/redis/go-redis/v9 v9.18.0-beta.2 8 | 9 | require ( 10 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 11 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 12 | go.uber.org/atomic v1.11.0 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /example/lua-scripting/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/example/lua-scripting 2 | 3 | go 1.21 4 | 5 | replace github.com/redis/go-redis/v9 => ../.. 6 | 7 | require github.com/redis/go-redis/v9 v9.18.0-beta.2 8 | 9 | require ( 10 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 11 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 12 | go.uber.org/atomic v1.11.0 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /.github/workflows/spellcheck.yml: -------------------------------------------------------------------------------- 1 | name: spellcheck 2 | on: 3 | pull_request: 4 | jobs: 5 | check-spelling: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout 9 | uses: actions/checkout@v6 10 | - name: Check Spelling 11 | uses: rojopolis/spellcheck-github-actions@0.55.0 12 | with: 13 | config_path: .github/spellcheck-settings.yml 14 | task_name: Markdown 15 | -------------------------------------------------------------------------------- /internal/internal_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | . "github.com/bsm/gomega" 8 | ) 9 | 10 | func TestRetryBackoff(t *testing.T) { 11 | RegisterTestingT(t) 12 | 13 | for i := 0; i <= 16; i++ { 14 | backoff := RetryBackoff(i, time.Millisecond, 512*time.Millisecond) 15 | Expect(backoff >= 0).To(BeTrue()) 16 | Expect(backoff <= 512*time.Millisecond).To(BeTrue()) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /internal/util/unsafe.go: -------------------------------------------------------------------------------- 1 | //go:build !appengine 2 | 3 | package util 4 | 5 | import ( 6 | "unsafe" 7 | ) 8 | 9 | // BytesToString converts byte slice to string. 10 | func BytesToString(b []byte) string { 11 | return unsafe.String(unsafe.SliceData(b), len(b)) 12 | } 13 | 14 | // StringToBytes converts string to byte slice. 15 | func StringToBytes(s string) []byte { 16 | return unsafe.Slice(unsafe.StringData(s), len(s)) 17 | } 18 | -------------------------------------------------------------------------------- /example/disable-maintnotifications/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/example/disable-maintnotifications 2 | 3 | go 1.23 4 | 5 | replace github.com/redis/go-redis/v9 => ../.. 6 | 7 | require github.com/redis/go-redis/v9 v9.7.0 8 | 9 | require ( 10 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 11 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 12 | go.uber.org/atomic v1.11.0 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /extra/rediscmd/unsafe.go: -------------------------------------------------------------------------------- 1 | //go:build !appengine 2 | // +build !appengine 3 | 4 | package rediscmd 5 | 6 | import "unsafe" 7 | 8 | // String converts byte slice to string. 9 | func String(b []byte) string { 10 | return *(*string)(unsafe.Pointer(&b)) 11 | } 12 | 13 | // Bytes converts string to byte slice. 14 | func Bytes(s string) []byte { 15 | return *(*[]byte)(unsafe.Pointer( 16 | &struct { 17 | string 18 | Cap int 19 | }{s, len(s)}, 20 | )) 21 | } 22 | -------------------------------------------------------------------------------- /example/hset-struct/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/example/scan-struct 2 | 3 | go 1.21 4 | 5 | replace github.com/redis/go-redis/v9 => ../.. 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 9 | github.com/redis/go-redis/v9 v9.18.0-beta.2 10 | ) 11 | 12 | require ( 13 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 14 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 15 | go.uber.org/atomic v1.11.0 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /example/scan-struct/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/example/scan-struct 2 | 3 | go 1.21 4 | 5 | replace github.com/redis/go-redis/v9 => ../.. 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 9 | github.com/redis/go-redis/v9 v9.18.0-beta.2 10 | ) 11 | 12 | require ( 13 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 14 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 15 | go.uber.org/atomic v1.11.0 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /internal/pool/export_test.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "net" 5 | "time" 6 | ) 7 | 8 | func (cn *Conn) SetCreatedAt(tm time.Time) { 9 | cn.createdAt = tm 10 | } 11 | 12 | func (cn *Conn) NetConn() net.Conn { 13 | return cn.getNetConn() 14 | } 15 | 16 | func (p *ConnPool) CheckMinIdleConns() { 17 | p.connsMu.Lock() 18 | p.checkMinIdleConns() 19 | p.connsMu.Unlock() 20 | } 21 | 22 | func (p *ConnPool) QueueLen() int { 23 | return int(p.semaphore.Len()) 24 | } 25 | -------------------------------------------------------------------------------- /push/push.go: -------------------------------------------------------------------------------- 1 | // Package push provides push notifications for Redis. 2 | // This is an EXPERIMENTAL API for handling push notifications from Redis. 3 | // It is not yet stable and may change in the future. 4 | // Although this is in a public package, in its current form public use is not advised. 5 | // Pending push notifications should be processed before executing any readReply from the connection 6 | // as per RESP3 specification push notifications can be sent at any time. 7 | package push 8 | -------------------------------------------------------------------------------- /example/del-keys-without-ttl/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/example/del-keys-without-ttl 2 | 3 | go 1.21 4 | 5 | replace github.com/redis/go-redis/v9 => ../.. 6 | 7 | require ( 8 | github.com/redis/go-redis/v9 v9.18.0-beta.2 9 | go.uber.org/zap v1.24.0 10 | ) 11 | 12 | require ( 13 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 14 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 15 | go.uber.org/atomic v1.11.0 // indirect 16 | go.uber.org/multierr v1.9.0 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /maintnotifications/e2e/.gitignore: -------------------------------------------------------------------------------- 1 | # E2E test artifacts 2 | *.log 3 | *.out 4 | test-results/ 5 | coverage/ 6 | profiles/ 7 | 8 | # Test data 9 | test-data/ 10 | temp/ 11 | *.tmp 12 | 13 | # CI artifacts 14 | artifacts/ 15 | reports/ 16 | 17 | # Redis data files (if running local Redis for testing) 18 | dump.rdb 19 | appendonly.aof 20 | redis.conf.local 21 | 22 | # Performance test results 23 | *.prof 24 | *.trace 25 | benchmarks/ 26 | 27 | # Docker compose files for local testing 28 | docker-compose.override.yml 29 | .env.local 30 | infra/ 31 | -------------------------------------------------------------------------------- /example/digest-optimistic-locking/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/example/digest-optimistic-locking 2 | 3 | go 1.21 4 | 5 | replace github.com/redis/go-redis/v9 => ../.. 6 | 7 | require ( 8 | github.com/redis/go-redis/v9 v9.18.0-beta.2 9 | github.com/zeebo/xxh3 v1.0.2 10 | ) 11 | 12 | require ( 13 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 14 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 15 | github.com/klauspost/cpuid/v2 v2.0.9 // indirect 16 | go.uber.org/atomic v1.11.0 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /internal/customvet/go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= 2 | golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 3 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 4 | golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= 5 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 6 | golang.org/x/tools v0.5.0 h1:+bSpV5HIeWkuvgaMfI3UmKRThoTA5ODJTUd8T17NO+4= 7 | golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k= 8 | -------------------------------------------------------------------------------- /internal/customvet/checks/setval/testdata/src/a/a.go: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | type GoodCmd struct { 4 | val int 5 | } 6 | 7 | func (c *GoodCmd) SetVal(val int) { 8 | c.val = val 9 | } 10 | 11 | func (c *GoodCmd) Result() (int, error) { 12 | return c.val, nil 13 | } 14 | 15 | type BadCmd struct { 16 | val int 17 | } 18 | 19 | func (c *BadCmd) Result() (int, error) { // want "\\*a.BadCmd is missing a SetVal method" 20 | return c.val, nil 21 | } 22 | 23 | type NotACmd struct { 24 | val int 25 | } 26 | 27 | func (c *NotACmd) Val() int { 28 | return c.val 29 | } 30 | -------------------------------------------------------------------------------- /internal/util/strconv.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "strconv" 4 | 5 | func Atoi(b []byte) (int, error) { 6 | return strconv.Atoi(BytesToString(b)) 7 | } 8 | 9 | func ParseInt(b []byte, base int, bitSize int) (int64, error) { 10 | return strconv.ParseInt(BytesToString(b), base, bitSize) 11 | } 12 | 13 | func ParseUint(b []byte, base int, bitSize int) (uint64, error) { 14 | return strconv.ParseUint(BytesToString(b), base, bitSize) 15 | } 16 | 17 | func ParseFloat(b []byte, bitSize int) (float64, error) { 18 | return strconv.ParseFloat(BytesToString(b), bitSize) 19 | } 20 | -------------------------------------------------------------------------------- /doctests/Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @if [ -z "$(REDIS_VERSION)" ]; then \ 3 | echo "REDIS_VERSION not set, running all tests"; \ 4 | go test -v ./...; \ 5 | else \ 6 | MAJOR_VERSION=$$(echo "$(REDIS_VERSION)" | cut -d. -f1); \ 7 | if [ "$$MAJOR_VERSION" -ge 8 ]; then \ 8 | echo "REDIS_VERSION $(REDIS_VERSION) >= 8, running all tests"; \ 9 | go test -v ./...; \ 10 | else \ 11 | echo "REDIS_VERSION $(REDIS_VERSION) < 8, skipping vector_sets tests"; \ 12 | go test -v ./... -run '^(?!.*(?:vectorset|ExampleClient_vectorset)).*$$'; \ 13 | fi; \ 14 | fi 15 | 16 | .PHONY: test -------------------------------------------------------------------------------- /internal/pool/conn_check_dummy.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !darwin && !dragonfly && !freebsd && !netbsd && !openbsd && !solaris && !illumos 2 | 3 | package pool 4 | 5 | import ( 6 | "errors" 7 | "net" 8 | ) 9 | 10 | // errUnexpectedRead is placeholder error variable for non-unix build constraints 11 | var errUnexpectedRead = errors.New("unexpected read from socket") 12 | 13 | func connCheck(_ net.Conn) error { 14 | return nil 15 | } 16 | 17 | // since we can't check for data on the socket, we just assume there is some 18 | func maybeHasData(_ net.Conn) bool { 19 | return true 20 | } 21 | -------------------------------------------------------------------------------- /internal/internal.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/redis/go-redis/v9/internal/rand" 7 | ) 8 | 9 | func RetryBackoff(retry int, minBackoff, maxBackoff time.Duration) time.Duration { 10 | if retry < 0 { 11 | panic("not reached") 12 | } 13 | if minBackoff == 0 { 14 | return 0 15 | } 16 | 17 | d := minBackoff << uint(retry) 18 | if d < minBackoff { 19 | return maxBackoff 20 | } 21 | 22 | d = minBackoff + time.Duration(rand.Int63n(int64(d))) 23 | 24 | if d > maxBackoff || d < minBackoff { 25 | d = maxBackoff 26 | } 27 | 28 | return d 29 | } 30 | -------------------------------------------------------------------------------- /example/cluster-mget/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 2 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 3 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 4 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 6 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 7 | -------------------------------------------------------------------------------- /example/hll/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/redis/go-redis/v9" 8 | ) 9 | 10 | func main() { 11 | ctx := context.Background() 12 | 13 | rdb := redis.NewClient(&redis.Options{ 14 | Addr: ":6379", 15 | }) 16 | _ = rdb.FlushDB(ctx).Err() 17 | 18 | for i := 0; i < 10; i++ { 19 | if err := rdb.PFAdd(ctx, "myset", fmt.Sprint(i)).Err(); err != nil { 20 | panic(err) 21 | } 22 | } 23 | 24 | card, err := rdb.PFCount(ctx, "myset").Result() 25 | if err != nil { 26 | panic(err) 27 | } 28 | 29 | fmt.Println("set cardinality", card) 30 | } 31 | -------------------------------------------------------------------------------- /maintnotifications/state.go: -------------------------------------------------------------------------------- 1 | package maintnotifications 2 | 3 | // State represents the current state of a maintenance operation 4 | type State int 5 | 6 | const ( 7 | // StateIdle indicates no upgrade is in progress 8 | StateIdle State = iota 9 | 10 | // StateHandoff indicates a connection handoff is in progress 11 | StateMoving 12 | ) 13 | 14 | // String returns a string representation of the state. 15 | func (s State) String() string { 16 | switch s { 17 | case StateIdle: 18 | return "idle" 19 | case StateMoving: 20 | return "moving" 21 | default: 22 | return "unknown" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | branches: 8 | - master 9 | - main 10 | - v9 11 | - v9.8 12 | pull_request: 13 | 14 | permissions: 15 | contents: read 16 | pull-requests: read # for golangci/golangci-lint-action to fetch pull requests 17 | 18 | jobs: 19 | golangci: 20 | name: lint 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v6 24 | - name: golangci-lint 25 | uses: golangci/golangci-lint-action@v9.2.0 26 | with: 27 | verify: true 28 | 29 | -------------------------------------------------------------------------------- /push/handler.go: -------------------------------------------------------------------------------- 1 | package push 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // NotificationHandler defines the interface for push notification handlers. 8 | type NotificationHandler interface { 9 | // HandlePushNotification processes a push notification with context information. 10 | // The handlerCtx provides information about the client, connection pool, and connection 11 | // on which the notification was received, allowing handlers to make informed decisions. 12 | // Returns an error if the notification could not be handled. 13 | HandlePushNotification(ctx context.Context, handlerCtx NotificationHandlerContext, notification []interface{}) error 14 | } 15 | -------------------------------------------------------------------------------- /unit_test.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // mockCmdable is a mock implementation of cmdable that records the last command. 8 | // This is used for unit testing command construction without requiring a Redis server. 9 | type mockCmdable struct { 10 | lastCmd Cmder 11 | returnErr error 12 | } 13 | 14 | func (m *mockCmdable) call(_ context.Context, cmd Cmder) error { 15 | m.lastCmd = cmd 16 | if m.returnErr != nil { 17 | cmd.SetErr(m.returnErr) 18 | } 19 | return m.returnErr 20 | } 21 | 22 | func (m *mockCmdable) asCmdable() cmdable { 23 | return func(ctx context.Context, cmd Cmder) error { 24 | return m.call(ctx, cmd) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/spellcheck-settings.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | - name: Markdown 3 | expect_match: false 4 | apsell: 5 | lang: en 6 | d: en_US 7 | ignore-case: true 8 | dictionary: 9 | wordlists: 10 | - .github/wordlist.txt 11 | output: wordlist.dic 12 | pipeline: 13 | - pyspelling.filters.markdown: 14 | markdown_extensions: 15 | - markdown.extensions.extra: 16 | - pyspelling.filters.html: 17 | comments: false 18 | attributes: 19 | - alt 20 | ignores: 21 | - ':matches(code, pre)' 22 | - code 23 | - pre 24 | - blockquote 25 | - img 26 | sources: 27 | - 'README.md' 28 | - 'FAQ.md' 29 | - 'docs/**' 30 | -------------------------------------------------------------------------------- /extra/rediscmd/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/extra/rediscmd/v9 2 | 3 | go 1.21 4 | 5 | replace github.com/redis/go-redis/v9 => ../.. 6 | 7 | require ( 8 | github.com/bsm/ginkgo/v2 v2.12.0 9 | github.com/bsm/gomega v1.27.10 10 | github.com/redis/go-redis/v9 v9.18.0-beta.2 11 | ) 12 | 13 | require ( 14 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 15 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 16 | go.uber.org/atomic v1.11.0 // indirect 17 | ) 18 | 19 | retract ( 20 | v9.7.2 // This version was accidentally released. Please use version 9.7.3 instead. 21 | v9.5.3 // This version was accidentally released. Please use version 9.6.0 instead. 22 | ) 23 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/v9 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/bsm/ginkgo/v2 v2.12.0 7 | github.com/bsm/gomega v1.27.10 8 | github.com/cespare/xxhash/v2 v2.3.0 9 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f 10 | ) 11 | 12 | require go.uber.org/atomic v1.11.0 13 | 14 | retract ( 15 | v9.15.1 // This version is used to retract v9.15.0 16 | v9.15.0 // This version was accidentally released. It is identical to 9.15.0-beta.2 17 | v9.7.2 // This version was accidentally released. Please use version 9.7.3 instead. 18 | v9.5.4 // This version was accidentally released. Please use version 9.6.0 instead. 19 | v9.5.3 // This version was accidentally released. Please use version 9.6.0 instead. 20 | ) 21 | -------------------------------------------------------------------------------- /extra/rediscmd/rediscmd_test.go: -------------------------------------------------------------------------------- 1 | package rediscmd 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/bsm/ginkgo/v2" 7 | . "github.com/bsm/gomega" 8 | ) 9 | 10 | func TestGinkgo(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "redisext") 13 | } 14 | 15 | var _ = Describe("AppendArg", func() { 16 | DescribeTable("...", 17 | func(src string, wanted string) { 18 | b := appendArg(nil, src) 19 | Expect(string(b)).To(Equal(wanted)) 20 | }, 21 | 22 | Entry("", "-inf", "-inf"), 23 | Entry("", "+inf", "+inf"), 24 | Entry("", "foo.bar", "foo.bar"), 25 | Entry("", "foo:bar", "foo:bar"), 26 | Entry("", "foo{bar}", "foo{bar}"), 27 | Entry("", "foo-123_BAR", "foo-123_BAR"), 28 | Entry("", "foo\nbar", "666f6f0a626172"), 29 | Entry("", "\000", "00"), 30 | ) 31 | }) 32 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | run: 3 | timeout: 5m 4 | tests: false 5 | linters: 6 | settings: 7 | staticcheck: 8 | checks: 9 | - all 10 | # Incorrect or missing package comment. 11 | # https://staticcheck.dev/docs/checks/#ST1000 12 | - -ST1000 13 | # Omit embedded fields from selector expression. 14 | # https://staticcheck.dev/docs/checks/#QF1008 15 | - -QF1008 16 | - -ST1003 17 | exclusions: 18 | generated: lax 19 | presets: 20 | - comments 21 | - common-false-positives 22 | - legacy 23 | - std-error-handling 24 | paths: 25 | - third_party$ 26 | - builtin$ 27 | - examples$ 28 | formatters: 29 | exclusions: 30 | generated: lax 31 | paths: 32 | - third_party$ 33 | - builtin$ 34 | - examples$ 35 | -------------------------------------------------------------------------------- /extra/redisotel/README.md: -------------------------------------------------------------------------------- 1 | # OpenTelemetry instrumentation for go-redis 2 | 3 | ## Installation 4 | 5 | ```bash 6 | go get github.com/redis/go-redis/extra/redisotel/v9 7 | ``` 8 | 9 | ## Usage 10 | 11 | Tracing is enabled by adding a hook: 12 | 13 | ```go 14 | import ( 15 | "github.com/redis/go-redis/v9" 16 | "github.com/redis/go-redis/extra/redisotel/v9" 17 | ) 18 | 19 | rdb := rdb.NewClient(&rdb.Options{...}) 20 | 21 | // Enable tracing instrumentation. 22 | if err := redisotel.InstrumentTracing(rdb); err != nil { 23 | panic(err) 24 | } 25 | 26 | // Enable metrics instrumentation. 27 | if err := redisotel.InstrumentMetrics(rdb); err != nil { 28 | panic(err) 29 | } 30 | ``` 31 | 32 | See [example](../../example/otel) and 33 | [Monitoring Go Redis Performance and Errors](https://redis.uptrace.dev/guide/go-redis-monitoring.html) 34 | for details. 35 | -------------------------------------------------------------------------------- /extra/rediscensus/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/extra/rediscensus/v9 2 | 3 | go 1.21 4 | 5 | replace github.com/redis/go-redis/v9 => ../.. 6 | 7 | replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd 8 | 9 | require ( 10 | github.com/redis/go-redis/extra/rediscmd/v9 v9.18.0-beta.2 11 | github.com/redis/go-redis/v9 v9.18.0-beta.2 12 | go.opencensus.io v0.24.0 13 | ) 14 | 15 | require ( 16 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 17 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 18 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 19 | go.uber.org/atomic v1.11.0 // indirect 20 | ) 21 | 22 | retract ( 23 | v9.7.2 // This version was accidentally released. Please use version 9.7.3 instead. 24 | v9.5.3 // This version was accidentally released. Please use version 9.6.0 instead. 25 | ) 26 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | # branches to consider in the event; optional, defaults to all 6 | branches: 7 | - master 8 | 9 | permissions: {} 10 | jobs: 11 | update_release_draft: 12 | permissions: 13 | pull-requests: write # to add label to PR (release-drafter/release-drafter) 14 | contents: write # to create a github release (release-drafter/release-drafter) 15 | 16 | runs-on: ubuntu-latest 17 | steps: 18 | # Drafts your next Release notes as Pull Requests are merged into "master" 19 | - uses: release-drafter/release-drafter@v6 20 | with: 21 | # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml 22 | config-name: release-drafter-config.yml 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | -------------------------------------------------------------------------------- /internal/util/convert_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | ) 7 | 8 | func TestParseStringToFloat(t *testing.T) { 9 | tests := []struct { 10 | in string 11 | want float64 12 | ok bool 13 | }{ 14 | {"1.23", 1.23, true}, 15 | {"inf", math.Inf(1), true}, 16 | {"-inf", math.Inf(-1), true}, 17 | {"nan", math.NaN(), true}, 18 | {"oops", 0, false}, 19 | } 20 | 21 | for _, tc := range tests { 22 | got, err := ParseStringToFloat(tc.in) 23 | if tc.ok { 24 | if err != nil { 25 | t.Fatalf("ParseFloat(%q) error: %v", tc.in, err) 26 | } 27 | if math.IsNaN(tc.want) { 28 | if !math.IsNaN(got) { 29 | t.Errorf("ParseFloat(%q) = %v; want NaN", tc.in, got) 30 | } 31 | } else if got != tc.want { 32 | t.Errorf("ParseFloat(%q) = %v; want %v", tc.in, got, tc.want) 33 | } 34 | } else { 35 | if err == nil { 36 | t.Errorf("ParseFloat(%q) expected error, got nil", tc.in) 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /doctests/README.md: -------------------------------------------------------------------------------- 1 | # Command examples for redis.io 2 | 3 | These examples appear on the [Redis documentation](https://redis.io) site as part of the tabbed examples interface. 4 | 5 | ## How to add examples 6 | 7 | - Create a Go test file with a meaningful name in the current folder. 8 | - Create a single method prefixed with `Example` and write your test in it. 9 | - Determine the id for the example you're creating and add it as the first line of the file: `// EXAMPLE: set_and_get`. 10 | - We're using the [Testable Examples](https://go.dev/blog/examples) feature of Go to test the desired output has been written to stdout. 11 | 12 | ### Special markup 13 | 14 | See https://github.com/redis-stack/redis-stack-website#readme for more details. 15 | 16 | ## How to test the examples 17 | 18 | - Start a Redis server locally on port 6379 19 | - CD into the `doctests` directory 20 | - Run `go test` to test all examples in the directory. 21 | - Run `go test filename.go` to test a single file 22 | 23 | -------------------------------------------------------------------------------- /push_notifications.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "github.com/redis/go-redis/v9/push" 5 | ) 6 | 7 | // NewPushNotificationProcessor creates a new push notification processor 8 | // This processor maintains a registry of handlers and processes push notifications 9 | // It is used for RESP3 connections where push notifications are available 10 | func NewPushNotificationProcessor() push.NotificationProcessor { 11 | return push.NewProcessor() 12 | } 13 | 14 | // NewVoidPushNotificationProcessor creates a new void push notification processor 15 | // This processor does not maintain any handlers and always returns nil for all operations 16 | // It is used for RESP2 connections where push notifications are not available 17 | // It can also be used to disable push notifications for RESP3 connections, where 18 | // it will discard all push notifications without processing them 19 | func NewVoidPushNotificationProcessor() push.NotificationProcessor { 20 | return push.NewVoidProcessor() 21 | } 22 | -------------------------------------------------------------------------------- /example/hll/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 2 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 3 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 4 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 7 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 8 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 9 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 10 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 11 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 12 | -------------------------------------------------------------------------------- /.github/workflows/doctests.yaml: -------------------------------------------------------------------------------- 1 | name: Documentation Tests 2 | 3 | on: 4 | push: 5 | branches: [master, examples] 6 | pull_request: 7 | branches: [master, examples] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | doctests: 14 | name: doctests 15 | runs-on: ubuntu-latest 16 | 17 | services: 18 | redis-stack: 19 | image: redislabs/client-libs-test:8.4.0 20 | env: 21 | TLS_ENABLED: no 22 | REDIS_CLUSTER: no 23 | PORT: 6379 24 | ports: 25 | - 6379:6379 26 | 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | go-version: ["1.24"] 31 | 32 | steps: 33 | - name: Set up ${{ matrix.go-version }} 34 | uses: actions/setup-go@v6 35 | with: 36 | go-version: ${{ matrix.go-version }} 37 | 38 | - name: Checkout code 39 | uses: actions/checkout@v6 40 | 41 | - name: Test doc examples 42 | working-directory: ./doctests 43 | run: make test 44 | -------------------------------------------------------------------------------- /example/redis-bloom/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 2 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 3 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 4 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 7 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 8 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 9 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 10 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 11 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 12 | -------------------------------------------------------------------------------- /example/lua-scripting/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 2 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 3 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 4 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 7 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 8 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 9 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 10 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 11 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 12 | -------------------------------------------------------------------------------- /example/otel/config/vector.toml: -------------------------------------------------------------------------------- 1 | [sources.syslog_logs] 2 | type = "demo_logs" 3 | format = "syslog" 4 | 5 | [sources.apache_common_logs] 6 | type = "demo_logs" 7 | format = "apache_common" 8 | 9 | [sources.apache_error_logs] 10 | type = "demo_logs" 11 | format = "apache_error" 12 | 13 | [sources.json_logs] 14 | type = "demo_logs" 15 | format = "json" 16 | 17 | # Parse Syslog logs 18 | # See the Vector Remap Language reference for more info: https://vrl.dev 19 | [transforms.parse_logs] 20 | type = "remap" 21 | inputs = ["syslog_logs"] 22 | source = ''' 23 | . = parse_syslog!(string!(.message)) 24 | ''' 25 | 26 | # Export data to Uptrace. 27 | [sinks.uptrace] 28 | type = "http" 29 | inputs = ["parse_logs", "apache_common_logs", "apache_error_logs", "json_logs"] 30 | encoding.codec = "json" 31 | framing.method = "newline_delimited" 32 | compression = "gzip" 33 | uri = "http://uptrace:14318/api/v1/vector/logs" 34 | #uri = "https://api.uptrace.dev/api/v1/vector/logs" 35 | headers.uptrace-dsn = "http://project2_secret_token@localhost:14317/2" 36 | -------------------------------------------------------------------------------- /example/maintnotifiations-pubsub/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 2 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 3 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 4 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 7 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 8 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 9 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 10 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 11 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 12 | -------------------------------------------------------------------------------- /.github/wordlist.txt: -------------------------------------------------------------------------------- 1 | ACLs 2 | APIs 3 | autoload 4 | autoloader 5 | autoloading 6 | analytics 7 | Autoloading 8 | backend 9 | backends 10 | behaviour 11 | CAS 12 | ClickHouse 13 | config 14 | customizable 15 | Customizable 16 | dataset 17 | de 18 | DisableIdentity 19 | ElastiCache 20 | extensibility 21 | FPM 22 | Golang 23 | IANA 24 | keyspace 25 | keyspaces 26 | Kvrocks 27 | localhost 28 | Lua 29 | MSSQL 30 | namespace 31 | NoSQL 32 | OpenTelemetry 33 | ORM 34 | Packagist 35 | PhpRedis 36 | pipelining 37 | pluggable 38 | Predis 39 | PSR 40 | Quickstart 41 | README 42 | rebalanced 43 | rebalancing 44 | redis 45 | Redis 46 | RocksDB 47 | runtime 48 | SHA 49 | sharding 50 | SETNAME 51 | SpellCheck 52 | SSL 53 | struct 54 | stunnel 55 | SynDump 56 | TCP 57 | TLS 58 | UnstableResp 59 | uri 60 | URI 61 | url 62 | variadic 63 | RedisStack 64 | RedisGears 65 | RedisTimeseries 66 | RediSearch 67 | RawResult 68 | RawVal 69 | entra 70 | EntraID 71 | Entra 72 | OAuth 73 | Azure 74 | StreamingCredentialsProvider 75 | oauth 76 | entraid 77 | MiB 78 | KiB 79 | oldstable 80 | -------------------------------------------------------------------------------- /extra/redisotel/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/extra/redisotel/v9 2 | 3 | go 1.21 4 | 5 | replace github.com/redis/go-redis/v9 => ../.. 6 | 7 | replace github.com/redis/go-redis/extra/rediscmd/v9 => ../rediscmd 8 | 9 | require ( 10 | github.com/redis/go-redis/extra/rediscmd/v9 v9.18.0-beta.2 11 | github.com/redis/go-redis/v9 v9.18.0-beta.2 12 | go.opentelemetry.io/otel v1.22.0 13 | go.opentelemetry.io/otel/metric v1.22.0 14 | go.opentelemetry.io/otel/sdk v1.22.0 15 | go.opentelemetry.io/otel/trace v1.22.0 16 | ) 17 | 18 | require ( 19 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 20 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 21 | github.com/go-logr/logr v1.4.1 // indirect 22 | github.com/go-logr/stdr v1.2.2 // indirect 23 | go.uber.org/atomic v1.11.0 // indirect 24 | golang.org/x/sys v0.16.0 // indirect 25 | ) 26 | 27 | retract ( 28 | v9.7.2 // This version was accidentally released. Please use version 9.7.3 instead. 29 | v9.5.3 // This version was accidentally released. Please use version 9.6.0 instead. 30 | ) 31 | -------------------------------------------------------------------------------- /doctests/set_get_test.go: -------------------------------------------------------------------------------- 1 | // EXAMPLE: set_and_get 2 | // HIDE_START 3 | package example_commands_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/redis/go-redis/v9" 10 | ) 11 | 12 | func ExampleClient_Set_and_get() { 13 | ctx := context.Background() 14 | 15 | rdb := redis.NewClient(&redis.Options{ 16 | Addr: "localhost:6379", 17 | Password: "", // no password docs 18 | DB: 0, // use default DB 19 | }) 20 | 21 | // HIDE_END 22 | 23 | // REMOVE_START 24 | // start with fresh database 25 | rdb.FlushDB(ctx) 26 | errFlush := rdb.FlushDB(ctx).Err() // Clear the database before each test 27 | if errFlush != nil { 28 | panic(errFlush) 29 | } 30 | // REMOVE_END 31 | 32 | err := rdb.Set(ctx, "bike:1", "Process 134", 0).Err() 33 | if err != nil { 34 | panic(err) 35 | } 36 | 37 | fmt.Println("OK") 38 | 39 | value, err := rdb.Get(ctx, "bike:1").Result() 40 | if err != nil { 41 | panic(err) 42 | } 43 | fmt.Printf("The name of the bike is %s", value) 44 | // HIDE_START 45 | 46 | // Output: OK 47 | // The name of the bike is Process 134 48 | } 49 | 50 | // HIDE_END 51 | -------------------------------------------------------------------------------- /example/hset-struct/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 2 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 3 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 4 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 8 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 11 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 12 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 13 | -------------------------------------------------------------------------------- /example/scan-struct/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 2 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 3 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 4 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 8 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 11 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 12 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 13 | -------------------------------------------------------------------------------- /maintnotifications/e2e/main_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "testing" 7 | "time" 8 | 9 | "github.com/redis/go-redis/v9" 10 | "github.com/redis/go-redis/v9/logging" 11 | ) 12 | 13 | // Global log collector 14 | var logCollector *TestLogCollector 15 | 16 | const defaultTestTimeout = 30 * time.Minute 17 | 18 | // Global fault injector client 19 | var faultInjector *FaultInjectorClient 20 | 21 | func TestMain(m *testing.M) { 22 | var err error 23 | if os.Getenv("E2E_SCENARIO_TESTS") != "true" { 24 | log.Println("Skipping scenario tests, E2E_SCENARIO_TESTS is not set") 25 | return 26 | } 27 | 28 | faultInjector, err = CreateTestFaultInjector() 29 | if err != nil { 30 | panic("Failed to create fault injector: " + err.Error()) 31 | } 32 | // use log collector to capture logs from redis clients 33 | logCollector = NewTestLogCollector() 34 | redis.SetLogger(logCollector) 35 | redis.SetLogLevel(logging.LogLevelDebug) 36 | 37 | logCollector.Clear() 38 | defer logCollector.Clear() 39 | log.Println("Running scenario tests...") 40 | status := m.Run() 41 | os.Exit(status) 42 | } 43 | -------------------------------------------------------------------------------- /doctests/lpush_lrange_test.go: -------------------------------------------------------------------------------- 1 | // EXAMPLE: lpush_and_lrange 2 | // HIDE_START 3 | package example_commands_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "time" 9 | 10 | "github.com/redis/go-redis/v9" 11 | ) 12 | 13 | func ExampleClient_LPush_and_lrange() { 14 | ctx := context.Background() 15 | 16 | rdb := redis.NewClient(&redis.Options{ 17 | Addr: "localhost:6379", 18 | Password: "", // no password docs 19 | DB: 0, // use default DB 20 | }) 21 | 22 | // HIDE_END 23 | 24 | // REMOVE_START 25 | errFlush := rdb.FlushDB(ctx).Err() // Clear the database before each test 26 | if errFlush != nil { 27 | panic(errFlush) 28 | } 29 | // REMOVE_END 30 | 31 | listSize, err := rdb.LPush(ctx, "my_bikes", "bike:1", "bike:2").Result() 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | fmt.Println(listSize) 37 | time.Sleep(10 * time.Millisecond) // Simulate some delay 38 | 39 | value, err := rdb.LRange(ctx, "my_bikes", 0, -1).Result() 40 | if err != nil { 41 | panic(err) 42 | } 43 | fmt.Println(value) 44 | // HIDE_START 45 | 46 | // Output: 2 47 | // [bike:2 bike:1] 48 | } 49 | 50 | // HIDE_END 51 | -------------------------------------------------------------------------------- /extra/redisprometheus/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/extra/redisprometheus/v9 2 | 3 | go 1.21 4 | 5 | replace github.com/redis/go-redis/v9 => ../.. 6 | 7 | require ( 8 | github.com/prometheus/client_golang v1.14.0 9 | github.com/redis/go-redis/v9 v9.18.0-beta.2 10 | ) 11 | 12 | require ( 13 | github.com/beorn7/perks v1.0.1 // indirect 14 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 15 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 16 | github.com/golang/protobuf v1.5.2 // indirect 17 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 18 | github.com/prometheus/client_model v0.3.0 // indirect 19 | github.com/prometheus/common v0.39.0 // indirect 20 | github.com/prometheus/procfs v0.9.0 // indirect 21 | go.uber.org/atomic v1.11.0 // indirect 22 | golang.org/x/sys v0.4.0 // indirect 23 | google.golang.org/protobuf v1.33.0 // indirect 24 | gopkg.in/yaml.v3 v3.0.1 // indirect 25 | ) 26 | 27 | retract ( 28 | v9.7.2 // This version was accidentally released. Please use version 9.7.3 instead. 29 | v9.5.3 // This version was accidentally released. Please use version 9.6.0 instead. 30 | ) 31 | -------------------------------------------------------------------------------- /fuzz/fuzz.go: -------------------------------------------------------------------------------- 1 | //go:build gofuzz 2 | // +build gofuzz 3 | 4 | package fuzz 5 | 6 | import ( 7 | "context" 8 | "time" 9 | 10 | "github.com/redis/go-redis/v9" 11 | ) 12 | 13 | var ( 14 | ctx = context.Background() 15 | rdb *redis.Client 16 | ) 17 | 18 | func init() { 19 | rdb = redis.NewClient(&redis.Options{ 20 | Addr: ":6379", 21 | DialTimeout: 10 * time.Second, 22 | ReadTimeout: 10 * time.Second, 23 | WriteTimeout: 10 * time.Second, 24 | PoolSize: 10, 25 | PoolTimeout: 10 * time.Second, 26 | }) 27 | } 28 | 29 | func Fuzz(data []byte) int { 30 | arrayLen := len(data) 31 | if arrayLen < 4 { 32 | return -1 33 | } 34 | maxIter := int(uint(data[0])) 35 | for i := 0; i < maxIter && i < arrayLen; i++ { 36 | n := i % arrayLen 37 | if n == 0 { 38 | _ = rdb.Set(ctx, string(data[i:]), string(data[i:]), 0).Err() 39 | } else if n == 1 { 40 | _, _ = rdb.Get(ctx, string(data[i:])).Result() 41 | } else if n == 2 { 42 | _, _ = rdb.Incr(ctx, string(data[i:])).Result() 43 | } else if n == 3 { 44 | var cursor uint64 45 | _, _, _ = rdb.Scan(ctx, cursor, string(data[i:]), 10).Result() 46 | } 47 | } 48 | return 1 49 | } 50 | -------------------------------------------------------------------------------- /extra/rediscmd/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 2 | github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= 3 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 4 | github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 5 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 6 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 9 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 11 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 12 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 13 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 14 | -------------------------------------------------------------------------------- /maintnotifications/e2e/doc.go: -------------------------------------------------------------------------------- 1 | // Package e2e provides end-to-end testing scenarios for the maintenance notifications system. 2 | // 3 | // This package contains comprehensive test scenarios that validate the maintenance notifications 4 | // functionality in realistic environments. The tests are designed to work with Redis Enterprise 5 | // clusters and require specific environment configuration. 6 | // 7 | // Environment Variables: 8 | // - E2E_SCENARIO_TESTS: Set to "true" to enable scenario tests 9 | // - REDIS_ENDPOINTS_CONFIG_PATH: Path to endpoints configuration file 10 | // - FAULT_INJECTION_API_URL: URL for fault injection API (optional) 11 | // 12 | // Test Scenarios: 13 | // - Basic Push Notifications: Core functionality testing 14 | // - Endpoint Types: Different endpoint resolution strategies 15 | // - Timeout Configurations: Various timeout strategies 16 | // - TLS Configurations: Different TLS setups 17 | // - Stress Testing: Extreme load and concurrent operations 18 | // 19 | // Note: Maintenance notifications are currently supported only in standalone Redis clients. 20 | // Cluster clients (ClusterClient, FailoverClient, etc.) do not yet support this functionality. 21 | package e2e 22 | -------------------------------------------------------------------------------- /.github/release-drafter-config.yml: -------------------------------------------------------------------------------- 1 | name-template: '$NEXT_MINOR_VERSION' 2 | tag-template: 'v$NEXT_MINOR_VERSION' 3 | autolabeler: 4 | - label: 'maintenance' 5 | files: 6 | - '*.md' 7 | - '.github/*' 8 | - label: 'bug' 9 | branch: 10 | - '/bug-.+' 11 | - label: 'maintenance' 12 | branch: 13 | - '/maintenance-.+' 14 | - label: 'feature' 15 | branch: 16 | - '/feature-.+' 17 | categories: 18 | - title: 'Breaking Changes' 19 | labels: 20 | - 'breakingchange' 21 | - title: '🧪 Experimental Features' 22 | labels: 23 | - 'experimental' 24 | - title: '🚀 New Features' 25 | labels: 26 | - 'feature' 27 | - 'enhancement' 28 | - title: '🐛 Bug Fixes' 29 | labels: 30 | - 'fix' 31 | - 'bugfix' 32 | - 'bug' 33 | - 'BUG' 34 | - title: '🧰 Maintenance' 35 | label: 'maintenance' 36 | change-template: '- $TITLE (#$NUMBER)' 37 | exclude-labels: 38 | - 'skip-changelog' 39 | exclude-contributors: 40 | - 'dependabot' 41 | template: | 42 | # Changes 43 | 44 | $CHANGES 45 | 46 | ## Contributors 47 | We'd like to thank all the contributors who worked on this release! 48 | 49 | $CONTRIBUTORS 50 | 51 | -------------------------------------------------------------------------------- /doctests/cmds_string_test.go: -------------------------------------------------------------------------------- 1 | // EXAMPLE: cmds_string 2 | // HIDE_START 3 | package example_commands_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/redis/go-redis/v9" 10 | ) 11 | 12 | // HIDE_END 13 | 14 | func ExampleClient_cmd_incr() { 15 | ctx := context.Background() 16 | 17 | rdb := redis.NewClient(&redis.Options{ 18 | Addr: "localhost:6379", 19 | Password: "", // no password docs 20 | DB: 0, // use default DB 21 | }) 22 | 23 | // REMOVE_START 24 | // make sure we are working with fresh database 25 | rdb.FlushDB(ctx) 26 | rdb.Del(ctx, "mykey") 27 | // REMOVE_END 28 | 29 | // STEP_START incr 30 | incrResult1, err := rdb.Set(ctx, "mykey", "10", 0).Result() 31 | 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | fmt.Println(incrResult1) // >>> OK 37 | 38 | incrResult2, err := rdb.Incr(ctx, "mykey").Result() 39 | 40 | if err != nil { 41 | panic(err) 42 | } 43 | 44 | fmt.Println(incrResult2) // >>> 11 45 | 46 | incrResult3, err := rdb.Get(ctx, "mykey").Result() 47 | 48 | if err != nil { 49 | panic(err) 50 | } 51 | 52 | fmt.Println(incrResult3) // >>> 11 53 | // STEP_END 54 | 55 | // Output: 56 | // OK 57 | // 11 58 | // 11 59 | } 60 | -------------------------------------------------------------------------------- /hyperloglog_commands.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import "context" 4 | 5 | type HyperLogLogCmdable interface { 6 | PFAdd(ctx context.Context, key string, els ...interface{}) *IntCmd 7 | PFCount(ctx context.Context, keys ...string) *IntCmd 8 | PFMerge(ctx context.Context, dest string, keys ...string) *StatusCmd 9 | } 10 | 11 | func (c cmdable) PFAdd(ctx context.Context, key string, els ...interface{}) *IntCmd { 12 | args := make([]interface{}, 2, 2+len(els)) 13 | args[0] = "pfadd" 14 | args[1] = key 15 | args = appendArgs(args, els) 16 | cmd := NewIntCmd(ctx, args...) 17 | _ = c(ctx, cmd) 18 | return cmd 19 | } 20 | 21 | func (c cmdable) PFCount(ctx context.Context, keys ...string) *IntCmd { 22 | args := make([]interface{}, 1+len(keys)) 23 | args[0] = "pfcount" 24 | for i, key := range keys { 25 | args[1+i] = key 26 | } 27 | cmd := NewIntCmd(ctx, args...) 28 | _ = c(ctx, cmd) 29 | return cmd 30 | } 31 | 32 | func (c cmdable) PFMerge(ctx context.Context, dest string, keys ...string) *StatusCmd { 33 | args := make([]interface{}, 2+len(keys)) 34 | args[0] = "pfmerge" 35 | args[1] = dest 36 | for i, key := range keys { 37 | args[2+i] = key 38 | } 39 | cmd := NewStatusCmd(ctx, args...) 40 | _ = c(ctx, cmd) 41 | return cmd 42 | } 43 | -------------------------------------------------------------------------------- /internal/proto/scan_test.go: -------------------------------------------------------------------------------- 1 | package proto_test 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | . "github.com/bsm/ginkgo/v2" 7 | . "github.com/bsm/gomega" 8 | 9 | "github.com/redis/go-redis/v9/internal/proto" 10 | ) 11 | 12 | type testScanSliceStruct struct { 13 | ID int 14 | Name string 15 | } 16 | 17 | func (s *testScanSliceStruct) MarshalBinary() ([]byte, error) { 18 | return json.Marshal(s) 19 | } 20 | 21 | func (s *testScanSliceStruct) UnmarshalBinary(b []byte) error { 22 | return json.Unmarshal(b, s) 23 | } 24 | 25 | var _ = Describe("ScanSlice", func() { 26 | data := []string{ 27 | `{"ID":-1,"Name":"Back Yu"}`, 28 | `{"ID":1,"Name":"szyhf"}`, 29 | } 30 | 31 | It("[]testScanSliceStruct", func() { 32 | var slice []testScanSliceStruct 33 | err := proto.ScanSlice(data, &slice) 34 | Expect(err).NotTo(HaveOccurred()) 35 | Expect(slice).To(Equal([]testScanSliceStruct{ 36 | {-1, "Back Yu"}, 37 | {1, "szyhf"}, 38 | })) 39 | }) 40 | 41 | It("var testContainer []*testScanSliceStruct", func() { 42 | var slice []*testScanSliceStruct 43 | err := proto.ScanSlice(data, &slice) 44 | Expect(err).NotTo(HaveOccurred()) 45 | Expect(slice).To(Equal([]*testScanSliceStruct{ 46 | {-1, "Back Yu"}, 47 | {1, "szyhf"}, 48 | })) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /internal/util/convert.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "strconv" 7 | ) 8 | 9 | // ParseFloat parses a Redis RESP3 float reply into a Go float64, 10 | // handling "inf", "-inf", "nan" per Redis conventions. 11 | func ParseStringToFloat(s string) (float64, error) { 12 | switch s { 13 | case "inf": 14 | return math.Inf(1), nil 15 | case "-inf": 16 | return math.Inf(-1), nil 17 | case "nan", "-nan": 18 | return math.NaN(), nil 19 | } 20 | return strconv.ParseFloat(s, 64) 21 | } 22 | 23 | // MustParseFloat is like ParseFloat but panics on parse errors. 24 | func MustParseFloat(s string) float64 { 25 | f, err := ParseStringToFloat(s) 26 | if err != nil { 27 | panic(fmt.Sprintf("redis: failed to parse float %q: %v", s, err)) 28 | } 29 | return f 30 | } 31 | 32 | // SafeIntToInt32 safely converts an int to int32, returning an error if overflow would occur. 33 | func SafeIntToInt32(value int, fieldName string) (int32, error) { 34 | if value > math.MaxInt32 { 35 | return 0, fmt.Errorf("redis: %s value %d exceeds maximum allowed value %d", fieldName, value, math.MaxInt32) 36 | } 37 | if value < math.MinInt32 { 38 | return 0, fmt.Errorf("redis: %s value %d is below minimum allowed value %d", fieldName, value, math.MinInt32) 39 | } 40 | return int32(value), nil 41 | } 42 | -------------------------------------------------------------------------------- /internal/pool/conn_check_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux || darwin || dragonfly || freebsd || netbsd || openbsd || solaris || illumos 2 | 3 | package pool 4 | 5 | import ( 6 | "net" 7 | "net/http/httptest" 8 | "time" 9 | 10 | . "github.com/bsm/ginkgo/v2" 11 | . "github.com/bsm/gomega" 12 | ) 13 | 14 | var _ = Describe("tests conn_check with real conns", func() { 15 | var ts *httptest.Server 16 | var conn net.Conn 17 | var err error 18 | 19 | BeforeEach(func() { 20 | ts = httptest.NewServer(nil) 21 | conn, err = net.DialTimeout(ts.Listener.Addr().Network(), ts.Listener.Addr().String(), time.Second) 22 | Expect(err).NotTo(HaveOccurred()) 23 | }) 24 | 25 | AfterEach(func() { 26 | ts.Close() 27 | }) 28 | 29 | It("good conn check", func() { 30 | Expect(connCheck(conn)).NotTo(HaveOccurred()) 31 | 32 | Expect(conn.Close()).NotTo(HaveOccurred()) 33 | Expect(connCheck(conn)).To(HaveOccurred()) 34 | }) 35 | 36 | It("bad conn check", func() { 37 | Expect(conn.Close()).NotTo(HaveOccurred()) 38 | Expect(connCheck(conn)).To(HaveOccurred()) 39 | }) 40 | 41 | It("check conn deadline", func() { 42 | Expect(conn.SetDeadline(time.Now())).NotTo(HaveOccurred()) 43 | time.Sleep(time.Millisecond * 10) 44 | Expect(connCheck(conn)).NotTo(HaveOccurred()) 45 | Expect(conn.Close()).NotTo(HaveOccurred()) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /internal/routing/shard_picker.go: -------------------------------------------------------------------------------- 1 | package routing 2 | 3 | import ( 4 | "math/rand" 5 | "sync/atomic" 6 | ) 7 | 8 | // ShardPicker chooses “one arbitrary shard” when the request_policy is 9 | // ReqDefault and the command has no keys. 10 | type ShardPicker interface { 11 | Next(total int) int // returns an index in [0,total) 12 | } 13 | 14 | // StaticShardPicker always returns the same shard index. 15 | type StaticShardPicker struct { 16 | index int 17 | } 18 | 19 | func NewStaticShardPicker(index int) *StaticShardPicker { 20 | return &StaticShardPicker{index: index} 21 | } 22 | 23 | func (p *StaticShardPicker) Next(total int) int { 24 | if total == 0 || p.index >= total { 25 | return 0 26 | } 27 | return p.index 28 | } 29 | 30 | /*─────────────────────────────── 31 | Round-robin (default) 32 | ────────────────────────────────*/ 33 | 34 | type RoundRobinPicker struct { 35 | cnt atomic.Uint32 36 | } 37 | 38 | func (p *RoundRobinPicker) Next(total int) int { 39 | if total == 0 { 40 | return 0 41 | } 42 | i := p.cnt.Add(1) 43 | return int(i-1) % total 44 | } 45 | 46 | /*─────────────────────────────── 47 | Random 48 | ────────────────────────────────*/ 49 | 50 | type RandomPicker struct{} 51 | 52 | func (RandomPicker) Next(total int) int { 53 | if total == 0 { 54 | return 0 55 | } 56 | return rand.Intn(total) 57 | } 58 | -------------------------------------------------------------------------------- /example/lua-scripting/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/redis/go-redis/v9" 8 | ) 9 | 10 | func main() { 11 | ctx := context.Background() 12 | 13 | rdb := redis.NewClient(&redis.Options{ 14 | Addr: ":6379", 15 | }) 16 | _ = rdb.FlushDB(ctx).Err() 17 | 18 | fmt.Printf("# INCR BY\n") 19 | for _, change := range []int{+1, +5, 0} { 20 | num, err := incrBy.Run(ctx, rdb, []string{"my_counter"}, change).Int() 21 | if err != nil { 22 | panic(err) 23 | } 24 | fmt.Printf("incr by %d: %d\n", change, num) 25 | } 26 | 27 | fmt.Printf("\n# SUM\n") 28 | sum, err := sum.Run(ctx, rdb, []string{"my_sum"}, 1, 2, 3).Int() 29 | if err != nil { 30 | panic(err) 31 | } 32 | fmt.Printf("sum is: %d\n", sum) 33 | } 34 | 35 | var incrBy = redis.NewScript(` 36 | local key = KEYS[1] 37 | local change = ARGV[1] 38 | 39 | local value = redis.call("GET", key) 40 | if not value then 41 | value = 0 42 | end 43 | 44 | value = value + change 45 | redis.call("SET", key, value) 46 | 47 | return value 48 | `) 49 | 50 | var sum = redis.NewScript(` 51 | local key = KEYS[1] 52 | 53 | local sum = redis.call("GET", key) 54 | if not sum then 55 | sum = 0 56 | end 57 | 58 | local num_arg = #ARGV 59 | for i = 1, num_arg do 60 | sum = sum + ARGV[i] 61 | end 62 | 63 | redis.call("SET", key, sum) 64 | 65 | return sum 66 | `) 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 The github.com/redis/go-redis Authors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following disclaimer 12 | in the documentation and/or other materials provided with the 13 | distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 18 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 19 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 21 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 23 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | Issue tracker is used for reporting bugs and discussing new features. Please use 10 | [stackoverflow](https://stackoverflow.com) for supporting issues. 11 | 12 | 13 | 14 | ## Expected Behavior 15 | 16 | 17 | 18 | ## Current Behavior 19 | 20 | 21 | 22 | ## Possible Solution 23 | 24 | 25 | 26 | ## Steps to Reproduce 27 | 28 | 29 | 30 | 31 | 1. 32 | 2. 33 | 3. 34 | 4. 35 | 36 | ## Context (Environment) 37 | 38 | 39 | 40 | 41 | 42 | 43 | ## Detailed Description 44 | 45 | 46 | 47 | ## Possible Implementation 48 | 49 | 50 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 2 | github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= 3 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 4 | github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 5 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 6 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 10 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 11 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 13 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 14 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 15 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 16 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 17 | -------------------------------------------------------------------------------- /example/digest-optimistic-locking/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 2 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 3 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 4 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 7 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 8 | github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= 9 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 11 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 12 | github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= 13 | github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= 14 | github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= 15 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 16 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 17 | -------------------------------------------------------------------------------- /example/disable-maintnotifications/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 2 | github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= 3 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 4 | github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 5 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 6 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 10 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 11 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 13 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 14 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 15 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 16 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 17 | -------------------------------------------------------------------------------- /internal/rand/rand.go: -------------------------------------------------------------------------------- 1 | package rand 2 | 3 | import ( 4 | "math/rand" 5 | "sync" 6 | ) 7 | 8 | // Int returns a non-negative pseudo-random int. 9 | func Int() int { return pseudo.Int() } 10 | 11 | // Intn returns, as an int, a non-negative pseudo-random number in [0,n). 12 | // It panics if n <= 0. 13 | func Intn(n int) int { return pseudo.Intn(n) } 14 | 15 | // Int63n returns, as an int64, a non-negative pseudo-random number in [0,n). 16 | // It panics if n <= 0. 17 | func Int63n(n int64) int64 { return pseudo.Int63n(n) } 18 | 19 | // Perm returns, as a slice of n ints, a pseudo-random permutation of the integers [0,n). 20 | func Perm(n int) []int { return pseudo.Perm(n) } 21 | 22 | // Seed uses the provided seed value to initialize the default Source to a 23 | // deterministic state. If Seed is not called, the generator behaves as if 24 | // seeded by Seed(1). 25 | func Seed(n int64) { pseudo.Seed(n) } 26 | 27 | var pseudo = rand.New(&source{src: rand.NewSource(1)}) 28 | 29 | type source struct { 30 | src rand.Source 31 | mu sync.Mutex 32 | } 33 | 34 | func (s *source) Int63() int64 { 35 | s.mu.Lock() 36 | n := s.src.Int63() 37 | s.mu.Unlock() 38 | return n 39 | } 40 | 41 | func (s *source) Seed(seed int64) { 42 | s.mu.Lock() 43 | s.src.Seed(seed) 44 | s.mu.Unlock() 45 | } 46 | 47 | // Shuffle pseudo-randomizes the order of elements. 48 | // n is the number of elements. 49 | // swap swaps the elements with indexes i and j. 50 | func Shuffle(n int, swap func(i, j int)) { pseudo.Shuffle(n, swap) } 51 | -------------------------------------------------------------------------------- /iterator.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // ScanIterator is used to incrementally iterate over a collection of elements. 8 | type ScanIterator struct { 9 | cmd *ScanCmd 10 | pos int 11 | } 12 | 13 | // Err returns the last iterator error, if any. 14 | func (it *ScanIterator) Err() error { 15 | return it.cmd.Err() 16 | } 17 | 18 | // Next advances the cursor and returns true if more values can be read. 19 | func (it *ScanIterator) Next(ctx context.Context) bool { 20 | // Instantly return on errors. 21 | if it.cmd.Err() != nil { 22 | return false 23 | } 24 | 25 | // Advance cursor, check if we are still within range. 26 | if it.pos < len(it.cmd.page) { 27 | it.pos++ 28 | return true 29 | } 30 | 31 | for { 32 | // Return if there is no more data to fetch. 33 | if it.cmd.cursor == 0 { 34 | return false 35 | } 36 | 37 | // Fetch next page. 38 | switch it.cmd.args[0] { 39 | case "scan", "qscan": 40 | it.cmd.args[1] = it.cmd.cursor 41 | default: 42 | it.cmd.args[2] = it.cmd.cursor 43 | } 44 | 45 | err := it.cmd.process(ctx, it.cmd) 46 | if err != nil { 47 | return false 48 | } 49 | 50 | it.pos = 1 51 | 52 | // Redis can occasionally return empty page. 53 | if len(it.cmd.page) > 0 { 54 | return true 55 | } 56 | } 57 | } 58 | 59 | // Val returns the key/field at the current cursor position. 60 | func (it *ScanIterator) Val() string { 61 | var v string 62 | if it.cmd.Err() == nil && it.pos > 0 && it.pos <= len(it.cmd.page) { 63 | v = it.cmd.page[it.pos-1] 64 | } 65 | return v 66 | } 67 | -------------------------------------------------------------------------------- /internal/arg.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/redis/go-redis/v9/internal/util" 9 | ) 10 | 11 | func AppendArg(b []byte, v interface{}) []byte { 12 | switch v := v.(type) { 13 | case nil: 14 | return append(b, ""...) 15 | case string: 16 | return appendUTF8String(b, util.StringToBytes(v)) 17 | case []byte: 18 | return appendUTF8String(b, v) 19 | case int: 20 | return strconv.AppendInt(b, int64(v), 10) 21 | case int8: 22 | return strconv.AppendInt(b, int64(v), 10) 23 | case int16: 24 | return strconv.AppendInt(b, int64(v), 10) 25 | case int32: 26 | return strconv.AppendInt(b, int64(v), 10) 27 | case int64: 28 | return strconv.AppendInt(b, v, 10) 29 | case uint: 30 | return strconv.AppendUint(b, uint64(v), 10) 31 | case uint8: 32 | return strconv.AppendUint(b, uint64(v), 10) 33 | case uint16: 34 | return strconv.AppendUint(b, uint64(v), 10) 35 | case uint32: 36 | return strconv.AppendUint(b, uint64(v), 10) 37 | case uint64: 38 | return strconv.AppendUint(b, v, 10) 39 | case float32: 40 | return strconv.AppendFloat(b, float64(v), 'f', -1, 64) 41 | case float64: 42 | return strconv.AppendFloat(b, v, 'f', -1, 64) 43 | case bool: 44 | if v { 45 | return append(b, "true"...) 46 | } 47 | return append(b, "false"...) 48 | case time.Time: 49 | return v.AppendFormat(b, time.RFC3339Nano) 50 | default: 51 | return append(b, fmt.Sprint(v)...) 52 | } 53 | } 54 | 55 | func appendUTF8String(dst []byte, src []byte) []byte { 56 | dst = append(dst, src...) 57 | return dst 58 | } 59 | -------------------------------------------------------------------------------- /doctests/hll_tutorial_test.go: -------------------------------------------------------------------------------- 1 | // EXAMPLE: hll_tutorial 2 | // HIDE_START 3 | package example_commands_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/redis/go-redis/v9" 10 | ) 11 | 12 | // HIDE_END 13 | 14 | func ExampleClient_pfadd() { 15 | ctx := context.Background() 16 | 17 | rdb := redis.NewClient(&redis.Options{ 18 | Addr: "localhost:6379", 19 | Password: "", // no password docs 20 | DB: 0, // use default DB 21 | }) 22 | 23 | // REMOVE_START 24 | // make sure we are working with fresh database 25 | rdb.FlushDB(ctx) 26 | rdb.Del(ctx, "bikes", "commuter_bikes", "all_bikes") 27 | // REMOVE_END 28 | 29 | // STEP_START pfadd 30 | res1, err := rdb.PFAdd(ctx, "bikes", "Hyperion", "Deimos", "Phoebe", "Quaoar").Result() 31 | 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | fmt.Println(res1) // 1 37 | 38 | res2, err := rdb.PFCount(ctx, "bikes").Result() 39 | 40 | if err != nil { 41 | panic(err) 42 | } 43 | 44 | fmt.Println(res2) // 4 45 | 46 | res3, err := rdb.PFAdd(ctx, "commuter_bikes", "Salacia", "Mimas", "Quaoar").Result() 47 | 48 | if err != nil { 49 | panic(err) 50 | } 51 | 52 | fmt.Println(res3) // 1 53 | 54 | res4, err := rdb.PFMerge(ctx, "all_bikes", "bikes", "commuter_bikes").Result() 55 | 56 | if err != nil { 57 | panic(err) 58 | } 59 | 60 | fmt.Println(res4) // OK 61 | 62 | res5, err := rdb.PFCount(ctx, "all_bikes").Result() 63 | 64 | if err != nil { 65 | panic(err) 66 | } 67 | 68 | fmt.Println(res5) // 6 69 | // STEP_END 70 | 71 | // Output: 72 | // 1 73 | // 4 74 | // 1 75 | // OK 76 | // 6 77 | } 78 | -------------------------------------------------------------------------------- /logging/logging_test.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import "testing" 4 | 5 | func TestLogLevel_String(t *testing.T) { 6 | tests := []struct { 7 | level LogLevelT 8 | expected string 9 | }{ 10 | {LogLevelError, "ERROR"}, 11 | {LogLevelWarn, "WARN"}, 12 | {LogLevelInfo, "INFO"}, 13 | {LogLevelDebug, "DEBUG"}, 14 | {LogLevelT(99), "UNKNOWN"}, 15 | } 16 | 17 | for _, test := range tests { 18 | if got := test.level.String(); got != test.expected { 19 | t.Errorf("LogLevel(%d).String() = %q, want %q", test.level, got, test.expected) 20 | } 21 | } 22 | } 23 | 24 | func TestLogLevel_IsValid(t *testing.T) { 25 | tests := []struct { 26 | level LogLevelT 27 | expected bool 28 | }{ 29 | {LogLevelError, true}, 30 | {LogLevelWarn, true}, 31 | {LogLevelInfo, true}, 32 | {LogLevelDebug, true}, 33 | {LogLevelT(-1), false}, 34 | {LogLevelT(4), false}, 35 | {LogLevelT(99), false}, 36 | } 37 | 38 | for _, test := range tests { 39 | if got := test.level.IsValid(); got != test.expected { 40 | t.Errorf("LogLevel(%d).IsValid() = %v, want %v", test.level, got, test.expected) 41 | } 42 | } 43 | } 44 | 45 | func TestLogLevelConstants(t *testing.T) { 46 | // Test that constants have expected values 47 | if LogLevelError != 0 { 48 | t.Errorf("LogLevelError = %d, want 0", LogLevelError) 49 | } 50 | if LogLevelWarn != 1 { 51 | t.Errorf("LogLevelWarn = %d, want 1", LogLevelWarn) 52 | } 53 | if LogLevelInfo != 2 { 54 | t.Errorf("LogLevelInfo = %d, want 2", LogLevelInfo) 55 | } 56 | if LogLevelDebug != 3 { 57 | t.Errorf("LogLevelDebug = %d, want 3", LogLevelDebug) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /internal/pool/conn_check.go: -------------------------------------------------------------------------------- 1 | //go:build linux || darwin || dragonfly || freebsd || netbsd || openbsd || solaris || illumos 2 | 3 | package pool 4 | 5 | import ( 6 | "errors" 7 | "io" 8 | "net" 9 | "syscall" 10 | "time" 11 | ) 12 | 13 | var errUnexpectedRead = errors.New("unexpected read from socket") 14 | 15 | // connCheck checks if the connection is still alive and if there is data in the socket 16 | // it will try to peek at the next byte without consuming it since we may want to work with it 17 | // later on (e.g. push notifications) 18 | func connCheck(conn net.Conn) error { 19 | // Reset previous timeout. 20 | _ = conn.SetDeadline(time.Time{}) 21 | 22 | sysConn, ok := conn.(syscall.Conn) 23 | if !ok { 24 | return nil 25 | } 26 | rawConn, err := sysConn.SyscallConn() 27 | if err != nil { 28 | return err 29 | } 30 | 31 | var sysErr error 32 | 33 | if err := rawConn.Read(func(fd uintptr) bool { 34 | var buf [1]byte 35 | // Use MSG_PEEK to peek at data without consuming it 36 | n, _, err := syscall.Recvfrom(int(fd), buf[:], syscall.MSG_PEEK|syscall.MSG_DONTWAIT) 37 | 38 | switch { 39 | case n == 0 && err == nil: 40 | sysErr = io.EOF 41 | case n > 0: 42 | sysErr = errUnexpectedRead 43 | case err == syscall.EAGAIN || err == syscall.EWOULDBLOCK: 44 | sysErr = nil 45 | default: 46 | sysErr = err 47 | } 48 | return true 49 | }); err != nil { 50 | return err 51 | } 52 | 53 | return sysErr 54 | } 55 | 56 | // maybeHasData checks if there is data in the socket without consuming it 57 | func maybeHasData(conn net.Conn) bool { 58 | return connCheck(conn) == errUnexpectedRead 59 | } 60 | -------------------------------------------------------------------------------- /doctests/topk_tutorial_test.go: -------------------------------------------------------------------------------- 1 | // EXAMPLE: topk_tutorial 2 | // HIDE_START 3 | package example_commands_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/redis/go-redis/v9" 10 | ) 11 | 12 | // HIDE_END 13 | 14 | func ExampleClient_topk() { 15 | ctx := context.Background() 16 | 17 | rdb := redis.NewClient(&redis.Options{ 18 | Addr: "localhost:6379", 19 | Password: "", // no password docs 20 | DB: 0, // use default DB 21 | }) 22 | 23 | // REMOVE_START 24 | // start with fresh database 25 | rdb.FlushDB(ctx) 26 | rdb.Del(ctx, "bikes:keywords") 27 | // REMOVE_END 28 | 29 | // STEP_START topk 30 | res1, err := rdb.TopKReserve(ctx, "bikes:keywords", 5).Result() 31 | 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | fmt.Println(res1) // >>> OK 37 | 38 | res2, err := rdb.TopKAdd(ctx, "bikes:keywords", 39 | "store", 40 | "seat", 41 | "handlebars", 42 | "handles", 43 | "pedals", 44 | "tires", 45 | "store", 46 | "seat", 47 | ).Result() 48 | 49 | if err != nil { 50 | panic(err) 51 | } 52 | 53 | fmt.Println(res2) // >>> [ handlebars ] 54 | 55 | res3, err := rdb.TopKList(ctx, "bikes:keywords").Result() 56 | 57 | if err != nil { 58 | panic(err) 59 | } 60 | 61 | fmt.Println(res3) // [store seat pedals tires handles] 62 | 63 | res4, err := rdb.TopKQuery(ctx, "bikes:keywords", "store", "handlebars").Result() 64 | 65 | if err != nil { 66 | panic(err) 67 | } 68 | 69 | fmt.Println(res4) // [true false] 70 | // STEP_END 71 | 72 | // Output: 73 | // OK 74 | // [ handlebars ] 75 | // [store seat pedals tires handles] 76 | // [true false] 77 | } 78 | -------------------------------------------------------------------------------- /.github/workflows/test-redis-enterprise.yml: -------------------------------------------------------------------------------- 1 | name: RE Tests 2 | 3 | on: 4 | push: 5 | branches: [master, v9, v9.7, v9.8] 6 | pull_request: 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | build: 13 | name: build 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | go-version: [1.24.x] 19 | re-build: ["7.4.2-54"] 20 | 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v6 24 | 25 | - name: Clone Redis EE docker repository 26 | uses: actions/checkout@v6 27 | with: 28 | repository: RedisLabs/redis-ee-docker 29 | path: redis-ee 30 | 31 | - name: Set up ${{ matrix.go-version }} 32 | uses: actions/setup-go@v6 33 | with: 34 | go-version: ${{ matrix.go-version }} 35 | 36 | - name: Build cluster 37 | working-directory: redis-ee 38 | env: 39 | IMAGE: "redislabs/redis:${{ matrix.re-build }}" 40 | RE_USERNAME: test@test.com 41 | RE_PASS: 12345 42 | RE_CLUSTER_NAME: re-test 43 | RE_USE_OSS_CLUSTER: false 44 | RE_DB_PORT: 6379 45 | run: ./build.sh 46 | 47 | - name: Test 48 | env: 49 | RE_CLUSTER: true 50 | REDIS_VERSION: "7.4" 51 | run: | 52 | go test \ 53 | --ginkgo.skip-file="ring_test.go" \ 54 | --ginkgo.skip-file="sentinel_test.go" \ 55 | --ginkgo.skip-file="osscluster_test.go" \ 56 | --ginkgo.skip-file="pubsub_test.go" \ 57 | --ginkgo.label-filter='!NonRedisEnterprise' 58 | -------------------------------------------------------------------------------- /extra/redisprometheus/README.md: -------------------------------------------------------------------------------- 1 | # Prometheus Metric Collector 2 | 3 | This package implements a [`prometheus.Collector`](https://pkg.go.dev/github.com/prometheus/client_golang@v1.12.2/prometheus#Collector) 4 | for collecting metrics about the connection pool used by the various redis clients. 5 | Supported clients are `redis.Client`, `redis.ClusterClient`, `redis.Ring` and `redis.UniversalClient`. 6 | 7 | ### Example 8 | 9 | ```go 10 | client := redis.NewClient(options) 11 | collector := redisprometheus.NewCollector(namespace, subsystem, client) 12 | prometheus.MustRegister(collector) 13 | ``` 14 | 15 | ### Metrics 16 | 17 | | Name | Type | Description | 18 | |---------------------------|----------------|-----------------------------------------------------------------------------| 19 | | `pool_hit_total` | Counter metric | number of times a connection was found in the pool | 20 | | `pool_miss_total` | Counter metric | number of times a connection was not found in the pool | 21 | | `pool_timeout_total` | Counter metric | number of times a timeout occurred when getting a connection from the pool | 22 | | `pool_conn_total_current` | Gauge metric | current number of connections in the pool | 23 | | `pool_conn_idle_current` | Gauge metric | current number of idle connections in the pool | 24 | | `pool_conn_stale_total` | Counter metric | number of times a connection was removed from the pool because it was stale | 25 | 26 | 27 | -------------------------------------------------------------------------------- /doctests/cuckoo_tutorial_test.go: -------------------------------------------------------------------------------- 1 | // EXAMPLE: cuckoo_tutorial 2 | // HIDE_START 3 | package example_commands_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/redis/go-redis/v9" 10 | ) 11 | 12 | // HIDE_END 13 | 14 | func ExampleClient_cuckoo() { 15 | ctx := context.Background() 16 | 17 | rdb := redis.NewClient(&redis.Options{ 18 | Addr: "localhost:6379", 19 | Password: "", // no password docs 20 | DB: 0, // use default DB 21 | }) 22 | 23 | // REMOVE_START 24 | // make sure we are working with fresh database 25 | rdb.FlushDB(ctx) 26 | rdb.Del(ctx, "bikes:models") 27 | // REMOVE_END 28 | 29 | // STEP_START cuckoo 30 | res1, err := rdb.CFReserve(ctx, "bikes:models", 1000000).Result() 31 | 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | fmt.Println(res1) // >>> OK 37 | 38 | res2, err := rdb.CFAdd(ctx, "bikes:models", "Smoky Mountain Striker").Result() 39 | 40 | if err != nil { 41 | panic(err) 42 | } 43 | 44 | fmt.Println(res2) // >>> true 45 | 46 | res3, err := rdb.CFExists(ctx, "bikes:models", "Smoky Mountain Striker").Result() 47 | 48 | if err != nil { 49 | panic(err) 50 | } 51 | 52 | fmt.Println(res3) // >>> true 53 | 54 | res4, err := rdb.CFExists(ctx, "bikes:models", "Terrible Bike Name").Result() 55 | 56 | if err != nil { 57 | panic(err) 58 | } 59 | 60 | fmt.Println(res4) // >>> false 61 | 62 | res5, err := rdb.CFDel(ctx, "bikes:models", "Smoky Mountain Striker").Result() 63 | 64 | if err != nil { 65 | panic(err) 66 | } 67 | 68 | fmt.Println(res5) // >>> true 69 | // STEP_END 70 | 71 | // Output: 72 | // OK 73 | // true 74 | // true 75 | // false 76 | // true 77 | } 78 | -------------------------------------------------------------------------------- /doctests/bitfield_tutorial_test.go: -------------------------------------------------------------------------------- 1 | // EXAMPLE: bitfield_tutorial 2 | // HIDE_START 3 | package example_commands_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/redis/go-redis/v9" 10 | ) 11 | 12 | // HIDE_END 13 | 14 | func ExampleClient_bf() { 15 | ctx := context.Background() 16 | 17 | rdb := redis.NewClient(&redis.Options{ 18 | Addr: "localhost:6379", 19 | Password: "", // no password docs 20 | DB: 0, // use default DB 21 | }) 22 | 23 | // REMOVE_START 24 | // make sure we are working with fresh database 25 | rdb.FlushDB(ctx) 26 | rdb.Del(ctx, "bike:1:stats") 27 | // REMOVE_END 28 | 29 | // STEP_START bf 30 | res1, err := rdb.BitField(ctx, "bike:1:stats", 31 | "set", "u32", "#0", "1000", 32 | ).Result() 33 | 34 | if err != nil { 35 | panic(err) 36 | } 37 | 38 | fmt.Println(res1) // >>> [0] 39 | 40 | res2, err := rdb.BitField(ctx, 41 | "bike:1:stats", 42 | "incrby", "u32", "#0", "-50", 43 | "incrby", "u32", "#1", "1", 44 | ).Result() 45 | 46 | if err != nil { 47 | panic(err) 48 | } 49 | 50 | fmt.Println(res2) // >>> [950 1] 51 | 52 | res3, err := rdb.BitField(ctx, 53 | "bike:1:stats", 54 | "incrby", "u32", "#0", "500", 55 | "incrby", "u32", "#1", "1", 56 | ).Result() 57 | 58 | if err != nil { 59 | panic(err) 60 | } 61 | 62 | fmt.Println(res3) // >>> [1450 2] 63 | 64 | res4, err := rdb.BitField(ctx, "bike:1:stats", 65 | "get", "u32", "#0", 66 | "get", "u32", "#1", 67 | ).Result() 68 | 69 | if err != nil { 70 | panic(err) 71 | } 72 | 73 | fmt.Println(res4) // >>> [1450 2] 74 | // STEP_END 75 | 76 | // Output: 77 | // [0] 78 | // [950 1] 79 | // [1450 2] 80 | // [1450 2] 81 | } 82 | -------------------------------------------------------------------------------- /example/del-keys-without-ttl/go.sum: -------------------------------------------------------------------------------- 1 | github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= 2 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 3 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 4 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 5 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 8 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 9 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 11 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 12 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 13 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 14 | go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= 15 | go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= 16 | go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= 17 | go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= 18 | go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= 19 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 20 | -------------------------------------------------------------------------------- /internal/customvet/checks/setval/setval.go: -------------------------------------------------------------------------------- 1 | package setval 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "go/types" 7 | 8 | "golang.org/x/tools/go/analysis" 9 | ) 10 | 11 | var Analyzer = &analysis.Analyzer{ 12 | Name: "setval", 13 | Doc: "find Cmder types that are missing a SetVal method", 14 | 15 | Run: func(pass *analysis.Pass) (interface{}, error) { 16 | cmderTypes := make(map[string]token.Pos) 17 | typesWithSetValMethod := make(map[string]bool) 18 | 19 | for _, file := range pass.Files { 20 | for _, decl := range file.Decls { 21 | funcName, receiverType := parseFuncDecl(decl, pass.TypesInfo) 22 | 23 | switch funcName { 24 | case "Result": 25 | cmderTypes[receiverType] = decl.Pos() 26 | case "SetVal": 27 | typesWithSetValMethod[receiverType] = true 28 | } 29 | } 30 | } 31 | 32 | for cmder, pos := range cmderTypes { 33 | if !typesWithSetValMethod[cmder] { 34 | pass.Reportf(pos, "%s is missing a SetVal method", cmder) 35 | } 36 | } 37 | 38 | return nil, nil 39 | }, 40 | } 41 | 42 | func parseFuncDecl(decl ast.Decl, typesInfo *types.Info) (funcName, receiverType string) { 43 | funcDecl, ok := decl.(*ast.FuncDecl) 44 | if !ok { 45 | return "", "" // Not a function declaration. 46 | } 47 | 48 | if funcDecl.Recv == nil { 49 | return "", "" // Not a method. 50 | } 51 | 52 | if len(funcDecl.Recv.List) != 1 { 53 | return "", "" // Unexpected number of receiver arguments. (Can this happen?) 54 | } 55 | 56 | receiverTypeObj := typesInfo.TypeOf(funcDecl.Recv.List[0].Type) 57 | if receiverTypeObj == nil { 58 | return "", "" // Unable to determine the receiver type. 59 | } 60 | 61 | return funcDecl.Name.Name, receiverTypeObj.String() 62 | } 63 | -------------------------------------------------------------------------------- /doctests/cmds_servermgmt_test.go: -------------------------------------------------------------------------------- 1 | // EXAMPLE: cmds_servermgmt 2 | // HIDE_START 3 | package example_commands_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/redis/go-redis/v9" 10 | ) 11 | 12 | // HIDE_END 13 | 14 | func ExampleClient_cmd_flushall() { 15 | ctx := context.Background() 16 | 17 | rdb := redis.NewClient(&redis.Options{ 18 | Addr: "localhost:6379", 19 | Password: "", // no password docs 20 | DB: 0, // use default DB 21 | }) 22 | 23 | // STEP_START flushall 24 | // REMOVE_START 25 | // make sure we are working with fresh database 26 | rdb.FlushDB(ctx) 27 | rdb.Set(ctx, "testkey1", "1", 0) 28 | rdb.Set(ctx, "testkey2", "2", 0) 29 | rdb.Set(ctx, "testkey3", "3", 0) 30 | // REMOVE_END 31 | flushAllResult1, err := rdb.FlushAll(ctx).Result() 32 | 33 | if err != nil { 34 | panic(err) 35 | } 36 | 37 | fmt.Println(flushAllResult1) // >>> OK 38 | 39 | flushAllResult2, err := rdb.Keys(ctx, "*").Result() 40 | 41 | if err != nil { 42 | panic(err) 43 | } 44 | 45 | fmt.Println(flushAllResult2) // >>> [] 46 | // STEP_END 47 | 48 | // Output: 49 | // OK 50 | // [] 51 | } 52 | 53 | func ExampleClient_cmd_info() { 54 | ctx := context.Background() 55 | 56 | rdb := redis.NewClient(&redis.Options{ 57 | Addr: "localhost:6379", 58 | Password: "", // no password docs 59 | DB: 0, // use default DB 60 | }) 61 | 62 | // STEP_START info 63 | infoResult, err := rdb.Info(ctx).Result() 64 | 65 | if err != nil { 66 | panic(err) 67 | } 68 | 69 | // Check the first 8 characters (the full info string contains 70 | // much more text than this). 71 | fmt.Println(infoResult[:8]) // >>> # Server 72 | // STEP_END 73 | 74 | // Output: 75 | // # Server 76 | } 77 | -------------------------------------------------------------------------------- /auth/reauth_credentials_listener.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | // ReAuthCredentialsListener is a struct that implements the CredentialsListener interface. 4 | // It is used to re-authenticate the credentials when they are updated. 5 | // It contains: 6 | // - reAuth: a function that takes the new credentials and returns an error if any. 7 | // - onErr: a function that takes an error and handles it. 8 | type ReAuthCredentialsListener struct { 9 | reAuth func(credentials Credentials) error 10 | onErr func(err error) 11 | } 12 | 13 | // OnNext is called when the credentials are updated. 14 | // It calls the reAuth function with the new credentials. 15 | // If the reAuth function returns an error, it calls the onErr function with the error. 16 | func (c *ReAuthCredentialsListener) OnNext(credentials Credentials) { 17 | if c.reAuth == nil { 18 | return 19 | } 20 | 21 | err := c.reAuth(credentials) 22 | if err != nil { 23 | c.OnError(err) 24 | } 25 | } 26 | 27 | // OnError is called when an error occurs. 28 | // It can be called from both the credentials provider and the reAuth function. 29 | func (c *ReAuthCredentialsListener) OnError(err error) { 30 | if c.onErr == nil { 31 | return 32 | } 33 | 34 | c.onErr(err) 35 | } 36 | 37 | // NewReAuthCredentialsListener creates a new ReAuthCredentialsListener. 38 | // Implements the auth.CredentialsListener interface. 39 | func NewReAuthCredentialsListener(reAuth func(credentials Credentials) error, onErr func(err error)) *ReAuthCredentialsListener { 40 | return &ReAuthCredentialsListener{ 41 | reAuth: reAuth, 42 | onErr: onErr, 43 | } 44 | } 45 | 46 | // Ensure ReAuthCredentialsListener implements the CredentialsListener interface. 47 | var _ CredentialsListener = (*ReAuthCredentialsListener)(nil) -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | help() { 6 | cat <<- EOF 7 | Usage: TAG=tag $0 8 | 9 | Updates version in go.mod files and pushes a new brash to GitHub. 10 | 11 | VARIABLES: 12 | TAG git tag, for example, v1.0.0 13 | EOF 14 | exit 0 15 | } 16 | 17 | if [ -z "$TAG" ] 18 | then 19 | printf "TAG is required\n\n" 20 | help 21 | fi 22 | 23 | TAG_REGEX="^v(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(\\-[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$" 24 | if ! [[ "${TAG}" =~ ${TAG_REGEX} ]]; then 25 | printf "TAG is not valid: ${TAG}\n\n" 26 | exit 1 27 | fi 28 | 29 | TAG_FOUND=`git tag --list ${TAG}` 30 | if [[ ${TAG_FOUND} = ${TAG} ]] ; then 31 | printf "tag ${TAG} already exists\n\n" 32 | exit 1 33 | fi 34 | 35 | if ! git diff --quiet 36 | then 37 | printf "working tree is not clean\n\n" 38 | git status 39 | exit 1 40 | fi 41 | 42 | git checkout master 43 | 44 | PACKAGE_DIRS=$(find . -mindepth 2 -type f -name 'go.mod' -exec dirname {} \; \ 45 | | sed 's/^\.\///' \ 46 | | sort) 47 | 48 | for dir in $PACKAGE_DIRS 49 | do 50 | printf "${dir}: go get -u && go mod tidy\n" 51 | #(cd ./${dir} && go get -u && go mod tidy -compat=1.21) 52 | done 53 | 54 | for dir in $PACKAGE_DIRS 55 | do 56 | sed --in-place \ 57 | "s/redis\/go-redis\([^ ]*\) v.*/redis\/go-redis\1 ${TAG}/" "${dir}/go.mod" 58 | #(cd ./${dir} && go get -u && go mod tidy -compat=1.21) 59 | (cd ./${dir} && go mod tidy -compat=1.21) 60 | done 61 | 62 | sed --in-place "s/\(return \)\"[^\"]*\"/\1\"${TAG#v}\"/" ./version.go 63 | 64 | git checkout -b release/${TAG} master 65 | git add -u 66 | git commit -m "chore: release $TAG (release.sh)" 67 | git push origin release/${TAG} 68 | -------------------------------------------------------------------------------- /example/otel/config/otel-collector.yaml: -------------------------------------------------------------------------------- 1 | extensions: 2 | health_check: 3 | pprof: 4 | endpoint: 0.0.0.0:1777 5 | zpages: 6 | endpoint: 0.0.0.0:55679 7 | 8 | receivers: 9 | otlp: 10 | protocols: 11 | grpc: 12 | http: 13 | hostmetrics: 14 | collection_interval: 10s 15 | scrapers: 16 | cpu: 17 | disk: 18 | load: 19 | filesystem: 20 | memory: 21 | network: 22 | paging: 23 | redis: 24 | endpoint: 'redis-server:6379' 25 | collection_interval: 10s 26 | postgresql: 27 | endpoint: postgres:5432 28 | transport: tcp 29 | username: uptrace 30 | password: uptrace 31 | databases: 32 | - uptrace 33 | tls: 34 | insecure: true 35 | 36 | processors: 37 | resourcedetection: 38 | detectors: ['system'] 39 | cumulativetodelta: 40 | batch: 41 | send_batch_size: 10000 42 | timeout: 10s 43 | 44 | exporters: 45 | otlp/uptrace: 46 | endpoint: http://uptrace:4317 47 | tls: 48 | insecure: true 49 | headers: { 'uptrace-dsn': 'http://project1_secret@localhost:14318/2?grpc=14317' } 50 | debug: 51 | 52 | service: 53 | # telemetry: 54 | # logs: 55 | # level: DEBUG 56 | pipelines: 57 | traces: 58 | receivers: [otlp] 59 | processors: [batch] 60 | exporters: [otlp/uptrace] 61 | metrics: 62 | receivers: [otlp] 63 | processors: [cumulativetodelta, batch] 64 | exporters: [otlp/uptrace] 65 | metrics/hostmetrics: 66 | receivers: [hostmetrics, redis, postgresql] 67 | processors: [cumulativetodelta, batch, resourcedetection] 68 | exporters: [otlp/uptrace] 69 | logs: 70 | receivers: [otlp] 71 | processors: [batch] 72 | exporters: [otlp/uptrace] 73 | 74 | extensions: [health_check, pprof, zpages] 75 | -------------------------------------------------------------------------------- /doctests/bf_tutorial_test.go: -------------------------------------------------------------------------------- 1 | // EXAMPLE: bf_tutorial 2 | // HIDE_START 3 | package example_commands_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/redis/go-redis/v9" 10 | ) 11 | 12 | // HIDE_END 13 | 14 | func ExampleClient_bloom() { 15 | ctx := context.Background() 16 | 17 | rdb := redis.NewClient(&redis.Options{ 18 | Addr: "localhost:6379", 19 | Password: "", // no password docs 20 | DB: 0, // use default DB 21 | }) 22 | 23 | // REMOVE_START 24 | // make sure we are working with fresh database 25 | rdb.FlushDB(ctx) 26 | rdb.Del(ctx, "bikes:models") 27 | // REMOVE_END 28 | 29 | // STEP_START bloom 30 | res1, err := rdb.BFReserve(ctx, "bikes:models", 0.01, 1000).Result() 31 | 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | fmt.Println(res1) // >>> OK 37 | 38 | res2, err := rdb.BFAdd(ctx, "bikes:models", "Smoky Mountain Striker").Result() 39 | 40 | if err != nil { 41 | panic(err) 42 | } 43 | 44 | fmt.Println(res2) // >>> true 45 | 46 | res3, err := rdb.BFExists(ctx, "bikes:models", "Smoky Mountain Striker").Result() 47 | 48 | if err != nil { 49 | panic(err) 50 | } 51 | 52 | fmt.Println(res3) // >>> true 53 | 54 | res4, err := rdb.BFMAdd(ctx, "bikes:models", 55 | "Rocky Mountain Racer", 56 | "Cloudy City Cruiser", 57 | "Windy City Wippet", 58 | ).Result() 59 | 60 | if err != nil { 61 | panic(err) 62 | } 63 | 64 | fmt.Println(res4) // >>> [true true true] 65 | 66 | res5, err := rdb.BFMExists(ctx, "bikes:models", 67 | "Rocky Mountain Racer", 68 | "Cloudy City Cruiser", 69 | "Windy City Wippet", 70 | ).Result() 71 | 72 | if err != nil { 73 | panic(err) 74 | } 75 | 76 | fmt.Println(res5) // >>> [true true true] 77 | // STEP_END 78 | 79 | // Output: 80 | // OK 81 | // true 82 | // true 83 | // [true true true] 84 | // [true true true] 85 | } 86 | -------------------------------------------------------------------------------- /.github/actions/run-tests/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Run go-redis tests' 2 | description: 'Runs go-redis tests against different Redis versions and configurations' 3 | inputs: 4 | go-version: 5 | description: 'Go version to use for running tests' 6 | default: '1.23' 7 | redis-version: 8 | description: 'Redis version to test against' 9 | required: true 10 | runs: 11 | using: "composite" 12 | steps: 13 | - name: Set up ${{ inputs.go-version }} 14 | uses: actions/setup-go@v5 15 | with: 16 | go-version: ${{ inputs.go-version }} 17 | 18 | - name: Setup Test environment 19 | env: 20 | REDIS_VERSION: ${{ inputs.redis-version }} 21 | run: | 22 | set -e 23 | redis_version_np=$(echo "$REDIS_VERSION" | grep -oP '^\d+.\d+') 24 | 25 | # Mapping of redis version to redis testing containers 26 | declare -A redis_version_mapping=( 27 | ["8.4.x"]="8.4.0" 28 | ["8.2.x"]="8.2.1-pre" 29 | ["8.0.x"]="8.0.2" 30 | ) 31 | 32 | if [[ -v redis_version_mapping[$REDIS_VERSION] ]]; then 33 | echo "REDIS_VERSION=${redis_version_np}" >> $GITHUB_ENV 34 | echo "REDIS_IMAGE=redis:${REDIS_VERSION}" >> $GITHUB_ENV 35 | echo "CLIENT_LIBS_TEST_IMAGE=redislabs/client-libs-test:${redis_version_mapping[$REDIS_VERSION]}" >> $GITHUB_ENV 36 | else 37 | echo "Version not found in the mapping." 38 | exit 1 39 | fi 40 | sleep 10 # wait for redis to start 41 | shell: bash 42 | - name: Set up Docker Compose environment with redis ${{ inputs.redis-version }} 43 | run: | 44 | make docker.start 45 | shell: bash 46 | - name: Run tests 47 | env: 48 | RCE_DOCKER: "true" 49 | RE_CLUSTER: "false" 50 | run: | 51 | make test.ci 52 | shell: bash -------------------------------------------------------------------------------- /doctests/cms_tutorial_test.go: -------------------------------------------------------------------------------- 1 | // EXAMPLE: cms_tutorial 2 | // HIDE_START 3 | package example_commands_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/redis/go-redis/v9" 10 | ) 11 | 12 | // HIDE_END 13 | 14 | func ExampleClient_cms() { 15 | ctx := context.Background() 16 | 17 | rdb := redis.NewClient(&redis.Options{ 18 | Addr: "localhost:6379", 19 | Password: "", // no password docs 20 | DB: 0, // use default DB 21 | }) 22 | 23 | // REMOVE_START 24 | // make sure we are working with fresh database 25 | rdb.FlushDB(ctx) 26 | rdb.Del(ctx, "bikes:profit") 27 | // REMOVE_END 28 | 29 | // STEP_START cms 30 | res1, err := rdb.CMSInitByProb(ctx, "bikes:profit", 0.001, 0.002).Result() 31 | 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | fmt.Println(res1) // >>> OK 37 | 38 | res2, err := rdb.CMSIncrBy(ctx, "bikes:profit", 39 | "Smoky Mountain Striker", 100, 40 | ).Result() 41 | 42 | if err != nil { 43 | panic(err) 44 | } 45 | 46 | fmt.Println(res2) // >>> [100] 47 | 48 | res3, err := rdb.CMSIncrBy(ctx, "bikes:profit", 49 | "Rocky Mountain Racer", 200, 50 | "Cloudy City Cruiser", 150, 51 | ).Result() 52 | 53 | if err != nil { 54 | panic(err) 55 | } 56 | 57 | fmt.Println(res3) // >>> [200 150] 58 | 59 | res4, err := rdb.CMSQuery(ctx, "bikes:profit", 60 | "Smoky Mountain Striker", 61 | ).Result() 62 | 63 | if err != nil { 64 | panic(err) 65 | } 66 | 67 | fmt.Println(res4) // >>> [100] 68 | 69 | res5, err := rdb.CMSInfo(ctx, "bikes:profit").Result() 70 | 71 | if err != nil { 72 | panic(err) 73 | } 74 | 75 | fmt.Printf("Width: %v, Depth: %v, Count: %v", 76 | res5.Width, res5.Depth, res5.Count) 77 | // >>> Width: 2000, Depth: 9, Count: 450 78 | // STEP_END 79 | 80 | // Output: 81 | // OK 82 | // [100] 83 | // [200 150] 84 | // [100] 85 | // Width: 2000, Depth: 9, Count: 450 86 | } 87 | -------------------------------------------------------------------------------- /push/registry.go: -------------------------------------------------------------------------------- 1 | package push 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // Registry manages push notification handlers 8 | type Registry struct { 9 | mu sync.RWMutex 10 | handlers map[string]NotificationHandler 11 | protected map[string]bool 12 | } 13 | 14 | // NewRegistry creates a new push notification registry 15 | func NewRegistry() *Registry { 16 | return &Registry{ 17 | handlers: make(map[string]NotificationHandler), 18 | protected: make(map[string]bool), 19 | } 20 | } 21 | 22 | // RegisterHandler registers a handler for a specific push notification name 23 | func (r *Registry) RegisterHandler(pushNotificationName string, handler NotificationHandler, protected bool) error { 24 | if handler == nil { 25 | return ErrHandlerNil 26 | } 27 | 28 | r.mu.Lock() 29 | defer r.mu.Unlock() 30 | 31 | // Check if handler already exists 32 | if _, exists := r.protected[pushNotificationName]; exists { 33 | return ErrHandlerExists(pushNotificationName) 34 | } 35 | 36 | r.handlers[pushNotificationName] = handler 37 | r.protected[pushNotificationName] = protected 38 | return nil 39 | } 40 | 41 | // GetHandler returns the handler for a specific push notification name 42 | func (r *Registry) GetHandler(pushNotificationName string) NotificationHandler { 43 | r.mu.RLock() 44 | defer r.mu.RUnlock() 45 | return r.handlers[pushNotificationName] 46 | } 47 | 48 | // UnregisterHandler removes a handler for a specific push notification name 49 | func (r *Registry) UnregisterHandler(pushNotificationName string) error { 50 | r.mu.Lock() 51 | defer r.mu.Unlock() 52 | 53 | // Check if handler is protected 54 | if protected, exists := r.protected[pushNotificationName]; exists && protected { 55 | return ErrProtectedHandler(pushNotificationName) 56 | } 57 | 58 | delete(r.handlers, pushNotificationName) 59 | delete(r.protected, pushNotificationName) 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /internal/interfaces/interfaces.go: -------------------------------------------------------------------------------- 1 | // Package interfaces provides shared interfaces used by both the main redis package 2 | // and the maintnotifications upgrade package to avoid circular dependencies. 3 | package interfaces 4 | 5 | import ( 6 | "context" 7 | "net" 8 | "time" 9 | ) 10 | 11 | // NotificationProcessor is (most probably) a push.NotificationProcessor 12 | // forward declaration to avoid circular imports 13 | type NotificationProcessor interface { 14 | RegisterHandler(pushNotificationName string, handler interface{}, protected bool) error 15 | UnregisterHandler(pushNotificationName string) error 16 | GetHandler(pushNotificationName string) interface{} 17 | } 18 | 19 | // ClientInterface defines the interface that clients must implement for maintnotifications upgrades. 20 | type ClientInterface interface { 21 | // GetOptions returns the client options. 22 | GetOptions() OptionsInterface 23 | 24 | // GetPushProcessor returns the client's push notification processor. 25 | GetPushProcessor() NotificationProcessor 26 | } 27 | 28 | // OptionsInterface defines the interface for client options. 29 | // Uses an adapter pattern to avoid circular dependencies. 30 | type OptionsInterface interface { 31 | // GetReadTimeout returns the read timeout. 32 | GetReadTimeout() time.Duration 33 | 34 | // GetWriteTimeout returns the write timeout. 35 | GetWriteTimeout() time.Duration 36 | 37 | // GetNetwork returns the network type. 38 | GetNetwork() string 39 | 40 | // GetAddr returns the connection address. 41 | GetAddr() string 42 | 43 | // IsTLSEnabled returns true if TLS is enabled. 44 | IsTLSEnabled() bool 45 | 46 | // GetProtocol returns the protocol version. 47 | GetProtocol() int 48 | 49 | // GetPoolSize returns the connection pool size. 50 | GetPoolSize() int 51 | 52 | // NewDialer returns a new dialer function for the connection. 53 | NewDialer() func(context.Context) (net.Conn, error) 54 | } 55 | -------------------------------------------------------------------------------- /maintnotifications/e2e/utils_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "runtime" 7 | "time" 8 | ) 9 | 10 | func isTimeout(errMsg string) bool { 11 | return contains(errMsg, "i/o timeout") || 12 | contains(errMsg, "deadline exceeded") || 13 | contains(errMsg, "context deadline exceeded") 14 | } 15 | 16 | // isTimeoutError checks if an error is a timeout error 17 | func isTimeoutError(err error) bool { 18 | if err == nil { 19 | return false 20 | } 21 | 22 | // Check for various timeout error types 23 | errStr := err.Error() 24 | return isTimeout(errStr) 25 | } 26 | 27 | // contains checks if a string contains a substring (case-insensitive) 28 | func contains(s, substr string) bool { 29 | return len(s) >= len(substr) && 30 | (s == substr || 31 | (len(s) > len(substr) && 32 | (s[:len(substr)] == substr || 33 | s[len(s)-len(substr):] == substr || 34 | containsSubstring(s, substr)))) 35 | } 36 | 37 | func containsSubstring(s, substr string) bool { 38 | for i := 0; i <= len(s)-len(substr); i++ { 39 | if s[i:i+len(substr)] == substr { 40 | return true 41 | } 42 | } 43 | return false 44 | } 45 | 46 | func printLog(group string, isError bool, format string, args ...interface{}) { 47 | _, filename, line, _ := runtime.Caller(2) 48 | filename = filepath.Base(filename) 49 | finalFormat := "%s:%d [%s][%s] " + format + "\n" 50 | if isError { 51 | finalFormat = "%s:%d [%s][%s][ERROR] " + format + "\n" 52 | } 53 | ts := time.Now().Format("15:04:05.000") 54 | args = append([]interface{}{filename, line, ts, group}, args...) 55 | fmt.Printf(finalFormat, args...) 56 | } 57 | 58 | func actionOutputIfFailed(status *ActionStatusResponse) string { 59 | if status.Status != StatusFailed { 60 | return "" 61 | } 62 | if status.Error != nil { 63 | return fmt.Sprintf("%v", status.Error) 64 | } 65 | if status.Output == nil { 66 | return "" 67 | } 68 | return fmt.Sprintf("%+v", status.Output) 69 | } 70 | -------------------------------------------------------------------------------- /push/handler_context.go: -------------------------------------------------------------------------------- 1 | package push 2 | 3 | // No imports needed for this file 4 | 5 | // NotificationHandlerContext provides context information about where a push notification was received. 6 | // This struct allows handlers to make informed decisions based on the source of the notification 7 | // with strongly typed access to different client types using concrete types. 8 | type NotificationHandlerContext struct { 9 | // Client is the Redis client instance that received the notification. 10 | // It is interface to both allow for future expansion and to avoid 11 | // circular dependencies. The developer is responsible for type assertion. 12 | // It can be one of the following types: 13 | // - *redis.baseClient 14 | // - *redis.Client 15 | // - *redis.ClusterClient 16 | // - *redis.Conn 17 | Client interface{} 18 | 19 | // ConnPool is the connection pool from which the connection was obtained. 20 | // It is interface to both allow for future expansion and to avoid 21 | // circular dependencies. The developer is responsible for type assertion. 22 | // It can be one of the following types: 23 | // - *pool.ConnPool 24 | // - *pool.SingleConnPool 25 | // - *pool.StickyConnPool 26 | ConnPool interface{} 27 | 28 | // PubSub is the PubSub instance that received the notification. 29 | // It is interface to both allow for future expansion and to avoid 30 | // circular dependencies. The developer is responsible for type assertion. 31 | // It can be one of the following types: 32 | // - *redis.PubSub 33 | PubSub interface{} 34 | 35 | // Conn is the specific connection on which the notification was received. 36 | // It is interface to both allow for future expansion and to avoid 37 | // circular dependencies. The developer is responsible for type assertion. 38 | // It can be one of the following types: 39 | // - *pool.Conn 40 | Conn interface{} 41 | 42 | // IsBlocking indicates if the notification was received on a blocking connection. 43 | IsBlocking bool 44 | } 45 | -------------------------------------------------------------------------------- /internal/once.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The Camlistore Authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package internal 18 | 19 | import ( 20 | "sync" 21 | "sync/atomic" 22 | ) 23 | 24 | // A Once will perform a successful action exactly once. 25 | // 26 | // Unlike a sync.Once, this Once's func returns an error 27 | // and is re-armed on failure. 28 | type Once struct { 29 | m sync.Mutex 30 | done uint32 31 | } 32 | 33 | // Do calls the function f if and only if Do has not been invoked 34 | // without error for this instance of Once. In other words, given 35 | // 36 | // var once Once 37 | // 38 | // if once.Do(f) is called multiple times, only the first call will 39 | // invoke f, even if f has a different value in each invocation unless 40 | // f returns an error. A new instance of Once is required for each 41 | // function to execute. 42 | // 43 | // Do is intended for initialization that must be run exactly once. Since f 44 | // is niladic, it may be necessary to use a function literal to capture the 45 | // arguments to a function to be invoked by Do: 46 | // 47 | // err := config.once.Do(func() error { return config.init(filename) }) 48 | func (o *Once) Do(f func() error) error { 49 | if atomic.LoadUint32(&o.done) == 1 { 50 | return nil 51 | } 52 | // Slow-path. 53 | o.m.Lock() 54 | defer o.m.Unlock() 55 | var err error 56 | if o.done == 0 { 57 | err = f() 58 | if err == nil { 59 | atomic.StoreUint32(&o.done, 1) 60 | } 61 | } 62 | return err 63 | } 64 | -------------------------------------------------------------------------------- /internal/pool/want_conn.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | ) 7 | 8 | type wantConn struct { 9 | mu sync.Mutex // protects ctx, done and sending of the result 10 | ctx context.Context // context for dial, cleared after delivered or canceled 11 | cancelCtx context.CancelFunc 12 | done bool // true after delivered or canceled 13 | result chan wantConnResult // channel to deliver connection or error 14 | } 15 | 16 | // getCtxForDial returns context for dial or nil if connection was delivered or canceled. 17 | func (w *wantConn) getCtxForDial() context.Context { 18 | w.mu.Lock() 19 | defer w.mu.Unlock() 20 | 21 | return w.ctx 22 | } 23 | 24 | func (w *wantConn) tryDeliver(cn *Conn, err error) bool { 25 | w.mu.Lock() 26 | defer w.mu.Unlock() 27 | if w.done { 28 | return false 29 | } 30 | 31 | w.done = true 32 | w.ctx = nil 33 | 34 | w.result <- wantConnResult{cn: cn, err: err} 35 | close(w.result) 36 | 37 | return true 38 | } 39 | 40 | func (w *wantConn) cancel() *Conn { 41 | w.mu.Lock() 42 | var cn *Conn 43 | if w.done { 44 | select { 45 | case result := <-w.result: 46 | cn = result.cn 47 | default: 48 | } 49 | } else { 50 | close(w.result) 51 | } 52 | 53 | w.done = true 54 | w.ctx = nil 55 | w.mu.Unlock() 56 | 57 | return cn 58 | } 59 | 60 | type wantConnResult struct { 61 | cn *Conn 62 | err error 63 | } 64 | 65 | type wantConnQueue struct { 66 | mu sync.RWMutex 67 | items []*wantConn 68 | } 69 | 70 | func newWantConnQueue() *wantConnQueue { 71 | return &wantConnQueue{ 72 | items: make([]*wantConn, 0), 73 | } 74 | } 75 | 76 | func (q *wantConnQueue) enqueue(w *wantConn) { 77 | q.mu.Lock() 78 | defer q.mu.Unlock() 79 | q.items = append(q.items, w) 80 | } 81 | 82 | func (q *wantConnQueue) dequeue() (*wantConn, bool) { 83 | q.mu.Lock() 84 | defer q.mu.Unlock() 85 | 86 | if len(q.items) == 0 { 87 | return nil, false 88 | } 89 | 90 | item := q.items[0] 91 | q.items = q.items[1:] 92 | return item, true 93 | } 94 | -------------------------------------------------------------------------------- /doctests/bitmap_tutorial_test.go: -------------------------------------------------------------------------------- 1 | // EXAMPLE: bitmap_tutorial 2 | // HIDE_START 3 | package example_commands_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/redis/go-redis/v9" 10 | ) 11 | 12 | // HIDE_END 13 | 14 | func ExampleClient_ping() { 15 | ctx := context.Background() 16 | 17 | rdb := redis.NewClient(&redis.Options{ 18 | Addr: "localhost:6379", 19 | Password: "", // no password docs 20 | DB: 0, // use default DB 21 | }) 22 | 23 | // REMOVE_START 24 | // make sure we are working with fresh database 25 | rdb.FlushDB(ctx) 26 | rdb.Del(ctx, "pings:2024-01-01-00:00") 27 | // REMOVE_END 28 | 29 | // STEP_START ping 30 | res1, err := rdb.SetBit(ctx, "pings:2024-01-01-00:00", 123, 1).Result() 31 | 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | fmt.Println(res1) // >>> 0 37 | 38 | res2, err := rdb.GetBit(ctx, "pings:2024-01-01-00:00", 123).Result() 39 | 40 | if err != nil { 41 | panic(err) 42 | } 43 | 44 | fmt.Println(res2) // >>> 1 45 | 46 | res3, err := rdb.GetBit(ctx, "pings:2024-01-01-00:00", 456).Result() 47 | 48 | if err != nil { 49 | panic(err) 50 | } 51 | 52 | fmt.Println(res3) // >>> 0 53 | // STEP_END 54 | 55 | // Output: 56 | // 0 57 | // 1 58 | // 0 59 | } 60 | 61 | func ExampleClient_bitcount() { 62 | ctx := context.Background() 63 | 64 | rdb := redis.NewClient(&redis.Options{ 65 | Addr: "localhost:6379", 66 | Password: "", // no password docs 67 | DB: 0, // use default DB 68 | }) 69 | 70 | // REMOVE_START 71 | // start with fresh database 72 | rdb.FlushDB(ctx) 73 | _, err := rdb.SetBit(ctx, "pings:2024-01-01-00:00", 123, 1).Result() 74 | 75 | if err != nil { 76 | panic(err) 77 | } 78 | // REMOVE_END 79 | 80 | // STEP_START bitcount 81 | res4, err := rdb.BitCount(ctx, "pings:2024-01-01-00:00", 82 | &redis.BitCount{ 83 | Start: 0, 84 | End: 456, 85 | }).Result() 86 | 87 | if err != nil { 88 | panic(err) 89 | } 90 | 91 | fmt.Println(res4) // >>> 1 92 | // STEP_END 93 | 94 | // Output: 95 | // 1 96 | } 97 | -------------------------------------------------------------------------------- /extra/redisotel/metrics_test.go: -------------------------------------------------------------------------------- 1 | package redisotel 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "go.opentelemetry.io/otel/attribute" 8 | ) 9 | 10 | func Test_poolStatsAttrs(t *testing.T) { 11 | t.Parallel() 12 | type args struct { 13 | conf *config 14 | } 15 | tests := []struct { 16 | name string 17 | args args 18 | wantPoolAttrs attribute.Set 19 | wantIdleAttrs attribute.Set 20 | wantUsedAttrs attribute.Set 21 | }{ 22 | { 23 | name: "#3122", 24 | args: func() args { 25 | conf := &config{ 26 | attrs: make([]attribute.KeyValue, 0, 4), 27 | } 28 | conf.attrs = append(conf.attrs, attribute.String("foo1", "bar1"), attribute.String("foo2", "bar2")) 29 | conf.attrs = append(conf.attrs, attribute.String("pool.name", "pool1")) 30 | return args{conf: conf} 31 | }(), 32 | wantPoolAttrs: attribute.NewSet(attribute.String("foo1", "bar1"), attribute.String("foo2", "bar2"), 33 | attribute.String("pool.name", "pool1")), 34 | wantIdleAttrs: attribute.NewSet(attribute.String("foo1", "bar1"), attribute.String("foo2", "bar2"), 35 | attribute.String("pool.name", "pool1"), attribute.String("state", "idle")), 36 | wantUsedAttrs: attribute.NewSet(attribute.String("foo1", "bar1"), attribute.String("foo2", "bar2"), 37 | attribute.String("pool.name", "pool1"), attribute.String("state", "used")), 38 | }, 39 | } 40 | for _, tt := range tests { 41 | t.Run(tt.name, func(t *testing.T) { 42 | gotPoolAttrs, gotIdleAttrs, gotUsedAttrs := poolStatsAttrs(tt.args.conf) 43 | if !reflect.DeepEqual(gotPoolAttrs, tt.wantPoolAttrs) { 44 | t.Errorf("poolStatsAttrs() gotPoolAttrs = %v, want %v", gotPoolAttrs, tt.wantPoolAttrs) 45 | } 46 | if !reflect.DeepEqual(gotIdleAttrs, tt.wantIdleAttrs) { 47 | t.Errorf("poolStatsAttrs() gotIdleAttrs = %v, want %v", gotIdleAttrs, tt.wantIdleAttrs) 48 | } 49 | if !reflect.DeepEqual(gotUsedAttrs, tt.wantUsedAttrs) { 50 | t.Errorf("poolStatsAttrs() gotUsedAttrs = %v, want %v", gotUsedAttrs, tt.wantUsedAttrs) 51 | } 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /extra/rediscensus/rediscensus.go: -------------------------------------------------------------------------------- 1 | package rediscensus 2 | 3 | import ( 4 | "context" 5 | "net" 6 | 7 | "go.opencensus.io/trace" 8 | 9 | "github.com/redis/go-redis/extra/rediscmd/v9" 10 | "github.com/redis/go-redis/v9" 11 | ) 12 | 13 | type TracingHook struct{} 14 | 15 | var _ redis.Hook = (*TracingHook)(nil) 16 | 17 | func NewTracingHook() *TracingHook { 18 | return new(TracingHook) 19 | } 20 | 21 | func (TracingHook) DialHook(next redis.DialHook) redis.DialHook { 22 | return func(ctx context.Context, network, addr string) (net.Conn, error) { 23 | ctx, span := trace.StartSpan(ctx, "dial") 24 | defer span.End() 25 | 26 | span.AddAttributes( 27 | trace.StringAttribute("db.system", "redis"), 28 | trace.StringAttribute("network", network), 29 | trace.StringAttribute("addr", addr), 30 | ) 31 | 32 | conn, err := next(ctx, network, addr) 33 | if err != nil { 34 | recordErrorOnOCSpan(ctx, span, err) 35 | 36 | return nil, err 37 | } 38 | 39 | return conn, nil 40 | } 41 | } 42 | 43 | func (TracingHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook { 44 | return func(ctx context.Context, cmd redis.Cmder) error { 45 | ctx, span := trace.StartSpan(ctx, cmd.FullName()) 46 | defer span.End() 47 | 48 | span.AddAttributes( 49 | trace.StringAttribute("db.system", "redis"), 50 | trace.StringAttribute("redis.cmd", rediscmd.CmdString(cmd)), 51 | ) 52 | 53 | err := next(ctx, cmd) 54 | if err != nil { 55 | recordErrorOnOCSpan(ctx, span, err) 56 | return err 57 | } 58 | 59 | if err = cmd.Err(); err != nil { 60 | recordErrorOnOCSpan(ctx, span, err) 61 | } 62 | 63 | return nil 64 | } 65 | } 66 | 67 | func (TracingHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook { 68 | return next 69 | } 70 | 71 | func recordErrorOnOCSpan(ctx context.Context, span *trace.Span, err error) { 72 | if err != redis.Nil { 73 | span.AddAttributes(trace.BoolAttribute("error", true)) 74 | span.Annotate([]trace.Attribute{trace.StringAttribute("Error", "redis error")}, err.Error()) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /internal/log.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | ) 9 | 10 | // TODO (ned): Revisit logging 11 | // Add more standardized approach with log levels and configurability 12 | 13 | type Logging interface { 14 | Printf(ctx context.Context, format string, v ...interface{}) 15 | } 16 | 17 | type DefaultLogger struct { 18 | log *log.Logger 19 | } 20 | 21 | func (l *DefaultLogger) Printf(ctx context.Context, format string, v ...interface{}) { 22 | _ = l.log.Output(2, fmt.Sprintf(format, v...)) 23 | } 24 | 25 | func NewDefaultLogger() Logging { 26 | return &DefaultLogger{ 27 | log: log.New(os.Stderr, "redis: ", log.LstdFlags|log.Lshortfile), 28 | } 29 | } 30 | 31 | // Logger calls Output to print to the stderr. 32 | // Arguments are handled in the manner of fmt.Print. 33 | var Logger Logging = NewDefaultLogger() 34 | 35 | var LogLevel LogLevelT = LogLevelError 36 | 37 | // LogLevelT represents the logging level 38 | type LogLevelT int 39 | 40 | // Log level constants for the entire go-redis library 41 | const ( 42 | LogLevelError LogLevelT = iota // 0 - errors only 43 | LogLevelWarn // 1 - warnings and errors 44 | LogLevelInfo // 2 - info, warnings, and errors 45 | LogLevelDebug // 3 - debug, info, warnings, and errors 46 | ) 47 | 48 | // String returns the string representation of the log level 49 | func (l LogLevelT) String() string { 50 | switch l { 51 | case LogLevelError: 52 | return "ERROR" 53 | case LogLevelWarn: 54 | return "WARN" 55 | case LogLevelInfo: 56 | return "INFO" 57 | case LogLevelDebug: 58 | return "DEBUG" 59 | default: 60 | return "UNKNOWN" 61 | } 62 | } 63 | 64 | // IsValid returns true if the log level is valid 65 | func (l LogLevelT) IsValid() bool { 66 | return l >= LogLevelError && l <= LogLevelDebug 67 | } 68 | 69 | func (l LogLevelT) WarnOrAbove() bool { 70 | return l >= LogLevelWarn 71 | } 72 | 73 | func (l LogLevelT) InfoOrAbove() bool { 74 | return l >= LogLevelInfo 75 | } 76 | 77 | func (l LogLevelT) DebugOrAbove() bool { 78 | return l >= LogLevelDebug 79 | } 80 | -------------------------------------------------------------------------------- /example/scan-struct/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/davecgh/go-spew/spew" 7 | 8 | "github.com/redis/go-redis/v9" 9 | ) 10 | 11 | type Model struct { 12 | Str1 string `redis:"str1"` 13 | Str2 string `redis:"str2"` 14 | Str3 *string `redis:"str3"` 15 | Bytes []byte `redis:"bytes"` 16 | Int int `redis:"int"` 17 | Int2 *int `redis:"int2"` 18 | Bool bool `redis:"bool"` 19 | Bool2 *bool `redis:"bool2"` 20 | Ignored struct{} `redis:"-"` 21 | } 22 | 23 | func main() { 24 | ctx := context.Background() 25 | 26 | rdb := redis.NewClient(&redis.Options{ 27 | Addr: ":6379", 28 | }) 29 | _ = rdb.FlushDB(ctx).Err() 30 | 31 | // Set some fields. 32 | if _, err := rdb.Pipelined(ctx, func(rdb redis.Pipeliner) error { 33 | rdb.HSet(ctx, "key", "str1", "hello") 34 | rdb.HSet(ctx, "key", "str2", "world") 35 | rdb.HSet(ctx, "key", "str3", "") 36 | rdb.HSet(ctx, "key", "int", 123) 37 | rdb.HSet(ctx, "key", "int2", 0) 38 | rdb.HSet(ctx, "key", "bool", 1) 39 | rdb.HSet(ctx, "key", "bool2", 0) 40 | rdb.HSet(ctx, "key", "bytes", []byte("this is bytes !")) 41 | return nil 42 | }); err != nil { 43 | panic(err) 44 | } 45 | 46 | var model1, model2 Model 47 | 48 | // Scan all fields into the model. 49 | if err := rdb.HGetAll(ctx, "key").Scan(&model1); err != nil { 50 | panic(err) 51 | } 52 | 53 | // Or scan a subset of the fields. 54 | if err := rdb.HMGet(ctx, "key", "str1", "int").Scan(&model2); err != nil { 55 | panic(err) 56 | } 57 | 58 | spew.Dump(model1) 59 | // Output: 60 | // (main.Model) { 61 | // Str1: (string) (len=5) "hello", 62 | // Str2: (string) (len=5) "world", 63 | // Bytes: ([]uint8) (len=15 cap=16) { 64 | // 00000000 74 68 69 73 20 69 73 20 62 79 74 65 73 20 21 |this is bytes !| 65 | // }, 66 | // Int: (int) 123, 67 | // Bool: (bool) true, 68 | // Ignored: (struct {}) { 69 | // } 70 | // } 71 | 72 | spew.Dump(model2) 73 | // Output: 74 | // (main.Model) { 75 | // Str1: (string) (len=5) "hello", 76 | // Str2: (string) "", 77 | // Bytes: ([]uint8) , 78 | // Int: (int) 123, 79 | // Bool: (bool) false, 80 | // Ignored: (struct {}) { 81 | // } 82 | // } 83 | } 84 | -------------------------------------------------------------------------------- /doctests/cmds_set_test.go: -------------------------------------------------------------------------------- 1 | // EXAMPLE: cmds_set 2 | // HIDE_START 3 | package example_commands_test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | 9 | "github.com/redis/go-redis/v9" 10 | ) 11 | 12 | // HIDE_END 13 | 14 | func ExampleClient_sadd_cmd() { 15 | ctx := context.Background() 16 | 17 | rdb := redis.NewClient(&redis.Options{ 18 | Addr: "localhost:6379", 19 | Password: "", // no password docs 20 | DB: 0, // use default DB 21 | }) 22 | 23 | // REMOVE_START 24 | rdb.Del(ctx, "myset") 25 | // REMOVE_END 26 | 27 | // STEP_START sadd 28 | sAddResult1, err := rdb.SAdd(ctx, "myset", "Hello").Result() 29 | 30 | if err != nil { 31 | panic(err) 32 | } 33 | 34 | fmt.Println(sAddResult1) // >>> 1 35 | 36 | sAddResult2, err := rdb.SAdd(ctx, "myset", "World").Result() 37 | 38 | if err != nil { 39 | panic(err) 40 | } 41 | 42 | fmt.Println(sAddResult2) // >>> 1 43 | 44 | sAddResult3, err := rdb.SAdd(ctx, "myset", "World").Result() 45 | 46 | if err != nil { 47 | panic(err) 48 | } 49 | 50 | fmt.Println(sAddResult3) // >>> 0 51 | 52 | sMembersResult, err := rdb.SMembers(ctx, "myset").Result() 53 | 54 | if err != nil { 55 | panic(err) 56 | } 57 | 58 | fmt.Println(sMembersResult) // >>> [Hello World] 59 | // STEP_END 60 | 61 | // Output: 62 | // 1 63 | // 1 64 | // 0 65 | // [Hello World] 66 | } 67 | 68 | func ExampleClient_smembers_cmd() { 69 | ctx := context.Background() 70 | 71 | rdb := redis.NewClient(&redis.Options{ 72 | Addr: "localhost:6379", 73 | Password: "", // no password docs 74 | DB: 0, // use default DB 75 | }) 76 | 77 | // REMOVE_START 78 | rdb.Del(ctx, "myset") 79 | // REMOVE_END 80 | 81 | // STEP_START smembers 82 | sAddResult, err := rdb.SAdd(ctx, "myset", "Hello", "World").Result() 83 | 84 | if err != nil { 85 | panic(err) 86 | } 87 | 88 | fmt.Println(sAddResult) // >>> 2 89 | 90 | sMembersResult, err := rdb.SMembers(ctx, "myset").Result() 91 | 92 | if err != nil { 93 | panic(err) 94 | } 95 | 96 | fmt.Println(sMembersResult) // >>> [Hello World] 97 | // STEP_END 98 | 99 | // Output: 100 | // 2 101 | // [Hello World] 102 | } 103 | -------------------------------------------------------------------------------- /error_test.go: -------------------------------------------------------------------------------- 1 | package redis_test 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | . "github.com/bsm/ginkgo/v2" 8 | . "github.com/bsm/gomega" 9 | "github.com/redis/go-redis/v9" 10 | "github.com/redis/go-redis/v9/internal/proto" 11 | ) 12 | 13 | type testTimeout struct { 14 | timeout bool 15 | } 16 | 17 | func (t testTimeout) Timeout() bool { 18 | return t.timeout 19 | } 20 | 21 | func (t testTimeout) Error() string { 22 | return "test timeout" 23 | } 24 | 25 | var _ = Describe("error", func() { 26 | BeforeEach(func() { 27 | 28 | }) 29 | 30 | AfterEach(func() { 31 | 32 | }) 33 | 34 | It("should retry", func() { 35 | data := map[error]bool{ 36 | io.EOF: true, 37 | io.ErrUnexpectedEOF: true, 38 | nil: false, 39 | context.Canceled: false, 40 | context.DeadlineExceeded: false, 41 | redis.ErrPoolTimeout: true, 42 | // Use typed errors instead of plain errors.New() 43 | proto.ParseErrorReply([]byte("-ERR max number of clients reached")): true, 44 | proto.ParseErrorReply([]byte("-LOADING Redis is loading the dataset in memory")): true, 45 | proto.ParseErrorReply([]byte("-READONLY You can't write against a read only replica")): true, 46 | proto.ParseErrorReply([]byte("-CLUSTERDOWN The cluster is down")): true, 47 | proto.ParseErrorReply([]byte("-TRYAGAIN Command cannot be processed, please try again")): true, 48 | proto.ParseErrorReply([]byte("-NOREPLICAS Not enough good replicas to write")): true, 49 | proto.ParseErrorReply([]byte("-ERR other")): false, 50 | } 51 | 52 | for err, expected := range data { 53 | Expect(redis.ShouldRetry(err, false)).To(Equal(expected)) 54 | Expect(redis.ShouldRetry(err, true)).To(Equal(expected)) 55 | } 56 | }) 57 | 58 | It("should retry timeout", func() { 59 | t1 := testTimeout{timeout: true} 60 | Expect(redis.ShouldRetry(t1, true)).To(Equal(true)) 61 | Expect(redis.ShouldRetry(t1, false)).To(Equal(false)) 62 | 63 | t2 := testTimeout{timeout: false} 64 | Expect(redis.ShouldRetry(t2, true)).To(Equal(true)) 65 | Expect(redis.ShouldRetry(t2, false)).To(Equal(true)) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /example/otel/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "sync" 8 | "time" 9 | 10 | "go.opentelemetry.io/otel" 11 | "go.opentelemetry.io/otel/codes" 12 | 13 | "github.com/uptrace/uptrace-go/uptrace" 14 | 15 | "github.com/redis/go-redis/extra/redisotel/v9" 16 | "github.com/redis/go-redis/v9" 17 | ) 18 | 19 | var tracer = otel.Tracer("github.com/redis/go-redis/example/otel") 20 | 21 | func main() { 22 | ctx := context.Background() 23 | 24 | uptrace.ConfigureOpentelemetry( 25 | // copy your project DSN here or use UPTRACE_DSN env var 26 | uptrace.WithDSN("http://project1_secret@localhost:14318/2?grpc=14317"), 27 | 28 | uptrace.WithServiceName("myservice"), 29 | uptrace.WithServiceVersion("v1.0.0"), 30 | ) 31 | defer uptrace.Shutdown(ctx) 32 | 33 | rdb := redis.NewClient(&redis.Options{ 34 | Addr: ":6379", 35 | }) 36 | if err := redisotel.InstrumentTracing(rdb); err != nil { 37 | panic(err) 38 | } 39 | if err := redisotel.InstrumentMetrics(rdb); err != nil { 40 | panic(err) 41 | } 42 | 43 | for i := 0; i < 1e6; i++ { 44 | ctx, rootSpan := tracer.Start(ctx, "handleRequest") 45 | 46 | if err := handleRequest(ctx, rdb); err != nil { 47 | rootSpan.RecordError(err) 48 | rootSpan.SetStatus(codes.Error, err.Error()) 49 | } 50 | 51 | rootSpan.End() 52 | 53 | if i == 0 { 54 | fmt.Printf("view trace: %s\n", uptrace.TraceURL(rootSpan)) 55 | } 56 | 57 | time.Sleep(time.Second) 58 | } 59 | } 60 | 61 | func handleRequest(ctx context.Context, rdb *redis.Client) error { 62 | if err := rdb.Set(ctx, "First value", "value_1", 0).Err(); err != nil { 63 | return err 64 | } 65 | if err := rdb.Set(ctx, "Second value", "value_2", 0).Err(); err != nil { 66 | return err 67 | } 68 | 69 | var group sync.WaitGroup 70 | 71 | for i := 0; i < 20; i++ { 72 | group.Add(1) 73 | go func() { 74 | defer group.Done() 75 | val := rdb.Get(ctx, "Second value").Val() 76 | if val != "value_2" { 77 | log.Printf("%q != %q", val, "value_2") 78 | } 79 | }() 80 | } 81 | 82 | group.Wait() 83 | 84 | if err := rdb.Del(ctx, "First value").Err(); err != nil { 85 | return err 86 | } 87 | if err := rdb.Del(ctx, "Second value").Err(); err != nil { 88 | return err 89 | } 90 | 91 | return nil 92 | } 93 | -------------------------------------------------------------------------------- /example/otel/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/example/otel 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.1 6 | 7 | replace github.com/redis/go-redis/v9 => ../.. 8 | 9 | replace github.com/redis/go-redis/extra/redisotel/v9 => ../../extra/redisotel 10 | 11 | replace github.com/redis/go-redis/extra/rediscmd/v9 => ../../extra/rediscmd 12 | 13 | require ( 14 | github.com/redis/go-redis/extra/redisotel/v9 v9.18.0-beta.2 15 | github.com/redis/go-redis/v9 v9.18.0-beta.2 16 | github.com/uptrace/uptrace-go v1.21.0 17 | go.opentelemetry.io/otel v1.22.0 18 | ) 19 | 20 | require ( 21 | github.com/cenkalti/backoff/v4 v4.2.1 // indirect 22 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 23 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 24 | github.com/go-logr/logr v1.4.1 // indirect 25 | github.com/go-logr/stdr v1.2.2 // indirect 26 | github.com/golang/protobuf v1.5.3 // indirect 27 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect 28 | github.com/redis/go-redis/extra/rediscmd/v9 v9.18.0-beta.2 // indirect 29 | go.opentelemetry.io/contrib/instrumentation/runtime v0.46.1 // indirect 30 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect 31 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect 32 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 // indirect 33 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0 // indirect 34 | go.opentelemetry.io/otel/metric v1.22.0 // indirect 35 | go.opentelemetry.io/otel/sdk v1.22.0 // indirect 36 | go.opentelemetry.io/otel/sdk/metric v1.21.0 // indirect 37 | go.opentelemetry.io/otel/trace v1.22.0 // indirect 38 | go.opentelemetry.io/proto/otlp v1.0.0 // indirect 39 | go.uber.org/atomic v1.11.0 // indirect 40 | golang.org/x/net v0.36.0 // indirect 41 | golang.org/x/sys v0.30.0 // indirect 42 | golang.org/x/text v0.22.0 // indirect 43 | google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1 // indirect 44 | google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1 // indirect 45 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 // indirect 46 | google.golang.org/grpc v1.60.1 // indirect 47 | google.golang.org/protobuf v1.33.0 // indirect 48 | ) 49 | -------------------------------------------------------------------------------- /internal/pool/pubsub.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "sync" 7 | "sync/atomic" 8 | ) 9 | 10 | type PubSubStats struct { 11 | Created uint32 12 | Untracked uint32 13 | Active uint32 14 | } 15 | 16 | // PubSubPool manages a pool of PubSub connections. 17 | type PubSubPool struct { 18 | opt *Options 19 | netDialer func(ctx context.Context, network, addr string) (net.Conn, error) 20 | 21 | // Map to track active PubSub connections 22 | activeConns sync.Map // map[uint64]*Conn (connID -> conn) 23 | closed atomic.Bool 24 | stats PubSubStats 25 | } 26 | 27 | // NewPubSubPool implements a pool for PubSub connections. 28 | // It intentionally does not implement the Pooler interface 29 | func NewPubSubPool(opt *Options, netDialer func(ctx context.Context, network, addr string) (net.Conn, error)) *PubSubPool { 30 | return &PubSubPool{ 31 | opt: opt, 32 | netDialer: netDialer, 33 | } 34 | } 35 | 36 | func (p *PubSubPool) NewConn(ctx context.Context, network string, addr string, channels []string) (*Conn, error) { 37 | if p.closed.Load() { 38 | return nil, ErrClosed 39 | } 40 | 41 | netConn, err := p.netDialer(ctx, network, addr) 42 | if err != nil { 43 | return nil, err 44 | } 45 | cn := NewConnWithBufferSize(netConn, p.opt.ReadBufferSize, p.opt.WriteBufferSize) 46 | cn.pubsub = true 47 | atomic.AddUint32(&p.stats.Created, 1) 48 | return cn, nil 49 | 50 | } 51 | 52 | func (p *PubSubPool) TrackConn(cn *Conn) { 53 | atomic.AddUint32(&p.stats.Active, 1) 54 | p.activeConns.Store(cn.GetID(), cn) 55 | } 56 | 57 | func (p *PubSubPool) UntrackConn(cn *Conn) { 58 | atomic.AddUint32(&p.stats.Active, ^uint32(0)) 59 | atomic.AddUint32(&p.stats.Untracked, 1) 60 | p.activeConns.Delete(cn.GetID()) 61 | } 62 | 63 | func (p *PubSubPool) Close() error { 64 | p.closed.Store(true) 65 | p.activeConns.Range(func(key, value interface{}) bool { 66 | cn := value.(*Conn) 67 | _ = cn.Close() 68 | return true 69 | }) 70 | return nil 71 | } 72 | 73 | func (p *PubSubPool) Stats() *PubSubStats { 74 | // load stats atomically 75 | return &PubSubStats{ 76 | Created: atomic.LoadUint32(&p.stats.Created), 77 | Untracked: atomic.LoadUint32(&p.stats.Untracked), 78 | Active: atomic.LoadUint32(&p.stats.Active), 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /internal/util.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/redis/go-redis/v9/internal/util" 11 | ) 12 | 13 | func Sleep(ctx context.Context, dur time.Duration) error { 14 | t := time.NewTimer(dur) 15 | defer t.Stop() 16 | 17 | select { 18 | case <-t.C: 19 | return nil 20 | case <-ctx.Done(): 21 | return ctx.Err() 22 | } 23 | } 24 | 25 | func ToLower(s string) string { 26 | if isLower(s) { 27 | return s 28 | } 29 | 30 | b := make([]byte, len(s)) 31 | for i := range b { 32 | c := s[i] 33 | if c >= 'A' && c <= 'Z' { 34 | c += 'a' - 'A' 35 | } 36 | b[i] = c 37 | } 38 | return util.BytesToString(b) 39 | } 40 | 41 | func isLower(s string) bool { 42 | for i := 0; i < len(s); i++ { 43 | c := s[i] 44 | if c >= 'A' && c <= 'Z' { 45 | return false 46 | } 47 | } 48 | return true 49 | } 50 | 51 | func ReplaceSpaces(s string) string { 52 | return strings.ReplaceAll(s, " ", "-") 53 | } 54 | 55 | func GetAddr(addr string) string { 56 | ind := strings.LastIndexByte(addr, ':') 57 | if ind == -1 { 58 | return "" 59 | } 60 | 61 | if strings.IndexByte(addr, '.') != -1 { 62 | return addr 63 | } 64 | 65 | if addr[0] == '[' { 66 | return addr 67 | } 68 | return net.JoinHostPort(addr[:ind], addr[ind+1:]) 69 | } 70 | 71 | func ToInteger(val interface{}) int { 72 | switch v := val.(type) { 73 | case int: 74 | return v 75 | case int64: 76 | return int(v) 77 | case string: 78 | i, _ := strconv.Atoi(v) 79 | return i 80 | default: 81 | return 0 82 | } 83 | } 84 | 85 | func ToFloat(val interface{}) float64 { 86 | switch v := val.(type) { 87 | case float64: 88 | return v 89 | case string: 90 | f, _ := strconv.ParseFloat(v, 64) 91 | return f 92 | default: 93 | return 0.0 94 | } 95 | } 96 | 97 | func ToString(val interface{}) string { 98 | if str, ok := val.(string); ok { 99 | return str 100 | } 101 | return "" 102 | } 103 | 104 | func ToStringSlice(val interface{}) []string { 105 | if arr, ok := val.([]interface{}); ok { 106 | result := make([]string, len(arr)) 107 | for i, v := range arr { 108 | result[i] = ToString(v) 109 | } 110 | return result 111 | } 112 | return nil 113 | } 114 | -------------------------------------------------------------------------------- /scripts/tag.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | DRY_RUN=1 6 | 7 | helps() { 8 | cat <<- EOF 9 | Usage: $0 TAGVERSION [-t] 10 | 11 | Creates git tags for public Go packages. 12 | 13 | ARGUMENTS: 14 | TAGVERSION Tag version to create, for example v1.0.0 15 | 16 | OPTIONS: 17 | -t Execute git commands (default: dry run) 18 | EOF 19 | exit 0 20 | } 21 | 22 | 23 | if [ $# -eq 0 ]; then 24 | echo "Error: Tag version is required" 25 | helps 26 | fi 27 | 28 | TAG=$1 29 | shift 30 | 31 | while getopts "t" opt; do 32 | case $opt in 33 | t) 34 | DRY_RUN=0 35 | ;; 36 | \?) 37 | echo "Invalid option: -$OPTARG" >&2 38 | exit 1 39 | ;; 40 | esac 41 | done 42 | 43 | 44 | if [ "$DRY_RUN" -eq 1 ]; then 45 | echo "Running in dry-run mode" 46 | fi 47 | 48 | if ! grep -Fq "\"${TAG#v}\"" version.go 49 | then 50 | printf "version.go does not contain ${TAG#v}\n" 51 | exit 1 52 | fi 53 | 54 | GOMOD_ERRORS=0 55 | 56 | # Check go.mod files for correct dependency versions 57 | while read -r mod_file; do 58 | # Look for go-redis packages in require statements 59 | while read -r pkg version; do 60 | if [ "$version" != "${TAG}" ]; then 61 | printf "Error: %s has incorrect version for package %s: %s (expected %s)\n" "$mod_file" "$pkg" "$version" "${TAG}" 62 | GOMOD_ERRORS=$((GOMOD_ERRORS + 1)) 63 | fi 64 | done < <(awk '/^require|^require \(/{p=1;next} /^\)/{p=0} p{if($1 ~ /^github\.com\/redis\/go-redis/){print $1, $2}}' "$mod_file") 65 | done < <(find . -type f -name 'go.mod') 66 | 67 | # Exit if there are gomod errors 68 | if [ $GOMOD_ERRORS -gt 0 ]; then 69 | exit 1 70 | fi 71 | 72 | 73 | PACKAGE_DIRS=$(find . -mindepth 2 -type f -name 'go.mod' -exec dirname {} \; \ 74 | | grep -E -v "example|internal" \ 75 | | sed 's/^\.\///' \ 76 | | sort) 77 | 78 | 79 | execute_git_command() { 80 | if [ "$DRY_RUN" -eq 0 ]; then 81 | "$@" 82 | else 83 | echo "DRY-RUN: Would execute: $@" 84 | fi 85 | } 86 | 87 | execute_git_command git tag ${TAG} 88 | execute_git_command git push origin ${TAG} 89 | 90 | for dir in $PACKAGE_DIRS 91 | do 92 | printf "tagging ${dir}/${TAG}\n" 93 | execute_git_command git tag ${dir}/${TAG} 94 | execute_git_command git push origin ${dir}/${TAG} 95 | done 96 | -------------------------------------------------------------------------------- /command_recorder_test.go: -------------------------------------------------------------------------------- 1 | package redis_test 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "sync" 7 | 8 | "github.com/redis/go-redis/v9" 9 | ) 10 | 11 | // commandRecorder records the last N commands executed by a Redis client. 12 | type commandRecorder struct { 13 | mu sync.Mutex 14 | commands []string 15 | maxSize int 16 | } 17 | 18 | // newCommandRecorder creates a new command recorder with the specified maximum size. 19 | func newCommandRecorder(maxSize int) *commandRecorder { 20 | return &commandRecorder{ 21 | commands: make([]string, 0, maxSize), 22 | maxSize: maxSize, 23 | } 24 | } 25 | 26 | // Record adds a command to the recorder. 27 | func (r *commandRecorder) Record(cmd string) { 28 | cmd = strings.ToLower(cmd) 29 | r.mu.Lock() 30 | defer r.mu.Unlock() 31 | 32 | r.commands = append(r.commands, cmd) 33 | if len(r.commands) > r.maxSize { 34 | r.commands = r.commands[1:] 35 | } 36 | } 37 | 38 | // LastCommands returns a copy of the recorded commands. 39 | func (r *commandRecorder) LastCommands() []string { 40 | r.mu.Lock() 41 | defer r.mu.Unlock() 42 | return append([]string(nil), r.commands...) 43 | } 44 | 45 | // Contains checks if the recorder contains a specific command. 46 | func (r *commandRecorder) Contains(cmd string) bool { 47 | cmd = strings.ToLower(cmd) 48 | r.mu.Lock() 49 | defer r.mu.Unlock() 50 | for _, c := range r.commands { 51 | if strings.Contains(c, cmd) { 52 | return true 53 | } 54 | } 55 | return false 56 | } 57 | 58 | // Hook returns a Redis hook that records commands. 59 | func (r *commandRecorder) Hook() redis.Hook { 60 | return &commandHook{recorder: r} 61 | } 62 | 63 | // commandHook implements the redis.Hook interface to record commands. 64 | type commandHook struct { 65 | recorder *commandRecorder 66 | } 67 | 68 | func (h *commandHook) DialHook(next redis.DialHook) redis.DialHook { 69 | return next 70 | } 71 | 72 | func (h *commandHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook { 73 | return func(ctx context.Context, cmd redis.Cmder) error { 74 | h.recorder.Record(cmd.String()) 75 | return next(ctx, cmd) 76 | } 77 | } 78 | 79 | func (h *commandHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook { 80 | return func(ctx context.Context, cmds []redis.Cmder) error { 81 | for _, cmd := range cmds { 82 | h.recorder.Record(cmd.String()) 83 | } 84 | return next(ctx, cmds) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /example/otel/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | clickhouse: 3 | image: clickhouse/clickhouse-server:25.3.5 4 | restart: on-failure 5 | environment: 6 | CLICKHOUSE_USER: uptrace 7 | CLICKHOUSE_PASSWORD: uptrace 8 | CLICKHOUSE_DB: uptrace 9 | healthcheck: 10 | test: ['CMD', 'wget', '--spider', '-q', 'localhost:8123/ping'] 11 | interval: 1s 12 | timeout: 1s 13 | retries: 30 14 | volumes: 15 | - ch_data:/var/lib/clickhouse 16 | ports: 17 | - '8123:8123' 18 | - '9000:9000' 19 | 20 | postgres: 21 | image: postgres:17-alpine 22 | restart: on-failure 23 | environment: 24 | PGDATA: /var/lib/postgresql/data/pgdata 25 | POSTGRES_USER: uptrace 26 | POSTGRES_PASSWORD: uptrace 27 | POSTGRES_DB: uptrace 28 | healthcheck: 29 | test: ['CMD-SHELL', 'pg_isready', '-U', 'uptrace', '-d', 'uptrace'] 30 | interval: 1s 31 | timeout: 1s 32 | retries: 30 33 | volumes: 34 | - 'pg_data:/var/lib/postgresql/data/pgdata' 35 | ports: 36 | - '5432:5432' 37 | 38 | uptrace: 39 | image: 'uptrace/uptrace:2.0.0' 40 | #image: 'uptrace/uptrace-dev:latest' 41 | restart: on-failure 42 | volumes: 43 | - ./uptrace.yml:/etc/uptrace/config.yml 44 | #environment: 45 | # - DEBUG=2 46 | ports: 47 | - '14317:4317' 48 | - '14318:80' 49 | depends_on: 50 | clickhouse: 51 | condition: service_healthy 52 | 53 | otelcol: 54 | image: otel/opentelemetry-collector-contrib:0.123.0 55 | restart: on-failure 56 | volumes: 57 | - ./config/otel-collector.yaml:/etc/otelcol-contrib/config.yaml 58 | ports: 59 | - '4317:4317' 60 | - '4318:4318' 61 | 62 | vector: 63 | image: timberio/vector:0.28.X-alpine 64 | volumes: 65 | - ./config/vector.toml:/etc/vector/vector.toml:ro 66 | 67 | mailpit: 68 | image: axllent/mailpit 69 | restart: always 70 | ports: 71 | - 1025:1025 72 | - 8025:8025 73 | environment: 74 | MP_MAX_MESSAGES: 5000 75 | MP_DATA_FILE: /data/mailpit.db 76 | MP_SMTP_AUTH_ACCEPT_ANY: 1 77 | MP_SMTP_AUTH_ALLOW_INSECURE: 1 78 | volumes: 79 | - mailpit_data:/data 80 | 81 | redis-server: 82 | image: redis 83 | ports: 84 | - '6379:6379' 85 | redis-cli: 86 | image: redis 87 | 88 | volumes: 89 | ch_data: 90 | pg_data: 91 | mailpit_data: 92 | -------------------------------------------------------------------------------- /maintnotifications/hooks.go: -------------------------------------------------------------------------------- 1 | package maintnotifications 2 | 3 | import ( 4 | "context" 5 | "slices" 6 | 7 | "github.com/redis/go-redis/v9/internal" 8 | "github.com/redis/go-redis/v9/internal/maintnotifications/logs" 9 | "github.com/redis/go-redis/v9/internal/pool" 10 | "github.com/redis/go-redis/v9/push" 11 | ) 12 | 13 | // LoggingHook is an example hook implementation that logs all notifications. 14 | type LoggingHook struct { 15 | LogLevel int // 0=Error, 1=Warn, 2=Info, 3=Debug 16 | } 17 | 18 | // PreHook logs the notification before processing and allows modification. 19 | func (lh *LoggingHook) PreHook(ctx context.Context, notificationCtx push.NotificationHandlerContext, notificationType string, notification []interface{}) ([]interface{}, bool) { 20 | if lh.LogLevel >= 2 { // Info level 21 | // Log the notification type and content 22 | connID := uint64(0) 23 | if conn, ok := notificationCtx.Conn.(*pool.Conn); ok { 24 | connID = conn.GetID() 25 | } 26 | seqID := int64(0) 27 | if slices.Contains(maintenanceNotificationTypes, notificationType) { 28 | // seqID is the second element in the notification array 29 | if len(notification) > 1 { 30 | if parsedSeqID, ok := notification[1].(int64); !ok { 31 | seqID = 0 32 | } else { 33 | seqID = parsedSeqID 34 | } 35 | } 36 | 37 | } 38 | internal.Logger.Printf(ctx, logs.ProcessingNotification(connID, seqID, notificationType, notification)) 39 | } 40 | return notification, true // Continue processing with unmodified notification 41 | } 42 | 43 | // PostHook logs the result after processing. 44 | func (lh *LoggingHook) PostHook(ctx context.Context, notificationCtx push.NotificationHandlerContext, notificationType string, notification []interface{}, result error) { 45 | connID := uint64(0) 46 | if conn, ok := notificationCtx.Conn.(*pool.Conn); ok { 47 | connID = conn.GetID() 48 | } 49 | if result != nil && lh.LogLevel >= 1 { // Warning level 50 | internal.Logger.Printf(ctx, logs.ProcessingNotificationFailed(connID, notificationType, result, notification)) 51 | } else if lh.LogLevel >= 3 { // Debug level 52 | internal.Logger.Printf(ctx, logs.ProcessingNotificationSucceeded(connID, notificationType)) 53 | } 54 | } 55 | 56 | // NewLoggingHook creates a new logging hook with the specified log level. 57 | // Log levels: 0=Error, 1=Warn, 2=Info, 3=Debug 58 | func NewLoggingHook(logLevel int) *LoggingHook { 59 | return &LoggingHook{LogLevel: logLevel} 60 | } 61 | -------------------------------------------------------------------------------- /internal/pool/bench_test.go: -------------------------------------------------------------------------------- 1 | package pool_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "testing" 8 | "time" 9 | 10 | "github.com/redis/go-redis/v9/internal/pool" 11 | ) 12 | 13 | type poolGetPutBenchmark struct { 14 | poolSize int 15 | } 16 | 17 | func (bm poolGetPutBenchmark) String() string { 18 | return fmt.Sprintf("pool=%d", bm.poolSize) 19 | } 20 | 21 | func BenchmarkPoolGetPut(b *testing.B) { 22 | ctx := context.Background() 23 | benchmarks := []poolGetPutBenchmark{ 24 | {1}, 25 | {2}, 26 | {8}, 27 | {32}, 28 | {64}, 29 | {128}, 30 | } 31 | for _, bm := range benchmarks { 32 | b.Run(bm.String(), func(b *testing.B) { 33 | connPool := pool.NewConnPool(&pool.Options{ 34 | Dialer: dummyDialer, 35 | PoolSize: int32(bm.poolSize), 36 | MaxConcurrentDials: bm.poolSize, 37 | PoolTimeout: time.Second, 38 | DialTimeout: 1 * time.Second, 39 | ConnMaxIdleTime: time.Hour, 40 | }) 41 | 42 | b.ResetTimer() 43 | 44 | b.RunParallel(func(pb *testing.PB) { 45 | for pb.Next() { 46 | cn, err := connPool.Get(ctx) 47 | if err != nil { 48 | b.Fatal(err) 49 | } 50 | connPool.Put(ctx, cn) 51 | } 52 | }) 53 | }) 54 | } 55 | } 56 | 57 | type poolGetRemoveBenchmark struct { 58 | poolSize int 59 | } 60 | 61 | func (bm poolGetRemoveBenchmark) String() string { 62 | return fmt.Sprintf("pool=%d", bm.poolSize) 63 | } 64 | 65 | func BenchmarkPoolGetRemove(b *testing.B) { 66 | ctx := context.Background() 67 | benchmarks := []poolGetRemoveBenchmark{ 68 | {1}, 69 | {2}, 70 | {8}, 71 | {32}, 72 | {64}, 73 | {128}, 74 | } 75 | 76 | for _, bm := range benchmarks { 77 | b.Run(bm.String(), func(b *testing.B) { 78 | connPool := pool.NewConnPool(&pool.Options{ 79 | Dialer: dummyDialer, 80 | PoolSize: int32(bm.poolSize), 81 | MaxConcurrentDials: bm.poolSize, 82 | PoolTimeout: time.Second, 83 | DialTimeout: 1 * time.Second, 84 | ConnMaxIdleTime: time.Hour, 85 | }) 86 | 87 | b.ResetTimer() 88 | rmvErr := errors.New("Bench test remove") 89 | b.RunParallel(func(pb *testing.PB) { 90 | for pb.Next() { 91 | cn, err := connPool.Get(ctx) 92 | if err != nil { 93 | b.Fatal(err) 94 | } 95 | connPool.Remove(ctx, cn, rmvErr) 96 | } 97 | }) 98 | }) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /extra/redisotel/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 2 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 3 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 4 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 7 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 8 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 9 | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 10 | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 11 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 12 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 13 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 14 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 15 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 16 | go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y= 17 | go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI= 18 | go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg= 19 | go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY= 20 | go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= 21 | go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= 22 | go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0= 23 | go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo= 24 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 25 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 26 | golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= 27 | golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 28 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 29 | -------------------------------------------------------------------------------- /maintnotifications/README.md: -------------------------------------------------------------------------------- 1 | # Maintenance Notifications 2 | 3 | Seamless Redis connection handoffs during cluster maintenance operations without dropping connections. 4 | 5 | ## ⚠️ **Important Note** 6 | **Maintenance notifications are currently supported only in standalone Redis clients.** Cluster clients (ClusterClient, FailoverClient, etc.) do not yet support this functionality. 7 | 8 | ## Quick Start 9 | 10 | ```go 11 | client := redis.NewClient(&redis.Options{ 12 | Addr: "localhost:6379", 13 | Protocol: 3, // RESP3 required 14 | MaintNotificationsConfig: &maintnotifications.Config{ 15 | Mode: maintnotifications.ModeEnabled, 16 | }, 17 | }) 18 | ``` 19 | 20 | ## Modes 21 | 22 | - **`ModeDisabled`** - Maintenance notifications disabled 23 | - **`ModeEnabled`** - Forcefully enabled (fails if server doesn't support) 24 | - **`ModeAuto`** - Auto-detect server support (default) 25 | 26 | ## Configuration 27 | 28 | ```go 29 | &maintnotifications.Config{ 30 | Mode: maintnotifications.ModeAuto, 31 | EndpointType: maintnotifications.EndpointTypeAuto, 32 | RelaxedTimeout: 10 * time.Second, 33 | HandoffTimeout: 15 * time.Second, 34 | MaxHandoffRetries: 3, 35 | MaxWorkers: 0, // Auto-calculated 36 | HandoffQueueSize: 0, // Auto-calculated 37 | PostHandoffRelaxedDuration: 0, // 2 * RelaxedTimeout 38 | } 39 | ``` 40 | 41 | ### Endpoint Types 42 | 43 | - **`EndpointTypeAuto`** - Auto-detect based on connection (default) 44 | - **`EndpointTypeInternalIP`** - Internal IP address 45 | - **`EndpointTypeInternalFQDN`** - Internal FQDN 46 | - **`EndpointTypeExternalIP`** - External IP address 47 | - **`EndpointTypeExternalFQDN`** - External FQDN 48 | - **`EndpointTypeNone`** - No endpoint (reconnect with current config) 49 | 50 | ### Auto-Scaling 51 | 52 | **Workers**: `min(PoolSize/2, max(10, PoolSize/3))` when auto-calculated 53 | **Queue**: `max(20×Workers, PoolSize)` capped by `MaxActiveConns+1` or `5×PoolSize` 54 | 55 | **Examples:** 56 | - Pool 100: 33 workers, 660 queue (capped at 500) 57 | - Pool 100 + MaxActiveConns 150: 33 workers, 151 queue 58 | 59 | ## How It Works 60 | 61 | 1. Redis sends push notifications about cluster maintenance operations 62 | 2. Client creates new connections to updated endpoints 63 | 3. Active operations transfer to new connections 64 | 4. Old connections close gracefully 65 | 66 | 67 | ## For more information, see [FEATURES](FEATURES.md) 68 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [master, v9, v9.7, v9.8] 17 | pull_request: 18 | branches: [master, v9, v9.7, v9.8] 19 | 20 | jobs: 21 | analyze: 22 | name: Analyze 23 | runs-on: ubuntu-latest 24 | permissions: 25 | actions: read 26 | contents: read 27 | security-events: write 28 | 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | language: [ 'go' ] 33 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 34 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v6 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v4 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v4 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v4 68 | -------------------------------------------------------------------------------- /pubsub_commands.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import "context" 4 | 5 | type PubSubCmdable interface { 6 | Publish(ctx context.Context, channel string, message interface{}) *IntCmd 7 | SPublish(ctx context.Context, channel string, message interface{}) *IntCmd 8 | PubSubChannels(ctx context.Context, pattern string) *StringSliceCmd 9 | PubSubNumSub(ctx context.Context, channels ...string) *MapStringIntCmd 10 | PubSubNumPat(ctx context.Context) *IntCmd 11 | PubSubShardChannels(ctx context.Context, pattern string) *StringSliceCmd 12 | PubSubShardNumSub(ctx context.Context, channels ...string) *MapStringIntCmd 13 | } 14 | 15 | // Publish posts the message to the channel. 16 | func (c cmdable) Publish(ctx context.Context, channel string, message interface{}) *IntCmd { 17 | cmd := NewIntCmd(ctx, "publish", channel, message) 18 | _ = c(ctx, cmd) 19 | return cmd 20 | } 21 | 22 | func (c cmdable) SPublish(ctx context.Context, channel string, message interface{}) *IntCmd { 23 | cmd := NewIntCmd(ctx, "spublish", channel, message) 24 | _ = c(ctx, cmd) 25 | return cmd 26 | } 27 | 28 | func (c cmdable) PubSubChannels(ctx context.Context, pattern string) *StringSliceCmd { 29 | args := []interface{}{"pubsub", "channels"} 30 | if pattern != "*" { 31 | args = append(args, pattern) 32 | } 33 | cmd := NewStringSliceCmd(ctx, args...) 34 | _ = c(ctx, cmd) 35 | return cmd 36 | } 37 | 38 | func (c cmdable) PubSubNumSub(ctx context.Context, channels ...string) *MapStringIntCmd { 39 | args := make([]interface{}, 2+len(channels)) 40 | args[0] = "pubsub" 41 | args[1] = "numsub" 42 | for i, channel := range channels { 43 | args[2+i] = channel 44 | } 45 | cmd := NewMapStringIntCmd(ctx, args...) 46 | _ = c(ctx, cmd) 47 | return cmd 48 | } 49 | 50 | func (c cmdable) PubSubShardChannels(ctx context.Context, pattern string) *StringSliceCmd { 51 | args := []interface{}{"pubsub", "shardchannels"} 52 | if pattern != "*" { 53 | args = append(args, pattern) 54 | } 55 | cmd := NewStringSliceCmd(ctx, args...) 56 | _ = c(ctx, cmd) 57 | return cmd 58 | } 59 | 60 | func (c cmdable) PubSubShardNumSub(ctx context.Context, channels ...string) *MapStringIntCmd { 61 | args := make([]interface{}, 2+len(channels)) 62 | args[0] = "pubsub" 63 | args[1] = "shardnumsub" 64 | for i, channel := range channels { 65 | args[2+i] = channel 66 | } 67 | cmd := NewMapStringIntCmd(ctx, args...) 68 | _ = c(ctx, cmd) 69 | return cmd 70 | } 71 | 72 | func (c cmdable) PubSubNumPat(ctx context.Context) *IntCmd { 73 | cmd := NewIntCmd(ctx, "pubsub", "numpat") 74 | _ = c(ctx, cmd) 75 | return cmd 76 | } 77 | -------------------------------------------------------------------------------- /internal/hashtag/hashtag_test.go: -------------------------------------------------------------------------------- 1 | package hashtag 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/bsm/ginkgo/v2" 7 | . "github.com/bsm/gomega" 8 | 9 | "github.com/redis/go-redis/v9/internal/rand" 10 | ) 11 | 12 | func TestGinkgoSuite(t *testing.T) { 13 | RegisterFailHandler(Fail) 14 | RunSpecs(t, "hashtag") 15 | } 16 | 17 | var _ = Describe("CRC16", func() { 18 | // http://redis.io/topics/cluster-spec#keys-distribution-model 19 | It("should calculate CRC16", func() { 20 | tests := []struct { 21 | s string 22 | n uint16 23 | }{ 24 | {"123456789", 0x31C3}, 25 | {string([]byte{83, 153, 134, 118, 229, 214, 244, 75, 140, 37, 215, 215}), 21847}, 26 | } 27 | 28 | for _, test := range tests { 29 | Expect(crc16sum(test.s)).To(Equal(test.n), "for %s", test.s) 30 | } 31 | }) 32 | }) 33 | 34 | var _ = Describe("HashSlot", func() { 35 | It("should calculate hash slots", func() { 36 | tests := []struct { 37 | key string 38 | slot int 39 | }{ 40 | {"123456789", 12739}, 41 | {"{}foo", 9500}, 42 | {"foo{}", 5542}, 43 | {"foo{}{bar}", 8363}, 44 | {"", 10503}, 45 | {"", 5176}, 46 | {string([]byte{83, 153, 134, 118, 229, 214, 244, 75, 140, 37, 215, 215}), 5463}, 47 | } 48 | // Empty keys receive random slot. 49 | rand.Seed(100) 50 | 51 | for _, test := range tests { 52 | Expect(Slot(test.key)).To(Equal(test.slot), "for %s", test.key) 53 | } 54 | }) 55 | 56 | It("should extract keys from tags", func() { 57 | tests := []struct { 58 | one, two string 59 | }{ 60 | {"foo{bar}", "bar"}, 61 | {"{foo}bar", "foo"}, 62 | {"{user1000}.following", "{user1000}.followers"}, 63 | {"foo{{bar}}zap", "{bar"}, 64 | {"foo{bar}{zap}", "bar"}, 65 | } 66 | 67 | for _, test := range tests { 68 | Expect(Slot(test.one)).To(Equal(Slot(test.two)), "for %s <-> %s", test.one, test.two) 69 | } 70 | }) 71 | }) 72 | 73 | var _ = Describe("Present", func() { 74 | It("should calculate hash slots", func() { 75 | tests := []struct { 76 | key string 77 | present bool 78 | }{ 79 | {"123456789", false}, 80 | {"{}foo", false}, 81 | {"foo{}", false}, 82 | {"foo{}{bar}", false}, 83 | {"", false}, 84 | {string([]byte{83, 153, 134, 118, 229, 214, 244, 75, 140, 37, 215, 215}), false}, 85 | {"foo{bar}", true}, 86 | {"{foo}bar", true}, 87 | {"{user1000}.following", true}, 88 | {"foo{{bar}}zap", true}, 89 | {"foo{bar}{zap}", true}, 90 | } 91 | 92 | for _, test := range tests { 93 | Expect(Present(test.key)).To(Equal(test.present), "for %s", test.key) 94 | } 95 | }) 96 | }) 97 | -------------------------------------------------------------------------------- /bitmap_commands_test.go: -------------------------------------------------------------------------------- 1 | package redis_test 2 | 3 | import ( 4 | . "github.com/bsm/ginkgo/v2" 5 | . "github.com/bsm/gomega" 6 | "github.com/redis/go-redis/v9" 7 | ) 8 | 9 | type bitCountExpected struct { 10 | Start int64 11 | End int64 12 | Expected int64 13 | } 14 | 15 | var _ = Describe("BitCountBite", func() { 16 | var client *redis.Client 17 | key := "bit_count_test" 18 | 19 | BeforeEach(func() { 20 | client = redis.NewClient(redisOptions()) 21 | Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) 22 | values := []int{0, 1, 0, 0, 1, 0, 1, 0, 1, 1} 23 | for i, v := range values { 24 | cmd := client.SetBit(ctx, key, int64(i), v) 25 | Expect(cmd.Err()).NotTo(HaveOccurred()) 26 | } 27 | }) 28 | 29 | AfterEach(func() { 30 | Expect(client.Close()).NotTo(HaveOccurred()) 31 | }) 32 | 33 | It("bit count bite", func() { 34 | var expected = []bitCountExpected{ 35 | {0, 0, 0}, 36 | {0, 1, 1}, 37 | {0, 2, 1}, 38 | {0, 3, 1}, 39 | {0, 4, 2}, 40 | {0, 5, 2}, 41 | {0, 6, 3}, 42 | {0, 7, 3}, 43 | {0, 8, 4}, 44 | {0, 9, 5}, 45 | } 46 | 47 | for _, e := range expected { 48 | cmd := client.BitCount(ctx, key, &redis.BitCount{Start: e.Start, End: e.End, Unit: redis.BitCountIndexBit}) 49 | Expect(cmd.Err()).NotTo(HaveOccurred()) 50 | Expect(cmd.Val()).To(Equal(e.Expected)) 51 | } 52 | }) 53 | }) 54 | 55 | var _ = Describe("BitCountByte", func() { 56 | var client *redis.Client 57 | key := "bit_count_test" 58 | 59 | BeforeEach(func() { 60 | client = redis.NewClient(redisOptions()) 61 | Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) 62 | values := []int{0, 0, 0, 0, 0, 0, 0, 1, 1, 1} 63 | for i, v := range values { 64 | cmd := client.SetBit(ctx, key, int64(i), v) 65 | Expect(cmd.Err()).NotTo(HaveOccurred()) 66 | } 67 | }) 68 | 69 | AfterEach(func() { 70 | Expect(client.Close()).NotTo(HaveOccurred()) 71 | }) 72 | 73 | It("bit count byte", func() { 74 | var expected = []bitCountExpected{ 75 | {0, 0, 1}, 76 | {0, 1, 3}, 77 | } 78 | 79 | for _, e := range expected { 80 | cmd := client.BitCount(ctx, key, &redis.BitCount{Start: e.Start, End: e.End, Unit: redis.BitCountIndexByte}) 81 | Expect(cmd.Err()).NotTo(HaveOccurred()) 82 | Expect(cmd.Val()).To(Equal(e.Expected)) 83 | } 84 | }) 85 | 86 | It("bit count byte with no unit specified", func() { 87 | var expected = []bitCountExpected{ 88 | {0, 0, 1}, 89 | {0, 1, 3}, 90 | } 91 | 92 | for _, e := range expected { 93 | cmd := client.BitCount(ctx, key, &redis.BitCount{Start: e.Start, End: e.End}) 94 | Expect(cmd.Err()).NotTo(HaveOccurred()) 95 | Expect(cmd.Val()).To(Equal(e.Expected)) 96 | } 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /internal/pool/main_test.go: -------------------------------------------------------------------------------- 1 | package pool_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "sync" 8 | "syscall" 9 | "testing" 10 | "time" 11 | 12 | . "github.com/bsm/ginkgo/v2" 13 | . "github.com/bsm/gomega" 14 | ) 15 | 16 | func TestGinkgoSuite(t *testing.T) { 17 | RegisterFailHandler(Fail) 18 | RunSpecs(t, "pool") 19 | } 20 | 21 | func perform(n int, cbs ...func(int)) { 22 | var wg sync.WaitGroup 23 | for _, cb := range cbs { 24 | for i := 0; i < n; i++ { 25 | wg.Add(1) 26 | go func(cb func(int), i int) { 27 | defer GinkgoRecover() 28 | defer wg.Done() 29 | 30 | cb(i) 31 | }(cb, i) 32 | } 33 | } 34 | wg.Wait() 35 | } 36 | 37 | func dummyDialer(context.Context) (net.Conn, error) { 38 | return newDummyConn(), nil 39 | } 40 | 41 | func newDummyConn() net.Conn { 42 | return &dummyConn{ 43 | rawConn: new(dummyRawConn), 44 | } 45 | } 46 | 47 | var ( 48 | _ net.Conn = (*dummyConn)(nil) 49 | _ syscall.Conn = (*dummyConn)(nil) 50 | ) 51 | 52 | type dummyConn struct { 53 | rawConn *dummyRawConn 54 | } 55 | 56 | func (d *dummyConn) SyscallConn() (syscall.RawConn, error) { 57 | return d.rawConn, nil 58 | } 59 | 60 | var errDummy = fmt.Errorf("dummyConn err") 61 | 62 | func (d *dummyConn) Read(b []byte) (n int, err error) { 63 | return 0, errDummy 64 | } 65 | 66 | func (d *dummyConn) Write(b []byte) (n int, err error) { 67 | return 0, errDummy 68 | } 69 | 70 | func (d *dummyConn) Close() error { 71 | d.rawConn.Close() 72 | return nil 73 | } 74 | 75 | func (d *dummyConn) LocalAddr() net.Addr { 76 | return &net.TCPAddr{} 77 | } 78 | 79 | func (d *dummyConn) RemoteAddr() net.Addr { 80 | return &net.TCPAddr{} 81 | } 82 | 83 | func (d *dummyConn) SetDeadline(t time.Time) error { 84 | return nil 85 | } 86 | 87 | func (d *dummyConn) SetReadDeadline(t time.Time) error { 88 | return nil 89 | } 90 | 91 | func (d *dummyConn) SetWriteDeadline(t time.Time) error { 92 | return nil 93 | } 94 | 95 | var _ syscall.RawConn = (*dummyRawConn)(nil) 96 | 97 | type dummyRawConn struct { 98 | mu sync.Mutex 99 | closed bool 100 | } 101 | 102 | func (d *dummyRawConn) Control(f func(fd uintptr)) error { 103 | return nil 104 | } 105 | 106 | func (d *dummyRawConn) Read(f func(fd uintptr) (done bool)) error { 107 | d.mu.Lock() 108 | defer d.mu.Unlock() 109 | if d.closed { 110 | return fmt.Errorf("dummyRawConn closed") 111 | } 112 | return nil 113 | } 114 | 115 | func (d *dummyRawConn) Write(f func(fd uintptr) (done bool)) error { 116 | return nil 117 | } 118 | 119 | func (d *dummyRawConn) Close() { 120 | d.mu.Lock() 121 | d.closed = true 122 | d.mu.Unlock() 123 | } 124 | -------------------------------------------------------------------------------- /command_test.go: -------------------------------------------------------------------------------- 1 | package redis_test 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/redis/go-redis/v9" 8 | 9 | . "github.com/bsm/ginkgo/v2" 10 | . "github.com/bsm/gomega" 11 | ) 12 | 13 | var _ = Describe("Cmd", func() { 14 | var client *redis.Client 15 | 16 | BeforeEach(func() { 17 | client = redis.NewClient(redisOptions()) 18 | Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) 19 | }) 20 | 21 | AfterEach(func() { 22 | Expect(client.Close()).NotTo(HaveOccurred()) 23 | }) 24 | 25 | It("implements Stringer", func() { 26 | set := client.Set(ctx, "foo", "bar", 0) 27 | Expect(set.String()).To(Equal("set foo bar: OK")) 28 | 29 | get := client.Get(ctx, "foo") 30 | Expect(get.String()).To(Equal("get foo: bar")) 31 | }) 32 | 33 | It("has val/err", func() { 34 | set := client.Set(ctx, "key", "hello", 0) 35 | Expect(set.Err()).NotTo(HaveOccurred()) 36 | Expect(set.Val()).To(Equal("OK")) 37 | 38 | get := client.Get(ctx, "key") 39 | Expect(get.Err()).NotTo(HaveOccurred()) 40 | Expect(get.Val()).To(Equal("hello")) 41 | 42 | Expect(set.Err()).NotTo(HaveOccurred()) 43 | Expect(set.Val()).To(Equal("OK")) 44 | }) 45 | 46 | It("has helpers", func() { 47 | set := client.Set(ctx, "key", "10", 0) 48 | Expect(set.Err()).NotTo(HaveOccurred()) 49 | 50 | n, err := client.Get(ctx, "key").Int64() 51 | Expect(err).NotTo(HaveOccurred()) 52 | Expect(n).To(Equal(int64(10))) 53 | 54 | un, err := client.Get(ctx, "key").Uint64() 55 | Expect(err).NotTo(HaveOccurred()) 56 | Expect(un).To(Equal(uint64(10))) 57 | 58 | f, err := client.Get(ctx, "key").Float64() 59 | Expect(err).NotTo(HaveOccurred()) 60 | Expect(f).To(Equal(float64(10))) 61 | }) 62 | 63 | It("supports float32", func() { 64 | f := float32(66.97) 65 | 66 | err := client.Set(ctx, "float_key", f, 0).Err() 67 | Expect(err).NotTo(HaveOccurred()) 68 | 69 | val, err := client.Get(ctx, "float_key").Float32() 70 | Expect(err).NotTo(HaveOccurred()) 71 | Expect(val).To(Equal(f)) 72 | }) 73 | 74 | It("supports time.Time", func() { 75 | tm := time.Date(2019, 1, 1, 9, 45, 10, 222125, time.UTC) 76 | 77 | err := client.Set(ctx, "time_key", tm, 0).Err() 78 | Expect(err).NotTo(HaveOccurred()) 79 | 80 | s, err := client.Get(ctx, "time_key").Result() 81 | Expect(err).NotTo(HaveOccurred()) 82 | Expect(s).To(Equal("2019-01-01T09:45:10.000222125Z")) 83 | 84 | tm2, err := client.Get(ctx, "time_key").Time() 85 | Expect(err).NotTo(HaveOccurred()) 86 | Expect(tm2).To(BeTemporally("==", tm)) 87 | }) 88 | 89 | It("allows to set custom error", func() { 90 | e := errors.New("custom error") 91 | cmd := redis.Cmd{} 92 | cmd.SetErr(e) 93 | _, err := cmd.Result() 94 | Expect(err).To(Equal(e)) 95 | }) 96 | }) 97 | -------------------------------------------------------------------------------- /script.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "crypto/sha1" 6 | "encoding/hex" 7 | "io" 8 | ) 9 | 10 | type Scripter interface { 11 | Eval(ctx context.Context, script string, keys []string, args ...interface{}) *Cmd 12 | EvalSha(ctx context.Context, sha1 string, keys []string, args ...interface{}) *Cmd 13 | EvalRO(ctx context.Context, script string, keys []string, args ...interface{}) *Cmd 14 | EvalShaRO(ctx context.Context, sha1 string, keys []string, args ...interface{}) *Cmd 15 | ScriptExists(ctx context.Context, hashes ...string) *BoolSliceCmd 16 | ScriptLoad(ctx context.Context, script string) *StringCmd 17 | } 18 | 19 | var ( 20 | _ Scripter = (*Client)(nil) 21 | _ Scripter = (*Ring)(nil) 22 | _ Scripter = (*ClusterClient)(nil) 23 | ) 24 | 25 | type Script struct { 26 | src, hash string 27 | } 28 | 29 | func NewScript(src string) *Script { 30 | h := sha1.New() 31 | _, _ = io.WriteString(h, src) 32 | return &Script{ 33 | src: src, 34 | hash: hex.EncodeToString(h.Sum(nil)), 35 | } 36 | } 37 | 38 | func (s *Script) Hash() string { 39 | return s.hash 40 | } 41 | 42 | func (s *Script) Load(ctx context.Context, c Scripter) *StringCmd { 43 | return c.ScriptLoad(ctx, s.src) 44 | } 45 | 46 | func (s *Script) Exists(ctx context.Context, c Scripter) *BoolSliceCmd { 47 | return c.ScriptExists(ctx, s.hash) 48 | } 49 | 50 | func (s *Script) Eval(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd { 51 | return c.Eval(ctx, s.src, keys, args...) 52 | } 53 | 54 | func (s *Script) EvalRO(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd { 55 | return c.EvalRO(ctx, s.src, keys, args...) 56 | } 57 | 58 | func (s *Script) EvalSha(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd { 59 | return c.EvalSha(ctx, s.hash, keys, args...) 60 | } 61 | 62 | func (s *Script) EvalShaRO(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd { 63 | return c.EvalShaRO(ctx, s.hash, keys, args...) 64 | } 65 | 66 | // Run optimistically uses EVALSHA to run the script. If script does not exist 67 | // it is retried using EVAL. 68 | func (s *Script) Run(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd { 69 | r := s.EvalSha(ctx, c, keys, args...) 70 | if HasErrorPrefix(r.Err(), "NOSCRIPT") { 71 | return s.Eval(ctx, c, keys, args...) 72 | } 73 | return r 74 | } 75 | 76 | // RunRO optimistically uses EVALSHA_RO to run the script. If script does not exist 77 | // it is retried using EVAL_RO. 78 | func (s *Script) RunRO(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd { 79 | r := s.EvalShaRO(ctx, c, keys, args...) 80 | if HasErrorPrefix(r.Err(), "NOSCRIPT") { 81 | return s.EvalRO(ctx, c, keys, args...) 82 | } 83 | return r 84 | } 85 | -------------------------------------------------------------------------------- /auth/auth.go: -------------------------------------------------------------------------------- 1 | // Package auth package provides authentication-related interfaces and types. 2 | // It also includes a basic implementation of credentials using username and password. 3 | package auth 4 | 5 | // StreamingCredentialsProvider is an interface that defines the methods for a streaming credentials provider. 6 | // It is used to provide credentials for authentication. 7 | // The CredentialsListener is used to receive updates when the credentials change. 8 | type StreamingCredentialsProvider interface { 9 | // Subscribe subscribes to the credentials provider for updates. 10 | // It returns the current credentials, a cancel function to unsubscribe from the provider, 11 | // and an error if any. 12 | // TODO(ndyakov): Should we add context to the Subscribe method? 13 | Subscribe(listener CredentialsListener) (Credentials, UnsubscribeFunc, error) 14 | } 15 | 16 | // UnsubscribeFunc is a function that is used to cancel the subscription to the credentials provider. 17 | // It is used to unsubscribe from the provider when the credentials are no longer needed. 18 | type UnsubscribeFunc func() error 19 | 20 | // CredentialsListener is an interface that defines the methods for a credentials listener. 21 | // It is used to receive updates when the credentials change. 22 | // The OnNext method is called when the credentials change. 23 | // The OnError method is called when an error occurs while requesting the credentials. 24 | type CredentialsListener interface { 25 | OnNext(credentials Credentials) 26 | OnError(err error) 27 | } 28 | 29 | // Credentials is an interface that defines the methods for credentials. 30 | // It is used to provide the credentials for authentication. 31 | type Credentials interface { 32 | // BasicAuth returns the username and password for basic authentication. 33 | BasicAuth() (username string, password string) 34 | // RawCredentials returns the raw credentials as a string. 35 | // This can be used to extract the username and password from the raw credentials or 36 | // additional information if present in the token. 37 | RawCredentials() string 38 | } 39 | 40 | type basicAuth struct { 41 | username string 42 | password string 43 | } 44 | 45 | // RawCredentials returns the raw credentials as a string. 46 | func (b *basicAuth) RawCredentials() string { 47 | return b.username + ":" + b.password 48 | } 49 | 50 | // BasicAuth returns the username and password for basic authentication. 51 | func (b *basicAuth) BasicAuth() (username string, password string) { 52 | return b.username, b.password 53 | } 54 | 55 | // NewBasicCredentials creates a new Credentials object from the given username and password. 56 | func NewBasicCredentials(username, password string) Credentials { 57 | return &basicAuth{ 58 | username: username, 59 | password: password, 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /internal/util_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "runtime" 5 | "strings" 6 | "testing" 7 | 8 | . "github.com/bsm/ginkgo/v2" 9 | . "github.com/bsm/gomega" 10 | ) 11 | 12 | func BenchmarkToLowerStd(b *testing.B) { 13 | str := "AaBbCcDdEeFfGgHhIiJjKk" 14 | for i := 0; i < b.N; i++ { 15 | _ = strings.ToLower(str) 16 | } 17 | } 18 | 19 | // util.ToLower is 3x faster than strings.ToLower. 20 | func BenchmarkToLowerInternal(b *testing.B) { 21 | str := "AaBbCcDdEeFfGgHhIiJjKk" 22 | for i := 0; i < b.N; i++ { 23 | _ = ToLower(str) 24 | } 25 | } 26 | 27 | func TestToLower(t *testing.T) { 28 | It("toLower", func() { 29 | str := "AaBbCcDdEeFfGg" 30 | Expect(ToLower(str)).To(Equal(strings.ToLower(str))) 31 | 32 | str = "ABCDE" 33 | Expect(ToLower(str)).To(Equal(strings.ToLower(str))) 34 | 35 | str = "ABCDE" 36 | Expect(ToLower(str)).To(Equal(strings.ToLower(str))) 37 | 38 | str = "abced" 39 | Expect(ToLower(str)).To(Equal(strings.ToLower(str))) 40 | }) 41 | } 42 | 43 | func TestIsLower(t *testing.T) { 44 | It("isLower", func() { 45 | str := "AaBbCcDdEeFfGg" 46 | Expect(isLower(str)).To(BeFalse()) 47 | 48 | str = "ABCDE" 49 | Expect(isLower(str)).To(BeFalse()) 50 | 51 | str = "abcdefg" 52 | Expect(isLower(str)).To(BeTrue()) 53 | }) 54 | } 55 | 56 | func TestGetAddr(t *testing.T) { 57 | It("getAddr", func() { 58 | str := "127.0.0.1:1234" 59 | Expect(GetAddr(str)).To(Equal(str)) 60 | 61 | str = "[::1]:1234" 62 | Expect(GetAddr(str)).To(Equal(str)) 63 | 64 | str = "[fd01:abcd::7d03]:6379" 65 | Expect(GetAddr(str)).To(Equal(str)) 66 | 67 | Expect(GetAddr("::1:1234")).To(Equal("[::1]:1234")) 68 | 69 | Expect(GetAddr("fd01:abcd::7d03:6379")).To(Equal("[fd01:abcd::7d03]:6379")) 70 | 71 | Expect(GetAddr("127.0.0.1")).To(Equal("")) 72 | 73 | Expect(GetAddr("127")).To(Equal("")) 74 | }) 75 | } 76 | 77 | func BenchmarkReplaceSpaces(b *testing.B) { 78 | version := runtime.Version() 79 | for i := 0; i < b.N; i++ { 80 | _ = ReplaceSpaces(version) 81 | } 82 | } 83 | 84 | func ReplaceSpacesUseBuilder(s string) string { 85 | // Pre-allocate a builder with the same length as s to minimize allocations. 86 | // This is a basic optimization; adjust the initial size based on your use case. 87 | var builder strings.Builder 88 | builder.Grow(len(s)) 89 | 90 | for _, char := range s { 91 | if char == ' ' { 92 | // Replace space with a hyphen. 93 | builder.WriteRune('-') 94 | } else { 95 | // Copy the character as-is. 96 | builder.WriteRune(char) 97 | } 98 | } 99 | 100 | return builder.String() 101 | } 102 | 103 | func BenchmarkReplaceSpacesUseBuilder(b *testing.B) { 104 | version := runtime.Version() 105 | for i := 0; i < b.N; i++ { 106 | _ = ReplaceSpacesUseBuilder(version) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /internal/auth/streaming/cred_listeners.go: -------------------------------------------------------------------------------- 1 | package streaming 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/redis/go-redis/v9/auth" 7 | ) 8 | 9 | // CredentialsListeners is a thread-safe collection of credentials listeners 10 | // indexed by connection ID. 11 | // 12 | // This collection is used by the Manager to maintain a registry of listeners 13 | // for each connection in the pool. Listeners are reused when connections are 14 | // reinitialized (e.g., after a handoff) to avoid creating duplicate subscriptions 15 | // to the StreamingCredentialsProvider. 16 | // 17 | // The collection supports concurrent access from multiple goroutines during 18 | // connection initialization, credential updates, and connection removal. 19 | type CredentialsListeners struct { 20 | // listeners maps connection ID to credentials listener 21 | listeners map[uint64]auth.CredentialsListener 22 | 23 | // lock protects concurrent access to the listeners map 24 | lock sync.RWMutex 25 | } 26 | 27 | // NewCredentialsListeners creates a new thread-safe credentials listeners collection. 28 | func NewCredentialsListeners() *CredentialsListeners { 29 | return &CredentialsListeners{ 30 | listeners: make(map[uint64]auth.CredentialsListener), 31 | } 32 | } 33 | 34 | // Add adds or updates a credentials listener for a connection. 35 | // 36 | // If a listener already exists for the connection ID, it is replaced. 37 | // This is safe because the old listener should have been unsubscribed 38 | // before the connection was reinitialized. 39 | // 40 | // Thread-safe: Can be called concurrently from multiple goroutines. 41 | func (c *CredentialsListeners) Add(connID uint64, listener auth.CredentialsListener) { 42 | c.lock.Lock() 43 | defer c.lock.Unlock() 44 | if c.listeners == nil { 45 | c.listeners = make(map[uint64]auth.CredentialsListener) 46 | } 47 | c.listeners[connID] = listener 48 | } 49 | 50 | // Get retrieves the credentials listener for a connection. 51 | // 52 | // Returns: 53 | // - listener: The credentials listener for the connection, or nil if not found 54 | // - ok: true if a listener exists for the connection ID, false otherwise 55 | // 56 | // Thread-safe: Can be called concurrently from multiple goroutines. 57 | func (c *CredentialsListeners) Get(connID uint64) (auth.CredentialsListener, bool) { 58 | c.lock.RLock() 59 | defer c.lock.RUnlock() 60 | if len(c.listeners) == 0 { 61 | return nil, false 62 | } 63 | listener, ok := c.listeners[connID] 64 | return listener, ok 65 | } 66 | 67 | // Remove removes the credentials listener for a connection. 68 | // 69 | // This is called when a connection is removed from the pool to prevent 70 | // memory leaks. If no listener exists for the connection ID, this is a no-op. 71 | // 72 | // Thread-safe: Can be called concurrently from multiple goroutines. 73 | func (c *CredentialsListeners) Remove(connID uint64) { 74 | c.lock.Lock() 75 | defer c.lock.Unlock() 76 | delete(c.listeners, connID) 77 | } 78 | -------------------------------------------------------------------------------- /osscluster_commands.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "sync/atomic" 7 | ) 8 | 9 | func (c *ClusterClient) DBSize(ctx context.Context) *IntCmd { 10 | cmd := NewIntCmd(ctx, "dbsize") 11 | _ = c.withProcessHook(ctx, cmd, func(ctx context.Context, _ Cmder) error { 12 | var size int64 13 | err := c.ForEachMaster(ctx, func(ctx context.Context, master *Client) error { 14 | n, err := master.DBSize(ctx).Result() 15 | if err != nil { 16 | return err 17 | } 18 | atomic.AddInt64(&size, n) 19 | return nil 20 | }) 21 | if err != nil { 22 | cmd.SetErr(err) 23 | } else { 24 | cmd.val = size 25 | } 26 | return nil 27 | }) 28 | return cmd 29 | } 30 | 31 | func (c *ClusterClient) ScriptLoad(ctx context.Context, script string) *StringCmd { 32 | cmd := NewStringCmd(ctx, "script", "load", script) 33 | _ = c.withProcessHook(ctx, cmd, func(ctx context.Context, _ Cmder) error { 34 | var mu sync.Mutex 35 | err := c.ForEachShard(ctx, func(ctx context.Context, shard *Client) error { 36 | val, err := shard.ScriptLoad(ctx, script).Result() 37 | if err != nil { 38 | return err 39 | } 40 | 41 | mu.Lock() 42 | if cmd.Val() == "" { 43 | cmd.val = val 44 | } 45 | mu.Unlock() 46 | 47 | return nil 48 | }) 49 | if err != nil { 50 | cmd.SetErr(err) 51 | } 52 | return nil 53 | }) 54 | return cmd 55 | } 56 | 57 | func (c *ClusterClient) ScriptFlush(ctx context.Context) *StatusCmd { 58 | cmd := NewStatusCmd(ctx, "script", "flush") 59 | _ = c.withProcessHook(ctx, cmd, func(ctx context.Context, _ Cmder) error { 60 | err := c.ForEachShard(ctx, func(ctx context.Context, shard *Client) error { 61 | return shard.ScriptFlush(ctx).Err() 62 | }) 63 | if err != nil { 64 | cmd.SetErr(err) 65 | } 66 | return nil 67 | }) 68 | return cmd 69 | } 70 | 71 | func (c *ClusterClient) ScriptExists(ctx context.Context, hashes ...string) *BoolSliceCmd { 72 | args := make([]interface{}, 2+len(hashes)) 73 | args[0] = "script" 74 | args[1] = "exists" 75 | for i, hash := range hashes { 76 | args[2+i] = hash 77 | } 78 | cmd := NewBoolSliceCmd(ctx, args...) 79 | 80 | result := make([]bool, len(hashes)) 81 | for i := range result { 82 | result[i] = true 83 | } 84 | 85 | _ = c.withProcessHook(ctx, cmd, func(ctx context.Context, _ Cmder) error { 86 | var mu sync.Mutex 87 | err := c.ForEachShard(ctx, func(ctx context.Context, shard *Client) error { 88 | val, err := shard.ScriptExists(ctx, hashes...).Result() 89 | if err != nil { 90 | return err 91 | } 92 | 93 | mu.Lock() 94 | for i, v := range val { 95 | result[i] = result[i] && v 96 | } 97 | mu.Unlock() 98 | 99 | return nil 100 | }) 101 | if err != nil { 102 | cmd.SetErr(err) 103 | } else { 104 | cmd.val = result 105 | } 106 | return nil 107 | }) 108 | return cmd 109 | } 110 | -------------------------------------------------------------------------------- /export_test.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "strings" 8 | 9 | "github.com/redis/go-redis/v9/internal" 10 | "github.com/redis/go-redis/v9/internal/hashtag" 11 | "github.com/redis/go-redis/v9/internal/pool" 12 | ) 13 | 14 | func (c *baseClient) Pool() pool.Pooler { 15 | return c.connPool 16 | } 17 | 18 | func (c *PubSub) SetNetConn(netConn net.Conn) { 19 | c.cn = pool.NewConn(netConn) 20 | } 21 | 22 | func (c *ClusterClient) LoadState(ctx context.Context) (*clusterState, error) { 23 | // return c.state.Reload(ctx) 24 | return c.loadState(ctx) 25 | } 26 | 27 | func (c *ClusterClient) SlotAddrs(ctx context.Context, slot int) []string { 28 | state, err := c.state.Get(ctx) 29 | if err != nil { 30 | panic(err) 31 | } 32 | 33 | var addrs []string 34 | for _, n := range state.slotNodes(slot) { 35 | addrs = append(addrs, n.Client.getAddr()) 36 | } 37 | return addrs 38 | } 39 | 40 | func (c *ClusterClient) Nodes(ctx context.Context, key string) ([]*clusterNode, error) { 41 | state, err := c.state.Reload(ctx) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | slot := hashtag.Slot(key) 47 | nodes := state.slotNodes(slot) 48 | if len(nodes) != 2 { 49 | return nil, fmt.Errorf("slot=%d does not have enough nodes: %v", slot, nodes) 50 | } 51 | return nodes, nil 52 | } 53 | 54 | func (c *ClusterClient) SwapNodes(ctx context.Context, key string) error { 55 | nodes, err := c.Nodes(ctx, key) 56 | if err != nil { 57 | return err 58 | } 59 | nodes[0], nodes[1] = nodes[1], nodes[0] 60 | return nil 61 | } 62 | 63 | func (c *clusterState) IsConsistent(ctx context.Context) bool { 64 | if len(c.Masters) < 3 { 65 | return false 66 | } 67 | for _, master := range c.Masters { 68 | s := master.Client.Info(ctx, "replication").Val() 69 | if !strings.Contains(s, "role:master") { 70 | return false 71 | } 72 | } 73 | 74 | if len(c.Slaves) < 3 { 75 | return false 76 | } 77 | for _, slave := range c.Slaves { 78 | s := slave.Client.Info(ctx, "replication").Val() 79 | if !strings.Contains(s, "role:slave") { 80 | return false 81 | } 82 | } 83 | 84 | return true 85 | } 86 | 87 | func GetSlavesAddrByName(ctx context.Context, c *SentinelClient, name string) []string { 88 | addrs, err := c.Replicas(ctx, name).Result() 89 | if err != nil { 90 | internal.Logger.Printf(ctx, "sentinel: Replicas name=%q failed: %s", 91 | name, err) 92 | return []string{} 93 | } 94 | return parseReplicaAddrs(addrs, false) 95 | } 96 | 97 | func (c *Ring) ShardByName(name string) *ringShard { 98 | shard, _ := c.sharding.GetByName(name) 99 | return shard 100 | } 101 | 102 | func (c *ModuleLoadexConfig) ToArgs() []interface{} { 103 | return c.toArgs() 104 | } 105 | 106 | func ShouldRetry(err error, retryTimeout bool) bool { 107 | return shouldRetry(err, retryTimeout) 108 | } 109 | 110 | func JoinErrors(errs []error) string { 111 | return joinErrors(errs) 112 | } 113 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO_MOD_DIRS := $(shell find . -type f -name 'go.mod' -exec dirname {} \; | sort) 2 | REDIS_VERSION ?= 8.4 3 | RE_CLUSTER ?= false 4 | RCE_DOCKER ?= true 5 | CLIENT_LIBS_TEST_IMAGE ?= redislabs/client-libs-test:8.4.0 6 | 7 | docker.start: 8 | export RE_CLUSTER=$(RE_CLUSTER) && \ 9 | export RCE_DOCKER=$(RCE_DOCKER) && \ 10 | export REDIS_VERSION=$(REDIS_VERSION) && \ 11 | export CLIENT_LIBS_TEST_IMAGE=$(CLIENT_LIBS_TEST_IMAGE) && \ 12 | docker compose --profile all up -d --quiet-pull 13 | 14 | docker.stop: 15 | docker compose --profile all down 16 | 17 | test: 18 | $(MAKE) docker.start 19 | @if [ -z "$(REDIS_VERSION)" ]; then \ 20 | echo "REDIS_VERSION not set, running all tests"; \ 21 | $(MAKE) test.ci; \ 22 | else \ 23 | MAJOR_VERSION=$$(echo "$(REDIS_VERSION)" | cut -d. -f1); \ 24 | if [ "$$MAJOR_VERSION" -ge 8 ]; then \ 25 | echo "REDIS_VERSION $(REDIS_VERSION) >= 8, running all tests"; \ 26 | $(MAKE) test.ci; \ 27 | else \ 28 | echo "REDIS_VERSION $(REDIS_VERSION) < 8, skipping vector_sets tests"; \ 29 | $(MAKE) test.ci.skip-vectorsets; \ 30 | fi; \ 31 | fi 32 | $(MAKE) docker.stop 33 | 34 | test.ci: 35 | set -e; for dir in $(GO_MOD_DIRS); do \ 36 | echo "go test in $${dir}"; \ 37 | (cd "$${dir}" && \ 38 | export RE_CLUSTER=$(RE_CLUSTER) && \ 39 | export RCE_DOCKER=$(RCE_DOCKER) && \ 40 | export REDIS_VERSION=$(REDIS_VERSION) && \ 41 | go mod tidy -compat=1.18 && \ 42 | go vet && \ 43 | go test -v -coverprofile=coverage.txt -covermode=atomic ./... -race -skip Example); \ 44 | done 45 | cd internal/customvet && go build . 46 | go vet -vettool ./internal/customvet/customvet 47 | 48 | test.ci.skip-vectorsets: 49 | set -e; for dir in $(GO_MOD_DIRS); do \ 50 | echo "go test in $${dir} (skipping vector sets)"; \ 51 | (cd "$${dir}" && \ 52 | export RE_CLUSTER=$(RE_CLUSTER) && \ 53 | export RCE_DOCKER=$(RCE_DOCKER) && \ 54 | export REDIS_VERSION=$(REDIS_VERSION) && \ 55 | go mod tidy -compat=1.18 && \ 56 | go vet && \ 57 | go test -v -coverprofile=coverage.txt -covermode=atomic ./... -race \ 58 | -run '^(?!.*(?:VectorSet|vectorset|ExampleClient_vectorset)).*$$' -skip Example); \ 59 | done 60 | cd internal/customvet && go build . 61 | go vet -vettool ./internal/customvet/customvet 62 | 63 | bench: 64 | export RE_CLUSTER=$(RE_CLUSTER) && \ 65 | export RCE_DOCKER=$(RCE_DOCKER) && \ 66 | export REDIS_VERSION=$(REDIS_VERSION) && \ 67 | go test ./... -test.run=NONE -test.bench=. -test.benchmem -skip Example 68 | 69 | .PHONY: all test test.ci test.ci.skip-vectorsets bench fmt 70 | 71 | build: 72 | export RE_CLUSTER=$(RE_CLUSTER) && \ 73 | export RCE_DOCKER=$(RCE_DOCKER) && \ 74 | export REDIS_VERSION=$(REDIS_VERSION) && \ 75 | go build . 76 | 77 | fmt: 78 | gofumpt -w ./ 79 | goimports -w -local github.com/redis/go-redis ./ 80 | 81 | go_mod_tidy: 82 | set -e; for dir in $(GO_MOD_DIRS); do \ 83 | echo "go mod tidy in $${dir}"; \ 84 | (cd "$${dir}" && \ 85 | go get -u ./... && \ 86 | go mod tidy -compat=1.18); \ 87 | done 88 | -------------------------------------------------------------------------------- /command_digest_test.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/redis/go-redis/v9/internal/proto" 9 | ) 10 | 11 | func TestDigestCmd(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | hexStr string 15 | expected uint64 16 | wantErr bool 17 | }{ 18 | { 19 | name: "zero value", 20 | hexStr: "0", 21 | expected: 0, 22 | wantErr: false, 23 | }, 24 | { 25 | name: "small value", 26 | hexStr: "ff", 27 | expected: 255, 28 | wantErr: false, 29 | }, 30 | { 31 | name: "medium value", 32 | hexStr: "1234abcd", 33 | expected: 0x1234abcd, 34 | wantErr: false, 35 | }, 36 | { 37 | name: "large value", 38 | hexStr: "ffffffffffffffff", 39 | expected: 0xffffffffffffffff, 40 | wantErr: false, 41 | }, 42 | { 43 | name: "uppercase hex", 44 | hexStr: "DEADBEEF", 45 | expected: 0xdeadbeef, 46 | wantErr: false, 47 | }, 48 | { 49 | name: "mixed case hex", 50 | hexStr: "DeAdBeEf", 51 | expected: 0xdeadbeef, 52 | wantErr: false, 53 | }, 54 | { 55 | name: "typical xxh3 hash", 56 | hexStr: "a1b2c3d4e5f67890", 57 | expected: 0xa1b2c3d4e5f67890, 58 | wantErr: false, 59 | }, 60 | } 61 | 62 | for _, tt := range tests { 63 | t.Run(tt.name, func(t *testing.T) { 64 | // Create a mock reader that returns the hex string in RESP format 65 | // Format: $\r\n\r\n 66 | respData := []byte(fmt.Sprintf("$%d\r\n%s\r\n", len(tt.hexStr), tt.hexStr)) 67 | 68 | rd := proto.NewReader(newMockConn(respData)) 69 | 70 | cmd := NewDigestCmd(context.Background(), "digest", "key") 71 | err := cmd.readReply(rd) 72 | 73 | if (err != nil) != tt.wantErr { 74 | t.Errorf("DigestCmd.readReply() error = %v, wantErr %v", err, tt.wantErr) 75 | return 76 | } 77 | 78 | if !tt.wantErr && cmd.Val() != tt.expected { 79 | t.Errorf("DigestCmd.Val() = %d (0x%x), want %d (0x%x)", cmd.Val(), cmd.Val(), tt.expected, tt.expected) 80 | } 81 | }) 82 | } 83 | } 84 | 85 | func TestDigestCmdResult(t *testing.T) { 86 | cmd := NewDigestCmd(context.Background(), "digest", "key") 87 | expected := uint64(0xdeadbeefcafebabe) 88 | cmd.SetVal(expected) 89 | 90 | val, err := cmd.Result() 91 | if err != nil { 92 | t.Errorf("DigestCmd.Result() error = %v", err) 93 | } 94 | 95 | if val != expected { 96 | t.Errorf("DigestCmd.Result() = %d (0x%x), want %d (0x%x)", val, val, expected, expected) 97 | } 98 | } 99 | 100 | // mockConn is a simple mock connection for testing 101 | type mockConn struct { 102 | data []byte 103 | pos int 104 | } 105 | 106 | func newMockConn(data []byte) *mockConn { 107 | return &mockConn{data: data} 108 | } 109 | 110 | func (c *mockConn) Read(p []byte) (n int, err error) { 111 | if c.pos >= len(c.data) { 112 | return 0, nil 113 | } 114 | n = copy(p, c.data[c.pos:]) 115 | c.pos += n 116 | return n, nil 117 | } 118 | 119 | -------------------------------------------------------------------------------- /internal/pool/conn_relaxed_timeout_test.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "net" 5 | "sync" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | // TestConcurrentRelaxedTimeoutClearing tests the race condition fix in ClearRelaxedTimeout 11 | func TestConcurrentRelaxedTimeoutClearing(t *testing.T) { 12 | // Create a dummy connection for testing 13 | netConn := &net.TCPConn{} 14 | cn := NewConn(netConn) 15 | defer cn.Close() 16 | 17 | // Set relaxed timeout multiple times to increase counter 18 | cn.SetRelaxedTimeout(time.Second, time.Second) 19 | cn.SetRelaxedTimeout(time.Second, time.Second) 20 | cn.SetRelaxedTimeout(time.Second, time.Second) 21 | 22 | // Verify counter is 3 23 | if count := cn.relaxedCounter.Load(); count != 3 { 24 | t.Errorf("Expected relaxed counter to be 3, got %d", count) 25 | } 26 | 27 | // Clear timeouts concurrently to test race condition fix 28 | var wg sync.WaitGroup 29 | for i := 0; i < 10; i++ { 30 | wg.Add(1) 31 | go func() { 32 | defer wg.Done() 33 | cn.ClearRelaxedTimeout() 34 | }() 35 | } 36 | wg.Wait() 37 | 38 | // Verify counter is 0 and timeouts are cleared 39 | if count := cn.relaxedCounter.Load(); count != 0 { 40 | t.Errorf("Expected relaxed counter to be 0 after clearing, got %d", count) 41 | } 42 | if timeout := cn.relaxedReadTimeoutNs.Load(); timeout != 0 { 43 | t.Errorf("Expected relaxed read timeout to be 0, got %d", timeout) 44 | } 45 | if timeout := cn.relaxedWriteTimeoutNs.Load(); timeout != 0 { 46 | t.Errorf("Expected relaxed write timeout to be 0, got %d", timeout) 47 | } 48 | } 49 | 50 | // TestRelaxedTimeoutCounterRaceCondition tests the specific race condition scenario 51 | func TestRelaxedTimeoutCounterRaceCondition(t *testing.T) { 52 | netConn := &net.TCPConn{} 53 | cn := NewConn(netConn) 54 | defer cn.Close() 55 | 56 | // Set relaxed timeout once 57 | cn.SetRelaxedTimeout(time.Second, time.Second) 58 | 59 | // Verify counter is 1 60 | if count := cn.relaxedCounter.Load(); count != 1 { 61 | t.Errorf("Expected relaxed counter to be 1, got %d", count) 62 | } 63 | 64 | // Test concurrent clearing with race condition scenario 65 | var wg sync.WaitGroup 66 | 67 | // Multiple goroutines try to clear simultaneously 68 | for i := 0; i < 5; i++ { 69 | wg.Add(1) 70 | go func() { 71 | defer wg.Done() 72 | cn.ClearRelaxedTimeout() 73 | }() 74 | } 75 | wg.Wait() 76 | 77 | // Verify final state is consistent 78 | if count := cn.relaxedCounter.Load(); count != 0 { 79 | t.Errorf("Expected relaxed counter to be 0 after concurrent clearing, got %d", count) 80 | } 81 | 82 | // Verify timeouts are actually cleared 83 | if timeout := cn.relaxedReadTimeoutNs.Load(); timeout != 0 { 84 | t.Errorf("Expected relaxed read timeout to be cleared, got %d", timeout) 85 | } 86 | if timeout := cn.relaxedWriteTimeoutNs.Load(); timeout != 0 { 87 | t.Errorf("Expected relaxed write timeout to be cleared, got %d", timeout) 88 | } 89 | if deadline := cn.relaxedDeadlineNs.Load(); deadline != 0 { 90 | t.Errorf("Expected relaxed deadline to be cleared, got %d", deadline) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /internal/util/strconv_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | ) 7 | 8 | func TestAtoi(t *testing.T) { 9 | tests := []struct { 10 | input []byte 11 | expected int 12 | wantErr bool 13 | }{ 14 | {[]byte("123"), 123, false}, 15 | {[]byte("-456"), -456, false}, 16 | {[]byte("abc"), 0, true}, 17 | } 18 | 19 | for _, tt := range tests { 20 | result, err := Atoi(tt.input) 21 | if (err != nil) != tt.wantErr { 22 | t.Errorf("Atoi(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr) 23 | } 24 | if result != tt.expected && !tt.wantErr { 25 | t.Errorf("Atoi(%q) = %d, want %d", tt.input, result, tt.expected) 26 | } 27 | } 28 | } 29 | 30 | func TestParseInt(t *testing.T) { 31 | tests := []struct { 32 | input []byte 33 | base int 34 | bitSize int 35 | expected int64 36 | wantErr bool 37 | }{ 38 | {[]byte("123"), 10, 64, 123, false}, 39 | {[]byte("-7F"), 16, 64, -127, false}, 40 | {[]byte("zzz"), 36, 64, 46655, false}, 41 | {[]byte("invalid"), 10, 64, 0, true}, 42 | } 43 | 44 | for _, tt := range tests { 45 | result, err := ParseInt(tt.input, tt.base, tt.bitSize) 46 | if (err != nil) != tt.wantErr { 47 | t.Errorf("ParseInt(%q, base=%d) error = %v, wantErr %v", tt.input, tt.base, err, tt.wantErr) 48 | } 49 | if result != tt.expected && !tt.wantErr { 50 | t.Errorf("ParseInt(%q, base=%d) = %d, want %d", tt.input, tt.base, result, tt.expected) 51 | } 52 | } 53 | } 54 | 55 | func TestParseUint(t *testing.T) { 56 | tests := []struct { 57 | input []byte 58 | base int 59 | bitSize int 60 | expected uint64 61 | wantErr bool 62 | }{ 63 | {[]byte("255"), 10, 8, 255, false}, 64 | {[]byte("FF"), 16, 16, 255, false}, 65 | {[]byte("-1"), 10, 8, 0, true}, // negative should error for unsigned 66 | } 67 | 68 | for _, tt := range tests { 69 | result, err := ParseUint(tt.input, tt.base, tt.bitSize) 70 | if (err != nil) != tt.wantErr { 71 | t.Errorf("ParseUint(%q, base=%d) error = %v, wantErr %v", tt.input, tt.base, err, tt.wantErr) 72 | } 73 | if result != tt.expected && !tt.wantErr { 74 | t.Errorf("ParseUint(%q, base=%d) = %d, want %d", tt.input, tt.base, result, tt.expected) 75 | } 76 | } 77 | } 78 | 79 | func TestParseFloat(t *testing.T) { 80 | tests := []struct { 81 | input []byte 82 | bitSize int 83 | expected float64 84 | wantErr bool 85 | }{ 86 | {[]byte("3.14"), 64, 3.14, false}, 87 | {[]byte("-2.71"), 64, -2.71, false}, 88 | {[]byte("NaN"), 64, math.NaN(), false}, 89 | {[]byte("invalid"), 64, 0, true}, 90 | } 91 | 92 | for _, tt := range tests { 93 | result, err := ParseFloat(tt.input, tt.bitSize) 94 | if (err != nil) != tt.wantErr { 95 | t.Errorf("ParseFloat(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr) 96 | } 97 | if !tt.wantErr && !(math.IsNaN(tt.expected) && math.IsNaN(result)) && result != tt.expected { 98 | t.Errorf("ParseFloat(%q) = %v, want %v", tt.input, result, tt.expected) 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /logging/logging.go: -------------------------------------------------------------------------------- 1 | // Package logging provides logging level constants and utilities for the go-redis library. 2 | // This package centralizes logging configuration to ensure consistency across all components. 3 | package logging 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/redis/go-redis/v9/internal" 11 | ) 12 | 13 | type LogLevelT = internal.LogLevelT 14 | 15 | const ( 16 | LogLevelError = internal.LogLevelError 17 | LogLevelWarn = internal.LogLevelWarn 18 | LogLevelInfo = internal.LogLevelInfo 19 | LogLevelDebug = internal.LogLevelDebug 20 | ) 21 | 22 | // VoidLogger is a logger that does nothing. 23 | // Used to disable logging and thus speed up the library. 24 | type VoidLogger struct{} 25 | 26 | func (v *VoidLogger) Printf(_ context.Context, _ string, _ ...interface{}) { 27 | // do nothing 28 | } 29 | 30 | // Disable disables logging by setting the internal logger to a void logger. 31 | // This can be used to speed up the library if logging is not needed. 32 | // It will override any custom logger that was set before and set the VoidLogger. 33 | func Disable() { 34 | internal.Logger = &VoidLogger{} 35 | } 36 | 37 | // Enable enables logging by setting the internal logger to the default logger. 38 | // This is the default behavior. 39 | // You can use redis.SetLogger to set a custom logger. 40 | // 41 | // NOTE: This function is not thread-safe. 42 | // It will override any custom logger that was set before and set the DefaultLogger. 43 | func Enable() { 44 | internal.Logger = internal.NewDefaultLogger() 45 | } 46 | 47 | // SetLogLevel sets the log level for the library. 48 | func SetLogLevel(logLevel LogLevelT) { 49 | internal.LogLevel = logLevel 50 | } 51 | 52 | // NewBlacklistLogger returns a new logger that filters out messages containing any of the substrings. 53 | // This can be used to filter out messages containing sensitive information. 54 | func NewBlacklistLogger(substr []string) internal.Logging { 55 | l := internal.NewDefaultLogger() 56 | return &filterLogger{logger: l, substr: substr, blacklist: true} 57 | } 58 | 59 | // NewWhitelistLogger returns a new logger that only logs messages containing any of the substrings. 60 | // This can be used to only log messages related to specific commands or patterns. 61 | func NewWhitelistLogger(substr []string) internal.Logging { 62 | l := internal.NewDefaultLogger() 63 | return &filterLogger{logger: l, substr: substr, blacklist: false} 64 | } 65 | 66 | type filterLogger struct { 67 | logger internal.Logging 68 | blacklist bool 69 | substr []string 70 | } 71 | 72 | func (l *filterLogger) Printf(ctx context.Context, format string, v ...interface{}) { 73 | msg := fmt.Sprintf(format, v...) 74 | found := false 75 | for _, substr := range l.substr { 76 | if strings.Contains(msg, substr) { 77 | found = true 78 | if l.blacklist { 79 | return 80 | } 81 | } 82 | } 83 | // whitelist, only log if one of the substrings is present 84 | if !l.blacklist && !found { 85 | return 86 | } 87 | if l.logger != nil { 88 | l.logger.Printf(ctx, format, v...) 89 | return 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /internal/pool/pool_single.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | // SingleConnPool is a pool that always returns the same connection. 9 | // Note: This pool is not thread-safe. 10 | // It is intended to be used by clients that need a single connection. 11 | type SingleConnPool struct { 12 | pool Pooler 13 | cn *Conn 14 | stickyErr error 15 | } 16 | 17 | var _ Pooler = (*SingleConnPool)(nil) 18 | 19 | // NewSingleConnPool creates a new single connection pool. 20 | // The pool will always return the same connection. 21 | // The pool will not: 22 | // - Close the connection 23 | // - Reconnect the connection 24 | // - Track the connection in any way 25 | func NewSingleConnPool(pool Pooler, cn *Conn) *SingleConnPool { 26 | return &SingleConnPool{ 27 | pool: pool, 28 | cn: cn, 29 | } 30 | } 31 | 32 | func (p *SingleConnPool) NewConn(ctx context.Context) (*Conn, error) { 33 | return p.pool.NewConn(ctx) 34 | } 35 | 36 | func (p *SingleConnPool) CloseConn(cn *Conn) error { 37 | return p.pool.CloseConn(cn) 38 | } 39 | 40 | func (p *SingleConnPool) Get(_ context.Context) (*Conn, error) { 41 | if p.stickyErr != nil { 42 | return nil, p.stickyErr 43 | } 44 | if p.cn == nil { 45 | return nil, ErrClosed 46 | } 47 | 48 | // NOTE: SingleConnPool is NOT thread-safe by design and is used in special scenarios: 49 | // - During initialization (connection is in INITIALIZING state) 50 | // - During re-authentication (connection is in UNUSABLE state) 51 | // - For transactions (connection might be in various states) 52 | // We use SetUsed() which forces the transition, rather than TryTransition() which 53 | // would fail if the connection is not in IDLE/CREATED state. 54 | p.cn.SetUsed(true) 55 | p.cn.SetUsedAt(time.Now()) 56 | return p.cn, nil 57 | } 58 | 59 | func (p *SingleConnPool) Put(_ context.Context, cn *Conn) { 60 | if p.cn == nil { 61 | return 62 | } 63 | if p.cn != cn { 64 | return 65 | } 66 | p.cn.SetUsed(false) 67 | } 68 | 69 | func (p *SingleConnPool) Remove(_ context.Context, cn *Conn, reason error) { 70 | cn.SetUsed(false) 71 | p.cn = nil 72 | p.stickyErr = reason 73 | } 74 | 75 | // RemoveWithoutTurn has the same behavior as Remove for SingleConnPool 76 | // since SingleConnPool doesn't use a turn-based queue system. 77 | func (p *SingleConnPool) RemoveWithoutTurn(ctx context.Context, cn *Conn, reason error) { 78 | p.Remove(ctx, cn, reason) 79 | } 80 | 81 | func (p *SingleConnPool) Close() error { 82 | p.cn = nil 83 | p.stickyErr = ErrClosed 84 | return nil 85 | } 86 | 87 | func (p *SingleConnPool) Len() int { 88 | return 0 89 | } 90 | 91 | func (p *SingleConnPool) IdleLen() int { 92 | return 0 93 | } 94 | 95 | // Size returns the maximum pool size, which is always 1 for SingleConnPool. 96 | func (p *SingleConnPool) Size() int { return 1 } 97 | 98 | func (p *SingleConnPool) Stats() *Stats { 99 | return &Stats{} 100 | } 101 | 102 | func (p *SingleConnPool) AddPoolHook(_ PoolHook) {} 103 | 104 | func (p *SingleConnPool) RemovePoolHook(_ PoolHook) {} 105 | -------------------------------------------------------------------------------- /internal/util/atomic_min.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | /* 4 | © 2023–present Harald Rudell (https://haraldrudell.github.io/haraldrudell/) 5 | ISC License 6 | 7 | Modified by htemelski-redis 8 | Adapted from the modified atomic_max, but with inverted logic 9 | */ 10 | 11 | import ( 12 | "math" 13 | 14 | "go.uber.org/atomic" 15 | ) 16 | 17 | // AtomicMin is a thread-safe Min container 18 | // - hasValue indicator true if a value was equal to or greater than threshold 19 | // - optional threshold for minimum accepted Min value 20 | // - — 21 | // - wait-free CompareAndSwap mechanic 22 | type AtomicMin struct { 23 | 24 | // value is current Min 25 | value atomic.Float64 26 | // whether [AtomicMin.Value] has been invoked 27 | // with value equal or greater to threshold 28 | hasValue atomic.Bool 29 | } 30 | 31 | // NewAtomicMin returns a thread-safe Min container 32 | // - if threshold is not used, AtomicMin is initialization-free 33 | func NewAtomicMin() (atomicMin *AtomicMin) { 34 | m := AtomicMin{} 35 | m.value.Store(math.MaxFloat64) 36 | return &m 37 | } 38 | 39 | // Value updates the container with a possible Min value 40 | // - isNewMin is true if: 41 | // - — value is equal to or greater than any threshold and 42 | // - — invocation recorded the first 0 or 43 | // - — a new Min 44 | // - upon return, Min and Min1 are guaranteed to reflect the invocation 45 | // - the return order of concurrent Value invocations is not guaranteed 46 | // - Thread-safe 47 | func (m *AtomicMin) Value(value float64) (isNewMin bool) { 48 | // math.MaxFloat64 as Min case 49 | var hasValue0 = m.hasValue.Load() 50 | if value == math.MaxFloat64 { 51 | if !hasValue0 { 52 | isNewMin = m.hasValue.CompareAndSwap(false, true) 53 | } 54 | return // math.MaxFloat64 as Min: isNewMin true for first 0 writer 55 | } 56 | 57 | // check against present value 58 | var current = m.value.Load() 59 | if isNewMin = value < current; !isNewMin { 60 | return // not a new Min return: isNewMin false 61 | } 62 | 63 | // store the new Min 64 | for { 65 | 66 | // try to write value to *Min 67 | if isNewMin = m.value.CompareAndSwap(current, value); isNewMin { 68 | if !hasValue0 { 69 | // may be rarely written multiple times 70 | // still faster than CompareAndSwap 71 | m.hasValue.Store(true) 72 | } 73 | return // new Min written return: isNewMin true 74 | } 75 | if current = m.value.Load(); current <= value { 76 | return // no longer a need to write return: isNewMin false 77 | } 78 | } 79 | } 80 | 81 | // Min returns current min and value-present flag 82 | // - hasValue true indicates that value reflects a Value invocation 83 | // - hasValue false: value is zero-value 84 | // - Thread-safe 85 | func (m *AtomicMin) Min() (value float64, hasValue bool) { 86 | if hasValue = m.hasValue.Load(); !hasValue { 87 | return 88 | } 89 | value = m.value.Load() 90 | return 91 | } 92 | 93 | // Min1 returns current Minimum whether zero-value or set by Value 94 | // - threshold is ignored 95 | // - Thread-safe 96 | func (m *AtomicMin) Min1() (value float64) { return m.value.Load() } 97 | -------------------------------------------------------------------------------- /example/cluster-mget/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/redis/go-redis/v9" 8 | ) 9 | 10 | func main() { 11 | ctx := context.Background() 12 | 13 | // Create a cluster client 14 | rdb := redis.NewClusterClient(&redis.ClusterOptions{ 15 | Addrs: []string{ 16 | "localhost:16600", 17 | "localhost:16601", 18 | "localhost:16602", 19 | "localhost:16603", 20 | "localhost:16604", 21 | "localhost:16605", 22 | }, 23 | }) 24 | defer rdb.Close() 25 | 26 | // Test connection 27 | if err := rdb.Ping(ctx).Err(); err != nil { 28 | panic(fmt.Sprintf("Failed to connect to Redis cluster: %v", err)) 29 | } 30 | 31 | fmt.Println("✓ Connected to Redis cluster") 32 | 33 | // Define 10 keys and values 34 | keys := make([]string, 10) 35 | values := make([]string, 10) 36 | for i := 0; i < 10; i++ { 37 | keys[i] = fmt.Sprintf("key%d", i) 38 | values[i] = fmt.Sprintf("value%d", i) 39 | } 40 | 41 | // Set all 10 keys 42 | fmt.Println("\n=== Setting 10 keys ===") 43 | for i := 0; i < 10; i++ { 44 | err := rdb.Set(ctx, keys[i], values[i], 0).Err() 45 | if err != nil { 46 | panic(fmt.Sprintf("Failed to set %s: %v", keys[i], err)) 47 | } 48 | fmt.Printf("✓ SET %s = %s\n", keys[i], values[i]) 49 | } 50 | 51 | /* 52 | // Retrieve all keys using MGET 53 | fmt.Println("\n=== Retrieving keys with MGET ===") 54 | result, err := rdb.MGet(ctx, keys...).Result() 55 | if err != nil { 56 | panic(fmt.Sprintf("Failed to execute MGET: %v", err)) 57 | } 58 | */ 59 | 60 | /* 61 | // Validate the results 62 | fmt.Println("\n=== Validating MGET results ===") 63 | allValid := true 64 | for i, val := range result { 65 | expectedValue := values[i] 66 | actualValue, ok := val.(string) 67 | 68 | if !ok { 69 | fmt.Printf("✗ %s: expected string, got %T\n", keys[i], val) 70 | allValid = false 71 | continue 72 | } 73 | 74 | if actualValue != expectedValue { 75 | fmt.Printf("✗ %s: expected '%s', got '%s'\n", keys[i], expectedValue, actualValue) 76 | allValid = false 77 | } else { 78 | fmt.Printf("✓ %s: %s\n", keys[i], actualValue) 79 | } 80 | } 81 | 82 | // Print summary 83 | fmt.Println("\n=== Summary ===") 84 | if allValid { 85 | fmt.Println("✓ All values retrieved successfully and match expected values!") 86 | } else { 87 | fmt.Println("✗ Some values did not match expected values") 88 | } 89 | */ 90 | 91 | // Clean up - delete the keys 92 | fmt.Println("\n=== Cleaning up ===") 93 | for _, key := range keys { 94 | if err := rdb.Del(ctx, key).Err(); err != nil { 95 | fmt.Printf("Warning: Failed to delete %s: %v\n", key, err) 96 | } 97 | } 98 | fmt.Println("✓ Cleanup complete") 99 | 100 | err := rdb.Set(ctx, "{tag}exists", "asdf",0).Err() 101 | if err != nil { 102 | panic(err) 103 | } 104 | val, err := rdb.Get(ctx, "{tag}nilkeykey1").Result() 105 | fmt.Printf("\nval: %+v err: %+v\n", val, err) 106 | valm, err := rdb.MGet(ctx, "{tag}nilkeykey1", "{tag}exists").Result() 107 | fmt.Printf("\nval: %+v err: %+v\n", valm, err) 108 | } 109 | --------------------------------------------------------------------------------