diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 403e199d0..231233f79 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - go-version: [1.19.x, 1.20.x] + go-version: [1.19.x, 1.20.x, 1.21.x] services: redis: @@ -33,7 +33,7 @@ jobs: go-version: ${{ matrix.go-version }} - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Test run: make test diff --git a/.github/workflows/doctests.yaml b/.github/workflows/doctests.yaml index 00b0063bf..708dfcd3f 100644 --- a/.github/workflows/doctests.yaml +++ b/.github/workflows/doctests.yaml @@ -25,7 +25,7 @@ jobs: strategy: fail-fast: false matrix: - go-version: [ "1.18", "1.19", "1.20" ] + go-version: [ "1.18", "1.19", "1.20", "1.21" ] steps: - name: Set up ${{ matrix.go-version }} @@ -34,8 +34,8 @@ jobs: go-version: ${{ matrix.go-version }} - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Test doc examples working-directory: ./doctests - run: go test \ No newline at end of file + run: go test diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index d3232ecbd..e35ee85c0 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -21,6 +21,6 @@ jobs: name: lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: golangci-lint uses: golangci/golangci-lint-action@v3 diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml index e15284155..3bd776cf1 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/spellcheck.yml @@ -6,7 +6,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Check Spelling uses: rojopolis/spellcheck-github-actions@0.33.1 with: diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml new file mode 100644 index 000000000..32fd9e817 --- /dev/null +++ b/.github/workflows/stale-issues.yml @@ -0,0 +1,25 @@ +name: "Close stale issues" +on: + schedule: + - cron: "0 0 * * *" + +permissions: {} +jobs: + stale: + permissions: + issues: write # to close stale issues (actions/stale) + pull-requests: write # to close stale PRs (actions/stale) + + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v3 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: 'This issue is marked stale. It will be closed in 30 days if it is not updated.' + stale-pr-message: 'This pull request is marked stale. It will be closed in 30 days if it is not updated.' + days-before-stale: 365 + days-before-close: 30 + stale-issue-label: "Stale" + stale-pr-label: "Stale" + operations-per-run: 10 + remove-stale-when-updated: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..90030b89f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,101 @@ +# Contributing + +## Introduction + +We appreciate your interest in considering contributing to go-redis. +Community contributions mean a lot to us. + +## Contributions we need + +You may already know how you'd like to contribute, whether it's a fix for a bug you +encountered, or a new feature your team wants to use. + +If you don't know where to start, consider improving +documentation, bug triaging, and writing tutorials are all examples of +helpful contributions that mean less work for you. + +## Your First Contribution + +Unsure where to begin contributing? You can start by looking through +[help-wanted +issues](https://github.com/redis/go-redis/issues?q=is%3Aopen+is%3Aissue+label%3ahelp-wanted). + +Never contributed to open source before? Here are a couple of friendly +tutorials: + +- +- + +## Getting Started + +Here's how to get started with your code contribution: + +1. Create your own fork of go-redis +2. Do the changes in your fork +3. If you need a development environment, run `make test`. Note: this clones and builds the latest release of [redis](https://redis.io). You also need a redis-stack-server docker, in order to run the capabilities tests. This can be started by running: + ```docker run -p 6379:6379 -it redis/redis-stack-server:edge``` +4. While developing, make sure the tests pass by running `make tests` +5. If you like the change and think the project could use it, send a + pull request + +To see what else is part of the automation, run `invoke -l` + +## Testing + +Call `make test` to run all tests, including linters. + +Continuous Integration uses these same wrappers to run all of these +tests against multiple versions of python. Feel free to test your +changes against all the go versions supported, as declared by the +[build.yml](./.github/workflows/build.yml) file. + +### Troubleshooting + +If you get any errors when running `make test`, make sure +that you are using supported versions of Docker and go. + +## How to Report a Bug + +### Security Vulnerabilities + +**NOTE**: If you find a security vulnerability, do NOT open an issue. +Email [Redis Open Source ()](mailto:oss@redis.com) instead. + +In order to determine whether you are dealing with a security issue, ask +yourself these two questions: + +- Can I access something that's not mine, or something I shouldn't + have access to? +- Can I disable something for other people? + +If the answer to either of those two questions are *yes*, then you're +probably dealing with a security issue. Note that even if you answer +*no* to both questions, you may still be dealing with a security +issue, so if you're unsure, just email [us](mailto:oss@redis.com). + +### Everything Else + +When filing an issue, make sure to answer these five questions: + +1. What version of go-redis are you using? +2. What version of redis are you using? +3. What did you do? +4. What did you expect to see? +5. What did you see instead? + +## Suggest a feature or enhancement + +If you'd like to contribute a new feature, make sure you check our +issue list to see if someone has already proposed it. Work may already +be underway on the feature you want or we may have rejected a +feature like it already. + +If you don't see anything, open a new issue that describes the feature +you would like and how it should work. + +## Code review process + +The core team regularly looks at pull requests. We will provide +feedback as soon as possible. After receiving our feedback, please respond +within two weeks. After that time, we may close your PR if it isn't +showing any activity. diff --git a/Makefile b/Makefile index b59c39554..cf1f6428d 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ bench: testdeps testdata/redis: mkdir -p $@ - wget -qO- https://download.redis.io/releases/redis-7.2-rc3.tar.gz | tar xvz --strip-components=1 -C $@ + wget -qO- https://download.redis.io/releases/redis-7.2.1.tar.gz | tar xvz --strip-components=1 -C $@ testdata/redis/src/redis-server: testdata/redis cd $< && make all diff --git a/README.md b/README.md index 3486e8e5a..1005868c0 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,10 @@ func ExampleClient() { rdb := redis.NewClient(opts) ``` +## Contributing + +Please see [out contributing guidelines](CONTRIBUTING.md) to help us improve this library! + ## Look and feel Some corner cases: diff --git a/bench_decode_test.go b/bench_decode_test.go index de53064f2..50e339086 100644 --- a/bench_decode_test.go +++ b/bench_decode_test.go @@ -30,6 +30,7 @@ func NewClientStub(resp []byte) *ClientStub { Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) { return stub.stubConn(initHello), nil }, + DisableIndentity: true, }) return stub } @@ -45,6 +46,8 @@ func NewClusterClientStub(resp []byte) *ClientStub { Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) { return stub.stubConn(initHello), nil }, + DisableIndentity: true, + ClusterSlots: func(_ context.Context) ([]ClusterSlot, error) { return []ClusterSlot{ { diff --git a/cluster.go b/cluster.go index 941838dd0..b30e22637 100644 --- a/cluster.go +++ b/cluster.go @@ -83,7 +83,8 @@ type ClusterOptions struct { ConnMaxIdleTime time.Duration ConnMaxLifetime time.Duration - TLSConfig *tls.Config + TLSConfig *tls.Config + DisableIndentity bool // Disable set-lib on connect. Default is false. } func (opt *ClusterOptions) init() { @@ -277,15 +278,15 @@ func (opt *ClusterOptions) clientOptions() *Options { ReadTimeout: opt.ReadTimeout, WriteTimeout: opt.WriteTimeout, - PoolFIFO: opt.PoolFIFO, - PoolSize: opt.PoolSize, - PoolTimeout: opt.PoolTimeout, - MinIdleConns: opt.MinIdleConns, - MaxIdleConns: opt.MaxIdleConns, - ConnMaxIdleTime: opt.ConnMaxIdleTime, - ConnMaxLifetime: opt.ConnMaxLifetime, - - TLSConfig: opt.TLSConfig, + PoolFIFO: opt.PoolFIFO, + PoolSize: opt.PoolSize, + PoolTimeout: opt.PoolTimeout, + MinIdleConns: opt.MinIdleConns, + MaxIdleConns: opt.MaxIdleConns, + ConnMaxIdleTime: opt.ConnMaxIdleTime, + ConnMaxLifetime: opt.ConnMaxLifetime, + DisableIndentity: opt.DisableIndentity, + TLSConfig: opt.TLSConfig, // If ClusterSlots is populated, then we probably have an artificial // cluster whose nodes are not in clustering mode (otherwise there isn't // much use for ClusterSlots config). This means we cannot execute the diff --git a/commands.go b/commands.go index 2bb035cec..0c1f985e6 100644 --- a/commands.go +++ b/commands.go @@ -4,9 +4,11 @@ import ( "context" "encoding" "errors" + "fmt" "io" "net" "reflect" + "runtime" "strings" "time" @@ -236,7 +238,7 @@ type Cmdable interface { BitOpNot(ctx context.Context, destKey string, key string) *IntCmd BitPos(ctx context.Context, key string, bit int64, pos ...int64) *IntCmd BitPosSpan(ctx context.Context, key string, bit int8, start, end int64, span string) *IntCmd - BitField(ctx context.Context, key string, args ...interface{}) *IntSliceCmd + BitField(ctx context.Context, key string, values ...interface{}) *IntSliceCmd Scan(ctx context.Context, cursor uint64, match string, count int64) *ScanCmd ScanType(ctx context.Context, cursor uint64, match string, count int64, keyType string) *ScanCmd @@ -584,7 +586,8 @@ func (c statefulCmdable) ClientSetInfo(ctx context.Context, info LibraryInfo) *S var cmd *StatusCmd if info.LibName != nil { - cmd = NewStatusCmd(ctx, "client", "setinfo", "LIB-NAME", *info.LibName) + libName := fmt.Sprintf("go-redis(%s,%s)", *info.LibName, runtime.Version()) + cmd = NewStatusCmd(ctx, "client", "setinfo", "LIB-NAME", libName) } else { cmd = NewStatusCmd(ctx, "client", "setinfo", "LIB-VER", *info.LibVer) } @@ -1366,12 +1369,16 @@ func (c cmdable) BitPosSpan(ctx context.Context, key string, bit int8, start, en return cmd } -func (c cmdable) BitField(ctx context.Context, key string, args ...interface{}) *IntSliceCmd { - a := make([]interface{}, 0, 2+len(args)) - a = append(a, "bitfield") - a = append(a, key) - a = append(a, args...) - cmd := NewIntSliceCmd(ctx, a...) +// BitField accepts multiple values: +// - BitField("set", "i1", "offset1", "value1","cmd2", "type2", "offset2", "value2") +// - BitField([]string{"cmd1", "type1", "offset1", "value1","cmd2", "type2", "offset2", "value2"}) +// - BitField([]interface{}{"cmd1", "type1", "offset1", "value1","cmd2", "type2", "offset2", "value2"}) +func (c cmdable) BitField(ctx context.Context, key string, values ...interface{}) *IntSliceCmd { + args := make([]interface{}, 2, 2+len(values)) + args[0] = "bitfield" + args[1] = key + args = appendArgs(args, values) + cmd := NewIntSliceCmd(ctx, args...) _ = c(ctx, cmd) return cmd } diff --git a/commands_test.go b/commands_test.go index c8ea0f7bd..9b1c63e81 100644 --- a/commands_test.go +++ b/commands_test.go @@ -1253,6 +1253,10 @@ var _ = Describe("Commands", func() { nn, err := client.BitField(ctx, "mykey", "INCRBY", "i5", 100, 1, "GET", "u4", 0).Result() Expect(err).NotTo(HaveOccurred()) Expect(nn).To(Equal([]int64{1, 0})) + + nn, err = client.BitField(ctx, "mykey", "set", "i1", 1, 1, "GET", "u4", 0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(nn).To(Equal([]int64{0, 4})) }) It("should Decr", func() { @@ -2052,10 +2056,9 @@ var _ = Describe("Commands", func() { logEntries, err := client.ACLLog(ctx, 10).Result() Expect(err).NotTo(HaveOccurred()) - Expect(len(logEntries)).To(Equal(3)) + Expect(len(logEntries)).To(Equal(4)) for _, entry := range logEntries { - Expect(entry.Count).To(BeNumerically("==", 1)) Expect(entry.Reason).To(Equal("command")) Expect(entry.Context).To(Equal("toplevel")) Expect(entry.Object).NotTo(BeEmpty()) diff --git a/options.go b/options.go index bb4816b27..f10bad38b 100644 --- a/options.go +++ b/options.go @@ -136,6 +136,9 @@ type Options struct { // Enables read only queries on slave/follower nodes. readOnly bool + + // // Disable set-lib on connect. Default is false. + DisableIndentity bool } func (opt *Options) init() { diff --git a/probabilistic.go b/probabilistic.go index df351bfda..09dbda32b 100644 --- a/probabilistic.go +++ b/probabilistic.go @@ -24,7 +24,7 @@ type ProbabilisticCmdable interface { BFReserve(ctx context.Context, key string, errorRate float64, capacity int64) *StatusCmd BFReserveExpansion(ctx context.Context, key string, errorRate float64, capacity, expansion int64) *StatusCmd BFReserveNonScaling(ctx context.Context, key string, errorRate float64, capacity int64) *StatusCmd - BFReserveArgs(ctx context.Context, key string, options *BFReserveOptions) *StatusCmd + BFReserveWithArgs(ctx context.Context, key string, options *BFReserveOptions) *StatusCmd BFScanDump(ctx context.Context, key string, iterator int64) *ScanDumpCmd BFLoadChunk(ctx context.Context, key string, iterator int64, data interface{}) *StatusCmd @@ -38,7 +38,7 @@ type ProbabilisticCmdable interface { CFInsertNX(ctx context.Context, key string, options *CFInsertOptions, elements ...interface{}) *IntSliceCmd CFMExists(ctx context.Context, key string, elements ...interface{}) *BoolSliceCmd CFReserve(ctx context.Context, key string, capacity int64) *StatusCmd - CFReserveArgs(ctx context.Context, key string, options *CFReserveOptions) *StatusCmd + CFReserveWithArgs(ctx context.Context, key string, options *CFReserveOptions) *StatusCmd CFReserveExpansion(ctx context.Context, key string, capacity int64, expansion int64) *StatusCmd CFReserveBucketSize(ctx context.Context, key string, capacity int64, bucketsize int64) *StatusCmd CFReserveMaxIterations(ctx context.Context, key string, capacity int64, maxiterations int64) *StatusCmd @@ -143,11 +143,11 @@ func (c cmdable) BFReserveNonScaling(ctx context.Context, key string, errorRate return cmd } -// BFReserveArgs creates an empty Bloom filter with a single sub-filter +// BFReserveWithArgs creates an empty Bloom filter with a single sub-filter // for the initial specified capacity and with an upper bound error_rate. // This function also allows for specifying additional options such as expansion rate and non-scaling behavior. // For more information - https://redis.io/commands/bf.reserve/ -func (c cmdable) BFReserveArgs(ctx context.Context, key string, options *BFReserveOptions) *StatusCmd { +func (c cmdable) BFReserveWithArgs(ctx context.Context, key string, options *BFReserveOptions) *StatusCmd { args := []interface{}{"BF.RESERVE", key} if options != nil { if options.Error != 0 { @@ -493,10 +493,10 @@ func (c cmdable) CFReserveMaxIterations(ctx context.Context, key string, capacit return cmd } -// CFReserveArgs creates an empty Cuckoo filter with the specified options. +// CFReserveWithArgs creates an empty Cuckoo filter with the specified options. // This function allows for specifying additional options such as bucket size and maximum number of iterations. // For more information - https://redis.io/commands/cf.reserve/ -func (c cmdable) CFReserveArgs(ctx context.Context, key string, options *CFReserveOptions) *StatusCmd { +func (c cmdable) CFReserveWithArgs(ctx context.Context, key string, options *CFReserveOptions) *StatusCmd { args := []interface{}{"CF.RESERVE", key, options.Capacity} if options.BucketSize != 0 { args = append(args, "BUCKETSIZE", options.BucketSize) @@ -679,7 +679,7 @@ func (c cmdable) CFInfo(ctx context.Context, key string) *CFInfoCmd { // For more information - https://redis.io/commands/cf.insert/ func (c cmdable) CFInsert(ctx context.Context, key string, options *CFInsertOptions, elements ...interface{}) *BoolSliceCmd { args := []interface{}{"CF.INSERT", key} - args = c.getCfInsertArgs(args, options, elements...) + args = c.getCfInsertWithArgs(args, options, elements...) cmd := NewBoolSliceCmd(ctx, args...) _ = c(ctx, cmd) @@ -693,14 +693,14 @@ func (c cmdable) CFInsert(ctx context.Context, key string, options *CFInsertOpti // For more information - https://redis.io/commands/cf.insertnx/ func (c cmdable) CFInsertNX(ctx context.Context, key string, options *CFInsertOptions, elements ...interface{}) *IntSliceCmd { args := []interface{}{"CF.INSERTNX", key} - args = c.getCfInsertArgs(args, options, elements...) + args = c.getCfInsertWithArgs(args, options, elements...) cmd := NewIntSliceCmd(ctx, args...) _ = c(ctx, cmd) return cmd } -func (c cmdable) getCfInsertArgs(args []interface{}, options *CFInsertOptions, elements ...interface{}) []interface{} { +func (c cmdable) getCfInsertWithArgs(args []interface{}, options *CFInsertOptions, elements ...interface{}) []interface{} { if options != nil { if options.Capacity != 0 { args = append(args, "CAPACITY", options.Capacity) diff --git a/probabilistic_test.go b/probabilistic_test.go index 4b0dde646..b493abd46 100644 --- a/probabilistic_test.go +++ b/probabilistic_test.go @@ -227,14 +227,14 @@ var _ = Describe("Probabilistic commands", Label("probabilistic"), func() { Expect(infBefore).To(BeEquivalentTo(infAfter)) }) - It("should BFReserveArgs", Label("bloom", "bfreserveargs"), func() { + It("should BFReserveWithArgs", Label("bloom", "bfreserveargs"), func() { options := &redis.BFReserveOptions{ Capacity: 2000, Error: 0.001, Expansion: 3, NonScaling: false, } - err := client.BFReserveArgs(ctx, "testbf", options).Err() + err := client.BFReserveWithArgs(ctx, "testbf", options).Err() Expect(err).NotTo(HaveOccurred()) result, err := client.BFInfo(ctx, "testbf").Result() @@ -352,7 +352,7 @@ var _ = Describe("Probabilistic commands", Label("probabilistic"), func() { Expect(infBefore).To(BeEquivalentTo(infAfter)) }) - It("should CFInfo and CFReserveArgs", Label("cuckoo", "cfinfo", "cfreserveargs"), func() { + It("should CFInfo and CFReserveWithArgs", Label("cuckoo", "cfinfo", "cfreserveargs"), func() { args := &redis.CFReserveOptions{ Capacity: 2048, BucketSize: 3, @@ -360,7 +360,7 @@ var _ = Describe("Probabilistic commands", Label("probabilistic"), func() { Expansion: 2, } - err := client.CFReserveArgs(ctx, "testcf1", args).Err() + err := client.CFReserveWithArgs(ctx, "testcf1", args).Err() Expect(err).NotTo(HaveOccurred()) result, err := client.CFInfo(ctx, "testcf1").Result() diff --git a/redis.go b/redis.go index c7fbd0de8..9430eb75c 100644 --- a/redis.go +++ b/redis.go @@ -299,7 +299,14 @@ func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error { // difficult to rely on error strings to determine all results. return err } - + if !c.opt.DisableIndentity { + libName := "" + libVer := Version() + libInfo := LibraryInfo{LibName: &libName} + conn.ClientSetInfo(ctx, libInfo) + libInfo = LibraryInfo{LibVer: &libVer} + conn.ClientSetInfo(ctx, libInfo) + } _, err := conn.Pipelined(ctx, func(pipe Pipeliner) error { if !auth && password != "" { if username != "" {