diff --git a/.github/wordlist.txt b/.github/wordlist.txt index 741c51aa03..6c9223b34e 100644 --- a/.github/wordlist.txt +++ b/.github/wordlist.txt @@ -76,3 +76,4 @@ oauth entraid MiB KiB +oldstable diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b3fc813611..b104b1819f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,8 +22,10 @@ jobs: - "8.2.x" # Redis CE 8.2 - "8.0.x" # Redis CE 8.0 go-version: + - "1.21.x" - "1.23.x" - - "1.24.x" + - oldstable + - stable steps: - name: Set up ${{ matrix.go-version }} @@ -78,8 +80,10 @@ jobs: - "8.2.x" # Redis CE 8.2 - "8.0.x" # Redis CE 8.0 go-version: + - "1.21.x" - "1.23.x" - - "1.24.x" + - oldstable + - stable steps: - name: Checkout code diff --git a/README.md b/README.md index 38bd17b583..54dd0794b1 100644 --- a/README.md +++ b/README.md @@ -21,13 +21,12 @@ In `go-redis` we are aiming to support the last three releases of Redis. Current - [Redis 8.2](https://raw.githubusercontent.com/redis/redis/8.2/00-RELEASENOTES) - using Redis CE 8.2 - [Redis 8.4](https://raw.githubusercontent.com/redis/redis/8.4/00-RELEASENOTES) - using Redis CE 8.4 -Although the `go.mod` states it requires at minimum `go 1.18`, our CI is configured to run the tests against all three -versions of Redis and latest two versions of Go ([1.23](https://go.dev/doc/devel/release#go1.23.0), -[1.24](https://go.dev/doc/devel/release#go1.24.0)). We observe that some modules related test may not pass with +Although the `go.mod` states it requires at minimum `go 1.21`, our CI is configured to run the tests against all three +versions of Redis and multiple versions of Go ([1.21](https://go.dev/doc/devel/release#go1.21.0), +[1.23](https://go.dev/doc/devel/release#go1.23.0), oldstable, and stable). We observe that some modules related test may not pass with Redis Stack 7.2 and some commands are changed with Redis CE 8.0. Although it is not officially supported, `go-redis/v9` should be able to work with any Redis 7.0+. -Please do refer to the documentation and the tests if you experience any issues. We do plan to update the go version -in the `go.mod` to `go 1.24` in one of the next releases. +Please do refer to the documentation and the tests if you experience any issues. ## How do I Redis? diff --git a/doctests/timeseries_tut_test.go b/doctests/timeseries_tut_test.go index 28e1635399..a77583dd07 100644 --- a/doctests/timeseries_tut_test.go +++ b/doctests/timeseries_tut_test.go @@ -5,9 +5,7 @@ package example_commands_test import ( "context" "fmt" - "maps" "math" - "slices" "sort" "github.com/redis/go-redis/v9" @@ -15,6 +13,16 @@ import ( // HIDE_END +// mapKeys returns a slice of all keys from the map (Go 1.21 compatible) +// TODO: Once minimum Go version is upgraded to 1.23+, replace with slices.Collect(maps.Keys(m)) +func mapKeys[K comparable, V any](m map[K]V) []K { + keys := make([]K, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + return keys +} + func ExampleClient_timeseries_create() { ctx := context.Background() @@ -417,7 +425,7 @@ func ExampleClient_timeseries_query_multi() { panic(err) } - res28Keys := slices.Collect(maps.Keys(res28)) + res28Keys := mapKeys(res28) sort.Strings(res28Keys) for _, k := range res28Keys { @@ -457,7 +465,7 @@ func ExampleClient_timeseries_query_multi() { panic(err) } - res29Keys := slices.Collect(maps.Keys(res29)) + res29Keys := mapKeys(res29) sort.Strings(res29Keys) for _, k := range res29Keys { @@ -505,7 +513,7 @@ func ExampleClient_timeseries_query_multi() { panic(err) } - res30Keys := slices.Collect(maps.Keys(res30)) + res30Keys := mapKeys(res30) sort.Strings(res30Keys) for _, k := range res30Keys { @@ -550,7 +558,7 @@ func ExampleClient_timeseries_query_multi() { panic(err) } - res31Keys := slices.Collect(maps.Keys(res31)) + res31Keys := mapKeys(res31) sort.Strings(res31Keys) for _, k := range res31Keys { @@ -857,7 +865,7 @@ func ExampleClient_timeseries_aggmulti() { panic(err) } - res44Keys := slices.Collect(maps.Keys(res44)) + res44Keys := mapKeys(res44) sort.Strings(res44Keys) for _, k := range res44Keys { @@ -905,7 +913,7 @@ func ExampleClient_timeseries_aggmulti() { panic(err) } - res45Keys := slices.Collect(maps.Keys(res45)) + res45Keys := mapKeys(res45) sort.Strings(res45Keys) for _, k := range res45Keys { diff --git a/example/cluster-mget/go.mod b/example/cluster-mget/go.mod index 77b47b30da..334a1aabb0 100644 --- a/example/cluster-mget/go.mod +++ b/example/cluster-mget/go.mod @@ -1,6 +1,6 @@ module github.com/redis/go-redis/example/cluster-mget -go 1.18 +go 1.21 replace github.com/redis/go-redis/v9 => ../.. diff --git a/example/del-keys-without-ttl/go.mod b/example/del-keys-without-ttl/go.mod index 8b5c517cff..a0f07a5387 100644 --- a/example/del-keys-without-ttl/go.mod +++ b/example/del-keys-without-ttl/go.mod @@ -1,6 +1,6 @@ module github.com/redis/go-redis/example/del-keys-without-ttl -go 1.18 +go 1.21 replace github.com/redis/go-redis/v9 => ../.. diff --git a/example/digest-optimistic-locking/go.mod b/example/digest-optimistic-locking/go.mod index 4a93e7d3b9..0d7f9850b4 100644 --- a/example/digest-optimistic-locking/go.mod +++ b/example/digest-optimistic-locking/go.mod @@ -1,6 +1,6 @@ module github.com/redis/go-redis/example/digest-optimistic-locking -go 1.18 +go 1.21 replace github.com/redis/go-redis/v9 => ../.. diff --git a/example/hll/go.mod b/example/hll/go.mod index def62dc756..fb6b41fd11 100644 --- a/example/hll/go.mod +++ b/example/hll/go.mod @@ -1,6 +1,6 @@ module github.com/redis/go-redis/example/hll -go 1.18 +go 1.21 replace github.com/redis/go-redis/v9 => ../.. diff --git a/example/hset-struct/go.mod b/example/hset-struct/go.mod index 3f10b4be9f..b4a83a4c38 100644 --- a/example/hset-struct/go.mod +++ b/example/hset-struct/go.mod @@ -1,6 +1,6 @@ module github.com/redis/go-redis/example/scan-struct -go 1.18 +go 1.21 replace github.com/redis/go-redis/v9 => ../.. diff --git a/example/lua-scripting/go.mod b/example/lua-scripting/go.mod index f1f46577d4..585091f560 100644 --- a/example/lua-scripting/go.mod +++ b/example/lua-scripting/go.mod @@ -1,6 +1,6 @@ module github.com/redis/go-redis/example/lua-scripting -go 1.18 +go 1.21 replace github.com/redis/go-redis/v9 => ../.. diff --git a/example/maintnotifiations-pubsub/go.mod b/example/maintnotifiations-pubsub/go.mod index 9fabdb991e..d402dc0c98 100644 --- a/example/maintnotifiations-pubsub/go.mod +++ b/example/maintnotifiations-pubsub/go.mod @@ -1,6 +1,6 @@ module github.com/redis/go-redis/example/pubsub -go 1.18 +go 1.21 replace github.com/redis/go-redis/v9 => ../.. diff --git a/example/redis-bloom/go.mod b/example/redis-bloom/go.mod index c52ca9814a..6a1ef50ba0 100644 --- a/example/redis-bloom/go.mod +++ b/example/redis-bloom/go.mod @@ -1,6 +1,6 @@ module github.com/redis/go-redis/example/redis-bloom -go 1.18 +go 1.21 replace github.com/redis/go-redis/v9 => ../.. diff --git a/example/scan-struct/go.mod b/example/scan-struct/go.mod index 3f10b4be9f..b4a83a4c38 100644 --- a/example/scan-struct/go.mod +++ b/example/scan-struct/go.mod @@ -1,6 +1,6 @@ module github.com/redis/go-redis/example/scan-struct -go 1.18 +go 1.21 replace github.com/redis/go-redis/v9 => ../.. diff --git a/extra/rediscensus/go.mod b/extra/rediscensus/go.mod index 95dfc0ff8d..3cb2a108db 100644 --- a/extra/rediscensus/go.mod +++ b/extra/rediscensus/go.mod @@ -1,6 +1,6 @@ module github.com/redis/go-redis/extra/rediscensus/v9 -go 1.19 +go 1.21 replace github.com/redis/go-redis/v9 => ../.. diff --git a/extra/rediscmd/go.mod b/extra/rediscmd/go.mod index 3557b016f9..d8d8405d19 100644 --- a/extra/rediscmd/go.mod +++ b/extra/rediscmd/go.mod @@ -1,6 +1,6 @@ module github.com/redis/go-redis/extra/rediscmd/v9 -go 1.19 +go 1.21 replace github.com/redis/go-redis/v9 => ../.. diff --git a/extra/redisotel/go.mod b/extra/redisotel/go.mod index 56766fa07f..3ebcca1dd0 100644 --- a/extra/redisotel/go.mod +++ b/extra/redisotel/go.mod @@ -1,6 +1,6 @@ module github.com/redis/go-redis/extra/redisotel/v9 -go 1.19 +go 1.21 replace github.com/redis/go-redis/v9 => ../.. diff --git a/extra/redisprometheus/go.mod b/extra/redisprometheus/go.mod index f92734cc70..1b558bf264 100644 --- a/extra/redisprometheus/go.mod +++ b/extra/redisprometheus/go.mod @@ -1,6 +1,6 @@ module github.com/redis/go-redis/extra/redisprometheus/v9 -go 1.19 +go 1.21 replace github.com/redis/go-redis/v9 => ../.. diff --git a/go.mod b/go.mod index 0d3144f695..854e21323f 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/redis/go-redis/v9 -go 1.18 +go 1.21 require ( github.com/bsm/ginkgo/v2 v2.12.0 diff --git a/go.sum b/go.sum index ab06e043de..4ac0b36ede 100644 --- a/go.sum +++ b/go.sum @@ -5,9 +5,12 @@ github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= diff --git a/internal/customvet/go.mod b/internal/customvet/go.mod index edaa2d7bac..202fd404da 100644 --- a/internal/customvet/go.mod +++ b/internal/customvet/go.mod @@ -1,6 +1,6 @@ module github.com/redis/go-redis/internal/customvet -go 1.17 +go 1.21 require golang.org/x/tools v0.5.0 diff --git a/internal/routing/aggregator.go b/internal/routing/aggregator.go index db9860007b..0d6321ec11 100644 --- a/internal/routing/aggregator.go +++ b/internal/routing/aggregator.go @@ -65,7 +65,7 @@ func NewResponseAggregator(policy ResponsePolicy, cmdName string) ResponseAggreg } case RespAggLogicalAnd: andAgg := &AggLogicalAndAggregator{} - andAgg.res.Add(1) + andAgg.res.Store(true) return andAgg case RespAggLogicalOr: @@ -466,7 +466,7 @@ func (a *AggMaxAggregator) Result() (interface{}, error) { // AggLogicalAndAggregator performs logical AND on boolean values. type AggLogicalAndAggregator struct { err atomic.Value - res atomic.Int64 + res atomic.Bool hasResult atomic.Bool } @@ -482,10 +482,9 @@ func (a *AggLogicalAndAggregator) Add(result interface{}, err error) error { return e } - if val { - a.res.And(1) - } else { - a.res.And(0) + // Atomic AND operation: if val is false, result is always false + if !val { + a.res.Store(false) } a.hasResult.Store(true) @@ -544,13 +543,13 @@ func (a *AggLogicalAndAggregator) Result() (interface{}, error) { if !a.hasResult.Load() { return nil, ErrAndAggregation } - return a.res.Load() != 0, nil + return a.res.Load(), nil } // AggLogicalOrAggregator performs logical OR on boolean values. type AggLogicalOrAggregator struct { err atomic.Value - res atomic.Int64 + res atomic.Bool hasResult atomic.Bool } @@ -566,10 +565,9 @@ func (a *AggLogicalOrAggregator) Add(result interface{}, err error) error { return e } + // Atomic OR operation: if val is true, result is always true if val { - a.res.Or(1) - } else { - a.res.Or(0) + a.res.Store(true) } a.hasResult.Store(true) @@ -628,7 +626,7 @@ func (a *AggLogicalOrAggregator) Result() (interface{}, error) { if !a.hasResult.Load() { return nil, ErrOrAggregation } - return a.res.Load() != 0, nil + return a.res.Load(), nil } func toInt64(val interface{}) (int64, error) { diff --git a/internal/routing/aggregator_test.go b/internal/routing/aggregator_test.go new file mode 100644 index 0000000000..11e92d8c8f --- /dev/null +++ b/internal/routing/aggregator_test.go @@ -0,0 +1,271 @@ +package routing + +import ( + "errors" + "testing" +) + +func TestAggLogicalAndAggregator(t *testing.T) { + t.Run("all true values", func(t *testing.T) { + agg := NewResponseAggregator(RespAggLogicalAnd, "") + + err := agg.Add(true, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + err = agg.Add(int64(1), nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + err = agg.Add(1, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + result, err := agg.Result() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if result != true { + t.Errorf("expected true, got %v", result) + } + }) + + t.Run("one false value", func(t *testing.T) { + agg := NewResponseAggregator(RespAggLogicalAnd, "") + + err := agg.Add(true, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + err = agg.Add(false, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + err = agg.Add(true, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + result, err := agg.Result() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if result != false { + t.Errorf("expected false, got %v", result) + } + }) + + t.Run("no results", func(t *testing.T) { + agg := NewResponseAggregator(RespAggLogicalAnd, "") + + _, err := agg.Result() + if err != ErrAndAggregation { + t.Errorf("expected ErrAndAggregation, got %v", err) + } + }) + + t.Run("with error", func(t *testing.T) { + agg := NewResponseAggregator(RespAggLogicalAnd, "") + + testErr := errors.New("test error") + err := agg.Add(nil, testErr) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + _, err = agg.Result() + if err != testErr { + t.Errorf("expected test error, got %v", err) + } + }) +} + +func TestAggLogicalOrAggregator(t *testing.T) { + t.Run("all false values", func(t *testing.T) { + agg := NewResponseAggregator(RespAggLogicalOr, "") + + err := agg.Add(false, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + err = agg.Add(int64(0), nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + err = agg.Add(0, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + result, err := agg.Result() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if result != false { + t.Errorf("expected false, got %v", result) + } + }) + + t.Run("one true value", func(t *testing.T) { + agg := NewResponseAggregator(RespAggLogicalOr, "") + + err := agg.Add(false, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + err = agg.Add(true, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + err = agg.Add(false, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + result, err := agg.Result() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if result != true { + t.Errorf("expected true, got %v", result) + } + }) + + t.Run("no results", func(t *testing.T) { + agg := NewResponseAggregator(RespAggLogicalOr, "") + + _, err := agg.Result() + if err != ErrOrAggregation { + t.Errorf("expected ErrOrAggregation, got %v", err) + } + }) + + t.Run("with error", func(t *testing.T) { + agg := NewResponseAggregator(RespAggLogicalOr, "") + + testErr := errors.New("test error") + err := agg.Add(nil, testErr) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + _, err = agg.Result() + if err != testErr { + t.Errorf("expected test error, got %v", err) + } + }) +} + +func TestAggLogicalAndBatchAdd(t *testing.T) { + t.Run("batch add all true", func(t *testing.T) { + agg := NewResponseAggregator(RespAggLogicalAnd, "") + + results := map[string]AggregatorResErr{ + "key1": {Result: true, Err: nil}, + "key2": {Result: int64(1), Err: nil}, + "key3": {Result: 1, Err: nil}, + } + + err := agg.BatchAdd(results) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + result, err := agg.Result() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if result != true { + t.Errorf("expected true, got %v", result) + } + }) + + t.Run("batch add with false", func(t *testing.T) { + agg := NewResponseAggregator(RespAggLogicalAnd, "") + + results := map[string]AggregatorResErr{ + "key1": {Result: true, Err: nil}, + "key2": {Result: false, Err: nil}, + "key3": {Result: true, Err: nil}, + } + + err := agg.BatchAdd(results) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + result, err := agg.Result() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if result != false { + t.Errorf("expected false, got %v", result) + } + }) +} + +func TestAggLogicalOrBatchAdd(t *testing.T) { + t.Run("batch add all false", func(t *testing.T) { + agg := NewResponseAggregator(RespAggLogicalOr, "") + + results := map[string]AggregatorResErr{ + "key1": {Result: false, Err: nil}, + "key2": {Result: int64(0), Err: nil}, + "key3": {Result: 0, Err: nil}, + } + + err := agg.BatchAdd(results) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + result, err := agg.Result() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if result != false { + t.Errorf("expected false, got %v", result) + } + }) + + t.Run("batch add with true", func(t *testing.T) { + agg := NewResponseAggregator(RespAggLogicalOr, "") + + results := map[string]AggregatorResErr{ + "key1": {Result: false, Err: nil}, + "key2": {Result: true, Err: nil}, + "key3": {Result: false, Err: nil}, + } + + err := agg.BatchAdd(results) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + result, err := agg.Result() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if result != true { + t.Errorf("expected true, got %v", result) + } + }) +} + diff --git a/scripts/release.sh b/scripts/release.sh index fb7d58788c..e18db78e85 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -48,15 +48,15 @@ PACKAGE_DIRS=$(find . -mindepth 2 -type f -name 'go.mod' -exec dirname {} \; \ for dir in $PACKAGE_DIRS do printf "${dir}: go get -u && go mod tidy\n" - #(cd ./${dir} && go get -u && go mod tidy -compat=1.18) + #(cd ./${dir} && go get -u && go mod tidy -compat=1.21) done for dir in $PACKAGE_DIRS do sed --in-place \ "s/redis\/go-redis\([^ ]*\) v.*/redis\/go-redis\1 ${TAG}/" "${dir}/go.mod" - #(cd ./${dir} && go get -u && go mod tidy -compat=1.18) - (cd ./${dir} && go mod tidy -compat=1.18) + #(cd ./${dir} && go get -u && go mod tidy -compat=1.21) + (cd ./${dir} && go mod tidy -compat=1.21) done sed --in-place "s/\(return \)\"[^\"]*\"/\1\"${TAG#v}\"/" ./version.go