├── internal ├── customvet │ ├── .gitignore │ ├── go.mod │ ├── main.go │ ├── checks │ │ └── setval │ │ │ ├── setval_test.go │ │ │ ├── testdata │ │ │ └── src │ │ │ │ └── a │ │ │ │ └── a.go │ │ │ └── setval.go │ └── go.sum ├── util │ ├── type.go │ ├── safe.go │ ├── unsafe.go │ └── strconv.go ├── pool │ ├── conn_check_dummy.go │ ├── export_test.go │ ├── conn_check.go │ ├── pool_single.go │ ├── conn_check_test.go │ ├── bench_test.go │ ├── main_test.go │ ├── conn.go │ └── pool_sticky.go ├── proto │ ├── proto_test.go │ ├── scan_test.go │ ├── reader_test.go │ ├── writer.go │ ├── scan.go │ └── writer_test.go ├── internal_test.go ├── internal.go ├── log.go ├── util.go ├── rand │ └── rand.go ├── arg.go ├── hashtag │ ├── hashtag_test.go │ └── hashtag.go ├── once.go └── hscan │ └── structmap.go ├── .github ├── CODEOWNERS ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ └── bug_report.md ├── dependabot.yml ├── workflows │ ├── spellcheck.yml │ ├── golangci-lint.yml │ ├── release-drafter.yml │ ├── stale-issues.yml │ ├── build.yml │ └── doctests.yaml ├── spellcheck-settings.yml ├── wordlist.txt └── release-drafter-config.yml ├── .gitignore ├── .golangci.yml ├── doc.go ├── .prettierrc.yml ├── example ├── otel │ ├── image │ │ ├── metrics.png │ │ └── redis-trace.png │ ├── config │ │ ├── vector.toml │ │ └── otel-collector.yaml │ ├── README.md │ ├── docker-compose.yml │ ├── go.mod │ └── client.go ├── lua-scripting │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── main.go ├── hll │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── main.go ├── scan-struct │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── main.go ├── del-keys-without-ttl │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── main.go └── redis-bloom │ ├── go.mod │ ├── README.md │ ├── go.sum │ └── main.go ├── version.go ├── extra ├── rediscmd │ ├── safe.go │ ├── go.mod │ ├── unsafe.go │ ├── go.sum │ ├── rediscmd_test.go │ └── rediscmd.go ├── rediscensus │ ├── go.mod │ └── rediscensus.go ├── redisotel │ ├── go.mod │ ├── README.md │ ├── redisotel_test.go │ ├── go.sum │ └── config.go └── redisprometheus │ ├── go.mod │ ├── README.md │ ├── go.sum │ └── collector.go ├── package.json ├── go.mod ├── scripts ├── bump_deps.sh ├── tag.sh └── release.sh ├── RELEASING.md ├── go.sum ├── doctests ├── README.md ├── lpush_lrange_test.go └── set_get_test.go ├── acl_commands.go ├── universal_test.go ├── fuzz └── fuzz.go ├── hyperloglog_commands.go ├── Makefile ├── LICENSE ├── iterator.go ├── pubsub_commands.go ├── command_test.go ├── example_instrumentation_test.go ├── script.go ├── export_test.go ├── osscluster_commands.go ├── pipeline_test.go ├── pipeline.go ├── pool_test.go ├── iterator_test.go ├── error.go ├── CONTRIBUTING.md ├── tx_test.go ├── bitmap_commands.go ├── gears_commands_test.go ├── tx.go ├── gears_commands.go ├── geo_commands.go └── result.go /internal/customvet/.gitignore: -------------------------------------------------------------------------------- 1 | /customvet 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | doctests/* @dmaier-redislabs 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ['https://uptrace.dev/sponsor'] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.rdb 2 | testdata/* 3 | .idea/ 4 | .DS_Store 5 | *.tar.gz 6 | *.dic -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | concurrency: 8 3 | deadline: 5m 4 | tests: false 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /example/otel/image/metrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdsol/go-redis/master/example/otel/image/metrics.png -------------------------------------------------------------------------------- /example/otel/image/redis-trace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdsol/go-redis/master/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.3.0" 6 | } 7 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redis", 3 | "version": "9.3.0", 4 | "main": "index.js", 5 | "repository": "git@github.com:redis/go-redis.git", 6 | "author": "Vladimir Mihailenco ", 7 | "license": "BSD-2-clause" 8 | } 9 | -------------------------------------------------------------------------------- /internal/customvet/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/internal/customvet 2 | 3 | go 1.17 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/pool/conn_check_dummy.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !darwin && !dragonfly && !freebsd && !netbsd && !openbsd && !solaris && !illumos 2 | 3 | package pool 4 | 5 | import "net" 6 | 7 | func connCheck(conn net.Conn) error { 8 | return nil 9 | } 10 | -------------------------------------------------------------------------------- /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.netConn 14 | } 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/v9 2 | 3 | go 1.18 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.2.0 9 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f 10 | ) 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /extra/rediscmd/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/extra/rediscmd/v9 2 | 3 | go 1.15 4 | 5 | replace github.com/redis/go-redis/v9 => ../.. 6 | 7 | require ( 8 | github.com/bsm/ginkgo/v2 v2.7.0 9 | github.com/bsm/gomega v1.26.0 10 | github.com/redis/go-redis/v9 v9.3.0 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 | -------------------------------------------------------------------------------- /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/hll/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/example/hll 2 | 3 | go 1.18 4 | 5 | replace github.com/redis/go-redis/v9 => ../.. 6 | 7 | require github.com/redis/go-redis/v9 v9.3.0 8 | 9 | require ( 10 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 11 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /example/lua-scripting/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/example/redis-bloom 2 | 3 | go 1.18 4 | 5 | replace github.com/redis/go-redis/v9 => ../.. 6 | 7 | require github.com/redis/go-redis/v9 v9.3.0 8 | 9 | require ( 10 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 11 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /example/redis-bloom/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/example/redis-bloom 2 | 3 | go 1.18 4 | 5 | replace github.com/redis/go-redis/v9 => ../.. 6 | 7 | require github.com/redis/go-redis/v9 v9.3.0 8 | 9 | require ( 10 | github.com/cespare/xxhash/v2 v2.2.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 | -------------------------------------------------------------------------------- /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/scan-struct/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/example/scan-struct 2 | 3 | go 1.18 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.3.0 10 | ) 11 | 12 | require ( 13 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 14 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /.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@v4 10 | - name: Check Spelling 11 | uses: rojopolis/spellcheck-github-actions@0.34.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 | -------------------------------------------------------------------------------- /extra/rediscensus/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/extra/rediscensus/v9 2 | 3 | go 1.15 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/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 11 | github.com/redis/go-redis/extra/rediscmd/v9 v9.3.0 12 | github.com/redis/go-redis/v9 v9.3.0 13 | go.opencensus.io v0.24.0 14 | ) 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 *(*string)(unsafe.Pointer(&b)) 12 | } 13 | 14 | // StringToBytes converts string to byte slice. 15 | func StringToBytes(s string) []byte { 16 | return *(*[]byte)(unsafe.Pointer( 17 | &struct { 18 | string 19 | Cap int 20 | }{s, len(s)}, 21 | )) 22 | } 23 | -------------------------------------------------------------------------------- /example/del-keys-without-ttl/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/example/del-keys-without-ttl 2 | 3 | go 1.18 4 | 5 | replace github.com/redis/go-redis/v9 => ../.. 6 | 7 | require ( 8 | github.com/redis/go-redis/v9 v9.3.0 9 | go.uber.org/zap v1.24.0 10 | ) 11 | 12 | require ( 13 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 14 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 15 | go.uber.org/atomic v1.10.0 // indirect 16 | go.uber.org/multierr v1.9.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 | -------------------------------------------------------------------------------- /example/hll/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= 2 | github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= 3 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 4 | github.com/cespare/xxhash/v2 v2.2.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 | -------------------------------------------------------------------------------- /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/redis-bloom/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= 2 | github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= 3 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 4 | github.com/cespare/xxhash/v2 v2.2.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 | -------------------------------------------------------------------------------- /example/lua-scripting/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= 2 | github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= 3 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 4 | github.com/cespare/xxhash/v2 v2.2.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 | -------------------------------------------------------------------------------- /.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 | pull_request: 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | golangci: 18 | permissions: 19 | contents: read # for actions/checkout to fetch code 20 | pull-requests: read # for golangci/golangci-lint-action to fetch pull requests 21 | name: lint 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | - name: golangci-lint 26 | uses: golangci/golangci-lint-action@v3 27 | -------------------------------------------------------------------------------- /internal/log.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | ) 9 | 10 | type Logging interface { 11 | Printf(ctx context.Context, format string, v ...interface{}) 12 | } 13 | 14 | type logger struct { 15 | log *log.Logger 16 | } 17 | 18 | func (l *logger) Printf(ctx context.Context, format string, v ...interface{}) { 19 | _ = l.log.Output(2, fmt.Sprintf(format, v...)) 20 | } 21 | 22 | // Logger calls Output to print to the stderr. 23 | // Arguments are handled in the manner of fmt.Print. 24 | var Logger Logging = &logger{ 25 | log: log.New(os.Stderr, "redis: ", log.LstdFlags|log.Lshortfile), 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 | -------------------------------------------------------------------------------- /.github/wordlist.txt: -------------------------------------------------------------------------------- 1 | ACLs 2 | autoload 3 | autoloader 4 | autoloading 5 | Autoloading 6 | backend 7 | backends 8 | behaviour 9 | CAS 10 | ClickHouse 11 | config 12 | customizable 13 | Customizable 14 | dataset 15 | de 16 | ElastiCache 17 | extensibility 18 | FPM 19 | Golang 20 | IANA 21 | keyspace 22 | keyspaces 23 | Kvrocks 24 | localhost 25 | Lua 26 | MSSQL 27 | namespace 28 | NoSQL 29 | ORM 30 | Packagist 31 | PhpRedis 32 | pipelining 33 | pluggable 34 | Predis 35 | PSR 36 | Quickstart 37 | README 38 | rebalanced 39 | rebalancing 40 | redis 41 | Redis 42 | RocksDB 43 | runtime 44 | SHA 45 | sharding 46 | SSL 47 | struct 48 | stunnel 49 | TCP 50 | TLS 51 | uri 52 | URI 53 | url 54 | variadic 55 | RedisStack 56 | RedisGears 57 | RedisTimeseries -------------------------------------------------------------------------------- /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.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 6 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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 | -------------------------------------------------------------------------------- /extra/rediscmd/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= 2 | github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w= 3 | github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= 4 | github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 5 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 6 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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 | -------------------------------------------------------------------------------- /example/scan-struct/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= 2 | github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= 3 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 4 | github.com/cespare/xxhash/v2 v2.2.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /extra/redisotel/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/extra/redisotel/v9 2 | 3 | go 1.19 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.3.0 11 | github.com/redis/go-redis/v9 v9.3.0 12 | go.opentelemetry.io/otel v1.16.0 13 | go.opentelemetry.io/otel/metric v1.16.0 14 | go.opentelemetry.io/otel/sdk v1.16.0 15 | go.opentelemetry.io/otel/trace v1.16.0 16 | ) 17 | 18 | require ( 19 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 20 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 21 | github.com/go-logr/logr v1.2.4 // indirect 22 | github.com/go-logr/stdr v1.2.2 // indirect 23 | golang.org/x/sys v0.8.0 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /internal/util.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/redis/go-redis/v9/internal/util" 8 | ) 9 | 10 | func Sleep(ctx context.Context, dur time.Duration) error { 11 | t := time.NewTimer(dur) 12 | defer t.Stop() 13 | 14 | select { 15 | case <-t.C: 16 | return nil 17 | case <-ctx.Done(): 18 | return ctx.Err() 19 | } 20 | } 21 | 22 | func ToLower(s string) string { 23 | if isLower(s) { 24 | return s 25 | } 26 | 27 | b := make([]byte, len(s)) 28 | for i := range b { 29 | c := s[i] 30 | if c >= 'A' && c <= 'Z' { 31 | c += 'a' - 'A' 32 | } 33 | b[i] = c 34 | } 35 | return util.BytesToString(b) 36 | } 37 | 38 | func isLower(s string) bool { 39 | for i := 0; i < len(s); i++ { 40 | c := s[i] 41 | if c >= 'A' && c <= 'Z' { 42 | return false 43 | } 44 | } 45 | return true 46 | } 47 | -------------------------------------------------------------------------------- /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/redisprometheus/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/extra/redisprometheus/v9 2 | 3 | go 1.17 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.3.0 10 | ) 11 | 12 | require ( 13 | github.com/beorn7/perks v1.0.1 // indirect 14 | github.com/cespare/xxhash/v2 v2.2.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 | golang.org/x/sys v0.4.0 // indirect 22 | google.golang.org/protobuf v1.28.1 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /scripts/tag.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | help() { 6 | cat <<- EOF 7 | Usage: TAG=tag $0 8 | 9 | Creates git tags for public Go packages. 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 env var is required\n\n"; 20 | help 21 | fi 22 | 23 | if ! grep -Fq "\"${TAG#v}\"" version.go 24 | then 25 | printf "version.go does not contain ${TAG#v}\n" 26 | exit 1 27 | fi 28 | 29 | PACKAGE_DIRS=$(find . -mindepth 2 -type f -name 'go.mod' -exec dirname {} \; \ 30 | | grep -E -v "example|internal" \ 31 | | sed 's/^\.\///' \ 32 | | sort) 33 | 34 | git tag ${TAG} 35 | git push origin ${TAG} 36 | 37 | for dir in $PACKAGE_DIRS 38 | do 39 | printf "tagging ${dir}/${TAG}\n" 40 | git tag ${dir}/${TAG} 41 | git push origin ${dir}/${TAG} 42 | done 43 | -------------------------------------------------------------------------------- /.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@v5 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 | -------------------------------------------------------------------------------- /.github/workflows/stale-issues.yml: -------------------------------------------------------------------------------- 1 | name: "Close stale issues" 2 | on: 3 | schedule: 4 | - cron: "0 0 * * *" 5 | 6 | permissions: {} 7 | jobs: 8 | stale: 9 | permissions: 10 | issues: write # to close stale issues (actions/stale) 11 | pull-requests: write # to close stale PRs (actions/stale) 12 | 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/stale@v8 16 | with: 17 | repo-token: ${{ secrets.GITHUB_TOKEN }} 18 | stale-issue-message: 'This issue is marked stale. It will be closed in 30 days if it is not updated.' 19 | stale-pr-message: 'This pull request is marked stale. It will be closed in 30 days if it is not updated.' 20 | days-before-stale: 365 21 | days-before-close: 30 22 | stale-issue-label: "Stale" 23 | stale-pr-label: "Stale" 24 | operations-per-run: 10 25 | remove-stale-when-updated: true 26 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [master, v9] 6 | pull_request: 7 | branches: [master, v9] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | name: build 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | go-version: [1.19.x, 1.20.x, 1.21.x] 20 | 21 | services: 22 | redis: 23 | image: redis/redis-stack-server:edge 24 | options: >- 25 | --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 26 | ports: 27 | - 6379:6379 28 | 29 | steps: 30 | - name: Set up ${{ matrix.go-version }} 31 | uses: actions/setup-go@v4 32 | with: 33 | go-version: ${{ matrix.go-version }} 34 | 35 | - name: Checkout code 36 | uses: actions/checkout@v4 37 | 38 | - name: Test 39 | run: make test 40 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 9 | "github.com/redis/go-redis/v9" 10 | ) 11 | 12 | func ExampleClient_LPush_and_lrange() { 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 | errFlush := rdb.FlushDB(ctx).Err() // Clear the database before each test 25 | if errFlush != nil { 26 | panic(errFlush) 27 | } 28 | // REMOVE_END 29 | 30 | listSize, err := rdb.LPush(ctx, "my_bikes", "bike:1", "bike:2").Result() 31 | if err != nil { 32 | panic(err) 33 | } 34 | 35 | fmt.Println(listSize) 36 | 37 | value, err := rdb.LRange(ctx, "my_bikes", 0, -1).Result() 38 | if err != nil { 39 | panic(err) 40 | } 41 | fmt.Println(value) 42 | // HIDE_START 43 | 44 | // Output: 2 45 | // [bike:2 bike:1] 46 | } 47 | 48 | // HIDE_END 49 | -------------------------------------------------------------------------------- /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 | errFlush := rdb.FlushDB(ctx).Err() // Clear the database before each test 25 | if errFlush != nil { 26 | panic(errFlush) 27 | } 28 | // REMOVE_END 29 | 30 | err := rdb.Set(ctx, "bike:1", "Process 134", 0).Err() 31 | if err != nil { 32 | panic(err) 33 | } 34 | 35 | fmt.Println("OK") 36 | 37 | value, err := rdb.Get(ctx, "bike:1").Result() 38 | if err != nil { 39 | panic(err) 40 | } 41 | fmt.Printf("The name of the bike is %s", value) 42 | // HIDE_START 43 | 44 | // Output: OK 45 | // The name of the bike is Process 134 46 | } 47 | 48 | // HIDE_END 49 | -------------------------------------------------------------------------------- /.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: redis/redis-stack-server:latest 20 | options: >- 21 | --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 22 | ports: 23 | - 6379:6379 24 | 25 | strategy: 26 | fail-fast: false 27 | matrix: 28 | go-version: [ "1.18", "1.19", "1.20", "1.21" ] 29 | 30 | steps: 31 | - name: Set up ${{ matrix.go-version }} 32 | uses: actions/setup-go@v4 33 | with: 34 | go-version: ${{ matrix.go-version }} 35 | 36 | - name: Checkout code 37 | uses: actions/checkout@v4 38 | 39 | - name: Test doc examples 40 | working-directory: ./doctests 41 | run: go test 42 | -------------------------------------------------------------------------------- /acl_commands.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import "context" 4 | 5 | type ACLCmdable interface { 6 | ACLDryRun(ctx context.Context, username string, command ...interface{}) *StringCmd 7 | ACLLog(ctx context.Context, count int64) *ACLLogCmd 8 | ACLLogReset(ctx context.Context) *StatusCmd 9 | } 10 | 11 | func (c cmdable) ACLDryRun(ctx context.Context, username string, command ...interface{}) *StringCmd { 12 | args := make([]interface{}, 0, 3+len(command)) 13 | args = append(args, "acl", "dryrun", username) 14 | args = append(args, command...) 15 | cmd := NewStringCmd(ctx, args...) 16 | _ = c(ctx, cmd) 17 | return cmd 18 | } 19 | 20 | func (c cmdable) ACLLog(ctx context.Context, count int64) *ACLLogCmd { 21 | args := make([]interface{}, 0, 3) 22 | args = append(args, "acl", "log") 23 | if count > 0 { 24 | args = append(args, count) 25 | } 26 | cmd := NewACLLogCmd(ctx, args...) 27 | _ = c(ctx, cmd) 28 | return cmd 29 | } 30 | 31 | func (c cmdable) ACLLogReset(ctx context.Context) *StatusCmd { 32 | cmd := NewStatusCmd(ctx, "acl", "log", "reset") 33 | _ = c(ctx, cmd) 34 | return cmd 35 | } 36 | -------------------------------------------------------------------------------- /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 | func connCheck(conn net.Conn) error { 16 | // Reset previous timeout. 17 | _ = conn.SetDeadline(time.Time{}) 18 | 19 | sysConn, ok := conn.(syscall.Conn) 20 | if !ok { 21 | return nil 22 | } 23 | rawConn, err := sysConn.SyscallConn() 24 | if err != nil { 25 | return err 26 | } 27 | 28 | var sysErr error 29 | 30 | if err := rawConn.Read(func(fd uintptr) bool { 31 | var buf [1]byte 32 | n, err := syscall.Read(int(fd), buf[:]) 33 | switch { 34 | case n == 0 && err == nil: 35 | sysErr = io.EOF 36 | case n > 0: 37 | sysErr = errUnexpectedRead 38 | case err == syscall.EAGAIN || err == syscall.EWOULDBLOCK: 39 | sysErr = nil 40 | default: 41 | sysErr = err 42 | } 43 | return true 44 | }); err != nil { 45 | return err 46 | } 47 | 48 | return sysErr 49 | } 50 | -------------------------------------------------------------------------------- /universal_test.go: -------------------------------------------------------------------------------- 1 | package redis_test 2 | 3 | import ( 4 | . "github.com/bsm/ginkgo/v2" 5 | . "github.com/bsm/gomega" 6 | 7 | "github.com/redis/go-redis/v9" 8 | ) 9 | 10 | var _ = Describe("UniversalClient", func() { 11 | var client redis.UniversalClient 12 | 13 | AfterEach(func() { 14 | if client != nil { 15 | Expect(client.Close()).To(Succeed()) 16 | } 17 | }) 18 | 19 | It("should connect to failover servers", func() { 20 | Skip("Flaky Test") 21 | client = redis.NewUniversalClient(&redis.UniversalOptions{ 22 | MasterName: sentinelName, 23 | Addrs: sentinelAddrs, 24 | }) 25 | Expect(client.Ping(ctx).Err()).NotTo(HaveOccurred()) 26 | }) 27 | 28 | It("should connect to simple servers", func() { 29 | client = redis.NewUniversalClient(&redis.UniversalOptions{ 30 | Addrs: []string{redisAddr}, 31 | }) 32 | Expect(client.Ping(ctx).Err()).NotTo(HaveOccurred()) 33 | }) 34 | 35 | It("should connect to clusters", func() { 36 | client = redis.NewUniversalClient(&redis.UniversalOptions{ 37 | Addrs: cluster.addrs(), 38 | }) 39 | Expect(client.Ping(ctx).Err()).NotTo(HaveOccurred()) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /.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 | template: | 40 | # Changes 41 | 42 | $CHANGES 43 | 44 | ## Contributors 45 | We'd like to thank all the contributors who worked on this release! 46 | 47 | $CONTRIBUTORS 48 | 49 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | Int int `redis:"int"` 15 | Bool bool `redis:"bool"` 16 | Ignored struct{} `redis:"-"` 17 | } 18 | 19 | func main() { 20 | ctx := context.Background() 21 | 22 | rdb := redis.NewClient(&redis.Options{ 23 | Addr: ":6379", 24 | }) 25 | 26 | // Set some fields. 27 | if _, err := rdb.Pipelined(ctx, func(rdb redis.Pipeliner) error { 28 | rdb.HSet(ctx, "key", "str1", "hello") 29 | rdb.HSet(ctx, "key", "str2", "world") 30 | rdb.HSet(ctx, "key", "int", 123) 31 | rdb.HSet(ctx, "key", "bool", 1) 32 | return nil 33 | }); err != nil { 34 | panic(err) 35 | } 36 | 37 | var model1, model2 Model 38 | 39 | // Scan all fields into the model. 40 | if err := rdb.HGetAll(ctx, "key").Scan(&model1); err != nil { 41 | panic(err) 42 | } 43 | 44 | // Or scan a subset of the fields. 45 | if err := rdb.HMGet(ctx, "key", "str1", "int").Scan(&model2); err != nil { 46 | panic(err) 47 | } 48 | 49 | spew.Dump(model1) 50 | spew.Dump(model2) 51 | } 52 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO_MOD_DIRS := $(shell find . -type f -name 'go.mod' -exec dirname {} \; | sort) 2 | 3 | test: testdeps 4 | set -e; for dir in $(GO_MOD_DIRS); do \ 5 | echo "go test in $${dir}"; \ 6 | (cd "$${dir}" && \ 7 | go mod tidy -compat=1.18 && \ 8 | go test && \ 9 | go test ./... -short -race && \ 10 | go test ./... -run=NONE -bench=. -benchmem && \ 11 | env GOOS=linux GOARCH=386 go test && \ 12 | go vet); \ 13 | done 14 | cd internal/customvet && go build . 15 | go vet -vettool ./internal/customvet/customvet 16 | 17 | testdeps: testdata/redis/src/redis-server 18 | 19 | bench: testdeps 20 | go test ./... -test.run=NONE -test.bench=. -test.benchmem 21 | 22 | .PHONY: all test testdeps bench fmt 23 | 24 | build: 25 | go build . 26 | 27 | testdata/redis: 28 | mkdir -p $@ 29 | wget -qO- https://download.redis.io/releases/redis-7.2.1.tar.gz | tar xvz --strip-components=1 -C $@ 30 | 31 | testdata/redis/src/redis-server: testdata/redis 32 | cd $< && make all 33 | 34 | fmt: 35 | gofumpt -w ./ 36 | goimports -w -local github.com/redis/go-redis ./ 37 | 38 | go_mod_tidy: 39 | set -e; for dir in $(GO_MOD_DIRS); do \ 40 | echo "go mod tidy in $${dir}"; \ 41 | (cd "$${dir}" && \ 42 | go get -u ./... && \ 43 | go mod tidy -compat=1.18); \ 44 | done 45 | -------------------------------------------------------------------------------- /internal/pool/pool_single.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import "context" 4 | 5 | type SingleConnPool struct { 6 | pool Pooler 7 | cn *Conn 8 | stickyErr error 9 | } 10 | 11 | var _ Pooler = (*SingleConnPool)(nil) 12 | 13 | func NewSingleConnPool(pool Pooler, cn *Conn) *SingleConnPool { 14 | return &SingleConnPool{ 15 | pool: pool, 16 | cn: cn, 17 | } 18 | } 19 | 20 | func (p *SingleConnPool) NewConn(ctx context.Context) (*Conn, error) { 21 | return p.pool.NewConn(ctx) 22 | } 23 | 24 | func (p *SingleConnPool) CloseConn(cn *Conn) error { 25 | return p.pool.CloseConn(cn) 26 | } 27 | 28 | func (p *SingleConnPool) Get(ctx context.Context) (*Conn, error) { 29 | if p.stickyErr != nil { 30 | return nil, p.stickyErr 31 | } 32 | return p.cn, nil 33 | } 34 | 35 | func (p *SingleConnPool) Put(ctx context.Context, cn *Conn) {} 36 | 37 | func (p *SingleConnPool) Remove(ctx context.Context, cn *Conn, reason error) { 38 | p.cn = nil 39 | p.stickyErr = reason 40 | } 41 | 42 | func (p *SingleConnPool) Close() error { 43 | p.cn = nil 44 | p.stickyErr = ErrClosed 45 | return nil 46 | } 47 | 48 | func (p *SingleConnPool) Len() int { 49 | return 0 50 | } 51 | 52 | func (p *SingleConnPool) IdleLen() int { 53 | return 0 54 | } 55 | 56 | func (p *SingleConnPool) Stats() *Stats { 57 | return &Stats{} 58 | } 59 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | jaeger: 27 | protocols: 28 | grpc: 29 | 30 | processors: 31 | resourcedetection: 32 | detectors: ['system'] 33 | batch: 34 | send_batch_size: 10000 35 | timeout: 10s 36 | 37 | exporters: 38 | logging: 39 | logLevel: debug 40 | otlp: 41 | endpoint: uptrace:14317 42 | tls: 43 | insecure: true 44 | headers: { 'uptrace-dsn': 'http://project2_secret_token@localhost:14317/2' } 45 | 46 | service: 47 | # telemetry: 48 | # logs: 49 | # level: DEBUG 50 | pipelines: 51 | traces: 52 | receivers: [otlp, jaeger] 53 | processors: [batch] 54 | exporters: [otlp, logging] 55 | metrics: 56 | receivers: [otlp] 57 | processors: [batch] 58 | exporters: [otlp] 59 | metrics/hostmetrics: 60 | receivers: [hostmetrics, redis] 61 | processors: [batch, resourcedetection] 62 | exporters: [otlp] 63 | logs: 64 | receivers: [otlp] 65 | processors: [batch] 66 | exporters: [otlp] 67 | 68 | extensions: [health_check, pprof, zpages] 69 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /example/del-keys-without-ttl/go.sum: -------------------------------------------------------------------------------- 1 | github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= 2 | github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= 3 | github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= 4 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 5 | github.com/cespare/xxhash/v2 v2.2.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.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= 13 | go.uber.org/atomic v1.10.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 | -------------------------------------------------------------------------------- /extra/redisotel/redisotel_test.go: -------------------------------------------------------------------------------- 1 | package redisotel 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | semconv "go.opentelemetry.io/otel/semconv/v1.7.0" 8 | 9 | "go.opentelemetry.io/otel" 10 | sdktrace "go.opentelemetry.io/otel/sdk/trace" 11 | "go.opentelemetry.io/otel/trace" 12 | 13 | "github.com/redis/go-redis/v9" 14 | ) 15 | 16 | type providerFunc func(name string, opts ...trace.TracerOption) trace.Tracer 17 | 18 | func (fn providerFunc) Tracer(name string, opts ...trace.TracerOption) trace.Tracer { 19 | return fn(name, opts...) 20 | } 21 | 22 | func TestNewWithTracerProvider(t *testing.T) { 23 | invoked := false 24 | 25 | tp := providerFunc(func(name string, opts ...trace.TracerOption) trace.Tracer { 26 | invoked = true 27 | return otel.GetTracerProvider().Tracer(name, opts...) 28 | }) 29 | 30 | _ = newTracingHook("", WithTracerProvider(tp)) 31 | 32 | if !invoked { 33 | t.Fatalf("did not call custom TraceProvider") 34 | } 35 | } 36 | 37 | func TestWithDBStatement(t *testing.T) { 38 | provider := sdktrace.NewTracerProvider() 39 | hook := newTracingHook( 40 | "", 41 | WithTracerProvider(provider), 42 | WithDBStatement(false), 43 | ) 44 | ctx, span := provider.Tracer("redis-test").Start(context.TODO(), "redis-test") 45 | cmd := redis.NewCmd(ctx, "ping") 46 | defer span.End() 47 | 48 | processHook := hook.ProcessHook(func(ctx context.Context, cmd redis.Cmder) error { 49 | attrs := trace.SpanFromContext(ctx).(sdktrace.ReadOnlySpan).Attributes() 50 | for _, attr := range attrs { 51 | if attr.Key == semconv.DBStatementKey { 52 | t.Fatal("Attribute with db statement should not exist") 53 | } 54 | } 55 | return nil 56 | }) 57 | err := processHook(ctx, cmd) 58 | if err != nil { 59 | t.Fatal(err) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.18) 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.18) 59 | (cd ./${dir} && go mod tidy -compat=1.18) 60 | done 61 | 62 | sed --in-place "s/\(return \)\"[^\"]*\"/\1\"${TAG#v}\"/" ./version.go 63 | sed --in-place "s/\(\"version\": \)\"[^\"]*\"/\1\"${TAG#v}\"/" ./package.json 64 | 65 | conventional-changelog -p angular -i CHANGELOG.md -s 66 | 67 | git checkout -b release/${TAG} master 68 | git add -u 69 | git commit -m "chore: release $TAG (release.sh)" 70 | git push origin release/${TAG} 71 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /example/otel/README.md: -------------------------------------------------------------------------------- 1 | # Example for go-redis OpenTelemetry instrumentation 2 | 3 | This example demonstrates how to monitor Redis using OpenTelemetry and 4 | [Uptrace](https://github.com/uptrace/uptrace). It requires Docker to start Redis Server and Uptrace. 5 | 6 | See 7 | [Monitoring Go Redis Performance and Errors](https://redis.uptrace.dev/guide/go-redis-monitoring.html) 8 | for details. 9 | 10 | **Step 1**. Download the example using Git: 11 | 12 | ```shell 13 | git clone https://github.com/redis/go-redis.git 14 | cd example/otel 15 | ``` 16 | 17 | **Step 2**. Start the services using Docker: 18 | 19 | ```shell 20 | docker-compose up -d 21 | ``` 22 | 23 | **Step 3**. Make sure Uptrace is running: 24 | 25 | ```shell 26 | docker-compose logs uptrace 27 | ``` 28 | 29 | **Step 4**. Run the Redis client example and Follow the link to view the trace: 30 | 31 | ```shell 32 | go run client.go 33 | trace: http://localhost:14318/traces/ee029d8782242c8ed38b16d961093b35 34 | ``` 35 | 36 | ![Redis trace](./image/redis-trace.png) 37 | 38 | You can also open Uptrace UI at [http://localhost:14318](http://localhost:14318) to view available 39 | spans, logs, and metrics. 40 | 41 | ## Redis monitoring 42 | 43 | You can also [monitor Redis performance](https://uptrace.dev/opentelemetry/redis-monitoring.html) 44 | metrics By installing OpenTelemetry Collector. 45 | 46 | [OpenTelemetry Collector](https://uptrace.dev/opentelemetry/collector.html) is an agent that pulls 47 | telemetry data from systems you want to monitor and sends it to APM tools using the OpenTelemetry 48 | protocol (OTLP). 49 | 50 | When telemetry data reaches Uptrace, it automatically generates a Redis dashboard from a pre-defined 51 | template. 52 | 53 | ![Redis dashboard](./image/metrics.png) 54 | 55 | ## Links 56 | 57 | - [Uptrace open-source APM](https://uptrace.dev/get/open-source-apm.html) 58 | - [OpenTelemetry Go instrumentations](https://uptrace.dev/opentelemetry/instrumentations/?lang=go) 59 | - [OpenTelemetry Go Tracing API](https://uptrace.dev/opentelemetry/go-tracing.html) 60 | -------------------------------------------------------------------------------- /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/pool/bench_test.go: -------------------------------------------------------------------------------- 1 | package pool_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | "time" 8 | 9 | "github.com/redis/go-redis/v9/internal/pool" 10 | ) 11 | 12 | type poolGetPutBenchmark struct { 13 | poolSize int 14 | } 15 | 16 | func (bm poolGetPutBenchmark) String() string { 17 | return fmt.Sprintf("pool=%d", bm.poolSize) 18 | } 19 | 20 | func BenchmarkPoolGetPut(b *testing.B) { 21 | ctx := context.Background() 22 | benchmarks := []poolGetPutBenchmark{ 23 | {1}, 24 | {2}, 25 | {8}, 26 | {32}, 27 | {64}, 28 | {128}, 29 | } 30 | for _, bm := range benchmarks { 31 | b.Run(bm.String(), func(b *testing.B) { 32 | connPool := pool.NewConnPool(&pool.Options{ 33 | Dialer: dummyDialer, 34 | PoolSize: bm.poolSize, 35 | PoolTimeout: time.Second, 36 | ConnMaxIdleTime: time.Hour, 37 | }) 38 | 39 | b.ResetTimer() 40 | 41 | b.RunParallel(func(pb *testing.PB) { 42 | for pb.Next() { 43 | cn, err := connPool.Get(ctx) 44 | if err != nil { 45 | b.Fatal(err) 46 | } 47 | connPool.Put(ctx, cn) 48 | } 49 | }) 50 | }) 51 | } 52 | } 53 | 54 | type poolGetRemoveBenchmark struct { 55 | poolSize int 56 | } 57 | 58 | func (bm poolGetRemoveBenchmark) String() string { 59 | return fmt.Sprintf("pool=%d", bm.poolSize) 60 | } 61 | 62 | func BenchmarkPoolGetRemove(b *testing.B) { 63 | ctx := context.Background() 64 | benchmarks := []poolGetRemoveBenchmark{ 65 | {1}, 66 | {2}, 67 | {8}, 68 | {32}, 69 | {64}, 70 | {128}, 71 | } 72 | 73 | for _, bm := range benchmarks { 74 | b.Run(bm.String(), func(b *testing.B) { 75 | connPool := pool.NewConnPool(&pool.Options{ 76 | Dialer: dummyDialer, 77 | PoolSize: bm.poolSize, 78 | PoolTimeout: time.Second, 79 | ConnMaxIdleTime: time.Hour, 80 | }) 81 | 82 | b.ResetTimer() 83 | 84 | b.RunParallel(func(pb *testing.PB) { 85 | for pb.Next() { 86 | cn, err := connPool.Get(ctx) 87 | if err != nil { 88 | b.Fatal(err) 89 | } 90 | connPool.Remove(ctx, cn, nil) 91 | } 92 | }) 93 | }) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /example/otel/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | clickhouse: 5 | image: clickhouse/clickhouse-server:22.10 6 | restart: on-failure 7 | environment: 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_data2:/var/lib/clickhouse 16 | ports: 17 | - '8123:8123' 18 | - '9000:9000' 19 | 20 | postgres: 21 | image: postgres:15-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_data2:/var/lib/postgresql/data/pgdata' 35 | ports: 36 | - '5432:5432' 37 | 38 | uptrace: 39 | image: 'uptrace/uptrace:1.5.0' 40 | #image: 'uptrace/uptrace-dev:latest' 41 | restart: on-failure 42 | volumes: 43 | - ./uptrace.yml:/etc/uptrace/uptrace.yml 44 | #environment: 45 | # - DEBUG=2 46 | ports: 47 | - '14317:14317' 48 | - '14318:14318' 49 | depends_on: 50 | clickhouse: 51 | condition: service_healthy 52 | 53 | otelcol: 54 | image: otel/opentelemetry-collector-contrib:0.70.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 | mailhog: 68 | image: mailhog/mailhog:v1.0.1 69 | restart: on-failure 70 | ports: 71 | - '8025:8025' 72 | 73 | redis-server: 74 | image: redis 75 | ports: 76 | - '6379:6379' 77 | redis-cli: 78 | image: redis 79 | 80 | volumes: 81 | ch_data2: 82 | pg_data2: 83 | -------------------------------------------------------------------------------- /example/otel/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/redis/go-redis/example/otel 2 | 3 | go 1.19 4 | 5 | replace github.com/redis/go-redis/v9 => ../.. 6 | 7 | replace github.com/redis/go-redis/extra/redisotel/v9 => ../../extra/redisotel 8 | 9 | replace github.com/redis/go-redis/extra/rediscmd/v9 => ../../extra/rediscmd 10 | 11 | require ( 12 | github.com/redis/go-redis/extra/redisotel/v9 v9.3.0 13 | github.com/redis/go-redis/v9 v9.3.0 14 | github.com/uptrace/uptrace-go v1.16.0 15 | go.opentelemetry.io/otel v1.16.0 16 | ) 17 | 18 | require ( 19 | github.com/cenkalti/backoff/v4 v4.2.1 // indirect 20 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 21 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 22 | github.com/go-logr/logr v1.2.4 // indirect 23 | github.com/go-logr/stdr v1.2.2 // indirect 24 | github.com/golang/protobuf v1.5.3 // indirect 25 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2 // indirect 26 | github.com/redis/go-redis/extra/rediscmd/v9 v9.3.0 // indirect 27 | go.opentelemetry.io/contrib/instrumentation/runtime v0.42.0 // indirect 28 | go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 // indirect 29 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.39.0 // indirect 30 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.39.0 // indirect 31 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 // indirect 32 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0 // indirect 33 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.16.0 // indirect 34 | go.opentelemetry.io/otel/metric v1.16.0 // indirect 35 | go.opentelemetry.io/otel/sdk v1.16.0 // indirect 36 | go.opentelemetry.io/otel/sdk/metric v0.39.0 // indirect 37 | go.opentelemetry.io/otel/trace v1.16.0 // indirect 38 | go.opentelemetry.io/proto/otlp v0.19.0 // indirect 39 | golang.org/x/net v0.17.0 // indirect 40 | golang.org/x/sys v0.13.0 // indirect 41 | golang.org/x/text v0.13.0 // indirect 42 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect 43 | google.golang.org/grpc v1.56.3 // indirect 44 | google.golang.org/protobuf v1.30.0 // indirect 45 | ) 46 | -------------------------------------------------------------------------------- /example/otel/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "sync" 8 | "time" 9 | 10 | "github.com/uptrace/uptrace-go/uptrace" 11 | "go.opentelemetry.io/otel" 12 | "go.opentelemetry.io/otel/codes" 13 | 14 | "github.com/redis/go-redis/extra/redisotel/v9" 15 | "github.com/redis/go-redis/v9" 16 | ) 17 | 18 | var tracer = otel.Tracer("github.com/redis/go-redis/example/otel") 19 | 20 | func main() { 21 | ctx := context.Background() 22 | 23 | uptrace.ConfigureOpentelemetry( 24 | // copy your project DSN here or use UPTRACE_DSN env var 25 | uptrace.WithDSN("http://project2_secret_token@localhost:14317/2"), 26 | 27 | uptrace.WithServiceName("myservice"), 28 | uptrace.WithServiceVersion("v1.0.0"), 29 | ) 30 | defer uptrace.Shutdown(ctx) 31 | 32 | rdb := redis.NewClient(&redis.Options{ 33 | Addr: ":6379", 34 | }) 35 | if err := redisotel.InstrumentTracing(rdb); err != nil { 36 | panic(err) 37 | } 38 | if err := redisotel.InstrumentMetrics(rdb); err != nil { 39 | panic(err) 40 | } 41 | 42 | for i := 0; i < 1e6; i++ { 43 | ctx, rootSpan := tracer.Start(ctx, "handleRequest") 44 | 45 | if err := handleRequest(ctx, rdb); err != nil { 46 | rootSpan.RecordError(err) 47 | rootSpan.SetStatus(codes.Error, err.Error()) 48 | } 49 | 50 | rootSpan.End() 51 | 52 | if i == 0 { 53 | fmt.Printf("view trace: %s\n", uptrace.TraceURL(rootSpan)) 54 | } 55 | 56 | time.Sleep(time.Second) 57 | } 58 | } 59 | 60 | func handleRequest(ctx context.Context, rdb *redis.Client) error { 61 | if err := rdb.Set(ctx, "First value", "value_1", 0).Err(); err != nil { 62 | return err 63 | } 64 | if err := rdb.Set(ctx, "Second value", "value_2", 0).Err(); err != nil { 65 | return err 66 | } 67 | 68 | var group sync.WaitGroup 69 | 70 | for i := 0; i < 20; i++ { 71 | group.Add(1) 72 | go func() { 73 | defer group.Done() 74 | val := rdb.Get(ctx, "Second value").Val() 75 | if val != "value_2" { 76 | log.Printf("%q != %q", val, "value_2") 77 | } 78 | }() 79 | } 80 | 81 | group.Wait() 82 | 83 | if err := rdb.Del(ctx, "First value").Err(); err != nil { 84 | return err 85 | } 86 | if err := rdb.Del(ctx, "Second value").Err(); err != nil { 87 | return err 88 | } 89 | 90 | return nil 91 | } 92 | -------------------------------------------------------------------------------- /extra/redisotel/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= 2 | github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w= 3 | github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= 4 | github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 5 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 6 | github.com/cespare/xxhash/v2 v2.2.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/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 11 | github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= 12 | github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 13 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 14 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 15 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 | github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= 18 | go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s= 19 | go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4= 20 | go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo= 21 | go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4= 22 | go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE= 23 | go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4= 24 | go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs= 25 | go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0= 26 | golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= 27 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 28 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 29 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /example_instrumentation_test.go: -------------------------------------------------------------------------------- 1 | package redis_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | 8 | "github.com/redis/go-redis/v9" 9 | ) 10 | 11 | type redisHook struct{} 12 | 13 | var _ redis.Hook = redisHook{} 14 | 15 | func (redisHook) DialHook(hook redis.DialHook) redis.DialHook { 16 | return func(ctx context.Context, network, addr string) (net.Conn, error) { 17 | fmt.Printf("dialing %s %s\n", network, addr) 18 | conn, err := hook(ctx, network, addr) 19 | fmt.Printf("finished dialing %s %s\n", network, addr) 20 | return conn, err 21 | } 22 | } 23 | 24 | func (redisHook) ProcessHook(hook redis.ProcessHook) redis.ProcessHook { 25 | return func(ctx context.Context, cmd redis.Cmder) error { 26 | fmt.Printf("starting processing: <%s>\n", cmd) 27 | err := hook(ctx, cmd) 28 | fmt.Printf("finished processing: <%s>\n", cmd) 29 | return err 30 | } 31 | } 32 | 33 | func (redisHook) ProcessPipelineHook(hook redis.ProcessPipelineHook) redis.ProcessPipelineHook { 34 | return func(ctx context.Context, cmds []redis.Cmder) error { 35 | fmt.Printf("pipeline starting processing: %v\n", cmds) 36 | err := hook(ctx, cmds) 37 | fmt.Printf("pipeline finished processing: %v\n", cmds) 38 | return err 39 | } 40 | } 41 | 42 | func Example_instrumentation() { 43 | rdb := redis.NewClient(&redis.Options{ 44 | Addr: ":6379", 45 | }) 46 | rdb.AddHook(redisHook{}) 47 | 48 | rdb.Ping(ctx) 49 | // Output: starting processing: 50 | // dialing tcp :6379 51 | // finished dialing tcp :6379 52 | // finished processing: 53 | } 54 | 55 | func ExamplePipeline_instrumentation() { 56 | rdb := redis.NewClient(&redis.Options{ 57 | Addr: ":6379", 58 | }) 59 | rdb.AddHook(redisHook{}) 60 | 61 | rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error { 62 | pipe.Ping(ctx) 63 | pipe.Ping(ctx) 64 | return nil 65 | }) 66 | // Output: pipeline starting processing: [ping: ping: ] 67 | // dialing tcp :6379 68 | // finished dialing tcp :6379 69 | // pipeline finished processing: [ping: PONG ping: PONG] 70 | } 71 | 72 | func ExampleClient_Watch_instrumentation() { 73 | rdb := redis.NewClient(&redis.Options{ 74 | Addr: ":6379", 75 | }) 76 | rdb.AddHook(redisHook{}) 77 | 78 | rdb.Watch(ctx, func(tx *redis.Tx) error { 79 | tx.Ping(ctx) 80 | tx.Ping(ctx) 81 | return nil 82 | }, "foo") 83 | // Output: 84 | // starting processing: 85 | // dialing tcp :6379 86 | // finished dialing tcp :6379 87 | // finished processing: 88 | // starting processing: 89 | // finished processing: 90 | // starting processing: 91 | // finished processing: 92 | // starting processing: 93 | // finished processing: 94 | } 95 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /internal/pool/conn.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "net" 7 | "sync/atomic" 8 | "time" 9 | 10 | "github.com/redis/go-redis/v9/internal/proto" 11 | ) 12 | 13 | var noDeadline = time.Time{} 14 | 15 | type Conn struct { 16 | usedAt int64 // atomic 17 | netConn net.Conn 18 | 19 | rd *proto.Reader 20 | bw *bufio.Writer 21 | wr *proto.Writer 22 | 23 | Inited bool 24 | pooled bool 25 | createdAt time.Time 26 | } 27 | 28 | func NewConn(netConn net.Conn) *Conn { 29 | cn := &Conn{ 30 | netConn: netConn, 31 | createdAt: time.Now(), 32 | } 33 | cn.rd = proto.NewReader(netConn) 34 | cn.bw = bufio.NewWriter(netConn) 35 | cn.wr = proto.NewWriter(cn.bw) 36 | cn.SetUsedAt(time.Now()) 37 | return cn 38 | } 39 | 40 | func (cn *Conn) UsedAt() time.Time { 41 | unix := atomic.LoadInt64(&cn.usedAt) 42 | return time.Unix(unix, 0) 43 | } 44 | 45 | func (cn *Conn) SetUsedAt(tm time.Time) { 46 | atomic.StoreInt64(&cn.usedAt, tm.Unix()) 47 | } 48 | 49 | func (cn *Conn) SetNetConn(netConn net.Conn) { 50 | cn.netConn = netConn 51 | cn.rd.Reset(netConn) 52 | cn.bw.Reset(netConn) 53 | } 54 | 55 | func (cn *Conn) Write(b []byte) (int, error) { 56 | return cn.netConn.Write(b) 57 | } 58 | 59 | func (cn *Conn) RemoteAddr() net.Addr { 60 | if cn.netConn != nil { 61 | return cn.netConn.RemoteAddr() 62 | } 63 | return nil 64 | } 65 | 66 | func (cn *Conn) WithReader( 67 | ctx context.Context, timeout time.Duration, fn func(rd *proto.Reader) error, 68 | ) error { 69 | if timeout >= 0 { 70 | if err := cn.netConn.SetReadDeadline(cn.deadline(ctx, timeout)); err != nil { 71 | return err 72 | } 73 | } 74 | return fn(cn.rd) 75 | } 76 | 77 | func (cn *Conn) WithWriter( 78 | ctx context.Context, timeout time.Duration, fn func(wr *proto.Writer) error, 79 | ) error { 80 | if timeout >= 0 { 81 | if err := cn.netConn.SetWriteDeadline(cn.deadline(ctx, timeout)); err != nil { 82 | return err 83 | } 84 | } 85 | 86 | if cn.bw.Buffered() > 0 { 87 | cn.bw.Reset(cn.netConn) 88 | } 89 | 90 | if err := fn(cn.wr); err != nil { 91 | return err 92 | } 93 | 94 | return cn.bw.Flush() 95 | } 96 | 97 | func (cn *Conn) Close() error { 98 | return cn.netConn.Close() 99 | } 100 | 101 | func (cn *Conn) deadline(ctx context.Context, timeout time.Duration) time.Time { 102 | tm := time.Now() 103 | cn.SetUsedAt(tm) 104 | 105 | if timeout > 0 { 106 | tm = tm.Add(timeout) 107 | } 108 | 109 | if ctx != nil { 110 | deadline, ok := ctx.Deadline() 111 | if ok { 112 | if timeout == 0 { 113 | return deadline 114 | } 115 | if deadline.Before(tm) { 116 | return deadline 117 | } 118 | return tm 119 | } 120 | } 121 | 122 | if timeout > 0 { 123 | return tm 124 | } 125 | 126 | return noDeadline 127 | } 128 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /extra/redisprometheus/go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 | github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= 4 | github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= 5 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 6 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 10 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 11 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 12 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 13 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 14 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 15 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 16 | github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= 17 | github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 18 | github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= 19 | github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= 20 | github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= 21 | github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= 22 | github.com/prometheus/common v0.39.0 h1:oOyhkDq05hPZKItWVBkJ6g6AtGxi+fy7F4JvUV8uhsI= 23 | github.com/prometheus/common v0.39.0/go.mod h1:6XBZ7lYdLCbkAVhwRsWTZn+IN5AB9F/NXd5w0BbEX0Y= 24 | github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= 25 | github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= 26 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 27 | golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= 28 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 29 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 30 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 31 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 32 | google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= 33 | google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 34 | -------------------------------------------------------------------------------- /pipeline_test.go: -------------------------------------------------------------------------------- 1 | package redis_test 2 | 3 | import ( 4 | "errors" 5 | "strconv" 6 | 7 | . "github.com/bsm/ginkgo/v2" 8 | . "github.com/bsm/gomega" 9 | 10 | "github.com/redis/go-redis/v9" 11 | ) 12 | 13 | var _ = Describe("pipelining", func() { 14 | var client *redis.Client 15 | var pipe *redis.Pipeline 16 | 17 | BeforeEach(func() { 18 | client = redis.NewClient(redisOptions()) 19 | Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) 20 | }) 21 | 22 | AfterEach(func() { 23 | Expect(client.Close()).NotTo(HaveOccurred()) 24 | }) 25 | 26 | It("supports block style", func() { 27 | var get *redis.StringCmd 28 | cmds, err := client.Pipelined(ctx, func(pipe redis.Pipeliner) error { 29 | get = pipe.Get(ctx, "foo") 30 | return nil 31 | }) 32 | Expect(err).To(Equal(redis.Nil)) 33 | Expect(cmds).To(HaveLen(1)) 34 | Expect(cmds[0]).To(Equal(get)) 35 | Expect(get.Err()).To(Equal(redis.Nil)) 36 | Expect(get.Val()).To(Equal("")) 37 | }) 38 | 39 | assertPipeline := func() { 40 | It("returns no errors when there are no commands", func() { 41 | _, err := pipe.Exec(ctx) 42 | Expect(err).NotTo(HaveOccurred()) 43 | }) 44 | 45 | It("discards queued commands", func() { 46 | pipe.Get(ctx, "key") 47 | pipe.Discard() 48 | cmds, err := pipe.Exec(ctx) 49 | Expect(err).NotTo(HaveOccurred()) 50 | Expect(cmds).To(BeNil()) 51 | }) 52 | 53 | It("handles val/err", func() { 54 | err := client.Set(ctx, "key", "value", 0).Err() 55 | Expect(err).NotTo(HaveOccurred()) 56 | 57 | get := pipe.Get(ctx, "key") 58 | cmds, err := pipe.Exec(ctx) 59 | Expect(err).NotTo(HaveOccurred()) 60 | Expect(cmds).To(HaveLen(1)) 61 | 62 | val, err := get.Result() 63 | Expect(err).NotTo(HaveOccurred()) 64 | Expect(val).To(Equal("value")) 65 | }) 66 | 67 | It("supports custom command", func() { 68 | pipe.Do(ctx, "ping") 69 | cmds, err := pipe.Exec(ctx) 70 | Expect(err).NotTo(HaveOccurred()) 71 | Expect(cmds).To(HaveLen(1)) 72 | }) 73 | 74 | It("handles large pipelines", func() { 75 | for callCount := 1; callCount < 16; callCount++ { 76 | for i := 1; i <= callCount; i++ { 77 | pipe.SetNX(ctx, strconv.Itoa(i)+"_key", strconv.Itoa(i)+"_value", 0) 78 | } 79 | 80 | cmds, err := pipe.Exec(ctx) 81 | Expect(err).NotTo(HaveOccurred()) 82 | Expect(cmds).To(HaveLen(callCount)) 83 | for _, cmd := range cmds { 84 | Expect(cmd).To(BeAssignableToTypeOf(&redis.BoolCmd{})) 85 | } 86 | } 87 | }) 88 | 89 | It("should Exec, not Do", func() { 90 | err := pipe.Do(ctx).Err() 91 | Expect(err).To(Equal(errors.New("redis: please enter the command to be executed"))) 92 | }) 93 | } 94 | 95 | Describe("Pipeline", func() { 96 | BeforeEach(func() { 97 | pipe = client.Pipeline().(*redis.Pipeline) 98 | }) 99 | 100 | assertPipeline() 101 | }) 102 | 103 | Describe("TxPipeline", func() { 104 | BeforeEach(func() { 105 | pipe = client.TxPipeline().(*redis.Pipeline) 106 | }) 107 | 108 | assertPipeline() 109 | }) 110 | }) 111 | -------------------------------------------------------------------------------- /internal/hscan/structmap.go: -------------------------------------------------------------------------------- 1 | package hscan 2 | 3 | import ( 4 | "encoding" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | "sync" 9 | 10 | "github.com/redis/go-redis/v9/internal/util" 11 | ) 12 | 13 | // structMap contains the map of struct fields for target structs 14 | // indexed by the struct type. 15 | type structMap struct { 16 | m sync.Map 17 | } 18 | 19 | func newStructMap() *structMap { 20 | return new(structMap) 21 | } 22 | 23 | func (s *structMap) get(t reflect.Type) *structSpec { 24 | if v, ok := s.m.Load(t); ok { 25 | return v.(*structSpec) 26 | } 27 | 28 | spec := newStructSpec(t, "redis") 29 | s.m.Store(t, spec) 30 | return spec 31 | } 32 | 33 | //------------------------------------------------------------------------------ 34 | 35 | // structSpec contains the list of all fields in a target struct. 36 | type structSpec struct { 37 | m map[string]*structField 38 | } 39 | 40 | func (s *structSpec) set(tag string, sf *structField) { 41 | s.m[tag] = sf 42 | } 43 | 44 | func newStructSpec(t reflect.Type, fieldTag string) *structSpec { 45 | numField := t.NumField() 46 | out := &structSpec{ 47 | m: make(map[string]*structField, numField), 48 | } 49 | 50 | for i := 0; i < numField; i++ { 51 | f := t.Field(i) 52 | 53 | tag := f.Tag.Get(fieldTag) 54 | if tag == "" || tag == "-" { 55 | continue 56 | } 57 | 58 | tag = strings.Split(tag, ",")[0] 59 | if tag == "" { 60 | continue 61 | } 62 | 63 | // Use the built-in decoder. 64 | out.set(tag, &structField{index: i, fn: decoders[f.Type.Kind()]}) 65 | } 66 | 67 | return out 68 | } 69 | 70 | //------------------------------------------------------------------------------ 71 | 72 | // structField represents a single field in a target struct. 73 | type structField struct { 74 | index int 75 | fn decoderFunc 76 | } 77 | 78 | //------------------------------------------------------------------------------ 79 | 80 | type StructValue struct { 81 | spec *structSpec 82 | value reflect.Value 83 | } 84 | 85 | func (s StructValue) Scan(key string, value string) error { 86 | field, ok := s.spec.m[key] 87 | if !ok { 88 | return nil 89 | } 90 | 91 | v := s.value.Field(field.index) 92 | isPtr := v.Kind() == reflect.Ptr 93 | 94 | if isPtr && v.IsNil() { 95 | v.Set(reflect.New(v.Type().Elem())) 96 | } 97 | if !isPtr && v.Type().Name() != "" && v.CanAddr() { 98 | v = v.Addr() 99 | isPtr = true 100 | } 101 | 102 | if isPtr && v.Type().NumMethod() > 0 && v.CanInterface() { 103 | switch scan := v.Interface().(type) { 104 | case Scanner: 105 | return scan.ScanRedis(value) 106 | case encoding.TextUnmarshaler: 107 | return scan.UnmarshalText(util.StringToBytes(value)) 108 | } 109 | } 110 | 111 | if isPtr { 112 | v = v.Elem() 113 | } 114 | 115 | if err := field.fn(v, value); err != nil { 116 | t := s.value.Type() 117 | return fmt.Errorf("cannot scan redis.result %s into struct field %s.%s of type %s, error-%s", 118 | value, t.Name(), t.Field(field.index).Name, t.Field(field.index).Type, err.Error()) 119 | } 120 | return nil 121 | } 122 | -------------------------------------------------------------------------------- /internal/proto/reader_test.go: -------------------------------------------------------------------------------- 1 | package proto_test 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | 8 | "github.com/redis/go-redis/v9/internal/proto" 9 | ) 10 | 11 | func BenchmarkReader_ParseReply_Status(b *testing.B) { 12 | benchmarkParseReply(b, "+OK\r\n", false) 13 | } 14 | 15 | func BenchmarkReader_ParseReply_Int(b *testing.B) { 16 | benchmarkParseReply(b, ":1\r\n", false) 17 | } 18 | 19 | func BenchmarkReader_ParseReply_Float(b *testing.B) { 20 | benchmarkParseReply(b, ",123.456\r\n", false) 21 | } 22 | 23 | func BenchmarkReader_ParseReply_Bool(b *testing.B) { 24 | benchmarkParseReply(b, "#t\r\n", false) 25 | } 26 | 27 | func BenchmarkReader_ParseReply_BigInt(b *testing.B) { 28 | benchmarkParseReply(b, "(3492890328409238509324850943850943825024385\r\n", false) 29 | } 30 | 31 | func BenchmarkReader_ParseReply_Error(b *testing.B) { 32 | benchmarkParseReply(b, "-Error message\r\n", true) 33 | } 34 | 35 | func BenchmarkReader_ParseReply_Nil(b *testing.B) { 36 | benchmarkParseReply(b, "_\r\n", true) 37 | } 38 | 39 | func BenchmarkReader_ParseReply_BlobError(b *testing.B) { 40 | benchmarkParseReply(b, "!21\r\nSYNTAX invalid syntax", true) 41 | } 42 | 43 | func BenchmarkReader_ParseReply_String(b *testing.B) { 44 | benchmarkParseReply(b, "$5\r\nhello\r\n", false) 45 | } 46 | 47 | func BenchmarkReader_ParseReply_Verb(b *testing.B) { 48 | benchmarkParseReply(b, "$9\r\ntxt:hello\r\n", false) 49 | } 50 | 51 | func BenchmarkReader_ParseReply_Slice(b *testing.B) { 52 | benchmarkParseReply(b, "*2\r\n$5\r\nhello\r\n$5\r\nworld\r\n", false) 53 | } 54 | 55 | func BenchmarkReader_ParseReply_Set(b *testing.B) { 56 | benchmarkParseReply(b, "~2\r\n$5\r\nhello\r\n$5\r\nworld\r\n", false) 57 | } 58 | 59 | func BenchmarkReader_ParseReply_Push(b *testing.B) { 60 | benchmarkParseReply(b, ">2\r\n$5\r\nhello\r\n$5\r\nworld\r\n", false) 61 | } 62 | 63 | func BenchmarkReader_ParseReply_Map(b *testing.B) { 64 | benchmarkParseReply(b, "%2\r\n$5\r\nhello\r\n$5\r\nworld\r\n+key\r\n+value\r\n", false) 65 | } 66 | 67 | func BenchmarkReader_ParseReply_Attr(b *testing.B) { 68 | benchmarkParseReply(b, "%1\r\n+key\r\n+value\r\n+hello\r\n", false) 69 | } 70 | 71 | func TestReader_ReadLine(t *testing.T) { 72 | original := bytes.Repeat([]byte("a"), 8192) 73 | original[len(original)-2] = '\r' 74 | original[len(original)-1] = '\n' 75 | r := proto.NewReader(bytes.NewReader(original)) 76 | read, err := r.ReadLine() 77 | if err != nil && err != io.EOF { 78 | t.Errorf("Should be able to read the full buffer: %v", err) 79 | } 80 | 81 | if !bytes.Equal(read, original[:len(original)-2]) { 82 | t.Errorf("Values must be equal: %d expected %d", len(read), len(original[:len(original)-2])) 83 | } 84 | } 85 | 86 | func benchmarkParseReply(b *testing.B, reply string, wanterr bool) { 87 | buf := new(bytes.Buffer) 88 | for i := 0; i < b.N; i++ { 89 | buf.WriteString(reply) 90 | } 91 | p := proto.NewReader(buf) 92 | b.ResetTimer() 93 | 94 | for i := 0; i < b.N; i++ { 95 | _, err := p.ReadReply() 96 | if !wanterr && err != nil { 97 | b.Fatal(err) 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /internal/hashtag/hashtag.go: -------------------------------------------------------------------------------- 1 | package hashtag 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/redis/go-redis/v9/internal/rand" 7 | ) 8 | 9 | const slotNumber = 16384 10 | 11 | // CRC16 implementation according to CCITT standards. 12 | // Copyright 2001-2010 Georges Menie (www.menie.org) 13 | // Copyright 2013 The Go Authors. All rights reserved. 14 | // http://redis.io/topics/cluster-spec#appendix-a-crc16-reference-implementation-in-ansi-c 15 | var crc16tab = [256]uint16{ 16 | 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 17 | 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 18 | 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, 19 | 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, 20 | 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, 21 | 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, 22 | 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, 23 | 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, 24 | 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, 25 | 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, 26 | 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, 27 | 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, 28 | 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, 29 | 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, 30 | 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, 31 | 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, 32 | 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, 33 | 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, 34 | 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, 35 | 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, 36 | 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, 37 | 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 38 | 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, 39 | 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, 40 | 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, 41 | 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, 42 | 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, 43 | 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, 44 | 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, 45 | 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, 46 | 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, 47 | 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0, 48 | } 49 | 50 | func Key(key string) string { 51 | if s := strings.IndexByte(key, '{'); s > -1 { 52 | if e := strings.IndexByte(key[s+1:], '}'); e > 0 { 53 | return key[s+1 : s+e+1] 54 | } 55 | } 56 | return key 57 | } 58 | 59 | func RandomSlot() int { 60 | return rand.Intn(slotNumber) 61 | } 62 | 63 | // Slot returns a consistent slot number between 0 and 16383 64 | // for any given string key. 65 | func Slot(key string) int { 66 | if key == "" { 67 | return RandomSlot() 68 | } 69 | key = Key(key) 70 | return int(crc16sum(key)) % slotNumber 71 | } 72 | 73 | func crc16sum(key string) (crc uint16) { 74 | for i := 0; i < len(key); i++ { 75 | crc = (crc << 8) ^ crc16tab[(byte(crc>>8)^key[i])&0x00ff] 76 | } 77 | return 78 | } 79 | -------------------------------------------------------------------------------- /extra/redisotel/config.go: -------------------------------------------------------------------------------- 1 | package redisotel 2 | 3 | import ( 4 | "go.opentelemetry.io/otel" 5 | "go.opentelemetry.io/otel/attribute" 6 | "go.opentelemetry.io/otel/metric" 7 | semconv "go.opentelemetry.io/otel/semconv/v1.12.0" 8 | "go.opentelemetry.io/otel/trace" 9 | ) 10 | 11 | type config struct { 12 | // Common options. 13 | 14 | dbSystem string 15 | attrs []attribute.KeyValue 16 | 17 | // Tracing options. 18 | 19 | tp trace.TracerProvider 20 | tracer trace.Tracer 21 | 22 | dbStmtEnabled bool 23 | 24 | // Metrics options. 25 | 26 | mp metric.MeterProvider 27 | meter metric.Meter 28 | 29 | poolName string 30 | } 31 | 32 | type baseOption interface { 33 | apply(conf *config) 34 | } 35 | 36 | type Option interface { 37 | baseOption 38 | tracing() 39 | metrics() 40 | } 41 | 42 | type option func(conf *config) 43 | 44 | func (fn option) apply(conf *config) { 45 | fn(conf) 46 | } 47 | 48 | func (fn option) tracing() {} 49 | 50 | func (fn option) metrics() {} 51 | 52 | func newConfig(opts ...baseOption) *config { 53 | conf := &config{ 54 | dbSystem: "redis", 55 | attrs: []attribute.KeyValue{}, 56 | 57 | tp: otel.GetTracerProvider(), 58 | mp: otel.GetMeterProvider(), 59 | dbStmtEnabled: true, 60 | } 61 | 62 | for _, opt := range opts { 63 | opt.apply(conf) 64 | } 65 | 66 | conf.attrs = append(conf.attrs, semconv.DBSystemKey.String(conf.dbSystem)) 67 | 68 | return conf 69 | } 70 | 71 | func WithDBSystem(dbSystem string) Option { 72 | return option(func(conf *config) { 73 | conf.dbSystem = dbSystem 74 | }) 75 | } 76 | 77 | // WithAttributes specifies additional attributes to be added to the span. 78 | func WithAttributes(attrs ...attribute.KeyValue) Option { 79 | return option(func(conf *config) { 80 | conf.attrs = append(conf.attrs, attrs...) 81 | }) 82 | } 83 | 84 | //------------------------------------------------------------------------------ 85 | 86 | type TracingOption interface { 87 | baseOption 88 | tracing() 89 | } 90 | 91 | type tracingOption func(conf *config) 92 | 93 | var _ TracingOption = (*tracingOption)(nil) 94 | 95 | func (fn tracingOption) apply(conf *config) { 96 | fn(conf) 97 | } 98 | 99 | func (fn tracingOption) tracing() {} 100 | 101 | // WithTracerProvider specifies a tracer provider to use for creating a tracer. 102 | // If none is specified, the global provider is used. 103 | func WithTracerProvider(provider trace.TracerProvider) TracingOption { 104 | return tracingOption(func(conf *config) { 105 | conf.tp = provider 106 | }) 107 | } 108 | 109 | // WithDBStatement tells the tracing hook not to log raw redis commands. 110 | func WithDBStatement(on bool) TracingOption { 111 | return tracingOption(func(conf *config) { 112 | conf.dbStmtEnabled = on 113 | }) 114 | } 115 | 116 | //------------------------------------------------------------------------------ 117 | 118 | type MetricsOption interface { 119 | baseOption 120 | metrics() 121 | } 122 | 123 | type metricsOption func(conf *config) 124 | 125 | var _ MetricsOption = (*metricsOption)(nil) 126 | 127 | func (fn metricsOption) apply(conf *config) { 128 | fn(conf) 129 | } 130 | 131 | func (fn metricsOption) metrics() {} 132 | 133 | // WithMeterProvider configures a metric.Meter used to create instruments. 134 | func WithMeterProvider(mp metric.MeterProvider) MetricsOption { 135 | return metricsOption(func(conf *config) { 136 | conf.mp = mp 137 | }) 138 | } 139 | -------------------------------------------------------------------------------- /example/del-keys-without-ttl/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | "time" 8 | 9 | "go.uber.org/zap" 10 | 11 | "github.com/redis/go-redis/v9" 12 | ) 13 | 14 | func main() { 15 | ctx := context.Background() 16 | 17 | rdb := redis.NewClient(&redis.Options{ 18 | Addr: ":6379", 19 | }) 20 | 21 | _ = rdb.Set(ctx, "key_with_ttl", "bar", time.Minute).Err() 22 | _ = rdb.Set(ctx, "key_without_ttl_1", "", 0).Err() 23 | _ = rdb.Set(ctx, "key_without_ttl_2", "", 0).Err() 24 | 25 | checker := NewKeyChecker(rdb, 100) 26 | 27 | start := time.Now() 28 | checker.Start(ctx) 29 | 30 | iter := rdb.Scan(ctx, 0, "", 0).Iterator() 31 | for iter.Next(ctx) { 32 | checker.Add(iter.Val()) 33 | } 34 | if err := iter.Err(); err != nil { 35 | panic(err) 36 | } 37 | 38 | deleted := checker.Stop() 39 | fmt.Println("deleted", deleted, "keys", "in", time.Since(start)) 40 | } 41 | 42 | type KeyChecker struct { 43 | rdb *redis.Client 44 | batchSize int 45 | ch chan string 46 | delCh chan string 47 | wg sync.WaitGroup 48 | deleted int 49 | logger *zap.Logger 50 | } 51 | 52 | func NewKeyChecker(rdb *redis.Client, batchSize int) *KeyChecker { 53 | return &KeyChecker{ 54 | rdb: rdb, 55 | batchSize: batchSize, 56 | ch: make(chan string, batchSize), 57 | delCh: make(chan string, batchSize), 58 | logger: zap.L(), 59 | } 60 | } 61 | 62 | func (c *KeyChecker) Add(key string) { 63 | c.ch <- key 64 | } 65 | 66 | func (c *KeyChecker) Start(ctx context.Context) { 67 | c.wg.Add(1) 68 | go func() { 69 | defer c.wg.Done() 70 | if err := c.del(ctx); err != nil { 71 | panic(err) 72 | } 73 | }() 74 | 75 | c.wg.Add(1) 76 | go func() { 77 | defer c.wg.Done() 78 | defer close(c.delCh) 79 | 80 | keys := make([]string, 0, c.batchSize) 81 | 82 | for key := range c.ch { 83 | keys = append(keys, key) 84 | if len(keys) < cap(keys) { 85 | continue 86 | } 87 | 88 | if err := c.checkKeys(ctx, keys); err != nil { 89 | c.logger.Error("checkKeys failed", zap.Error(err)) 90 | } 91 | keys = keys[:0] 92 | } 93 | 94 | if len(keys) > 0 { 95 | if err := c.checkKeys(ctx, keys); err != nil { 96 | c.logger.Error("checkKeys failed", zap.Error(err)) 97 | } 98 | keys = nil 99 | } 100 | }() 101 | } 102 | 103 | func (c *KeyChecker) Stop() int { 104 | close(c.ch) 105 | c.wg.Wait() 106 | return c.deleted 107 | } 108 | 109 | func (c *KeyChecker) checkKeys(ctx context.Context, keys []string) error { 110 | cmds, err := c.rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error { 111 | for _, key := range keys { 112 | pipe.TTL(ctx, key) 113 | } 114 | return nil 115 | }) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | for i, cmd := range cmds { 121 | d, err := cmd.(*redis.DurationCmd).Result() 122 | if err != nil { 123 | return err 124 | } 125 | if d == -1 { 126 | c.delCh <- keys[i] 127 | } 128 | } 129 | 130 | return nil 131 | } 132 | 133 | func (c *KeyChecker) del(ctx context.Context) error { 134 | pipe := c.rdb.Pipeline() 135 | 136 | for key := range c.delCh { 137 | fmt.Printf("deleting %s...\n", key) 138 | pipe.Del(ctx, key) 139 | c.deleted++ 140 | 141 | if pipe.Len() < c.batchSize { 142 | continue 143 | } 144 | 145 | if _, err := pipe.Exec(ctx); err != nil { 146 | return err 147 | } 148 | } 149 | 150 | if _, err := pipe.Exec(ctx); err != nil { 151 | return err 152 | } 153 | 154 | return nil 155 | } 156 | -------------------------------------------------------------------------------- /extra/rediscmd/rediscmd.go: -------------------------------------------------------------------------------- 1 | package rediscmd 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/redis/go-redis/v9" 11 | ) 12 | 13 | func CmdString(cmd redis.Cmder) string { 14 | b := make([]byte, 0, 32) 15 | b = AppendCmd(b, cmd) 16 | return String(b) 17 | } 18 | 19 | func CmdsString(cmds []redis.Cmder) (string, string) { 20 | const numCmdLimit = 100 21 | const numNameLimit = 10 22 | 23 | seen := make(map[string]struct{}, numNameLimit) 24 | unqNames := make([]string, 0, numNameLimit) 25 | 26 | b := make([]byte, 0, 32*len(cmds)) 27 | 28 | for i, cmd := range cmds { 29 | if i > numCmdLimit { 30 | break 31 | } 32 | 33 | if i > 0 { 34 | b = append(b, '\n') 35 | } 36 | b = AppendCmd(b, cmd) 37 | 38 | if len(unqNames) >= numNameLimit { 39 | continue 40 | } 41 | 42 | name := cmd.FullName() 43 | if _, ok := seen[name]; !ok { 44 | seen[name] = struct{}{} 45 | unqNames = append(unqNames, name) 46 | } 47 | } 48 | 49 | summary := strings.Join(unqNames, " ") 50 | return summary, String(b) 51 | } 52 | 53 | func AppendCmd(b []byte, cmd redis.Cmder) []byte { 54 | const numArgLimit = 32 55 | 56 | for i, arg := range cmd.Args() { 57 | if i > numArgLimit { 58 | break 59 | } 60 | if i > 0 { 61 | b = append(b, ' ') 62 | } 63 | b = appendArg(b, arg) 64 | } 65 | 66 | if err := cmd.Err(); err != nil { 67 | b = append(b, ": "...) 68 | b = append(b, err.Error()...) 69 | } 70 | 71 | return b 72 | } 73 | 74 | func appendArg(b []byte, v interface{}) []byte { 75 | const argLenLimit = 64 76 | 77 | switch v := v.(type) { 78 | case nil: 79 | return append(b, ""...) 80 | case string: 81 | if len(v) > argLenLimit { 82 | v = v[:argLenLimit] 83 | } 84 | return appendUTF8String(b, Bytes(v)) 85 | case []byte: 86 | if len(v) > argLenLimit { 87 | v = v[:argLenLimit] 88 | } 89 | return appendUTF8String(b, v) 90 | case int: 91 | return strconv.AppendInt(b, int64(v), 10) 92 | case int8: 93 | return strconv.AppendInt(b, int64(v), 10) 94 | case int16: 95 | return strconv.AppendInt(b, int64(v), 10) 96 | case int32: 97 | return strconv.AppendInt(b, int64(v), 10) 98 | case int64: 99 | return strconv.AppendInt(b, v, 10) 100 | case uint: 101 | return strconv.AppendUint(b, uint64(v), 10) 102 | case uint8: 103 | return strconv.AppendUint(b, uint64(v), 10) 104 | case uint16: 105 | return strconv.AppendUint(b, uint64(v), 10) 106 | case uint32: 107 | return strconv.AppendUint(b, uint64(v), 10) 108 | case uint64: 109 | return strconv.AppendUint(b, v, 10) 110 | case float32: 111 | return strconv.AppendFloat(b, float64(v), 'f', -1, 64) 112 | case float64: 113 | return strconv.AppendFloat(b, v, 'f', -1, 64) 114 | case bool: 115 | if v { 116 | return append(b, "true"...) 117 | } 118 | return append(b, "false"...) 119 | case time.Time: 120 | return v.AppendFormat(b, time.RFC3339Nano) 121 | default: 122 | return append(b, fmt.Sprint(v)...) 123 | } 124 | } 125 | 126 | func appendUTF8String(dst []byte, src []byte) []byte { 127 | if isSimple(src) { 128 | dst = append(dst, src...) 129 | return dst 130 | } 131 | 132 | s := len(dst) 133 | dst = append(dst, make([]byte, hex.EncodedLen(len(src)))...) 134 | hex.Encode(dst[s:], src) 135 | return dst 136 | } 137 | 138 | func isSimple(b []byte) bool { 139 | for _, c := range b { 140 | if !isSimpleByte(c) { 141 | return false 142 | } 143 | } 144 | return true 145 | } 146 | 147 | func isSimpleByte(c byte) bool { 148 | return c >= 0x21 && c <= 0x7e 149 | } 150 | -------------------------------------------------------------------------------- /pipeline.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | ) 7 | 8 | type pipelineExecer func(context.Context, []Cmder) error 9 | 10 | // Pipeliner is an mechanism to realise Redis Pipeline technique. 11 | // 12 | // Pipelining is a technique to extremely speed up processing by packing 13 | // operations to batches, send them at once to Redis and read a replies in a 14 | // single step. 15 | // See https://redis.io/topics/pipelining 16 | // 17 | // Pay attention, that Pipeline is not a transaction, so you can get unexpected 18 | // results in case of big pipelines and small read/write timeouts. 19 | // Redis client has retransmission logic in case of timeouts, pipeline 20 | // can be retransmitted and commands can be executed more then once. 21 | // To avoid this: it is good idea to use reasonable bigger read/write timeouts 22 | // depends of your batch size and/or use TxPipeline. 23 | type Pipeliner interface { 24 | StatefulCmdable 25 | 26 | // Len is to obtain the number of commands in the pipeline that have not yet been executed. 27 | Len() int 28 | 29 | // Do is an API for executing any command. 30 | // If a certain Redis command is not yet supported, you can use Do to execute it. 31 | Do(ctx context.Context, args ...interface{}) *Cmd 32 | 33 | // Process is to put the commands to be executed into the pipeline buffer. 34 | Process(ctx context.Context, cmd Cmder) error 35 | 36 | // Discard is to discard all commands in the cache that have not yet been executed. 37 | Discard() 38 | 39 | // Exec is to send all the commands buffered in the pipeline to the redis-server. 40 | Exec(ctx context.Context) ([]Cmder, error) 41 | } 42 | 43 | var _ Pipeliner = (*Pipeline)(nil) 44 | 45 | // Pipeline implements pipelining as described in 46 | // http://redis.io/topics/pipelining. 47 | // Please note: it is not safe for concurrent use by multiple goroutines. 48 | type Pipeline struct { 49 | cmdable 50 | statefulCmdable 51 | 52 | exec pipelineExecer 53 | cmds []Cmder 54 | } 55 | 56 | func (c *Pipeline) init() { 57 | c.cmdable = c.Process 58 | c.statefulCmdable = c.Process 59 | } 60 | 61 | // Len returns the number of queued commands. 62 | func (c *Pipeline) Len() int { 63 | return len(c.cmds) 64 | } 65 | 66 | // Do queues the custom command for later execution. 67 | func (c *Pipeline) Do(ctx context.Context, args ...interface{}) *Cmd { 68 | cmd := NewCmd(ctx, args...) 69 | if len(args) == 0 { 70 | cmd.SetErr(errors.New("redis: please enter the command to be executed")) 71 | return cmd 72 | } 73 | _ = c.Process(ctx, cmd) 74 | return cmd 75 | } 76 | 77 | // Process queues the cmd for later execution. 78 | func (c *Pipeline) Process(ctx context.Context, cmd Cmder) error { 79 | c.cmds = append(c.cmds, cmd) 80 | return nil 81 | } 82 | 83 | // Discard resets the pipeline and discards queued commands. 84 | func (c *Pipeline) Discard() { 85 | c.cmds = c.cmds[:0] 86 | } 87 | 88 | // Exec executes all previously queued commands using one 89 | // client-server roundtrip. 90 | // 91 | // Exec always returns list of commands and error of the first failed 92 | // command if any. 93 | func (c *Pipeline) Exec(ctx context.Context) ([]Cmder, error) { 94 | if len(c.cmds) == 0 { 95 | return nil, nil 96 | } 97 | 98 | cmds := c.cmds 99 | c.cmds = nil 100 | 101 | return cmds, c.exec(ctx, cmds) 102 | } 103 | 104 | func (c *Pipeline) Pipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) { 105 | if err := fn(c); err != nil { 106 | return nil, err 107 | } 108 | return c.Exec(ctx) 109 | } 110 | 111 | func (c *Pipeline) Pipeline() Pipeliner { 112 | return c 113 | } 114 | 115 | func (c *Pipeline) TxPipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) { 116 | return c.Pipelined(ctx, fn) 117 | } 118 | 119 | func (c *Pipeline) TxPipeline() Pipeliner { 120 | return c 121 | } 122 | -------------------------------------------------------------------------------- /pool_test.go: -------------------------------------------------------------------------------- 1 | package redis_test 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | . "github.com/bsm/ginkgo/v2" 8 | . "github.com/bsm/gomega" 9 | 10 | "github.com/redis/go-redis/v9" 11 | ) 12 | 13 | var _ = Describe("pool", func() { 14 | var client *redis.Client 15 | 16 | BeforeEach(func() { 17 | opt := redisOptions() 18 | opt.MinIdleConns = 0 19 | opt.ConnMaxLifetime = 0 20 | opt.ConnMaxIdleTime = time.Second 21 | client = redis.NewClient(opt) 22 | }) 23 | 24 | AfterEach(func() { 25 | Expect(client.Close()).NotTo(HaveOccurred()) 26 | }) 27 | 28 | It("respects max size", func() { 29 | perform(1000, func(id int) { 30 | val, err := client.Ping(ctx).Result() 31 | Expect(err).NotTo(HaveOccurred()) 32 | Expect(val).To(Equal("PONG")) 33 | }) 34 | 35 | pool := client.Pool() 36 | Expect(pool.Len()).To(BeNumerically("<=", 10)) 37 | Expect(pool.IdleLen()).To(BeNumerically("<=", 10)) 38 | Expect(pool.Len()).To(Equal(pool.IdleLen())) 39 | }) 40 | 41 | It("respects max size on multi", func() { 42 | perform(1000, func(id int) { 43 | var ping *redis.StatusCmd 44 | 45 | err := client.Watch(ctx, func(tx *redis.Tx) error { 46 | cmds, err := tx.Pipelined(ctx, func(pipe redis.Pipeliner) error { 47 | ping = pipe.Ping(ctx) 48 | return nil 49 | }) 50 | Expect(err).NotTo(HaveOccurred()) 51 | Expect(cmds).To(HaveLen(1)) 52 | return err 53 | }) 54 | Expect(err).NotTo(HaveOccurred()) 55 | 56 | Expect(ping.Err()).NotTo(HaveOccurred()) 57 | Expect(ping.Val()).To(Equal("PONG")) 58 | }) 59 | 60 | pool := client.Pool() 61 | Expect(pool.Len()).To(BeNumerically("<=", 10)) 62 | Expect(pool.IdleLen()).To(BeNumerically("<=", 10)) 63 | Expect(pool.Len()).To(Equal(pool.IdleLen())) 64 | }) 65 | 66 | It("respects max size on pipelines", func() { 67 | perform(1000, func(id int) { 68 | pipe := client.Pipeline() 69 | ping := pipe.Ping(ctx) 70 | cmds, err := pipe.Exec(ctx) 71 | Expect(err).NotTo(HaveOccurred()) 72 | Expect(cmds).To(HaveLen(1)) 73 | Expect(ping.Err()).NotTo(HaveOccurred()) 74 | Expect(ping.Val()).To(Equal("PONG")) 75 | }) 76 | 77 | pool := client.Pool() 78 | Expect(pool.Len()).To(BeNumerically("<=", 10)) 79 | Expect(pool.IdleLen()).To(BeNumerically("<=", 10)) 80 | Expect(pool.Len()).To(Equal(pool.IdleLen())) 81 | }) 82 | 83 | It("removes broken connections", func() { 84 | cn, err := client.Pool().Get(context.Background()) 85 | Expect(err).NotTo(HaveOccurred()) 86 | cn.SetNetConn(&badConn{}) 87 | client.Pool().Put(ctx, cn) 88 | 89 | val, err := client.Ping(ctx).Result() 90 | Expect(err).NotTo(HaveOccurred()) 91 | Expect(val).To(Equal("PONG")) 92 | 93 | val, err = client.Ping(ctx).Result() 94 | Expect(err).NotTo(HaveOccurred()) 95 | Expect(val).To(Equal("PONG")) 96 | 97 | pool := client.Pool() 98 | Expect(pool.Len()).To(Equal(1)) 99 | Expect(pool.IdleLen()).To(Equal(1)) 100 | 101 | stats := pool.Stats() 102 | Expect(stats.Hits).To(Equal(uint32(1))) 103 | Expect(stats.Misses).To(Equal(uint32(2))) 104 | Expect(stats.Timeouts).To(Equal(uint32(0))) 105 | }) 106 | 107 | It("reuses connections", func() { 108 | // explain: https://github.com/redis/go-redis/pull/1675 109 | opt := redisOptions() 110 | opt.MinIdleConns = 0 111 | opt.ConnMaxLifetime = 0 112 | opt.ConnMaxIdleTime = 10 * time.Second 113 | client = redis.NewClient(opt) 114 | 115 | for i := 0; i < 100; i++ { 116 | val, err := client.Ping(ctx).Result() 117 | Expect(err).NotTo(HaveOccurred()) 118 | Expect(val).To(Equal("PONG")) 119 | } 120 | 121 | pool := client.Pool() 122 | Expect(pool.Len()).To(Equal(1)) 123 | Expect(pool.IdleLen()).To(Equal(1)) 124 | 125 | stats := pool.Stats() 126 | Expect(stats.Hits).To(Equal(uint32(99))) 127 | Expect(stats.Misses).To(Equal(uint32(1))) 128 | Expect(stats.Timeouts).To(Equal(uint32(0))) 129 | }) 130 | }) 131 | -------------------------------------------------------------------------------- /extra/redisprometheus/collector.go: -------------------------------------------------------------------------------- 1 | package redisprometheus 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | 6 | "github.com/redis/go-redis/v9" 7 | ) 8 | 9 | // StatGetter provides a method to get pool statistics. 10 | type StatGetter interface { 11 | PoolStats() *redis.PoolStats 12 | } 13 | 14 | // Collector collects statistics from a redis client. 15 | // It implements the prometheus.Collector interface. 16 | type Collector struct { 17 | getter StatGetter 18 | hitDesc *prometheus.Desc 19 | missDesc *prometheus.Desc 20 | timeoutDesc *prometheus.Desc 21 | totalDesc *prometheus.Desc 22 | idleDesc *prometheus.Desc 23 | staleDesc *prometheus.Desc 24 | } 25 | 26 | var _ prometheus.Collector = (*Collector)(nil) 27 | 28 | // NewCollector returns a new Collector based on the provided StatGetter. 29 | // The given namespace and subsystem are used to build the fully qualified metric name, 30 | // i.e. "{namespace}_{subsystem}_{metric}". 31 | // The provided metrics are: 32 | // - pool_hit_total 33 | // - pool_miss_total 34 | // - pool_timeout_total 35 | // - pool_conn_total_current 36 | // - pool_conn_idle_current 37 | // - pool_conn_stale_total 38 | func NewCollector(namespace, subsystem string, getter StatGetter) *Collector { 39 | return &Collector{ 40 | getter: getter, 41 | hitDesc: prometheus.NewDesc( 42 | prometheus.BuildFQName(namespace, subsystem, "pool_hit_total"), 43 | "Number of times a connection was found in the pool", 44 | nil, nil, 45 | ), 46 | missDesc: prometheus.NewDesc( 47 | prometheus.BuildFQName(namespace, subsystem, "pool_miss_total"), 48 | "Number of times a connection was not found in the pool", 49 | nil, nil, 50 | ), 51 | timeoutDesc: prometheus.NewDesc( 52 | prometheus.BuildFQName(namespace, subsystem, "pool_timeout_total"), 53 | "Number of times a timeout occurred when looking for a connection in the pool", 54 | nil, nil, 55 | ), 56 | totalDesc: prometheus.NewDesc( 57 | prometheus.BuildFQName(namespace, subsystem, "pool_conn_total_current"), 58 | "Current number of connections in the pool", 59 | nil, nil, 60 | ), 61 | idleDesc: prometheus.NewDesc( 62 | prometheus.BuildFQName(namespace, subsystem, "pool_conn_idle_current"), 63 | "Current number of idle connections in the pool", 64 | nil, nil, 65 | ), 66 | staleDesc: prometheus.NewDesc( 67 | prometheus.BuildFQName(namespace, subsystem, "pool_conn_stale_total"), 68 | "Number of times a connection was removed from the pool because it was stale", 69 | nil, nil, 70 | ), 71 | } 72 | } 73 | 74 | // Describe implements the prometheus.Collector interface. 75 | func (s *Collector) Describe(descs chan<- *prometheus.Desc) { 76 | descs <- s.hitDesc 77 | descs <- s.missDesc 78 | descs <- s.timeoutDesc 79 | descs <- s.totalDesc 80 | descs <- s.idleDesc 81 | descs <- s.staleDesc 82 | } 83 | 84 | // Collect implements the prometheus.Collector interface. 85 | func (s *Collector) Collect(metrics chan<- prometheus.Metric) { 86 | stats := s.getter.PoolStats() 87 | metrics <- prometheus.MustNewConstMetric( 88 | s.hitDesc, 89 | prometheus.CounterValue, 90 | float64(stats.Hits), 91 | ) 92 | metrics <- prometheus.MustNewConstMetric( 93 | s.missDesc, 94 | prometheus.CounterValue, 95 | float64(stats.Misses), 96 | ) 97 | metrics <- prometheus.MustNewConstMetric( 98 | s.timeoutDesc, 99 | prometheus.CounterValue, 100 | float64(stats.Timeouts), 101 | ) 102 | metrics <- prometheus.MustNewConstMetric( 103 | s.totalDesc, 104 | prometheus.GaugeValue, 105 | float64(stats.TotalConns), 106 | ) 107 | metrics <- prometheus.MustNewConstMetric( 108 | s.idleDesc, 109 | prometheus.GaugeValue, 110 | float64(stats.IdleConns), 111 | ) 112 | metrics <- prometheus.MustNewConstMetric( 113 | s.staleDesc, 114 | prometheus.CounterValue, 115 | float64(stats.StaleConns), 116 | ) 117 | } 118 | -------------------------------------------------------------------------------- /iterator_test.go: -------------------------------------------------------------------------------- 1 | package redis_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | . "github.com/bsm/ginkgo/v2" 7 | . "github.com/bsm/gomega" 8 | 9 | "github.com/redis/go-redis/v9" 10 | ) 11 | 12 | var _ = Describe("ScanIterator", func() { 13 | var client *redis.Client 14 | 15 | seed := func(n int) error { 16 | pipe := client.Pipeline() 17 | for i := 1; i <= n; i++ { 18 | pipe.Set(ctx, fmt.Sprintf("K%02d", i), "x", 0).Err() 19 | } 20 | _, err := pipe.Exec(ctx) 21 | return err 22 | } 23 | 24 | extraSeed := func(n int, m int) error { 25 | pipe := client.Pipeline() 26 | for i := 1; i <= m; i++ { 27 | pipe.Set(ctx, fmt.Sprintf("A%02d", i), "x", 0).Err() 28 | } 29 | for i := 1; i <= n; i++ { 30 | pipe.Set(ctx, fmt.Sprintf("K%02d", i), "x", 0).Err() 31 | } 32 | _, err := pipe.Exec(ctx) 33 | return err 34 | } 35 | 36 | hashKey := "K_HASHTEST" 37 | hashSeed := func(n int) error { 38 | pipe := client.Pipeline() 39 | for i := 1; i <= n; i++ { 40 | pipe.HSet(ctx, hashKey, fmt.Sprintf("K%02d", i), "x").Err() 41 | } 42 | _, err := pipe.Exec(ctx) 43 | return err 44 | } 45 | 46 | BeforeEach(func() { 47 | client = redis.NewClient(redisOptions()) 48 | Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) 49 | }) 50 | 51 | AfterEach(func() { 52 | Expect(client.Close()).NotTo(HaveOccurred()) 53 | }) 54 | 55 | It("should scan across empty DBs", func() { 56 | iter := client.Scan(ctx, 0, "", 10).Iterator() 57 | Expect(iter.Next(ctx)).To(BeFalse()) 58 | Expect(iter.Err()).NotTo(HaveOccurred()) 59 | }) 60 | 61 | It("should scan across one page", func() { 62 | Expect(seed(7)).NotTo(HaveOccurred()) 63 | 64 | var vals []string 65 | iter := client.Scan(ctx, 0, "", 0).Iterator() 66 | for iter.Next(ctx) { 67 | vals = append(vals, iter.Val()) 68 | } 69 | Expect(iter.Err()).NotTo(HaveOccurred()) 70 | Expect(vals).To(ConsistOf([]string{"K01", "K02", "K03", "K04", "K05", "K06", "K07"})) 71 | }) 72 | 73 | It("should scan across multiple pages", func() { 74 | Expect(seed(71)).NotTo(HaveOccurred()) 75 | 76 | var vals []string 77 | iter := client.Scan(ctx, 0, "", 10).Iterator() 78 | for iter.Next(ctx) { 79 | vals = append(vals, iter.Val()) 80 | } 81 | Expect(iter.Err()).NotTo(HaveOccurred()) 82 | Expect(vals).To(HaveLen(71)) 83 | Expect(vals).To(ContainElement("K01")) 84 | Expect(vals).To(ContainElement("K71")) 85 | }) 86 | 87 | It("should hscan across multiple pages", func() { 88 | Expect(hashSeed(71)).NotTo(HaveOccurred()) 89 | 90 | var vals []string 91 | iter := client.HScan(ctx, hashKey, 0, "", 10).Iterator() 92 | for iter.Next(ctx) { 93 | vals = append(vals, iter.Val()) 94 | } 95 | Expect(iter.Err()).NotTo(HaveOccurred()) 96 | Expect(vals).To(HaveLen(71 * 2)) 97 | Expect(vals).To(ContainElement("K01")) 98 | Expect(vals).To(ContainElement("K71")) 99 | }) 100 | 101 | It("should scan to page borders", func() { 102 | Expect(seed(20)).NotTo(HaveOccurred()) 103 | 104 | var vals []string 105 | iter := client.Scan(ctx, 0, "", 10).Iterator() 106 | for iter.Next(ctx) { 107 | vals = append(vals, iter.Val()) 108 | } 109 | Expect(iter.Err()).NotTo(HaveOccurred()) 110 | Expect(vals).To(HaveLen(20)) 111 | }) 112 | 113 | It("should scan with match", func() { 114 | Expect(seed(33)).NotTo(HaveOccurred()) 115 | 116 | var vals []string 117 | iter := client.Scan(ctx, 0, "K*2*", 10).Iterator() 118 | for iter.Next(ctx) { 119 | vals = append(vals, iter.Val()) 120 | } 121 | Expect(iter.Err()).NotTo(HaveOccurred()) 122 | Expect(vals).To(HaveLen(13)) 123 | }) 124 | 125 | It("should scan with match across empty pages", func() { 126 | Expect(extraSeed(2, 10)).NotTo(HaveOccurred()) 127 | 128 | var vals []string 129 | iter := client.Scan(ctx, 0, "K*", 1).Iterator() 130 | for iter.Next(ctx) { 131 | vals = append(vals, iter.Val()) 132 | } 133 | Expect(iter.Err()).NotTo(HaveOccurred()) 134 | Expect(vals).To(HaveLen(2)) 135 | }) 136 | }) 137 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "net" 8 | "strings" 9 | 10 | "github.com/redis/go-redis/v9/internal/pool" 11 | "github.com/redis/go-redis/v9/internal/proto" 12 | ) 13 | 14 | // ErrClosed performs any operation on the closed client will return this error. 15 | var ErrClosed = pool.ErrClosed 16 | 17 | // HasErrorPrefix checks if the err is a Redis error and the message contains a prefix. 18 | func HasErrorPrefix(err error, prefix string) bool { 19 | var rErr Error 20 | if !errors.As(err, &rErr) { 21 | return false 22 | } 23 | msg := rErr.Error() 24 | msg = strings.TrimPrefix(msg, "ERR ") // KVRocks adds such prefix 25 | return strings.HasPrefix(msg, prefix) 26 | } 27 | 28 | type Error interface { 29 | error 30 | 31 | // RedisError is a no-op function but 32 | // serves to distinguish types that are Redis 33 | // errors from ordinary errors: a type is a 34 | // Redis error if it has a RedisError method. 35 | RedisError() 36 | } 37 | 38 | var _ Error = proto.RedisError("") 39 | 40 | func shouldRetry(err error, retryTimeout bool) bool { 41 | switch err { 42 | case io.EOF, io.ErrUnexpectedEOF: 43 | return true 44 | case nil, context.Canceled, context.DeadlineExceeded: 45 | return false 46 | } 47 | 48 | if v, ok := err.(timeoutError); ok { 49 | if v.Timeout() { 50 | return retryTimeout 51 | } 52 | return true 53 | } 54 | 55 | s := err.Error() 56 | if s == "ERR max number of clients reached" { 57 | return true 58 | } 59 | if strings.HasPrefix(s, "LOADING ") { 60 | return true 61 | } 62 | if strings.HasPrefix(s, "READONLY ") { 63 | return true 64 | } 65 | if strings.HasPrefix(s, "CLUSTERDOWN ") { 66 | return true 67 | } 68 | if strings.HasPrefix(s, "TRYAGAIN ") { 69 | return true 70 | } 71 | 72 | return false 73 | } 74 | 75 | func isRedisError(err error) bool { 76 | _, ok := err.(proto.RedisError) 77 | return ok 78 | } 79 | 80 | func isBadConn(err error, allowTimeout bool, addr string) bool { 81 | switch err { 82 | case nil: 83 | return false 84 | case context.Canceled, context.DeadlineExceeded: 85 | return true 86 | } 87 | 88 | if isRedisError(err) { 89 | switch { 90 | case isReadOnlyError(err): 91 | // Close connections in read only state in case domain addr is used 92 | // and domain resolves to a different Redis Server. See #790. 93 | return true 94 | case isMovedSameConnAddr(err, addr): 95 | // Close connections when we are asked to move to the same addr 96 | // of the connection. Force a DNS resolution when all connections 97 | // of the pool are recycled 98 | return true 99 | default: 100 | return false 101 | } 102 | } 103 | 104 | if allowTimeout { 105 | if netErr, ok := err.(net.Error); ok && netErr.Timeout() { 106 | return false 107 | } 108 | } 109 | 110 | return true 111 | } 112 | 113 | func isMovedError(err error) (moved bool, ask bool, addr string) { 114 | if !isRedisError(err) { 115 | return 116 | } 117 | 118 | s := err.Error() 119 | switch { 120 | case strings.HasPrefix(s, "MOVED "): 121 | moved = true 122 | case strings.HasPrefix(s, "ASK "): 123 | ask = true 124 | default: 125 | return 126 | } 127 | 128 | ind := strings.LastIndex(s, " ") 129 | if ind == -1 { 130 | return false, false, "" 131 | } 132 | addr = s[ind+1:] 133 | return 134 | } 135 | 136 | func isLoadingError(err error) bool { 137 | return strings.HasPrefix(err.Error(), "LOADING ") 138 | } 139 | 140 | func isReadOnlyError(err error) bool { 141 | return strings.HasPrefix(err.Error(), "READONLY ") 142 | } 143 | 144 | func isMovedSameConnAddr(err error, addr string) bool { 145 | redisError := err.Error() 146 | if !strings.HasPrefix(redisError, "MOVED ") { 147 | return false 148 | } 149 | return strings.HasSuffix(redisError, " "+addr) 150 | } 151 | 152 | //------------------------------------------------------------------------------ 153 | 154 | type timeoutError interface { 155 | Timeout() bool 156 | } 157 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Introduction 4 | 5 | We appreciate your interest in considering contributing to go-redis. 6 | Community contributions mean a lot to us. 7 | 8 | ## Contributions we need 9 | 10 | You may already know how you'd like to contribute, whether it's a fix for a bug you 11 | encountered, or a new feature your team wants to use. 12 | 13 | If you don't know where to start, consider improving 14 | documentation, bug triaging, and writing tutorials are all examples of 15 | helpful contributions that mean less work for you. 16 | 17 | ## Your First Contribution 18 | 19 | Unsure where to begin contributing? You can start by looking through 20 | [help-wanted 21 | issues](https://github.com/redis/go-redis/issues?q=is%3Aopen+is%3Aissue+label%3ahelp-wanted). 22 | 23 | Never contributed to open source before? Here are a couple of friendly 24 | tutorials: 25 | 26 | - 27 | - 28 | 29 | ## Getting Started 30 | 31 | Here's how to get started with your code contribution: 32 | 33 | 1. Create your own fork of go-redis 34 | 2. Do the changes in your fork 35 | 3. If you need a development environment, run `make test`. Note: this clones and builds the latest release of [redis](https://redis.io). You also need a redis-stack-server docker, in order to run the capabilities tests. This can be started by running: 36 | ```docker run -p 6379:6379 -it redis/redis-stack-server:edge``` 37 | 4. While developing, make sure the tests pass by running `make tests` 38 | 5. If you like the change and think the project could use it, send a 39 | pull request 40 | 41 | To see what else is part of the automation, run `invoke -l` 42 | 43 | ## Testing 44 | 45 | Call `make test` to run all tests, including linters. 46 | 47 | Continuous Integration uses these same wrappers to run all of these 48 | tests against multiple versions of python. Feel free to test your 49 | changes against all the go versions supported, as declared by the 50 | [build.yml](./.github/workflows/build.yml) file. 51 | 52 | ### Troubleshooting 53 | 54 | If you get any errors when running `make test`, make sure 55 | that you are using supported versions of Docker and go. 56 | 57 | ## How to Report a Bug 58 | 59 | ### Security Vulnerabilities 60 | 61 | **NOTE**: If you find a security vulnerability, do NOT open an issue. 62 | Email [Redis Open Source ()](mailto:oss@redis.com) instead. 63 | 64 | In order to determine whether you are dealing with a security issue, ask 65 | yourself these two questions: 66 | 67 | - Can I access something that's not mine, or something I shouldn't 68 | have access to? 69 | - Can I disable something for other people? 70 | 71 | If the answer to either of those two questions are *yes*, then you're 72 | probably dealing with a security issue. Note that even if you answer 73 | *no* to both questions, you may still be dealing with a security 74 | issue, so if you're unsure, just email [us](mailto:oss@redis.com). 75 | 76 | ### Everything Else 77 | 78 | When filing an issue, make sure to answer these five questions: 79 | 80 | 1. What version of go-redis are you using? 81 | 2. What version of redis are you using? 82 | 3. What did you do? 83 | 4. What did you expect to see? 84 | 5. What did you see instead? 85 | 86 | ## Suggest a feature or enhancement 87 | 88 | If you'd like to contribute a new feature, make sure you check our 89 | issue list to see if someone has already proposed it. Work may already 90 | be underway on the feature you want or we may have rejected a 91 | feature like it already. 92 | 93 | If you don't see anything, open a new issue that describes the feature 94 | you would like and how it should work. 95 | 96 | ## Code review process 97 | 98 | The core team regularly looks at pull requests. We will provide 99 | feedback as soon as possible. After receiving our feedback, please respond 100 | within two weeks. After that time, we may close your PR if it isn't 101 | showing any activity. 102 | -------------------------------------------------------------------------------- /example/redis-bloom/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand" 7 | 8 | "github.com/redis/go-redis/v9" 9 | ) 10 | 11 | func main() { 12 | ctx := context.Background() 13 | 14 | rdb := redis.NewClient(&redis.Options{ 15 | Addr: ":6379", 16 | }) 17 | _ = rdb.FlushDB(ctx).Err() 18 | 19 | fmt.Printf("# BLOOM\n") 20 | bloomFilter(ctx, rdb) 21 | 22 | fmt.Printf("\n# CUCKOO\n") 23 | cuckooFilter(ctx, rdb) 24 | 25 | fmt.Printf("\n# COUNT-MIN\n") 26 | countMinSketch(ctx, rdb) 27 | 28 | fmt.Printf("\n# TOP-K\n") 29 | topK(ctx, rdb) 30 | } 31 | 32 | func bloomFilter(ctx context.Context, rdb *redis.Client) { 33 | inserted, err := rdb.Do(ctx, "BF.ADD", "bf_key", "item0").Bool() 34 | if err != nil { 35 | panic(err) 36 | } 37 | if inserted { 38 | fmt.Println("item0 was inserted") 39 | } else { 40 | fmt.Println("item0 already exists") 41 | } 42 | 43 | for _, item := range []string{"item0", "item1"} { 44 | exists, err := rdb.Do(ctx, "BF.EXISTS", "bf_key", item).Bool() 45 | if err != nil { 46 | panic(err) 47 | } 48 | if exists { 49 | fmt.Printf("%s does exist\n", item) 50 | } else { 51 | fmt.Printf("%s does not exist\n", item) 52 | } 53 | } 54 | 55 | bools, err := rdb.Do(ctx, "BF.MADD", "bf_key", "item1", "item2", "item3").BoolSlice() 56 | if err != nil { 57 | panic(err) 58 | } 59 | fmt.Println("adding multiple items:", bools) 60 | } 61 | 62 | func cuckooFilter(ctx context.Context, rdb *redis.Client) { 63 | inserted, err := rdb.Do(ctx, "CF.ADDNX", "cf_key", "item0").Bool() 64 | if err != nil { 65 | panic(err) 66 | } 67 | if inserted { 68 | fmt.Println("item0 was inserted") 69 | } else { 70 | fmt.Println("item0 already exists") 71 | } 72 | 73 | for _, item := range []string{"item0", "item1"} { 74 | exists, err := rdb.Do(ctx, "CF.EXISTS", "cf_key", item).Bool() 75 | if err != nil { 76 | panic(err) 77 | } 78 | if exists { 79 | fmt.Printf("%s does exist\n", item) 80 | } else { 81 | fmt.Printf("%s does not exist\n", item) 82 | } 83 | } 84 | 85 | deleted, err := rdb.Do(ctx, "CF.DEL", "cf_key", "item0").Bool() 86 | if err != nil { 87 | panic(err) 88 | } 89 | if deleted { 90 | fmt.Println("item0 was deleted") 91 | } 92 | } 93 | 94 | func countMinSketch(ctx context.Context, rdb *redis.Client) { 95 | if err := rdb.Do(ctx, "CMS.INITBYPROB", "count_min", 0.001, 0.01).Err(); err != nil { 96 | panic(err) 97 | } 98 | 99 | items := []string{"item1", "item2", "item3", "item4", "item5"} 100 | counts := make(map[string]int, len(items)) 101 | 102 | for i := 0; i < 10000; i++ { 103 | n := rand.Intn(len(items)) 104 | item := items[n] 105 | 106 | if err := rdb.Do(ctx, "CMS.INCRBY", "count_min", item, 1).Err(); err != nil { 107 | panic(err) 108 | } 109 | counts[item]++ 110 | } 111 | 112 | for item, count := range counts { 113 | ns, err := rdb.Do(ctx, "CMS.QUERY", "count_min", item).Int64Slice() 114 | if err != nil { 115 | panic(err) 116 | } 117 | fmt.Printf("%s: count-min=%d actual=%d\n", item, ns[0], count) 118 | } 119 | } 120 | 121 | func topK(ctx context.Context, rdb *redis.Client) { 122 | if err := rdb.Do(ctx, "TOPK.RESERVE", "top_items", 3).Err(); err != nil { 123 | panic(err) 124 | } 125 | 126 | counts := map[string]int{ 127 | "item1": 1000, 128 | "item2": 2000, 129 | "item3": 3000, 130 | "item4": 4000, 131 | "item5": 5000, 132 | "item6": 6000, 133 | } 134 | 135 | for item, count := range counts { 136 | for i := 0; i < count; i++ { 137 | if err := rdb.Do(ctx, "TOPK.INCRBY", "top_items", item, 1).Err(); err != nil { 138 | panic(err) 139 | } 140 | } 141 | } 142 | 143 | items, err := rdb.Do(ctx, "TOPK.LIST", "top_items").StringSlice() 144 | if err != nil { 145 | panic(err) 146 | } 147 | 148 | for _, item := range items { 149 | ns, err := rdb.Do(ctx, "TOPK.COUNT", "top_items", item).Int64Slice() 150 | if err != nil { 151 | panic(err) 152 | } 153 | fmt.Printf("%s: top-k=%d actual=%d\n", item, ns[0], counts[item]) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /tx_test.go: -------------------------------------------------------------------------------- 1 | package redis_test 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | "sync" 7 | 8 | . "github.com/bsm/ginkgo/v2" 9 | . "github.com/bsm/gomega" 10 | 11 | "github.com/redis/go-redis/v9" 12 | ) 13 | 14 | var _ = Describe("Tx", func() { 15 | var client *redis.Client 16 | 17 | BeforeEach(func() { 18 | client = redis.NewClient(redisOptions()) 19 | Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) 20 | }) 21 | 22 | AfterEach(func() { 23 | Expect(client.Close()).NotTo(HaveOccurred()) 24 | }) 25 | 26 | It("should Watch", func() { 27 | var incr func(string) error 28 | 29 | // Transactionally increments key using GET and SET commands. 30 | incr = func(key string) error { 31 | err := client.Watch(ctx, func(tx *redis.Tx) error { 32 | n, err := tx.Get(ctx, key).Int64() 33 | if err != nil && err != redis.Nil { 34 | return err 35 | } 36 | 37 | _, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error { 38 | pipe.Set(ctx, key, strconv.FormatInt(n+1, 10), 0) 39 | return nil 40 | }) 41 | return err 42 | }, key) 43 | if err == redis.TxFailedErr { 44 | return incr(key) 45 | } 46 | return err 47 | } 48 | 49 | var wg sync.WaitGroup 50 | for i := 0; i < 100; i++ { 51 | wg.Add(1) 52 | go func() { 53 | defer GinkgoRecover() 54 | defer wg.Done() 55 | 56 | err := incr("key") 57 | Expect(err).NotTo(HaveOccurred()) 58 | }() 59 | } 60 | wg.Wait() 61 | 62 | n, err := client.Get(ctx, "key").Int64() 63 | Expect(err).NotTo(HaveOccurred()) 64 | Expect(n).To(Equal(int64(100))) 65 | }) 66 | 67 | It("should discard", func() { 68 | err := client.Watch(ctx, func(tx *redis.Tx) error { 69 | cmds, err := tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error { 70 | pipe.Set(ctx, "key1", "hello1", 0) 71 | pipe.Discard() 72 | pipe.Set(ctx, "key2", "hello2", 0) 73 | return nil 74 | }) 75 | Expect(err).NotTo(HaveOccurred()) 76 | Expect(cmds).To(HaveLen(1)) 77 | return err 78 | }, "key1", "key2") 79 | Expect(err).NotTo(HaveOccurred()) 80 | 81 | get := client.Get(ctx, "key1") 82 | Expect(get.Err()).To(Equal(redis.Nil)) 83 | Expect(get.Val()).To(Equal("")) 84 | 85 | get = client.Get(ctx, "key2") 86 | Expect(get.Err()).NotTo(HaveOccurred()) 87 | Expect(get.Val()).To(Equal("hello2")) 88 | }) 89 | 90 | It("returns no error when there are no commands", func() { 91 | err := client.Watch(ctx, func(tx *redis.Tx) error { 92 | _, err := tx.TxPipelined(ctx, func(redis.Pipeliner) error { return nil }) 93 | return err 94 | }) 95 | Expect(err).NotTo(HaveOccurred()) 96 | 97 | v, err := client.Ping(ctx).Result() 98 | Expect(err).NotTo(HaveOccurred()) 99 | Expect(v).To(Equal("PONG")) 100 | }) 101 | 102 | It("should exec bulks", func() { 103 | const N = 20000 104 | 105 | err := client.Watch(ctx, func(tx *redis.Tx) error { 106 | cmds, err := tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error { 107 | for i := 0; i < N; i++ { 108 | pipe.Incr(ctx, "key") 109 | } 110 | return nil 111 | }) 112 | Expect(err).NotTo(HaveOccurred()) 113 | Expect(len(cmds)).To(Equal(N)) 114 | for _, cmd := range cmds { 115 | Expect(cmd.Err()).NotTo(HaveOccurred()) 116 | } 117 | return err 118 | }) 119 | Expect(err).NotTo(HaveOccurred()) 120 | 121 | num, err := client.Get(ctx, "key").Int64() 122 | Expect(err).NotTo(HaveOccurred()) 123 | Expect(num).To(Equal(int64(N))) 124 | }) 125 | 126 | It("should recover from bad connection", func() { 127 | // Put bad connection in the pool. 128 | cn, err := client.Pool().Get(context.Background()) 129 | Expect(err).NotTo(HaveOccurred()) 130 | 131 | cn.SetNetConn(&badConn{}) 132 | client.Pool().Put(ctx, cn) 133 | 134 | do := func() error { 135 | err := client.Watch(ctx, func(tx *redis.Tx) error { 136 | _, err := tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error { 137 | pipe.Ping(ctx) 138 | return nil 139 | }) 140 | return err 141 | }) 142 | return err 143 | } 144 | 145 | err = do() 146 | Expect(err).NotTo(HaveOccurred()) 147 | }) 148 | }) 149 | -------------------------------------------------------------------------------- /internal/proto/writer.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | import ( 4 | "encoding" 5 | "fmt" 6 | "io" 7 | "net" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/redis/go-redis/v9/internal/util" 12 | ) 13 | 14 | type writer interface { 15 | io.Writer 16 | io.ByteWriter 17 | // WriteString implement io.StringWriter. 18 | WriteString(s string) (n int, err error) 19 | } 20 | 21 | type Writer struct { 22 | writer 23 | 24 | lenBuf []byte 25 | numBuf []byte 26 | } 27 | 28 | func NewWriter(wr writer) *Writer { 29 | return &Writer{ 30 | writer: wr, 31 | 32 | lenBuf: make([]byte, 64), 33 | numBuf: make([]byte, 64), 34 | } 35 | } 36 | 37 | func (w *Writer) WriteArgs(args []interface{}) error { 38 | if err := w.WriteByte(RespArray); err != nil { 39 | return err 40 | } 41 | 42 | if err := w.writeLen(len(args)); err != nil { 43 | return err 44 | } 45 | 46 | for _, arg := range args { 47 | if err := w.WriteArg(arg); err != nil { 48 | return err 49 | } 50 | } 51 | 52 | return nil 53 | } 54 | 55 | func (w *Writer) writeLen(n int) error { 56 | w.lenBuf = strconv.AppendUint(w.lenBuf[:0], uint64(n), 10) 57 | w.lenBuf = append(w.lenBuf, '\r', '\n') 58 | _, err := w.Write(w.lenBuf) 59 | return err 60 | } 61 | 62 | func (w *Writer) WriteArg(v interface{}) error { 63 | switch v := v.(type) { 64 | case nil: 65 | return w.string("") 66 | case string: 67 | return w.string(v) 68 | case *string: 69 | return w.string(*v) 70 | case []byte: 71 | return w.bytes(v) 72 | case int: 73 | return w.int(int64(v)) 74 | case *int: 75 | return w.int(int64(*v)) 76 | case int8: 77 | return w.int(int64(v)) 78 | case *int8: 79 | return w.int(int64(*v)) 80 | case int16: 81 | return w.int(int64(v)) 82 | case *int16: 83 | return w.int(int64(*v)) 84 | case int32: 85 | return w.int(int64(v)) 86 | case *int32: 87 | return w.int(int64(*v)) 88 | case int64: 89 | return w.int(v) 90 | case *int64: 91 | return w.int(*v) 92 | case uint: 93 | return w.uint(uint64(v)) 94 | case *uint: 95 | return w.uint(uint64(*v)) 96 | case uint8: 97 | return w.uint(uint64(v)) 98 | case *uint8: 99 | return w.uint(uint64(*v)) 100 | case uint16: 101 | return w.uint(uint64(v)) 102 | case *uint16: 103 | return w.uint(uint64(*v)) 104 | case uint32: 105 | return w.uint(uint64(v)) 106 | case *uint32: 107 | return w.uint(uint64(*v)) 108 | case uint64: 109 | return w.uint(v) 110 | case *uint64: 111 | return w.uint(*v) 112 | case float32: 113 | return w.float(float64(v)) 114 | case *float32: 115 | return w.float(float64(*v)) 116 | case float64: 117 | return w.float(v) 118 | case *float64: 119 | return w.float(*v) 120 | case bool: 121 | if v { 122 | return w.int(1) 123 | } 124 | return w.int(0) 125 | case *bool: 126 | if *v { 127 | return w.int(1) 128 | } 129 | return w.int(0) 130 | case time.Time: 131 | w.numBuf = v.AppendFormat(w.numBuf[:0], time.RFC3339Nano) 132 | return w.bytes(w.numBuf) 133 | case time.Duration: 134 | return w.int(v.Nanoseconds()) 135 | case encoding.BinaryMarshaler: 136 | b, err := v.MarshalBinary() 137 | if err != nil { 138 | return err 139 | } 140 | return w.bytes(b) 141 | case net.IP: 142 | return w.bytes(v) 143 | default: 144 | return fmt.Errorf( 145 | "redis: can't marshal %T (implement encoding.BinaryMarshaler)", v) 146 | } 147 | } 148 | 149 | func (w *Writer) bytes(b []byte) error { 150 | if err := w.WriteByte(RespString); err != nil { 151 | return err 152 | } 153 | 154 | if err := w.writeLen(len(b)); err != nil { 155 | return err 156 | } 157 | 158 | if _, err := w.Write(b); err != nil { 159 | return err 160 | } 161 | 162 | return w.crlf() 163 | } 164 | 165 | func (w *Writer) string(s string) error { 166 | return w.bytes(util.StringToBytes(s)) 167 | } 168 | 169 | func (w *Writer) uint(n uint64) error { 170 | w.numBuf = strconv.AppendUint(w.numBuf[:0], n, 10) 171 | return w.bytes(w.numBuf) 172 | } 173 | 174 | func (w *Writer) int(n int64) error { 175 | w.numBuf = strconv.AppendInt(w.numBuf[:0], n, 10) 176 | return w.bytes(w.numBuf) 177 | } 178 | 179 | func (w *Writer) float(f float64) error { 180 | w.numBuf = strconv.AppendFloat(w.numBuf[:0], f, 'f', -1, 64) 181 | return w.bytes(w.numBuf) 182 | } 183 | 184 | func (w *Writer) crlf() error { 185 | if err := w.WriteByte('\r'); err != nil { 186 | return err 187 | } 188 | return w.WriteByte('\n') 189 | } 190 | -------------------------------------------------------------------------------- /internal/proto/scan.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | import ( 4 | "encoding" 5 | "fmt" 6 | "net" 7 | "reflect" 8 | "time" 9 | 10 | "github.com/redis/go-redis/v9/internal/util" 11 | ) 12 | 13 | // Scan parses bytes `b` to `v` with appropriate type. 14 | // 15 | //nolint:gocyclo 16 | func Scan(b []byte, v interface{}) error { 17 | switch v := v.(type) { 18 | case nil: 19 | return fmt.Errorf("redis: Scan(nil)") 20 | case *string: 21 | *v = util.BytesToString(b) 22 | return nil 23 | case *[]byte: 24 | *v = b 25 | return nil 26 | case *int: 27 | var err error 28 | *v, err = util.Atoi(b) 29 | return err 30 | case *int8: 31 | n, err := util.ParseInt(b, 10, 8) 32 | if err != nil { 33 | return err 34 | } 35 | *v = int8(n) 36 | return nil 37 | case *int16: 38 | n, err := util.ParseInt(b, 10, 16) 39 | if err != nil { 40 | return err 41 | } 42 | *v = int16(n) 43 | return nil 44 | case *int32: 45 | n, err := util.ParseInt(b, 10, 32) 46 | if err != nil { 47 | return err 48 | } 49 | *v = int32(n) 50 | return nil 51 | case *int64: 52 | n, err := util.ParseInt(b, 10, 64) 53 | if err != nil { 54 | return err 55 | } 56 | *v = n 57 | return nil 58 | case *uint: 59 | n, err := util.ParseUint(b, 10, 64) 60 | if err != nil { 61 | return err 62 | } 63 | *v = uint(n) 64 | return nil 65 | case *uint8: 66 | n, err := util.ParseUint(b, 10, 8) 67 | if err != nil { 68 | return err 69 | } 70 | *v = uint8(n) 71 | return nil 72 | case *uint16: 73 | n, err := util.ParseUint(b, 10, 16) 74 | if err != nil { 75 | return err 76 | } 77 | *v = uint16(n) 78 | return nil 79 | case *uint32: 80 | n, err := util.ParseUint(b, 10, 32) 81 | if err != nil { 82 | return err 83 | } 84 | *v = uint32(n) 85 | return nil 86 | case *uint64: 87 | n, err := util.ParseUint(b, 10, 64) 88 | if err != nil { 89 | return err 90 | } 91 | *v = n 92 | return nil 93 | case *float32: 94 | n, err := util.ParseFloat(b, 32) 95 | if err != nil { 96 | return err 97 | } 98 | *v = float32(n) 99 | return err 100 | case *float64: 101 | var err error 102 | *v, err = util.ParseFloat(b, 64) 103 | return err 104 | case *bool: 105 | *v = len(b) == 1 && b[0] == '1' 106 | return nil 107 | case *time.Time: 108 | var err error 109 | *v, err = time.Parse(time.RFC3339Nano, util.BytesToString(b)) 110 | return err 111 | case *time.Duration: 112 | n, err := util.ParseInt(b, 10, 64) 113 | if err != nil { 114 | return err 115 | } 116 | *v = time.Duration(n) 117 | return nil 118 | case encoding.BinaryUnmarshaler: 119 | return v.UnmarshalBinary(b) 120 | case *net.IP: 121 | *v = b 122 | return nil 123 | default: 124 | return fmt.Errorf( 125 | "redis: can't unmarshal %T (consider implementing BinaryUnmarshaler)", v) 126 | } 127 | } 128 | 129 | func ScanSlice(data []string, slice interface{}) error { 130 | v := reflect.ValueOf(slice) 131 | if !v.IsValid() { 132 | return fmt.Errorf("redis: ScanSlice(nil)") 133 | } 134 | if v.Kind() != reflect.Ptr { 135 | return fmt.Errorf("redis: ScanSlice(non-pointer %T)", slice) 136 | } 137 | v = v.Elem() 138 | if v.Kind() != reflect.Slice { 139 | return fmt.Errorf("redis: ScanSlice(non-slice %T)", slice) 140 | } 141 | 142 | next := makeSliceNextElemFunc(v) 143 | for i, s := range data { 144 | elem := next() 145 | if err := Scan([]byte(s), elem.Addr().Interface()); err != nil { 146 | err = fmt.Errorf("redis: ScanSlice index=%d value=%q failed: %w", i, s, err) 147 | return err 148 | } 149 | } 150 | 151 | return nil 152 | } 153 | 154 | func makeSliceNextElemFunc(v reflect.Value) func() reflect.Value { 155 | elemType := v.Type().Elem() 156 | 157 | if elemType.Kind() == reflect.Ptr { 158 | elemType = elemType.Elem() 159 | return func() reflect.Value { 160 | if v.Len() < v.Cap() { 161 | v.Set(v.Slice(0, v.Len()+1)) 162 | elem := v.Index(v.Len() - 1) 163 | if elem.IsNil() { 164 | elem.Set(reflect.New(elemType)) 165 | } 166 | return elem.Elem() 167 | } 168 | 169 | elem := reflect.New(elemType) 170 | v.Set(reflect.Append(v, elem)) 171 | return elem.Elem() 172 | } 173 | } 174 | 175 | zero := reflect.Zero(elemType) 176 | return func() reflect.Value { 177 | if v.Len() < v.Cap() { 178 | v.Set(v.Slice(0, v.Len()+1)) 179 | return v.Index(v.Len() - 1) 180 | } 181 | 182 | v.Set(reflect.Append(v, zero)) 183 | return v.Index(v.Len() - 1) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /internal/proto/writer_test.go: -------------------------------------------------------------------------------- 1 | package proto_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding" 6 | "fmt" 7 | "net" 8 | "testing" 9 | "time" 10 | 11 | . "github.com/bsm/ginkgo/v2" 12 | . "github.com/bsm/gomega" 13 | 14 | "github.com/redis/go-redis/v9/internal/proto" 15 | "github.com/redis/go-redis/v9/internal/util" 16 | ) 17 | 18 | type MyType struct{} 19 | 20 | var _ encoding.BinaryMarshaler = (*MyType)(nil) 21 | 22 | func (t *MyType) MarshalBinary() ([]byte, error) { 23 | return []byte("hello"), nil 24 | } 25 | 26 | var _ = Describe("WriteBuffer", func() { 27 | var buf *bytes.Buffer 28 | var wr *proto.Writer 29 | 30 | BeforeEach(func() { 31 | buf = new(bytes.Buffer) 32 | wr = proto.NewWriter(buf) 33 | }) 34 | 35 | It("should write args", func() { 36 | err := wr.WriteArgs([]interface{}{ 37 | "string", 38 | 12, 39 | 34.56, 40 | []byte{'b', 'y', 't', 'e', 's'}, 41 | true, 42 | nil, 43 | }) 44 | Expect(err).NotTo(HaveOccurred()) 45 | 46 | Expect(buf.Bytes()).To(Equal([]byte("*6\r\n" + 47 | "$6\r\nstring\r\n" + 48 | "$2\r\n12\r\n" + 49 | "$5\r\n34.56\r\n" + 50 | "$5\r\nbytes\r\n" + 51 | "$1\r\n1\r\n" + 52 | "$0\r\n" + 53 | "\r\n"))) 54 | }) 55 | 56 | It("should append time", func() { 57 | tm := time.Date(2019, 1, 1, 9, 45, 10, 222125, time.UTC) 58 | err := wr.WriteArgs([]interface{}{tm}) 59 | Expect(err).NotTo(HaveOccurred()) 60 | 61 | Expect(buf.Len()).To(Equal(41)) 62 | }) 63 | 64 | It("should append marshalable args", func() { 65 | err := wr.WriteArgs([]interface{}{&MyType{}}) 66 | Expect(err).NotTo(HaveOccurred()) 67 | 68 | Expect(buf.Len()).To(Equal(15)) 69 | }) 70 | 71 | It("should append net.IP", func() { 72 | ip := net.ParseIP("192.168.1.1") 73 | err := wr.WriteArgs([]interface{}{ip}) 74 | Expect(err).NotTo(HaveOccurred()) 75 | Expect(buf.String()).To(Equal(fmt.Sprintf("*1\r\n$16\r\n%s\r\n", bytes.NewBuffer(ip)))) 76 | }) 77 | }) 78 | 79 | type discard struct{} 80 | 81 | func (discard) Write(b []byte) (int, error) { 82 | return len(b), nil 83 | } 84 | 85 | func (discard) WriteString(s string) (int, error) { 86 | return len(s), nil 87 | } 88 | 89 | func (discard) WriteByte(c byte) error { 90 | return nil 91 | } 92 | 93 | func BenchmarkWriteBuffer_Append(b *testing.B) { 94 | buf := proto.NewWriter(discard{}) 95 | args := []interface{}{"hello", "world", "foo", "bar"} 96 | 97 | for i := 0; i < b.N; i++ { 98 | err := buf.WriteArgs(args) 99 | if err != nil { 100 | b.Fatal(err) 101 | } 102 | } 103 | } 104 | 105 | var _ = Describe("WriteArg", func() { 106 | var buf *bytes.Buffer 107 | var wr *proto.Writer 108 | 109 | BeforeEach(func() { 110 | buf = new(bytes.Buffer) 111 | wr = proto.NewWriter(buf) 112 | }) 113 | 114 | args := map[any]string{ 115 | "hello": "$1\r\nhello\r\n", 116 | int(10): "$2\r\n10\r\n", 117 | util.ToPtr(int(10)): "$2\r\n10\r\n", 118 | int8(10): "$2\r\n10\r\n", 119 | util.ToPtr(int8(10)): "$2\r\n10\r\n", 120 | int16(10): "$2\r\n10\r\n", 121 | util.ToPtr(int16(10)): "$2\r\n10\r\n", 122 | int32(10): "$2\r\n10\r\n", 123 | util.ToPtr(int32(10)): "$2\r\n10\r\n", 124 | int64(10): "$2\r\n10\r\n", 125 | util.ToPtr(int64(10)): "$2\r\n10\r\n", 126 | uint(10): "$2\r\n10\r\n", 127 | util.ToPtr(uint(10)): "$2\r\n10\r\n", 128 | uint8(10): "$2\r\n10\r\n", 129 | util.ToPtr(uint8(10)): "$2\r\n10\r\n", 130 | uint16(10): "$2\r\n10\r\n", 131 | util.ToPtr(uint16(10)): "$2\r\n10\r\n", 132 | uint32(10): "$2\r\n10\r\n", 133 | util.ToPtr(uint32(10)): "$2\r\n10\r\n", 134 | uint64(10): "$2\r\n10\r\n", 135 | util.ToPtr(uint64(10)): "$2\r\n10\r\n", 136 | float32(10.3): "$4\r\n10.3\r\n", 137 | util.ToPtr(float32(10.3)): "$4\r\n10.3\r\n", 138 | float64(10.3): "$4\r\n10.3\r\n", 139 | util.ToPtr(float64(10.3)): "$4\r\n10.3\r\n", 140 | bool(true): "$1\r\n1\r\n", 141 | bool(false): "$1\r\n0\r\n", 142 | util.ToPtr(bool(true)): "$1\r\n1\r\n", 143 | util.ToPtr(bool(false)): "$1\r\n0\r\n", 144 | } 145 | 146 | for arg, expect := range args { 147 | It(fmt.Sprintf("should write arg of type %T", arg), func() { 148 | err := wr.WriteArg(arg) 149 | Expect(err).NotTo(HaveOccurred()) 150 | Expect(buf.String()).To(Equal(expect)) 151 | }) 152 | } 153 | }) 154 | -------------------------------------------------------------------------------- /bitmap_commands.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import "context" 4 | 5 | type BitMapCmdable interface { 6 | GetBit(ctx context.Context, key string, offset int64) *IntCmd 7 | SetBit(ctx context.Context, key string, offset int64, value int) *IntCmd 8 | BitCount(ctx context.Context, key string, bitCount *BitCount) *IntCmd 9 | BitOpAnd(ctx context.Context, destKey string, keys ...string) *IntCmd 10 | BitOpOr(ctx context.Context, destKey string, keys ...string) *IntCmd 11 | BitOpXor(ctx context.Context, destKey string, keys ...string) *IntCmd 12 | BitOpNot(ctx context.Context, destKey string, key string) *IntCmd 13 | BitPos(ctx context.Context, key string, bit int64, pos ...int64) *IntCmd 14 | BitPosSpan(ctx context.Context, key string, bit int8, start, end int64, span string) *IntCmd 15 | BitField(ctx context.Context, key string, values ...interface{}) *IntSliceCmd 16 | } 17 | 18 | func (c cmdable) GetBit(ctx context.Context, key string, offset int64) *IntCmd { 19 | cmd := NewIntCmd(ctx, "getbit", key, offset) 20 | _ = c(ctx, cmd) 21 | return cmd 22 | } 23 | 24 | func (c cmdable) SetBit(ctx context.Context, key string, offset int64, value int) *IntCmd { 25 | cmd := NewIntCmd( 26 | ctx, 27 | "setbit", 28 | key, 29 | offset, 30 | value, 31 | ) 32 | _ = c(ctx, cmd) 33 | return cmd 34 | } 35 | 36 | type BitCount struct { 37 | Start, End int64 38 | } 39 | 40 | func (c cmdable) BitCount(ctx context.Context, key string, bitCount *BitCount) *IntCmd { 41 | args := []interface{}{"bitcount", key} 42 | if bitCount != nil { 43 | args = append( 44 | args, 45 | bitCount.Start, 46 | bitCount.End, 47 | ) 48 | } 49 | cmd := NewIntCmd(ctx, args...) 50 | _ = c(ctx, cmd) 51 | return cmd 52 | } 53 | 54 | func (c cmdable) bitOp(ctx context.Context, op, destKey string, keys ...string) *IntCmd { 55 | args := make([]interface{}, 3+len(keys)) 56 | args[0] = "bitop" 57 | args[1] = op 58 | args[2] = destKey 59 | for i, key := range keys { 60 | args[3+i] = key 61 | } 62 | cmd := NewIntCmd(ctx, args...) 63 | _ = c(ctx, cmd) 64 | return cmd 65 | } 66 | 67 | func (c cmdable) BitOpAnd(ctx context.Context, destKey string, keys ...string) *IntCmd { 68 | return c.bitOp(ctx, "and", destKey, keys...) 69 | } 70 | 71 | func (c cmdable) BitOpOr(ctx context.Context, destKey string, keys ...string) *IntCmd { 72 | return c.bitOp(ctx, "or", destKey, keys...) 73 | } 74 | 75 | func (c cmdable) BitOpXor(ctx context.Context, destKey string, keys ...string) *IntCmd { 76 | return c.bitOp(ctx, "xor", destKey, keys...) 77 | } 78 | 79 | func (c cmdable) BitOpNot(ctx context.Context, destKey string, key string) *IntCmd { 80 | return c.bitOp(ctx, "not", destKey, key) 81 | } 82 | 83 | // BitPos is an API before Redis version 7.0, cmd: bitpos key bit start end 84 | // if you need the `byte | bit` parameter, please use `BitPosSpan`. 85 | func (c cmdable) BitPos(ctx context.Context, key string, bit int64, pos ...int64) *IntCmd { 86 | args := make([]interface{}, 3+len(pos)) 87 | args[0] = "bitpos" 88 | args[1] = key 89 | args[2] = bit 90 | switch len(pos) { 91 | case 0: 92 | case 1: 93 | args[3] = pos[0] 94 | case 2: 95 | args[3] = pos[0] 96 | args[4] = pos[1] 97 | default: 98 | panic("too many arguments") 99 | } 100 | cmd := NewIntCmd(ctx, args...) 101 | _ = c(ctx, cmd) 102 | return cmd 103 | } 104 | 105 | // BitPosSpan supports the `byte | bit` parameters in redis version 7.0, 106 | // the bitpos command defaults to using byte type for the `start-end` range, 107 | // which means it counts in bytes from start to end. you can set the value 108 | // of "span" to determine the type of `start-end`. 109 | // span = "bit", cmd: bitpos key bit start end bit 110 | // span = "byte", cmd: bitpos key bit start end byte 111 | func (c cmdable) BitPosSpan(ctx context.Context, key string, bit int8, start, end int64, span string) *IntCmd { 112 | cmd := NewIntCmd(ctx, "bitpos", key, bit, start, end, span) 113 | _ = c(ctx, cmd) 114 | return cmd 115 | } 116 | 117 | // BitField accepts multiple values: 118 | // - BitField("set", "i1", "offset1", "value1","cmd2", "type2", "offset2", "value2") 119 | // - BitField([]string{"cmd1", "type1", "offset1", "value1","cmd2", "type2", "offset2", "value2"}) 120 | // - BitField([]interface{}{"cmd1", "type1", "offset1", "value1","cmd2", "type2", "offset2", "value2"}) 121 | func (c cmdable) BitField(ctx context.Context, key string, values ...interface{}) *IntSliceCmd { 122 | args := make([]interface{}, 2, 2+len(values)) 123 | args[0] = "bitfield" 124 | args[1] = key 125 | args = appendArgs(args, values) 126 | cmd := NewIntSliceCmd(ctx, args...) 127 | _ = c(ctx, cmd) 128 | return cmd 129 | } 130 | -------------------------------------------------------------------------------- /gears_commands_test.go: -------------------------------------------------------------------------------- 1 | package redis_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | . "github.com/bsm/ginkgo/v2" 8 | . "github.com/bsm/gomega" 9 | 10 | "github.com/redis/go-redis/v9" 11 | ) 12 | 13 | func libCode(libName string) string { 14 | return fmt.Sprintf("#!js api_version=1.0 name=%s\n redis.registerFunction('foo', ()=>{{return 'bar'}})", libName) 15 | } 16 | 17 | func libCodeWithConfig(libName string) string { 18 | lib := `#!js api_version=1.0 name=%s 19 | 20 | var last_update_field_name = "__last_update__" 21 | 22 | if (redis.config.last_update_field_name !== undefined) { 23 | if (typeof redis.config.last_update_field_name != 'string') { 24 | throw "last_update_field_name must be a string"; 25 | } 26 | last_update_field_name = redis.config.last_update_field_name 27 | } 28 | 29 | redis.registerFunction("hset", function(client, key, field, val){ 30 | // get the current time in ms 31 | var curr_time = client.call("time")[0]; 32 | return client.call('hset', key, field, val, last_update_field_name, curr_time); 33 | });` 34 | return fmt.Sprintf(lib, libName) 35 | } 36 | 37 | var _ = Describe("RedisGears commands", Label("gears"), func() { 38 | ctx := context.TODO() 39 | var client *redis.Client 40 | 41 | BeforeEach(func() { 42 | client = redis.NewClient(&redis.Options{Addr: ":6379"}) 43 | Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) 44 | client.TFunctionDelete(ctx, "lib1") 45 | }) 46 | 47 | AfterEach(func() { 48 | Expect(client.Close()).NotTo(HaveOccurred()) 49 | }) 50 | 51 | It("should TFunctionLoad, TFunctionLoadArgs and TFunctionDelete ", Label("gears", "tfunctionload"), func() { 52 | resultAdd, err := client.TFunctionLoad(ctx, libCode("lib1")).Result() 53 | Expect(err).NotTo(HaveOccurred()) 54 | Expect(resultAdd).To(BeEquivalentTo("OK")) 55 | opt := &redis.TFunctionLoadOptions{Replace: true, Config: `{"last_update_field_name":"last_update"}`} 56 | resultAdd, err = client.TFunctionLoadArgs(ctx, libCodeWithConfig("lib1"), opt).Result() 57 | Expect(err).NotTo(HaveOccurred()) 58 | Expect(resultAdd).To(BeEquivalentTo("OK")) 59 | }) 60 | It("should TFunctionList", Label("gears", "tfunctionlist"), func() { 61 | resultAdd, err := client.TFunctionLoad(ctx, libCode("lib1")).Result() 62 | Expect(err).NotTo(HaveOccurred()) 63 | Expect(resultAdd).To(BeEquivalentTo("OK")) 64 | resultList, err := client.TFunctionList(ctx).Result() 65 | Expect(err).NotTo(HaveOccurred()) 66 | Expect(resultList[0]["engine"]).To(BeEquivalentTo("js")) 67 | opt := &redis.TFunctionListOptions{Withcode: true, Verbose: 2} 68 | resultListArgs, err := client.TFunctionListArgs(ctx, opt).Result() 69 | Expect(err).NotTo(HaveOccurred()) 70 | Expect(resultListArgs[0]["code"]).NotTo(BeEquivalentTo("")) 71 | }) 72 | 73 | It("should TFCall", Label("gears", "tfcall"), func() { 74 | var resultAdd interface{} 75 | resultAdd, err := client.TFunctionLoad(ctx, libCode("lib1")).Result() 76 | Expect(err).NotTo(HaveOccurred()) 77 | Expect(resultAdd).To(BeEquivalentTo("OK")) 78 | resultAdd, err = client.TFCall(ctx, "lib1", "foo", 0).Result() 79 | Expect(err).NotTo(HaveOccurred()) 80 | Expect(resultAdd).To(BeEquivalentTo("bar")) 81 | }) 82 | 83 | It("should TFCallArgs", Label("gears", "tfcallargs"), func() { 84 | var resultAdd interface{} 85 | resultAdd, err := client.TFunctionLoad(ctx, libCode("lib1")).Result() 86 | Expect(err).NotTo(HaveOccurred()) 87 | Expect(resultAdd).To(BeEquivalentTo("OK")) 88 | opt := &redis.TFCallOptions{Arguments: []string{"foo", "bar"}} 89 | resultAdd, err = client.TFCallArgs(ctx, "lib1", "foo", 0, opt).Result() 90 | Expect(err).NotTo(HaveOccurred()) 91 | Expect(resultAdd).To(BeEquivalentTo("bar")) 92 | }) 93 | 94 | It("should TFCallASYNC", Label("gears", "TFCallASYNC"), func() { 95 | var resultAdd interface{} 96 | resultAdd, err := client.TFunctionLoad(ctx, libCode("lib1")).Result() 97 | Expect(err).NotTo(HaveOccurred()) 98 | Expect(resultAdd).To(BeEquivalentTo("OK")) 99 | resultAdd, err = client.TFCallASYNC(ctx, "lib1", "foo", 0).Result() 100 | Expect(err).NotTo(HaveOccurred()) 101 | Expect(resultAdd).To(BeEquivalentTo("bar")) 102 | }) 103 | 104 | It("should TFCallASYNCArgs", Label("gears", "TFCallASYNCargs"), func() { 105 | var resultAdd interface{} 106 | resultAdd, err := client.TFunctionLoad(ctx, libCode("lib1")).Result() 107 | Expect(err).NotTo(HaveOccurred()) 108 | Expect(resultAdd).To(BeEquivalentTo("OK")) 109 | opt := &redis.TFCallOptions{Arguments: []string{"foo", "bar"}} 110 | resultAdd, err = client.TFCallASYNCArgs(ctx, "lib1", "foo", 0, opt).Result() 111 | Expect(err).NotTo(HaveOccurred()) 112 | Expect(resultAdd).To(BeEquivalentTo("bar")) 113 | }) 114 | }) 115 | -------------------------------------------------------------------------------- /tx.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/redis/go-redis/v9/internal/pool" 7 | "github.com/redis/go-redis/v9/internal/proto" 8 | ) 9 | 10 | // TxFailedErr transaction redis failed. 11 | const TxFailedErr = proto.RedisError("redis: transaction failed") 12 | 13 | // Tx implements Redis transactions as described in 14 | // http://redis.io/topics/transactions. It's NOT safe for concurrent use 15 | // by multiple goroutines, because Exec resets list of watched keys. 16 | // 17 | // If you don't need WATCH, use Pipeline instead. 18 | type Tx struct { 19 | baseClient 20 | cmdable 21 | statefulCmdable 22 | hooksMixin 23 | } 24 | 25 | func (c *Client) newTx() *Tx { 26 | tx := Tx{ 27 | baseClient: baseClient{ 28 | opt: c.opt, 29 | connPool: pool.NewStickyConnPool(c.connPool), 30 | }, 31 | hooksMixin: c.hooksMixin.clone(), 32 | } 33 | tx.init() 34 | return &tx 35 | } 36 | 37 | func (c *Tx) init() { 38 | c.cmdable = c.Process 39 | c.statefulCmdable = c.Process 40 | 41 | c.initHooks(hooks{ 42 | dial: c.baseClient.dial, 43 | process: c.baseClient.process, 44 | pipeline: c.baseClient.processPipeline, 45 | txPipeline: c.baseClient.processTxPipeline, 46 | }) 47 | } 48 | 49 | func (c *Tx) Process(ctx context.Context, cmd Cmder) error { 50 | err := c.processHook(ctx, cmd) 51 | cmd.SetErr(err) 52 | return err 53 | } 54 | 55 | // Watch prepares a transaction and marks the keys to be watched 56 | // for conditional execution if there are any keys. 57 | // 58 | // The transaction is automatically closed when fn exits. 59 | func (c *Client) Watch(ctx context.Context, fn func(*Tx) error, keys ...string) error { 60 | tx := c.newTx() 61 | defer tx.Close(ctx) 62 | if len(keys) > 0 { 63 | if err := tx.Watch(ctx, keys...).Err(); err != nil { 64 | return err 65 | } 66 | } 67 | return fn(tx) 68 | } 69 | 70 | // Close closes the transaction, releasing any open resources. 71 | func (c *Tx) Close(ctx context.Context) error { 72 | _ = c.Unwatch(ctx).Err() 73 | return c.baseClient.Close() 74 | } 75 | 76 | // Watch marks the keys to be watched for conditional execution 77 | // of a transaction. 78 | func (c *Tx) Watch(ctx context.Context, keys ...string) *StatusCmd { 79 | args := make([]interface{}, 1+len(keys)) 80 | args[0] = "watch" 81 | for i, key := range keys { 82 | args[1+i] = key 83 | } 84 | cmd := NewStatusCmd(ctx, args...) 85 | _ = c.Process(ctx, cmd) 86 | return cmd 87 | } 88 | 89 | // Unwatch flushes all the previously watched keys for a transaction. 90 | func (c *Tx) Unwatch(ctx context.Context, keys ...string) *StatusCmd { 91 | args := make([]interface{}, 1+len(keys)) 92 | args[0] = "unwatch" 93 | for i, key := range keys { 94 | args[1+i] = key 95 | } 96 | cmd := NewStatusCmd(ctx, args...) 97 | _ = c.Process(ctx, cmd) 98 | return cmd 99 | } 100 | 101 | // Pipeline creates a pipeline. Usually it is more convenient to use Pipelined. 102 | func (c *Tx) Pipeline() Pipeliner { 103 | pipe := Pipeline{ 104 | exec: func(ctx context.Context, cmds []Cmder) error { 105 | return c.processPipelineHook(ctx, cmds) 106 | }, 107 | } 108 | pipe.init() 109 | return &pipe 110 | } 111 | 112 | // Pipelined executes commands queued in the fn outside of the transaction. 113 | // Use TxPipelined if you need transactional behavior. 114 | func (c *Tx) Pipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) { 115 | return c.Pipeline().Pipelined(ctx, fn) 116 | } 117 | 118 | // TxPipelined executes commands queued in the fn in the transaction. 119 | // 120 | // When using WATCH, EXEC will execute commands only if the watched keys 121 | // were not modified, allowing for a check-and-set mechanism. 122 | // 123 | // Exec always returns list of commands. If transaction fails 124 | // TxFailedErr is returned. Otherwise Exec returns an error of the first 125 | // failed command or nil. 126 | func (c *Tx) TxPipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) { 127 | return c.TxPipeline().Pipelined(ctx, fn) 128 | } 129 | 130 | // TxPipeline creates a pipeline. Usually it is more convenient to use TxPipelined. 131 | func (c *Tx) TxPipeline() Pipeliner { 132 | pipe := Pipeline{ 133 | exec: func(ctx context.Context, cmds []Cmder) error { 134 | cmds = wrapMultiExec(ctx, cmds) 135 | return c.processTxPipelineHook(ctx, cmds) 136 | }, 137 | } 138 | pipe.init() 139 | return &pipe 140 | } 141 | 142 | func wrapMultiExec(ctx context.Context, cmds []Cmder) []Cmder { 143 | if len(cmds) == 0 { 144 | panic("not reached") 145 | } 146 | cmdsCopy := make([]Cmder, len(cmds)+2) 147 | cmdsCopy[0] = NewStatusCmd(ctx, "multi") 148 | copy(cmdsCopy[1:], cmds) 149 | cmdsCopy[len(cmdsCopy)-1] = NewSliceCmd(ctx, "exec") 150 | return cmdsCopy 151 | } 152 | -------------------------------------------------------------------------------- /internal/pool/pool_sticky.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "sync/atomic" 8 | ) 9 | 10 | const ( 11 | stateDefault = 0 12 | stateInited = 1 13 | stateClosed = 2 14 | ) 15 | 16 | type BadConnError struct { 17 | wrapped error 18 | } 19 | 20 | var _ error = (*BadConnError)(nil) 21 | 22 | func (e BadConnError) Error() string { 23 | s := "redis: Conn is in a bad state" 24 | if e.wrapped != nil { 25 | s += ": " + e.wrapped.Error() 26 | } 27 | return s 28 | } 29 | 30 | func (e BadConnError) Unwrap() error { 31 | return e.wrapped 32 | } 33 | 34 | //------------------------------------------------------------------------------ 35 | 36 | type StickyConnPool struct { 37 | pool Pooler 38 | shared int32 // atomic 39 | 40 | state uint32 // atomic 41 | ch chan *Conn 42 | 43 | _badConnError atomic.Value 44 | } 45 | 46 | var _ Pooler = (*StickyConnPool)(nil) 47 | 48 | func NewStickyConnPool(pool Pooler) *StickyConnPool { 49 | p, ok := pool.(*StickyConnPool) 50 | if !ok { 51 | p = &StickyConnPool{ 52 | pool: pool, 53 | ch: make(chan *Conn, 1), 54 | } 55 | } 56 | atomic.AddInt32(&p.shared, 1) 57 | return p 58 | } 59 | 60 | func (p *StickyConnPool) NewConn(ctx context.Context) (*Conn, error) { 61 | return p.pool.NewConn(ctx) 62 | } 63 | 64 | func (p *StickyConnPool) CloseConn(cn *Conn) error { 65 | return p.pool.CloseConn(cn) 66 | } 67 | 68 | func (p *StickyConnPool) Get(ctx context.Context) (*Conn, error) { 69 | // In worst case this races with Close which is not a very common operation. 70 | for i := 0; i < 1000; i++ { 71 | switch atomic.LoadUint32(&p.state) { 72 | case stateDefault: 73 | cn, err := p.pool.Get(ctx) 74 | if err != nil { 75 | return nil, err 76 | } 77 | if atomic.CompareAndSwapUint32(&p.state, stateDefault, stateInited) { 78 | return cn, nil 79 | } 80 | p.pool.Remove(ctx, cn, ErrClosed) 81 | case stateInited: 82 | if err := p.badConnError(); err != nil { 83 | return nil, err 84 | } 85 | cn, ok := <-p.ch 86 | if !ok { 87 | return nil, ErrClosed 88 | } 89 | return cn, nil 90 | case stateClosed: 91 | return nil, ErrClosed 92 | default: 93 | panic("not reached") 94 | } 95 | } 96 | return nil, fmt.Errorf("redis: StickyConnPool.Get: infinite loop") 97 | } 98 | 99 | func (p *StickyConnPool) Put(ctx context.Context, cn *Conn) { 100 | defer func() { 101 | if recover() != nil { 102 | p.freeConn(ctx, cn) 103 | } 104 | }() 105 | p.ch <- cn 106 | } 107 | 108 | func (p *StickyConnPool) freeConn(ctx context.Context, cn *Conn) { 109 | if err := p.badConnError(); err != nil { 110 | p.pool.Remove(ctx, cn, err) 111 | } else { 112 | p.pool.Put(ctx, cn) 113 | } 114 | } 115 | 116 | func (p *StickyConnPool) Remove(ctx context.Context, cn *Conn, reason error) { 117 | defer func() { 118 | if recover() != nil { 119 | p.pool.Remove(ctx, cn, ErrClosed) 120 | } 121 | }() 122 | p._badConnError.Store(BadConnError{wrapped: reason}) 123 | p.ch <- cn 124 | } 125 | 126 | func (p *StickyConnPool) Close() error { 127 | if shared := atomic.AddInt32(&p.shared, -1); shared > 0 { 128 | return nil 129 | } 130 | 131 | for i := 0; i < 1000; i++ { 132 | state := atomic.LoadUint32(&p.state) 133 | if state == stateClosed { 134 | return ErrClosed 135 | } 136 | if atomic.CompareAndSwapUint32(&p.state, state, stateClosed) { 137 | close(p.ch) 138 | cn, ok := <-p.ch 139 | if ok { 140 | p.freeConn(context.TODO(), cn) 141 | } 142 | return nil 143 | } 144 | } 145 | 146 | return errors.New("redis: StickyConnPool.Close: infinite loop") 147 | } 148 | 149 | func (p *StickyConnPool) Reset(ctx context.Context) error { 150 | if p.badConnError() == nil { 151 | return nil 152 | } 153 | 154 | select { 155 | case cn, ok := <-p.ch: 156 | if !ok { 157 | return ErrClosed 158 | } 159 | p.pool.Remove(ctx, cn, ErrClosed) 160 | p._badConnError.Store(BadConnError{wrapped: nil}) 161 | default: 162 | return errors.New("redis: StickyConnPool does not have a Conn") 163 | } 164 | 165 | if !atomic.CompareAndSwapUint32(&p.state, stateInited, stateDefault) { 166 | state := atomic.LoadUint32(&p.state) 167 | return fmt.Errorf("redis: invalid StickyConnPool state: %d", state) 168 | } 169 | 170 | return nil 171 | } 172 | 173 | func (p *StickyConnPool) badConnError() error { 174 | if v := p._badConnError.Load(); v != nil { 175 | if err := v.(BadConnError); err.wrapped != nil { 176 | return err 177 | } 178 | } 179 | return nil 180 | } 181 | 182 | func (p *StickyConnPool) Len() int { 183 | switch atomic.LoadUint32(&p.state) { 184 | case stateDefault: 185 | return 0 186 | case stateInited: 187 | return 1 188 | case stateClosed: 189 | return 0 190 | default: 191 | panic("not reached") 192 | } 193 | } 194 | 195 | func (p *StickyConnPool) IdleLen() int { 196 | return len(p.ch) 197 | } 198 | 199 | func (p *StickyConnPool) Stats() *Stats { 200 | return &Stats{} 201 | } 202 | -------------------------------------------------------------------------------- /gears_commands.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | type GearsCmdable interface { 10 | TFunctionLoad(ctx context.Context, lib string) *StatusCmd 11 | TFunctionLoadArgs(ctx context.Context, lib string, options *TFunctionLoadOptions) *StatusCmd 12 | TFunctionDelete(ctx context.Context, libName string) *StatusCmd 13 | TFunctionList(ctx context.Context) *MapStringInterfaceSliceCmd 14 | TFunctionListArgs(ctx context.Context, options *TFunctionListOptions) *MapStringInterfaceSliceCmd 15 | TFCall(ctx context.Context, libName string, funcName string, numKeys int) *Cmd 16 | TFCallArgs(ctx context.Context, libName string, funcName string, numKeys int, options *TFCallOptions) *Cmd 17 | TFCallASYNC(ctx context.Context, libName string, funcName string, numKeys int) *Cmd 18 | TFCallASYNCArgs(ctx context.Context, libName string, funcName string, numKeys int, options *TFCallOptions) *Cmd 19 | } 20 | 21 | type TFunctionLoadOptions struct { 22 | Replace bool 23 | Config string 24 | } 25 | 26 | type TFunctionListOptions struct { 27 | Withcode bool 28 | Verbose int 29 | Library string 30 | } 31 | 32 | type TFCallOptions struct { 33 | Keys []string 34 | Arguments []string 35 | } 36 | 37 | // TFunctionLoad - load a new JavaScript library into Redis. 38 | // For more information - https://redis.io/commands/tfunction-load/ 39 | func (c cmdable) TFunctionLoad(ctx context.Context, lib string) *StatusCmd { 40 | args := []interface{}{"TFUNCTION", "LOAD", lib} 41 | cmd := NewStatusCmd(ctx, args...) 42 | _ = c(ctx, cmd) 43 | return cmd 44 | } 45 | 46 | func (c cmdable) TFunctionLoadArgs(ctx context.Context, lib string, options *TFunctionLoadOptions) *StatusCmd { 47 | args := []interface{}{"TFUNCTION", "LOAD"} 48 | if options != nil { 49 | if options.Replace { 50 | args = append(args, "REPLACE") 51 | } 52 | if options.Config != "" { 53 | args = append(args, "CONFIG", options.Config) 54 | } 55 | } 56 | args = append(args, lib) 57 | cmd := NewStatusCmd(ctx, args...) 58 | _ = c(ctx, cmd) 59 | return cmd 60 | } 61 | 62 | // TFunctionDelete - delete a JavaScript library from Redis. 63 | // For more information - https://redis.io/commands/tfunction-delete/ 64 | func (c cmdable) TFunctionDelete(ctx context.Context, libName string) *StatusCmd { 65 | args := []interface{}{"TFUNCTION", "DELETE", libName} 66 | cmd := NewStatusCmd(ctx, args...) 67 | _ = c(ctx, cmd) 68 | return cmd 69 | } 70 | 71 | // TFunctionList - list the functions with additional information about each function. 72 | // For more information - https://redis.io/commands/tfunction-list/ 73 | func (c cmdable) TFunctionList(ctx context.Context) *MapStringInterfaceSliceCmd { 74 | args := []interface{}{"TFUNCTION", "LIST"} 75 | cmd := NewMapStringInterfaceSliceCmd(ctx, args...) 76 | _ = c(ctx, cmd) 77 | return cmd 78 | } 79 | 80 | func (c cmdable) TFunctionListArgs(ctx context.Context, options *TFunctionListOptions) *MapStringInterfaceSliceCmd { 81 | args := []interface{}{"TFUNCTION", "LIST"} 82 | if options != nil { 83 | if options.Withcode { 84 | args = append(args, "WITHCODE") 85 | } 86 | if options.Verbose != 0 { 87 | v := strings.Repeat("v", options.Verbose) 88 | args = append(args, v) 89 | } 90 | if options.Library != "" { 91 | args = append(args, "LIBRARY", options.Library) 92 | } 93 | } 94 | cmd := NewMapStringInterfaceSliceCmd(ctx, args...) 95 | _ = c(ctx, cmd) 96 | return cmd 97 | } 98 | 99 | // TFCall - invoke a function. 100 | // For more information - https://redis.io/commands/tfcall/ 101 | func (c cmdable) TFCall(ctx context.Context, libName string, funcName string, numKeys int) *Cmd { 102 | lf := libName + "." + funcName 103 | args := []interface{}{"TFCALL", lf, numKeys} 104 | cmd := NewCmd(ctx, args...) 105 | _ = c(ctx, cmd) 106 | return cmd 107 | } 108 | 109 | func (c cmdable) TFCallArgs(ctx context.Context, libName string, funcName string, numKeys int, options *TFCallOptions) *Cmd { 110 | lf := libName + "." + funcName 111 | args := []interface{}{"TFCALL", lf, numKeys} 112 | if options != nil { 113 | for _, key := range options.Keys { 114 | args = append(args, key) 115 | } 116 | for _, key := range options.Arguments { 117 | args = append(args, key) 118 | } 119 | } 120 | cmd := NewCmd(ctx, args...) 121 | _ = c(ctx, cmd) 122 | return cmd 123 | } 124 | 125 | // TFCallASYNC - invoke an asynchronous JavaScript function (coroutine). 126 | // For more information - https://redis.io/commands/TFCallASYNC/ 127 | func (c cmdable) TFCallASYNC(ctx context.Context, libName string, funcName string, numKeys int) *Cmd { 128 | lf := fmt.Sprintf("%s.%s", libName, funcName) 129 | args := []interface{}{"TFCALLASYNC", lf, numKeys} 130 | cmd := NewCmd(ctx, args...) 131 | _ = c(ctx, cmd) 132 | return cmd 133 | } 134 | 135 | func (c cmdable) TFCallASYNCArgs(ctx context.Context, libName string, funcName string, numKeys int, options *TFCallOptions) *Cmd { 136 | lf := fmt.Sprintf("%s.%s", libName, funcName) 137 | args := []interface{}{"TFCALLASYNC", lf, numKeys} 138 | if options != nil { 139 | for _, key := range options.Keys { 140 | args = append(args, key) 141 | } 142 | for _, key := range options.Arguments { 143 | args = append(args, key) 144 | } 145 | } 146 | cmd := NewCmd(ctx, args...) 147 | _ = c(ctx, cmd) 148 | return cmd 149 | } 150 | -------------------------------------------------------------------------------- /geo_commands.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | ) 7 | 8 | type GeoCmdable interface { 9 | GeoAdd(ctx context.Context, key string, geoLocation ...*GeoLocation) *IntCmd 10 | GeoPos(ctx context.Context, key string, members ...string) *GeoPosCmd 11 | GeoRadius(ctx context.Context, key string, longitude, latitude float64, query *GeoRadiusQuery) *GeoLocationCmd 12 | GeoRadiusStore(ctx context.Context, key string, longitude, latitude float64, query *GeoRadiusQuery) *IntCmd 13 | GeoRadiusByMember(ctx context.Context, key, member string, query *GeoRadiusQuery) *GeoLocationCmd 14 | GeoRadiusByMemberStore(ctx context.Context, key, member string, query *GeoRadiusQuery) *IntCmd 15 | GeoSearch(ctx context.Context, key string, q *GeoSearchQuery) *StringSliceCmd 16 | GeoSearchLocation(ctx context.Context, key string, q *GeoSearchLocationQuery) *GeoSearchLocationCmd 17 | GeoSearchStore(ctx context.Context, key, store string, q *GeoSearchStoreQuery) *IntCmd 18 | GeoDist(ctx context.Context, key string, member1, member2, unit string) *FloatCmd 19 | GeoHash(ctx context.Context, key string, members ...string) *StringSliceCmd 20 | } 21 | 22 | func (c cmdable) GeoAdd(ctx context.Context, key string, geoLocation ...*GeoLocation) *IntCmd { 23 | args := make([]interface{}, 2+3*len(geoLocation)) 24 | args[0] = "geoadd" 25 | args[1] = key 26 | for i, eachLoc := range geoLocation { 27 | args[2+3*i] = eachLoc.Longitude 28 | args[2+3*i+1] = eachLoc.Latitude 29 | args[2+3*i+2] = eachLoc.Name 30 | } 31 | cmd := NewIntCmd(ctx, args...) 32 | _ = c(ctx, cmd) 33 | return cmd 34 | } 35 | 36 | // GeoRadius is a read-only GEORADIUS_RO command. 37 | func (c cmdable) GeoRadius( 38 | ctx context.Context, key string, longitude, latitude float64, query *GeoRadiusQuery, 39 | ) *GeoLocationCmd { 40 | cmd := NewGeoLocationCmd(ctx, query, "georadius_ro", key, longitude, latitude) 41 | if query.Store != "" || query.StoreDist != "" { 42 | cmd.SetErr(errors.New("GeoRadius does not support Store or StoreDist")) 43 | return cmd 44 | } 45 | _ = c(ctx, cmd) 46 | return cmd 47 | } 48 | 49 | // GeoRadiusStore is a writing GEORADIUS command. 50 | func (c cmdable) GeoRadiusStore( 51 | ctx context.Context, key string, longitude, latitude float64, query *GeoRadiusQuery, 52 | ) *IntCmd { 53 | args := geoLocationArgs(query, "georadius", key, longitude, latitude) 54 | cmd := NewIntCmd(ctx, args...) 55 | if query.Store == "" && query.StoreDist == "" { 56 | cmd.SetErr(errors.New("GeoRadiusStore requires Store or StoreDist")) 57 | return cmd 58 | } 59 | _ = c(ctx, cmd) 60 | return cmd 61 | } 62 | 63 | // GeoRadiusByMember is a read-only GEORADIUSBYMEMBER_RO command. 64 | func (c cmdable) GeoRadiusByMember( 65 | ctx context.Context, key, member string, query *GeoRadiusQuery, 66 | ) *GeoLocationCmd { 67 | cmd := NewGeoLocationCmd(ctx, query, "georadiusbymember_ro", key, member) 68 | if query.Store != "" || query.StoreDist != "" { 69 | cmd.SetErr(errors.New("GeoRadiusByMember does not support Store or StoreDist")) 70 | return cmd 71 | } 72 | _ = c(ctx, cmd) 73 | return cmd 74 | } 75 | 76 | // GeoRadiusByMemberStore is a writing GEORADIUSBYMEMBER command. 77 | func (c cmdable) GeoRadiusByMemberStore( 78 | ctx context.Context, key, member string, query *GeoRadiusQuery, 79 | ) *IntCmd { 80 | args := geoLocationArgs(query, "georadiusbymember", key, member) 81 | cmd := NewIntCmd(ctx, args...) 82 | if query.Store == "" && query.StoreDist == "" { 83 | cmd.SetErr(errors.New("GeoRadiusByMemberStore requires Store or StoreDist")) 84 | return cmd 85 | } 86 | _ = c(ctx, cmd) 87 | return cmd 88 | } 89 | 90 | func (c cmdable) GeoSearch(ctx context.Context, key string, q *GeoSearchQuery) *StringSliceCmd { 91 | args := make([]interface{}, 0, 13) 92 | args = append(args, "geosearch", key) 93 | args = geoSearchArgs(q, args) 94 | cmd := NewStringSliceCmd(ctx, args...) 95 | _ = c(ctx, cmd) 96 | return cmd 97 | } 98 | 99 | func (c cmdable) GeoSearchLocation( 100 | ctx context.Context, key string, q *GeoSearchLocationQuery, 101 | ) *GeoSearchLocationCmd { 102 | args := make([]interface{}, 0, 16) 103 | args = append(args, "geosearch", key) 104 | args = geoSearchLocationArgs(q, args) 105 | cmd := NewGeoSearchLocationCmd(ctx, q, args...) 106 | _ = c(ctx, cmd) 107 | return cmd 108 | } 109 | 110 | func (c cmdable) GeoSearchStore(ctx context.Context, key, store string, q *GeoSearchStoreQuery) *IntCmd { 111 | args := make([]interface{}, 0, 15) 112 | args = append(args, "geosearchstore", store, key) 113 | args = geoSearchArgs(&q.GeoSearchQuery, args) 114 | if q.StoreDist { 115 | args = append(args, "storedist") 116 | } 117 | cmd := NewIntCmd(ctx, args...) 118 | _ = c(ctx, cmd) 119 | return cmd 120 | } 121 | 122 | func (c cmdable) GeoDist( 123 | ctx context.Context, key string, member1, member2, unit string, 124 | ) *FloatCmd { 125 | if unit == "" { 126 | unit = "km" 127 | } 128 | cmd := NewFloatCmd(ctx, "geodist", key, member1, member2, unit) 129 | _ = c(ctx, cmd) 130 | return cmd 131 | } 132 | 133 | func (c cmdable) GeoHash(ctx context.Context, key string, members ...string) *StringSliceCmd { 134 | args := make([]interface{}, 2+len(members)) 135 | args[0] = "geohash" 136 | args[1] = key 137 | for i, member := range members { 138 | args[2+i] = member 139 | } 140 | cmd := NewStringSliceCmd(ctx, args...) 141 | _ = c(ctx, cmd) 142 | return cmd 143 | } 144 | 145 | func (c cmdable) GeoPos(ctx context.Context, key string, members ...string) *GeoPosCmd { 146 | args := make([]interface{}, 2+len(members)) 147 | args[0] = "geopos" 148 | args[1] = key 149 | for i, member := range members { 150 | args[2+i] = member 151 | } 152 | cmd := NewGeoPosCmd(ctx, args...) 153 | _ = c(ctx, cmd) 154 | return cmd 155 | } 156 | -------------------------------------------------------------------------------- /result.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import "time" 4 | 5 | // NewCmdResult returns a Cmd initialised with val and err for testing. 6 | func NewCmdResult(val interface{}, err error) *Cmd { 7 | var cmd Cmd 8 | cmd.val = val 9 | cmd.SetErr(err) 10 | return &cmd 11 | } 12 | 13 | // NewSliceResult returns a SliceCmd initialised with val and err for testing. 14 | func NewSliceResult(val []interface{}, err error) *SliceCmd { 15 | var cmd SliceCmd 16 | cmd.val = val 17 | cmd.SetErr(err) 18 | return &cmd 19 | } 20 | 21 | // NewStatusResult returns a StatusCmd initialised with val and err for testing. 22 | func NewStatusResult(val string, err error) *StatusCmd { 23 | var cmd StatusCmd 24 | cmd.val = val 25 | cmd.SetErr(err) 26 | return &cmd 27 | } 28 | 29 | // NewIntResult returns an IntCmd initialised with val and err for testing. 30 | func NewIntResult(val int64, err error) *IntCmd { 31 | var cmd IntCmd 32 | cmd.val = val 33 | cmd.SetErr(err) 34 | return &cmd 35 | } 36 | 37 | // NewDurationResult returns a DurationCmd initialised with val and err for testing. 38 | func NewDurationResult(val time.Duration, err error) *DurationCmd { 39 | var cmd DurationCmd 40 | cmd.val = val 41 | cmd.SetErr(err) 42 | return &cmd 43 | } 44 | 45 | // NewBoolResult returns a BoolCmd initialised with val and err for testing. 46 | func NewBoolResult(val bool, err error) *BoolCmd { 47 | var cmd BoolCmd 48 | cmd.val = val 49 | cmd.SetErr(err) 50 | return &cmd 51 | } 52 | 53 | // NewStringResult returns a StringCmd initialised with val and err for testing. 54 | func NewStringResult(val string, err error) *StringCmd { 55 | var cmd StringCmd 56 | cmd.val = val 57 | cmd.SetErr(err) 58 | return &cmd 59 | } 60 | 61 | // NewFloatResult returns a FloatCmd initialised with val and err for testing. 62 | func NewFloatResult(val float64, err error) *FloatCmd { 63 | var cmd FloatCmd 64 | cmd.val = val 65 | cmd.SetErr(err) 66 | return &cmd 67 | } 68 | 69 | // NewStringSliceResult returns a StringSliceCmd initialised with val and err for testing. 70 | func NewStringSliceResult(val []string, err error) *StringSliceCmd { 71 | var cmd StringSliceCmd 72 | cmd.val = val 73 | cmd.SetErr(err) 74 | return &cmd 75 | } 76 | 77 | // NewBoolSliceResult returns a BoolSliceCmd initialised with val and err for testing. 78 | func NewBoolSliceResult(val []bool, err error) *BoolSliceCmd { 79 | var cmd BoolSliceCmd 80 | cmd.val = val 81 | cmd.SetErr(err) 82 | return &cmd 83 | } 84 | 85 | // NewMapStringStringResult returns a MapStringStringCmd initialised with val and err for testing. 86 | func NewMapStringStringResult(val map[string]string, err error) *MapStringStringCmd { 87 | var cmd MapStringStringCmd 88 | cmd.val = val 89 | cmd.SetErr(err) 90 | return &cmd 91 | } 92 | 93 | // NewMapStringIntCmdResult returns a MapStringIntCmd initialised with val and err for testing. 94 | func NewMapStringIntCmdResult(val map[string]int64, err error) *MapStringIntCmd { 95 | var cmd MapStringIntCmd 96 | cmd.val = val 97 | cmd.SetErr(err) 98 | return &cmd 99 | } 100 | 101 | // NewTimeCmdResult returns a TimeCmd initialised with val and err for testing. 102 | func NewTimeCmdResult(val time.Time, err error) *TimeCmd { 103 | var cmd TimeCmd 104 | cmd.val = val 105 | cmd.SetErr(err) 106 | return &cmd 107 | } 108 | 109 | // NewZSliceCmdResult returns a ZSliceCmd initialised with val and err for testing. 110 | func NewZSliceCmdResult(val []Z, err error) *ZSliceCmd { 111 | var cmd ZSliceCmd 112 | cmd.val = val 113 | cmd.SetErr(err) 114 | return &cmd 115 | } 116 | 117 | // NewZWithKeyCmdResult returns a ZWithKeyCmd initialised with val and err for testing. 118 | func NewZWithKeyCmdResult(val *ZWithKey, err error) *ZWithKeyCmd { 119 | var cmd ZWithKeyCmd 120 | cmd.val = val 121 | cmd.SetErr(err) 122 | return &cmd 123 | } 124 | 125 | // NewScanCmdResult returns a ScanCmd initialised with val and err for testing. 126 | func NewScanCmdResult(keys []string, cursor uint64, err error) *ScanCmd { 127 | var cmd ScanCmd 128 | cmd.page = keys 129 | cmd.cursor = cursor 130 | cmd.SetErr(err) 131 | return &cmd 132 | } 133 | 134 | // NewClusterSlotsCmdResult returns a ClusterSlotsCmd initialised with val and err for testing. 135 | func NewClusterSlotsCmdResult(val []ClusterSlot, err error) *ClusterSlotsCmd { 136 | var cmd ClusterSlotsCmd 137 | cmd.val = val 138 | cmd.SetErr(err) 139 | return &cmd 140 | } 141 | 142 | // NewGeoLocationCmdResult returns a GeoLocationCmd initialised with val and err for testing. 143 | func NewGeoLocationCmdResult(val []GeoLocation, err error) *GeoLocationCmd { 144 | var cmd GeoLocationCmd 145 | cmd.locations = val 146 | cmd.SetErr(err) 147 | return &cmd 148 | } 149 | 150 | // NewGeoPosCmdResult returns a GeoPosCmd initialised with val and err for testing. 151 | func NewGeoPosCmdResult(val []*GeoPos, err error) *GeoPosCmd { 152 | var cmd GeoPosCmd 153 | cmd.val = val 154 | cmd.SetErr(err) 155 | return &cmd 156 | } 157 | 158 | // NewCommandsInfoCmdResult returns a CommandsInfoCmd initialised with val and err for testing. 159 | func NewCommandsInfoCmdResult(val map[string]*CommandInfo, err error) *CommandsInfoCmd { 160 | var cmd CommandsInfoCmd 161 | cmd.val = val 162 | cmd.SetErr(err) 163 | return &cmd 164 | } 165 | 166 | // NewXMessageSliceCmdResult returns a XMessageSliceCmd initialised with val and err for testing. 167 | func NewXMessageSliceCmdResult(val []XMessage, err error) *XMessageSliceCmd { 168 | var cmd XMessageSliceCmd 169 | cmd.val = val 170 | cmd.SetErr(err) 171 | return &cmd 172 | } 173 | 174 | // NewXStreamSliceCmdResult returns a XStreamSliceCmd initialised with val and err for testing. 175 | func NewXStreamSliceCmdResult(val []XStream, err error) *XStreamSliceCmd { 176 | var cmd XStreamSliceCmd 177 | cmd.val = val 178 | cmd.SetErr(err) 179 | return &cmd 180 | } 181 | 182 | // NewXPendingResult returns a XPendingCmd initialised with val and err for testing. 183 | func NewXPendingResult(val *XPending, err error) *XPendingCmd { 184 | var cmd XPendingCmd 185 | cmd.val = val 186 | cmd.SetErr(err) 187 | return &cmd 188 | } 189 | --------------------------------------------------------------------------------