1
0
mirror of https://github.com/redis/go-redis.git synced 2025-11-30 18:01:23 +03:00

feat(cluster): Implement Request and Response Policy Based Routing in Cluster Mode (#3422)

* Add search module builders and tests (#1)

* Add search module builders and tests

* Add tests

* Use builders and Actions in more clean way

* Update search_builders.go

Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com>

* Update search_builders.go

Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com>

* feat(routing): add internal request/response policy enums

* feat: load the policy table in cluster client (#4)

* feat: load the policy table in cluster client

* Remove comments

* modify Tips and command pplicy in commandInfo (#5)

* centralize cluster command routing in osscluster_router.go and refactor osscluster.go (#6)

* centralize cluster command routing in osscluster_router.go and refactor osscluster.go

* enalbe ci on all branches

* Add debug prints

* Add debug prints

* FIX: deal with nil policy

* FIX: fixing clusterClient process

* chore(osscluster): simplify switch case

* wip(command): ai generated clone method for commands

* feat: implement response aggregator for Redis cluster commands

* feat: implement response aggregator for Redis cluster commands

* fix: solve concurrency errors

* fix: solve concurrency errors

* return MaxRedirects settings

* remove locks from getCommandPolicy

* Handle MOVED errors more robustly, remove cluster reloading at exectutions, ennsure better routing

* Fix: supports Process hook test

* Fix: remove response aggregation for single shard commands

* Add more preformant type conversion for Cmd type

* Add router logic into processPipeline

---------

Co-authored-by: Nedyalko Dyakov <nedyalko.dyakov@gmail.com>

* remove thread debugging code

* remove thread debugging code && reject commands with policy that cannot be used in pipeline

* refactor processPipline and cmdType enum

* remove FDescribe from cluster tests

* Add tests

* fix aggregation test

* fix mget test

* fix mget test

* remove aggregateKeyedResponses

* added scaffolding for the req-resp manager

* added default policies for the search commands

* split command map into module->command

* cleanup, added logic to refresh the cache

* added reactive cache refresh

* revert cluster refresh

* fixed lint

* addresed first batch of comments

* rewrote aggregator implementations with atomic for native or nearnative primitives

* addressed more comments, fixed lint

* added batch aggregator operations

* fixed lint

* updated batch aggregator, fixed extractcommandvalue

* fixed lint

* added batching to aggregateResponses

* fixed deadlocks

* changed aggregator logic, added error params

* added preemptive return to the aggregators

* more work on the aggregators

* updated and and or aggregators

* fixed lint

* added configurable policy resolvers

* slight refactor

* removed the interface, slight refactor

* change func signature from cmdName to cmder

* added nil safety assertions

* few small refactors

* added read only policies

* removed leftover prints

* Rebased to master, resolved comnflicts

* fixed lint

* updated gha

* fixed tests, minor consistency refactor

* preallocated simple errors

* changed numeric aggregators to use float64

* speculative test fix

* Update command.go

Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com>

* Update main_test.go

Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com>

* Add static shard picker

* Fix nil value handling in command aggregation

* Modify the Clone method to return a shallow copy

* Add clone method to digest command

* Optimize keyless command routing to respect ShardPicker policy

* Remove MGET references

* Fix MGET aggregation to map individual values to keys across shards

* Add clone method to hybrid search commands

* Undo changes in route keyless test

* remove comments

* Add test for DisableRoutingPolicies option

* Add Routing Policies Comprehensive Test Suite and Fix multi keyed aggregation for different step

---------

Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com>
Co-authored-by: Nedyalko Dyakov <nedyalko.dyakov@gmail.com>
Co-authored-by: Hristo Temelski <hristo.temelski@redis.com>
This commit is contained in:
ofekshenawa
2025-11-28 11:46:23 +02:00
committed by GitHub
parent 68d8c59557
commit f711eb0f62
46 changed files with 6875 additions and 549 deletions

View File

@@ -49,4 +49,4 @@ runs:
RE_CLUSTER: "false" RE_CLUSTER: "false"
run: | run: |
make test.ci make test.ci
shell: bash shell: bash

2053
command.go

File diff suppressed because it is too large Load Diff

209
command_policy_resolver.go Normal file
View File

@@ -0,0 +1,209 @@
package redis
import (
"context"
"strings"
"github.com/redis/go-redis/v9/internal/routing"
)
type (
module = string
commandName = string
)
var defaultPolicies = map[module]map[commandName]*routing.CommandPolicy{
"ft": {
"create": {
Request: routing.ReqDefault,
Response: routing.RespDefaultKeyless,
},
"search": {
Request: routing.ReqDefault,
Response: routing.RespDefaultKeyless,
Tips: map[string]string{
routing.ReadOnlyCMD: "",
},
},
"aggregate": {
Request: routing.ReqDefault,
Response: routing.RespDefaultKeyless,
Tips: map[string]string{
routing.ReadOnlyCMD: "",
},
},
"dictadd": {
Request: routing.ReqDefault,
Response: routing.RespDefaultKeyless,
},
"dictdump": {
Request: routing.ReqDefault,
Response: routing.RespDefaultKeyless,
Tips: map[string]string{
routing.ReadOnlyCMD: "",
},
},
"dictdel": {
Request: routing.ReqDefault,
Response: routing.RespDefaultKeyless,
},
"suglen": {
Request: routing.ReqDefault,
Response: routing.RespDefaultHashSlot,
Tips: map[string]string{
routing.ReadOnlyCMD: "",
},
},
"cursor": {
Request: routing.ReqSpecial,
Response: routing.RespDefaultKeyless,
Tips: map[string]string{
routing.ReadOnlyCMD: "",
},
},
"sugadd": {
Request: routing.ReqDefault,
Response: routing.RespDefaultHashSlot,
},
"sugget": {
Request: routing.ReqDefault,
Response: routing.RespDefaultHashSlot,
Tips: map[string]string{
routing.ReadOnlyCMD: "",
},
},
"sugdel": {
Request: routing.ReqDefault,
Response: routing.RespDefaultHashSlot,
},
"spellcheck": {
Request: routing.ReqDefault,
Response: routing.RespDefaultKeyless,
Tips: map[string]string{
routing.ReadOnlyCMD: "",
},
},
"explain": {
Request: routing.ReqDefault,
Response: routing.RespDefaultKeyless,
Tips: map[string]string{
routing.ReadOnlyCMD: "",
},
},
"explaincli": {
Request: routing.ReqDefault,
Response: routing.RespDefaultKeyless,
Tips: map[string]string{
routing.ReadOnlyCMD: "",
},
},
"aliasadd": {
Request: routing.ReqDefault,
Response: routing.RespDefaultKeyless,
},
"aliasupdate": {
Request: routing.ReqDefault,
Response: routing.RespDefaultKeyless,
},
"aliasdel": {
Request: routing.ReqDefault,
Response: routing.RespDefaultKeyless,
},
"info": {
Request: routing.ReqDefault,
Response: routing.RespDefaultKeyless,
Tips: map[string]string{
routing.ReadOnlyCMD: "",
},
},
"tagvals": {
Request: routing.ReqDefault,
Response: routing.RespDefaultKeyless,
Tips: map[string]string{
routing.ReadOnlyCMD: "",
},
},
"syndump": {
Request: routing.ReqDefault,
Response: routing.RespDefaultKeyless,
Tips: map[string]string{
routing.ReadOnlyCMD: "",
},
},
"synupdate": {
Request: routing.ReqDefault,
Response: routing.RespDefaultKeyless,
},
"profile": {
Request: routing.ReqDefault,
Response: routing.RespDefaultKeyless,
Tips: map[string]string{
routing.ReadOnlyCMD: "",
},
},
"alter": {
Request: routing.ReqDefault,
Response: routing.RespDefaultKeyless,
},
"dropindex": {
Request: routing.ReqDefault,
Response: routing.RespDefaultKeyless,
},
"drop": {
Request: routing.ReqDefault,
Response: routing.RespDefaultKeyless,
},
},
}
type CommandInfoResolveFunc func(ctx context.Context, cmd Cmder) *routing.CommandPolicy
type commandInfoResolver struct {
resolveFunc CommandInfoResolveFunc
fallBackResolver *commandInfoResolver
}
func NewCommandInfoResolver(resolveFunc CommandInfoResolveFunc) *commandInfoResolver {
return &commandInfoResolver{
resolveFunc: resolveFunc,
}
}
func NewDefaultCommandPolicyResolver() *commandInfoResolver {
return NewCommandInfoResolver(func(ctx context.Context, cmd Cmder) *routing.CommandPolicy {
module := "core"
command := cmd.Name()
cmdParts := strings.Split(command, ".")
if len(cmdParts) == 2 {
module = cmdParts[0]
command = cmdParts[1]
}
if policy, ok := defaultPolicies[module][command]; ok {
return policy
}
return nil
})
}
func (r *commandInfoResolver) GetCommandPolicy(ctx context.Context, cmd Cmder) *routing.CommandPolicy {
if r.resolveFunc == nil {
return nil
}
policy := r.resolveFunc(ctx, cmd)
if policy != nil {
return policy
}
if r.fallBackResolver != nil {
return r.fallBackResolver.GetCommandPolicy(ctx, cmd)
}
return nil
}
func (r *commandInfoResolver) SetFallbackResolver(fallbackResolver *commandInfoResolver) {
r.fallBackResolver = fallbackResolver
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"github.com/redis/go-redis/v9/internal/proto" "github.com/redis/go-redis/v9/internal/proto"
"github.com/redis/go-redis/v9/internal/routing"
) )
type TimeValue struct { type TimeValue struct {
@@ -680,6 +681,22 @@ var _ = Describe("Commands", func() {
Expect(cmd.StepCount).To(Equal(int8(0))) Expect(cmd.StepCount).To(Equal(int8(0)))
}) })
It("should Command Tips", Label("NonRedisEnterprise"), func() {
SkipAfterRedisVersion(7.9, "Redis 8 changed the COMMAND reply format")
cmds, err := client.Command(ctx).Result()
Expect(err).NotTo(HaveOccurred())
cmd := cmds["touch"]
Expect(cmd.Name).To(Equal("touch"))
Expect(cmd.CommandPolicy.Request).To(Equal(routing.ReqMultiShard))
Expect(cmd.CommandPolicy.Response).To(Equal(routing.RespAggSum))
cmd = cmds["flushall"]
Expect(cmd.Name).To(Equal("flushall"))
Expect(cmd.CommandPolicy.Request).To(Equal(routing.ReqAllShards))
Expect(cmd.CommandPolicy.Response).To(Equal(routing.RespAllSucceeded))
})
It("should return all command names", func() { It("should return all command names", func() {
cmdList := client.CommandList(ctx, nil) cmdList := client.CommandList(ctx, nil)
Expect(cmdList.Err()).NotTo(HaveOccurred()) Expect(cmdList.Err()).NotTo(HaveOccurred())

View File

@@ -12,6 +12,6 @@ require (
require ( require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
go.uber.org/atomic v1.10.0 // indirect go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect
) )

View File

@@ -9,8 +9,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cu
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=

View File

@@ -13,4 +13,5 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect github.com/klauspost/cpuid/v2 v2.0.9 // indirect
go.uber.org/atomic v1.11.0 // indirect
) )

View File

@@ -2,10 +2,15 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 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/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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=

View File

@@ -9,4 +9,5 @@ require github.com/redis/go-redis/v9 v9.7.0
require ( require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
go.uber.org/atomic v1.11.0 // indirect
) )

View File

@@ -4,5 +4,13 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 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/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 h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 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=

View File

@@ -9,4 +9,5 @@ require github.com/redis/go-redis/v9 v9.17.1
require ( require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
go.uber.org/atomic v1.11.0 // indirect
) )

View File

@@ -2,5 +2,10 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 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/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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 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/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/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=

View File

@@ -12,4 +12,5 @@ require (
require ( require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
go.uber.org/atomic v1.11.0 // indirect
) )

View File

@@ -6,3 +6,7 @@ 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/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 h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 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/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=

View File

@@ -9,4 +9,5 @@ require github.com/redis/go-redis/v9 v9.17.1
require ( require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
go.uber.org/atomic v1.11.0 // indirect
) )

View File

@@ -2,5 +2,10 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 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/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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 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/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/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=

View File

@@ -9,4 +9,5 @@ require github.com/redis/go-redis/v9 v9.11.0
require ( require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
go.uber.org/atomic v1.11.0 // indirect
) )

View File

@@ -2,5 +2,10 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 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/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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 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/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/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=

View File

@@ -36,6 +36,7 @@ require (
go.opentelemetry.io/otel/sdk/metric v1.21.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.21.0 // indirect
go.opentelemetry.io/otel/trace v1.22.0 // indirect go.opentelemetry.io/otel/trace v1.22.0 // indirect
go.opentelemetry.io/proto/otlp v1.0.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/net v0.36.0 // indirect golang.org/x/net v0.36.0 // indirect
golang.org/x/sys v0.30.0 // indirect golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect golang.org/x/text v0.22.0 // indirect

View File

@@ -51,6 +51,8 @@ go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx
go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo= go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=

View File

@@ -9,4 +9,5 @@ require github.com/redis/go-redis/v9 v9.17.1
require ( require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
go.uber.org/atomic v1.11.0 // indirect
) )

View File

@@ -2,5 +2,10 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 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/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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 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/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/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=

View File

@@ -12,4 +12,5 @@ require (
require ( require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
go.uber.org/atomic v1.11.0 // indirect
) )

View File

@@ -6,3 +6,7 @@ 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/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 h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 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/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=

View File

@@ -16,6 +16,7 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
go.uber.org/atomic v1.11.0 // indirect
) )
retract ( retract (

View File

@@ -8,6 +8,7 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/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 h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
@@ -36,6 +37,7 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -43,9 +45,12 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -95,6 +100,7 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -13,6 +13,7 @@ require (
require ( require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
go.uber.org/atomic v1.11.0 // indirect
) )
retract ( retract (

View File

@@ -4,5 +4,10 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 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/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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 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/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/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=

View File

@@ -20,6 +20,7 @@ require (
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/sys v0.16.0 // indirect golang.org/x/sys v0.16.0 // indirect
) )

View File

@@ -21,6 +21,8 @@ go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB
go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0= go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0=
go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo= go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -18,8 +18,10 @@ require (
github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.39.0 // indirect github.com/prometheus/common v0.39.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect github.com/prometheus/procfs v0.9.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/sys v0.4.0 // indirect golang.org/x/sys v0.4.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )
retract ( retract (

View File

@@ -4,6 +4,7 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 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/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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -15,6 +16,7 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
@@ -23,6 +25,9 @@ github.com/prometheus/common v0.39.0 h1:oOyhkDq05hPZKItWVBkJ6g6AtGxi+fy7F4JvUV8u
github.com/prometheus/common v0.39.0/go.mod h1:6XBZ7lYdLCbkAVhwRsWTZn+IN5AB9F/NXd5w0BbEX0Y= github.com/prometheus/common v0.39.0/go.mod h1:6XBZ7lYdLCbkAVhwRsWTZn+IN5AB9F/NXd5w0BbEX0Y=
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -31,3 +36,6 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

2
go.mod
View File

@@ -9,6 +9,8 @@ require (
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f
) )
require go.uber.org/atomic v1.11.0
retract ( retract (
v9.15.1 // This version is used to retract v9.15.0 v9.15.1 // This version is used to retract v9.15.0
v9.15.0 // This version was accidentally released. It is identical to 9.15.0-beta.2 v9.15.0 // This version was accidentally released. It is identical to 9.15.0-beta.2

5
go.sum
View File

@@ -4,5 +4,10 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 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/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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 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/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/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=

File diff suppressed because it is too large Load Diff

144
internal/routing/policy.go Normal file
View File

@@ -0,0 +1,144 @@
package routing
import (
"fmt"
"strings"
)
type RequestPolicy uint8
const (
ReqDefault RequestPolicy = iota
ReqAllNodes
ReqAllShards
ReqMultiShard
ReqSpecial
)
const (
ReadOnlyCMD string = "readonly"
)
func (p RequestPolicy) String() string {
switch p {
case ReqDefault:
return "default"
case ReqAllNodes:
return "all_nodes"
case ReqAllShards:
return "all_shards"
case ReqMultiShard:
return "multi_shard"
case ReqSpecial:
return "special"
default:
return fmt.Sprintf("unknown_request_policy(%d)", p)
}
}
func ParseRequestPolicy(raw string) (RequestPolicy, error) {
switch strings.ToLower(raw) {
case "", "default", "none":
return ReqDefault, nil
case "all_nodes":
return ReqAllNodes, nil
case "all_shards":
return ReqAllShards, nil
case "multi_shard":
return ReqMultiShard, nil
case "special":
return ReqSpecial, nil
default:
return ReqDefault, fmt.Errorf("routing: unknown request_policy %q", raw)
}
}
type ResponsePolicy uint8
const (
RespDefaultKeyless ResponsePolicy = iota
RespDefaultHashSlot
RespAllSucceeded
RespOneSucceeded
RespAggSum
RespAggMin
RespAggMax
RespAggLogicalAnd
RespAggLogicalOr
RespSpecial
)
func (p ResponsePolicy) String() string {
switch p {
case RespDefaultKeyless:
return "default(keyless)"
case RespDefaultHashSlot:
return "default(hashslot)"
case RespAllSucceeded:
return "all_succeeded"
case RespOneSucceeded:
return "one_succeeded"
case RespAggSum:
return "agg_sum"
case RespAggMin:
return "agg_min"
case RespAggMax:
return "agg_max"
case RespAggLogicalAnd:
return "agg_logical_and"
case RespAggLogicalOr:
return "agg_logical_or"
case RespSpecial:
return "special"
default:
return "all_succeeded"
}
}
func ParseResponsePolicy(raw string) (ResponsePolicy, error) {
switch strings.ToLower(raw) {
case "default(keyless)":
return RespDefaultKeyless, nil
case "default(hashslot)":
return RespDefaultHashSlot, nil
case "all_succeeded":
return RespAllSucceeded, nil
case "one_succeeded":
return RespOneSucceeded, nil
case "agg_sum":
return RespAggSum, nil
case "agg_min":
return RespAggMin, nil
case "agg_max":
return RespAggMax, nil
case "agg_logical_and":
return RespAggLogicalAnd, nil
case "agg_logical_or":
return RespAggLogicalOr, nil
case "special":
return RespSpecial, nil
default:
return RespDefaultKeyless, fmt.Errorf("routing: unknown response_policy %q", raw)
}
}
type CommandPolicy struct {
Request RequestPolicy
Response ResponsePolicy
// Tips that are not request_policy or response_policy
// e.g nondeterministic_output, nondeterministic_output_order.
Tips map[string]string
}
func (p *CommandPolicy) CanBeUsedInPipeline() bool {
return p.Request != ReqAllNodes && p.Request != ReqAllShards && p.Request != ReqMultiShard
}
func (p *CommandPolicy) IsReadOnly() bool {
_, readOnly := p.Tips[ReadOnlyCMD]
return readOnly
}

View File

@@ -0,0 +1,57 @@
package routing
import (
"math/rand"
"sync/atomic"
)
// ShardPicker chooses “one arbitrary shard” when the request_policy is
// ReqDefault and the command has no keys.
type ShardPicker interface {
Next(total int) int // returns an index in [0,total)
}
// StaticShardPicker always returns the same shard index.
type StaticShardPicker struct {
index int
}
func NewStaticShardPicker(index int) *StaticShardPicker {
return &StaticShardPicker{index: index}
}
func (p *StaticShardPicker) Next(total int) int {
if total == 0 || p.index >= total {
return 0
}
return p.index
}
/*───────────────────────────────
Round-robin (default)
────────────────────────────────*/
type RoundRobinPicker struct {
cnt atomic.Uint32
}
func (p *RoundRobinPicker) Next(total int) int {
if total == 0 {
return 0
}
i := p.cnt.Add(1)
return int(i-1) % total
}
/*───────────────────────────────
Random
────────────────────────────────*/
type RandomPicker struct{}
func (RandomPicker) Next(total int) int {
if total == 0 {
return 0
}
return rand.Intn(total)
}

View File

@@ -0,0 +1,97 @@
/*
© 2023present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)
ISC License
Modified by htemelski-redis
Removed the treshold, adapted it to work with float64
*/
package util
import (
"math"
"go.uber.org/atomic"
)
// AtomicMax is a thread-safe max container
// - hasValue indicator true if a value was equal to or greater than threshold
// - optional threshold for minimum accepted max value
// - if threshold is not used, initialization-free
// - —
// - wait-free CompareAndSwap mechanic
type AtomicMax struct {
// value is current max
value atomic.Float64
// whether [AtomicMax.Value] has been invoked
// with value equal or greater to threshold
hasValue atomic.Bool
}
// NewAtomicMax returns a thread-safe max container
// - if threshold is not used, AtomicMax is initialization-free
func NewAtomicMax() (atomicMax *AtomicMax) {
m := AtomicMax{}
m.value.Store((-math.MaxFloat64))
return &m
}
// Value updates the container with a possible max value
// - isNewMax is true if:
// - — value is equal to or greater than any threshold and
// - — invocation recorded the first 0 or
// - — a new max
// - upon return, Max and Max1 are guaranteed to reflect the invocation
// - the return order of concurrent Value invocations is not guaranteed
// - Thread-safe
func (m *AtomicMax) Value(value float64) (isNewMax bool) {
// -math.MaxFloat64 as max case
var hasValue0 = m.hasValue.Load()
if value == (-math.MaxFloat64) {
if !hasValue0 {
isNewMax = m.hasValue.CompareAndSwap(false, true)
}
return // -math.MaxFloat64 as max: isNewMax true for first 0 writer
}
// check against present value
var current = m.value.Load()
if isNewMax = value > current; !isNewMax {
return // not a new max return: isNewMax false
}
// store the new max
for {
// try to write value to *max
if isNewMax = m.value.CompareAndSwap(current, value); isNewMax {
if !hasValue0 {
// may be rarely written multiple times
// still faster than CompareAndSwap
m.hasValue.Store(true)
}
return // new max written return: isNewMax true
}
if current = m.value.Load(); current >= value {
return // no longer a need to write return: isNewMax false
}
}
}
// Max returns current max and value-present flag
// - hasValue true indicates that value reflects a Value invocation
// - hasValue false: value is zero-value
// - Thread-safe
func (m *AtomicMax) Max() (value float64, hasValue bool) {
if hasValue = m.hasValue.Load(); !hasValue {
return
}
value = m.value.Load()
return
}
// Max1 returns current maximum whether zero-value or set by Value
// - threshold is ignored
// - Thread-safe
func (m *AtomicMax) Max1() (value float64) { return m.value.Load() }

View File

@@ -0,0 +1,96 @@
package util
/*
© 2023present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)
ISC License
Modified by htemelski-redis
Adapted from the modified atomic_max, but with inverted logic
*/
import (
"math"
"go.uber.org/atomic"
)
// AtomicMin is a thread-safe Min container
// - hasValue indicator true if a value was equal to or greater than threshold
// - optional threshold for minimum accepted Min value
// - —
// - wait-free CompareAndSwap mechanic
type AtomicMin struct {
// value is current Min
value atomic.Float64
// whether [AtomicMin.Value] has been invoked
// with value equal or greater to threshold
hasValue atomic.Bool
}
// NewAtomicMin returns a thread-safe Min container
// - if threshold is not used, AtomicMin is initialization-free
func NewAtomicMin() (atomicMin *AtomicMin) {
m := AtomicMin{}
m.value.Store(math.MaxFloat64)
return &m
}
// Value updates the container with a possible Min value
// - isNewMin is true if:
// - — value is equal to or greater than any threshold and
// - — invocation recorded the first 0 or
// - — a new Min
// - upon return, Min and Min1 are guaranteed to reflect the invocation
// - the return order of concurrent Value invocations is not guaranteed
// - Thread-safe
func (m *AtomicMin) Value(value float64) (isNewMin bool) {
// math.MaxFloat64 as Min case
var hasValue0 = m.hasValue.Load()
if value == math.MaxFloat64 {
if !hasValue0 {
isNewMin = m.hasValue.CompareAndSwap(false, true)
}
return // math.MaxFloat64 as Min: isNewMin true for first 0 writer
}
// check against present value
var current = m.value.Load()
if isNewMin = value < current; !isNewMin {
return // not a new Min return: isNewMin false
}
// store the new Min
for {
// try to write value to *Min
if isNewMin = m.value.CompareAndSwap(current, value); isNewMin {
if !hasValue0 {
// may be rarely written multiple times
// still faster than CompareAndSwap
m.hasValue.Store(true)
}
return // new Min written return: isNewMin true
}
if current = m.value.Load(); current <= value {
return // no longer a need to write return: isNewMin false
}
}
}
// Min returns current min and value-present flag
// - hasValue true indicates that value reflects a Value invocation
// - hasValue false: value is zero-value
// - Thread-safe
func (m *AtomicMin) Min() (value float64, hasValue bool) {
if hasValue = m.hasValue.Load(); !hasValue {
return
}
value = m.value.Load()
return
}
// Min1 returns current Minimum whether zero-value or set by Value
// - threshold is ignored
// - Thread-safe
func (m *AtomicMin) Min1() (value float64) { return m.value.Load() }

47
json.go
View File

@@ -68,8 +68,9 @@ var _ Cmder = (*JSONCmd)(nil)
func newJSONCmd(ctx context.Context, args ...interface{}) *JSONCmd { func newJSONCmd(ctx context.Context, args ...interface{}) *JSONCmd {
return &JSONCmd{ return &JSONCmd{
baseCmd: baseCmd{ baseCmd: baseCmd{
ctx: ctx, ctx: ctx,
args: args, args: args,
cmdType: CmdTypeJSON,
}, },
} }
} }
@@ -165,6 +166,14 @@ func (cmd *JSONCmd) readReply(rd *proto.Reader) error {
return nil return nil
} }
func (cmd *JSONCmd) Clone() Cmder {
return &JSONCmd{
baseCmd: cmd.cloneBaseCmd(),
val: cmd.val,
expanded: cmd.expanded, // interface{} can be shared as it should be immutable after parsing
}
}
// ------------------------------------------- // -------------------------------------------
type JSONSliceCmd struct { type JSONSliceCmd struct {
@@ -175,8 +184,9 @@ type JSONSliceCmd struct {
func NewJSONSliceCmd(ctx context.Context, args ...interface{}) *JSONSliceCmd { func NewJSONSliceCmd(ctx context.Context, args ...interface{}) *JSONSliceCmd {
return &JSONSliceCmd{ return &JSONSliceCmd{
baseCmd: baseCmd{ baseCmd: baseCmd{
ctx: ctx, ctx: ctx,
args: args, args: args,
cmdType: CmdTypeJSONSlice,
}, },
} }
} }
@@ -233,6 +243,18 @@ func (cmd *JSONSliceCmd) readReply(rd *proto.Reader) error {
return nil return nil
} }
func (cmd *JSONSliceCmd) Clone() Cmder {
var val []interface{}
if cmd.val != nil {
val = make([]interface{}, len(cmd.val))
copy(val, cmd.val)
}
return &JSONSliceCmd{
baseCmd: cmd.cloneBaseCmd(),
val: val,
}
}
/******************************************************************************* /*******************************************************************************
* *
* IntPointerSliceCmd * IntPointerSliceCmd
@@ -249,8 +271,9 @@ type IntPointerSliceCmd struct {
func NewIntPointerSliceCmd(ctx context.Context, args ...interface{}) *IntPointerSliceCmd { func NewIntPointerSliceCmd(ctx context.Context, args ...interface{}) *IntPointerSliceCmd {
return &IntPointerSliceCmd{ return &IntPointerSliceCmd{
baseCmd: baseCmd{ baseCmd: baseCmd{
ctx: ctx, ctx: ctx,
args: args, args: args,
cmdType: CmdTypeIntPointerSlice,
}, },
} }
} }
@@ -290,6 +313,18 @@ func (cmd *IntPointerSliceCmd) readReply(rd *proto.Reader) error {
return nil return nil
} }
func (cmd *IntPointerSliceCmd) Clone() Cmder {
var val []*int64
if cmd.val != nil {
val = make([]*int64, len(cmd.val))
copy(val, cmd.val)
}
return &IntPointerSliceCmd{
baseCmd: cmd.cloneBaseCmd(),
val: val,
}
}
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// JSONArrAppend adds the provided JSON values to the end of the array at the given path. // JSONArrAppend adds the provided JSON values to the end of the array at the given path.

View File

@@ -3,6 +3,7 @@ package redis
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"errors"
"fmt" "fmt"
"math" "math"
"net" "net"
@@ -20,6 +21,7 @@ import (
"github.com/redis/go-redis/v9/internal/pool" "github.com/redis/go-redis/v9/internal/pool"
"github.com/redis/go-redis/v9/internal/proto" "github.com/redis/go-redis/v9/internal/proto"
"github.com/redis/go-redis/v9/internal/rand" "github.com/redis/go-redis/v9/internal/rand"
"github.com/redis/go-redis/v9/internal/routing"
"github.com/redis/go-redis/v9/maintnotifications" "github.com/redis/go-redis/v9/maintnotifications"
"github.com/redis/go-redis/v9/push" "github.com/redis/go-redis/v9/push"
) )
@@ -28,7 +30,11 @@ const (
minLatencyMeasurementInterval = 10 * time.Second minLatencyMeasurementInterval = 10 * time.Second
) )
var errClusterNoNodes = fmt.Errorf("redis: cluster has no nodes") var (
errClusterNoNodes = errors.New("redis: cluster has no nodes")
errNoWatchKeys = errors.New("redis: Watch requires at least one key")
errWatchCrosslot = errors.New("redis: Watch requires all keys to be in the same slot")
)
// ClusterOptions are used to configure a cluster client and should be // ClusterOptions are used to configure a cluster client and should be
// passed to NewClusterClient. // passed to NewClusterClient.
@@ -115,6 +121,11 @@ type ClusterOptions struct {
TLSConfig *tls.Config TLSConfig *tls.Config
// DisableRoutingPolicies disables the request/response policy routing system.
// When disabled, all commands use the legacy routing behavior.
// Experimental. Will be removed when shard picker is fully implemented.
DisableRoutingPolicies bool
// DisableIndentity - Disable set-lib on connect. // DisableIndentity - Disable set-lib on connect.
// //
// default: false // default: false
@@ -148,6 +159,9 @@ type ClusterOptions struct {
// If nil, maintnotifications upgrades are in "auto" mode and will be enabled if the server supports it. // If nil, maintnotifications upgrades are in "auto" mode and will be enabled if the server supports it.
// The ClusterClient does not directly work with maintnotifications, it is up to the clients in the Nodes map to work with maintnotifications. // The ClusterClient does not directly work with maintnotifications, it is up to the clients in the Nodes map to work with maintnotifications.
MaintNotificationsConfig *maintnotifications.Config MaintNotificationsConfig *maintnotifications.Config
// ShardPicker is used to pick a shard when the request_policy is
// ReqDefault and the command has no keys.
ShardPicker routing.ShardPicker
} }
func (opt *ClusterOptions) init() { func (opt *ClusterOptions) init() {
@@ -208,6 +222,10 @@ func (opt *ClusterOptions) init() {
if opt.FailingTimeoutSeconds == 0 { if opt.FailingTimeoutSeconds == 0 {
opt.FailingTimeoutSeconds = 15 opt.FailingTimeoutSeconds = 15
} }
if opt.ShardPicker == nil {
opt.ShardPicker = &routing.RoundRobinPicker{}
}
} }
// ParseClusterURL parses a URL into ClusterOptions that can be used to connect to Redis. // ParseClusterURL parses a URL into ClusterOptions that can be used to connect to Redis.
@@ -926,6 +944,29 @@ func (c *clusterState) slotRandomNode(slot int) (*clusterNode, error) {
return nodes[randomNodes[0]], nil return nodes[randomNodes[0]], nil
} }
func (c *clusterState) slotShardPickerSlaveNode(slot int, shardPicker routing.ShardPicker) (*clusterNode, error) {
nodes := c.slotNodes(slot)
if len(nodes) == 0 {
return c.nodes.Random()
}
// nodes[0] is master, nodes[1:] are slaves
// First, try all slave nodes for this slot using ShardPicker order
slaves := nodes[1:]
if len(slaves) > 0 {
for i := 0; i < len(slaves); i++ {
idx := shardPicker.Next(len(slaves))
slave := slaves[idx]
if !slave.Failing() && !slave.Loading() {
return slave, nil
}
}
}
// All slaves are failing or loading - return master
return nodes[0], nil
}
func (c *clusterState) slotNodes(slot int) []*clusterNode { func (c *clusterState) slotNodes(slot int) []*clusterNode {
i := sort.Search(len(c.slots), func(i int) bool { i := sort.Search(len(c.slots), func(i int) bool {
return c.slots[i].end >= slot return c.slots[i].end >= slot
@@ -943,15 +984,14 @@ func (c *clusterState) slotNodes(slot int) []*clusterNode {
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
type clusterStateHolder struct { type clusterStateHolder struct {
load func(ctx context.Context) (*clusterState, error) load func(ctx context.Context) (*clusterState, error)
state atomic.Value state atomic.Value
reloading uint32 // atomic reloading uint32 // atomic
} }
func newClusterStateHolder(fn func(ctx context.Context) (*clusterState, error)) *clusterStateHolder { func newClusterStateHolder(load func(ctx context.Context) (*clusterState, error)) *clusterStateHolder {
return &clusterStateHolder{ return &clusterStateHolder{
load: fn, load: load,
} }
} }
@@ -1006,10 +1046,11 @@ func (c *clusterStateHolder) ReloadOrGet(ctx context.Context) (*clusterState, er
// or more underlying connections. It's safe for concurrent use by // or more underlying connections. It's safe for concurrent use by
// multiple goroutines. // multiple goroutines.
type ClusterClient struct { type ClusterClient struct {
opt *ClusterOptions opt *ClusterOptions
nodes *clusterNodes nodes *clusterNodes
state *clusterStateHolder state *clusterStateHolder
cmdsInfoCache *cmdsInfoCache cmdsInfoCache *cmdsInfoCache
cmdInfoResolver *commandInfoResolver
cmdable cmdable
hooksMixin hooksMixin
} }
@@ -1017,9 +1058,6 @@ type ClusterClient struct {
// NewClusterClient returns a Redis Cluster client as described in // NewClusterClient returns a Redis Cluster client as described in
// http://redis.io/topics/cluster-spec. // http://redis.io/topics/cluster-spec.
func NewClusterClient(opt *ClusterOptions) *ClusterClient { func NewClusterClient(opt *ClusterOptions) *ClusterClient {
if opt == nil {
panic("redis: NewClusterClient nil options")
}
opt.init() opt.init()
c := &ClusterClient{ c := &ClusterClient{
@@ -1027,10 +1065,13 @@ func NewClusterClient(opt *ClusterOptions) *ClusterClient {
nodes: newClusterNodes(opt), nodes: newClusterNodes(opt),
} }
c.state = newClusterStateHolder(c.loadState)
c.cmdsInfoCache = newCmdsInfoCache(c.cmdsInfo) c.cmdsInfoCache = newCmdsInfoCache(c.cmdsInfo)
c.cmdable = c.Process
c.state = newClusterStateHolder(c.loadState)
c.SetCommandInfoResolver(NewDefaultCommandPolicyResolver())
c.cmdable = c.Process
c.initHooks(hooks{ c.initHooks(hooks{
dial: nil, dial: nil,
process: c.process, process: c.process,
@@ -1083,7 +1124,11 @@ func (c *ClusterClient) process(ctx context.Context, cmd Cmder) error {
if node == nil { if node == nil {
var err error var err error
node, err = c.cmdNode(ctx, cmd.Name(), slot) if !c.opt.DisableRoutingPolicies && c.opt.ShardPicker != nil {
node, err = c.cmdNodeWithShardPicker(ctx, cmd.Name(), slot, c.opt.ShardPicker)
} else {
node, err = c.cmdNode(ctx, cmd.Name(), slot)
}
if err != nil { if err != nil {
return err return err
} }
@@ -1091,13 +1136,16 @@ func (c *ClusterClient) process(ctx context.Context, cmd Cmder) error {
if ask { if ask {
ask = false ask = false
pipe := node.Client.Pipeline() pipe := node.Client.Pipeline()
_ = pipe.Process(ctx, NewCmd(ctx, "asking")) _ = pipe.Process(ctx, NewCmd(ctx, "asking"))
_ = pipe.Process(ctx, cmd) _ = pipe.Process(ctx, cmd)
_, lastErr = pipe.Exec(ctx) _, lastErr = pipe.Exec(ctx)
} else { } else {
lastErr = node.Client.Process(ctx, cmd) if !c.opt.DisableRoutingPolicies {
lastErr = c.routeAndRun(ctx, cmd, node)
} else {
lastErr = node.Client.Process(ctx, cmd)
}
} }
// If there is no error - we are done. // If there is no error - we are done.
@@ -1413,13 +1461,18 @@ func (c *ClusterClient) mapCmdsByNode(ctx context.Context, cmdsMap *cmdsMap, cmd
return err return err
} }
preferredRandomSlot := -1
if c.opt.ReadOnly && c.cmdsAreReadOnly(ctx, cmds) { if c.opt.ReadOnly && c.cmdsAreReadOnly(ctx, cmds) {
for _, cmd := range cmds { for _, cmd := range cmds {
slot := c.cmdSlot(cmd, preferredRandomSlot) var policy *routing.CommandPolicy
if preferredRandomSlot == -1 { if c.cmdInfoResolver != nil {
preferredRandomSlot = slot policy = c.cmdInfoResolver.GetCommandPolicy(ctx, cmd)
} }
if policy != nil && !policy.CanBeUsedInPipeline() {
return fmt.Errorf(
"redis: cannot pipeline command %q with request policy ReqAllNodes/ReqAllShards/ReqMultiShard; Note: This behavior is subject to change in the future", cmd.Name(),
)
}
slot := c.cmdSlot(cmd, -1)
node, err := c.slotReadOnlyNode(state, slot) node, err := c.slotReadOnlyNode(state, slot)
if err != nil { if err != nil {
return err return err
@@ -1430,10 +1483,16 @@ func (c *ClusterClient) mapCmdsByNode(ctx context.Context, cmdsMap *cmdsMap, cmd
} }
for _, cmd := range cmds { for _, cmd := range cmds {
slot := c.cmdSlot(cmd, preferredRandomSlot) var policy *routing.CommandPolicy
if preferredRandomSlot == -1 { if c.cmdInfoResolver != nil {
preferredRandomSlot = slot policy = c.cmdInfoResolver.GetCommandPolicy(ctx, cmd)
} }
if policy != nil && !policy.CanBeUsedInPipeline() {
return fmt.Errorf(
"redis: cannot pipeline command %q with request policy ReqAllNodes/ReqAllShards/ReqMultiShard; Note: This behavior is subject to change in the future", cmd.Name(),
)
}
slot := c.cmdSlot(cmd, -1)
node, err := state.slotMasterNode(slot) node, err := state.slotMasterNode(slot)
if err != nil { if err != nil {
return err return err
@@ -1595,7 +1654,7 @@ func (c *ClusterClient) processTxPipeline(ctx context.Context, cmds []Cmder) err
return err return err
} }
keyedCmdsBySlot := c.slottedKeyedCommands(cmds) keyedCmdsBySlot := c.slottedKeyedCommands(ctx, cmds)
slot := -1 slot := -1
switch len(keyedCmdsBySlot) { switch len(keyedCmdsBySlot) {
case 0: case 0:
@@ -1649,18 +1708,18 @@ func (c *ClusterClient) processTxPipeline(ctx context.Context, cmds []Cmder) err
// slottedKeyedCommands returns a map of slot to commands taking into account // slottedKeyedCommands returns a map of slot to commands taking into account
// only commands that have keys. // only commands that have keys.
func (c *ClusterClient) slottedKeyedCommands(cmds []Cmder) map[int][]Cmder { func (c *ClusterClient) slottedKeyedCommands(ctx context.Context, cmds []Cmder) map[int][]Cmder {
cmdsSlots := map[int][]Cmder{} cmdsSlots := map[int][]Cmder{}
preferredRandomSlot := -1 prefferedRandomSlot := -1
for _, cmd := range cmds { for _, cmd := range cmds {
if cmdFirstKeyPos(cmd) == 0 { if cmdFirstKeyPos(cmd) == 0 {
continue continue
} }
slot := c.cmdSlot(cmd, preferredRandomSlot) slot := c.cmdSlot(cmd, prefferedRandomSlot)
if preferredRandomSlot == -1 { if prefferedRandomSlot == -1 {
preferredRandomSlot = slot prefferedRandomSlot = slot
} }
cmdsSlots[slot] = append(cmdsSlots[slot], cmd) cmdsSlots[slot] = append(cmdsSlots[slot], cmd)
@@ -1819,14 +1878,13 @@ func (c *ClusterClient) cmdsMoved(
func (c *ClusterClient) Watch(ctx context.Context, fn func(*Tx) error, keys ...string) error { func (c *ClusterClient) Watch(ctx context.Context, fn func(*Tx) error, keys ...string) error {
if len(keys) == 0 { if len(keys) == 0 {
return fmt.Errorf("redis: Watch requires at least one key") return errNoWatchKeys
} }
slot := hashtag.Slot(keys[0]) slot := hashtag.Slot(keys[0])
for _, key := range keys[1:] { for _, key := range keys[1:] {
if hashtag.Slot(key) != slot { if hashtag.Slot(key) != slot {
err := fmt.Errorf("redis: Watch requires all keys to be in the same slot") return errWatchCrosslot
return err
} }
} }
@@ -1995,7 +2053,6 @@ func (c *ClusterClient) cmdsInfo(ctx context.Context) (map[string]*CommandInfo,
for _, idx := range perm { for _, idx := range perm {
addr := addrs[idx] addr := addrs[idx]
node, err := c.nodes.GetOrCreate(addr) node, err := c.nodes.GetOrCreate(addr)
if err != nil { if err != nil {
if firstErr == nil { if firstErr == nil {
@@ -2008,6 +2065,7 @@ func (c *ClusterClient) cmdsInfo(ctx context.Context) (map[string]*CommandInfo,
if err == nil { if err == nil {
return info, nil return info, nil
} }
if firstErr == nil { if firstErr == nil {
firstErr = err firstErr = err
} }
@@ -2019,33 +2077,45 @@ func (c *ClusterClient) cmdsInfo(ctx context.Context) (map[string]*CommandInfo,
return nil, firstErr return nil, firstErr
} }
// cmdInfo will fetch and cache the command policies after the first execution
func (c *ClusterClient) cmdInfo(ctx context.Context, name string) *CommandInfo { func (c *ClusterClient) cmdInfo(ctx context.Context, name string) *CommandInfo {
cmdsInfo, err := c.cmdsInfoCache.Get(ctx) // Use a separate context that won't be canceled to ensure command info lookup
// doesn't fail due to original context cancellation
cmdInfoCtx := c.context(ctx)
if c.opt.ContextTimeoutEnabled && ctx != nil {
// If context timeout is enabled, still use a reasonable timeout
var cancel context.CancelFunc
cmdInfoCtx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
}
cmdsInfo, err := c.cmdsInfoCache.Get(cmdInfoCtx)
if err != nil { if err != nil {
internal.Logger.Printf(context.TODO(), "getting command info: %s", err) internal.Logger.Printf(cmdInfoCtx, "getting command info: %s", err)
return nil return nil
} }
info := cmdsInfo[name] info := cmdsInfo[name]
if info == nil { if info == nil {
internal.Logger.Printf(context.TODO(), "info for cmd=%s not found", name) internal.Logger.Printf(cmdInfoCtx, "info for cmd=%s not found", name)
} }
return info return info
} }
func (c *ClusterClient) cmdSlot(cmd Cmder, preferredRandomSlot int) int { func (c *ClusterClient) cmdSlot(cmd Cmder, prefferedSlot int) int {
args := cmd.Args() args := cmd.Args()
if args[0] == "cluster" && (args[1] == "getkeysinslot" || args[1] == "countkeysinslot") { if args[0] == "cluster" && (args[1] == "getkeysinslot" || args[1] == "countkeysinslot") {
return args[2].(int) return args[2].(int)
} }
return cmdSlot(cmd, cmdFirstKeyPos(cmd), preferredRandomSlot) return cmdSlot(cmd, cmdFirstKeyPos(cmd), prefferedSlot)
} }
func cmdSlot(cmd Cmder, pos int, preferredRandomSlot int) int { func cmdSlot(cmd Cmder, pos int, prefferedRandomSlot int) int {
if pos == 0 { if pos == 0 {
if preferredRandomSlot != -1 { if prefferedRandomSlot != -1 {
return preferredRandomSlot return prefferedRandomSlot
} }
return hashtag.RandomSlot() return hashtag.RandomSlot()
} }
@@ -2072,6 +2142,36 @@ func (c *ClusterClient) cmdNode(
return state.slotMasterNode(slot) return state.slotMasterNode(slot)
} }
func (c *ClusterClient) cmdNodeWithShardPicker(
ctx context.Context,
cmdName string,
slot int,
shardPicker routing.ShardPicker,
) (*clusterNode, error) {
state, err := c.state.Get(ctx)
if err != nil {
return nil, err
}
// For keyless commands (slot == -1), use ShardPicker to select a shard
// This respects the user's configured ShardPicker policy
if slot == -1 {
if len(state.Masters) == 0 {
return nil, errClusterNoNodes
}
idx := shardPicker.Next(len(state.Masters))
return state.Masters[idx], nil
}
if c.opt.ReadOnly {
cmdInfo := c.cmdInfo(ctx, cmdName)
if cmdInfo != nil && cmdInfo.ReadOnly {
return c.slotReadOnlyNode(state, slot)
}
}
return state.slotMasterNode(slot)
}
func (c *ClusterClient) slotReadOnlyNode(state *clusterState, slot int) (*clusterNode, error) { func (c *ClusterClient) slotReadOnlyNode(state *clusterState, slot int) (*clusterNode, error) {
if c.opt.RouteByLatency { if c.opt.RouteByLatency {
return state.slotClosestNode(slot) return state.slotClosestNode(slot)
@@ -2079,6 +2179,11 @@ func (c *ClusterClient) slotReadOnlyNode(state *clusterState, slot int) (*cluste
if c.opt.RouteRandomly { if c.opt.RouteRandomly {
return state.slotRandomNode(slot) return state.slotRandomNode(slot)
} }
if c.opt.ShardPicker != nil {
return state.slotShardPickerSlaveNode(slot, c.opt.ShardPicker)
}
return state.slotSlaveNode(slot) return state.slotSlaveNode(slot)
} }
@@ -2126,6 +2231,31 @@ func (c *ClusterClient) context(ctx context.Context) context.Context {
return context.Background() return context.Background()
} }
func (c *ClusterClient) GetResolver() *commandInfoResolver {
return c.cmdInfoResolver
}
func (c *ClusterClient) SetCommandInfoResolver(cmdInfoResolver *commandInfoResolver) {
c.cmdInfoResolver = cmdInfoResolver
}
// extractCommandInfo retrieves the routing policy for a command
func (c *ClusterClient) extractCommandInfo(ctx context.Context, cmd Cmder) *routing.CommandPolicy {
if cmdInfo := c.cmdInfo(ctx, cmd.Name()); cmdInfo != nil && cmdInfo.CommandPolicy != nil {
return cmdInfo.CommandPolicy
}
return nil
}
// NewDynamicResolver returns a CommandInfoResolver
// that uses the underlying cmdInfo cache to resolve the policies
func (c *ClusterClient) NewDynamicResolver() *commandInfoResolver {
return &commandInfoResolver{
resolveFunc: c.extractCommandInfo,
}
}
func appendIfNotExist[T comparable](vals []T, newVal T) []T { func appendIfNotExist[T comparable](vals []T, newVal T) []T {
for _, v := range vals { for _, v := range vals {
if v == newVal { if v == newVal {

992
osscluster_router.go Normal file
View File

@@ -0,0 +1,992 @@
package redis
import (
"context"
"errors"
"fmt"
"reflect"
"sync"
"time"
"github.com/redis/go-redis/v9/internal/hashtag"
"github.com/redis/go-redis/v9/internal/routing"
)
var (
errInvalidCmdPointer = errors.New("redis: invalid command pointer")
errNoCmdsToAggregate = errors.New("redis: no commands to aggregate")
errNoResToAggregate = errors.New("redis: no results to aggregate")
errInvalidCursorCmdArgsCount = errors.New("redis: FT.CURSOR command requires at least 3 arguments")
errInvalidCursorIdType = errors.New("redis: invalid cursor ID type")
)
// slotResult represents the result of executing a command on a specific slot
type slotResult struct {
cmd Cmder
keys []string
err error
}
// routeAndRun routes a command to the appropriate cluster nodes and executes it
func (c *ClusterClient) routeAndRun(ctx context.Context, cmd Cmder, node *clusterNode) error {
var policy *routing.CommandPolicy
if c.cmdInfoResolver != nil {
policy = c.cmdInfoResolver.GetCommandPolicy(ctx, cmd)
}
// Set stepCount from cmdInfo if not already set
if cmd.stepCount() == 0 {
if cmdInfo := c.cmdInfo(ctx, cmd.Name()); cmdInfo != nil && cmdInfo.StepCount > 0 {
cmd.SetStepCount(cmdInfo.StepCount)
}
}
if policy == nil {
return c.executeDefault(ctx, cmd, policy, node)
}
switch policy.Request {
case routing.ReqAllNodes:
return c.executeOnAllNodes(ctx, cmd, policy)
case routing.ReqAllShards:
return c.executeOnAllShards(ctx, cmd, policy)
case routing.ReqMultiShard:
return c.executeMultiShard(ctx, cmd, policy)
case routing.ReqSpecial:
return c.executeSpecialCommand(ctx, cmd, policy, node)
default:
return c.executeDefault(ctx, cmd, policy, node)
}
}
// executeDefault handles standard command routing based on keys
func (c *ClusterClient) executeDefault(ctx context.Context, cmd Cmder, policy *routing.CommandPolicy, node *clusterNode) error {
if policy != nil && !c.hasKeys(cmd) {
if c.readOnlyEnabled() && policy.IsReadOnly() {
return c.executeOnArbitraryNode(ctx, cmd)
}
}
return node.Client.Process(ctx, cmd)
}
// executeOnArbitraryNode routes command to an arbitrary node
func (c *ClusterClient) executeOnArbitraryNode(ctx context.Context, cmd Cmder) error {
node := c.pickArbitraryNode(ctx)
if node == nil {
return errClusterNoNodes
}
return node.Client.Process(ctx, cmd)
}
// executeOnAllNodes executes command on all nodes (masters and replicas)
func (c *ClusterClient) executeOnAllNodes(ctx context.Context, cmd Cmder, policy *routing.CommandPolicy) error {
state, err := c.state.Get(ctx)
if err != nil {
return err
}
nodes := append(state.Masters, state.Slaves...)
if len(nodes) == 0 {
return errClusterNoNodes
}
return c.executeParallel(ctx, cmd, nodes, policy)
}
// executeOnAllShards executes command on all master shards
func (c *ClusterClient) executeOnAllShards(ctx context.Context, cmd Cmder, policy *routing.CommandPolicy) error {
state, err := c.state.Get(ctx)
if err != nil {
return err
}
if len(state.Masters) == 0 {
return errClusterNoNodes
}
return c.executeParallel(ctx, cmd, state.Masters, policy)
}
// executeMultiShard handles commands that operate on multiple keys across shards
func (c *ClusterClient) executeMultiShard(ctx context.Context, cmd Cmder, policy *routing.CommandPolicy) error {
args := cmd.Args()
firstKeyPos := int(cmdFirstKeyPos(cmd))
stepCount := int(cmd.stepCount())
if stepCount == 0 {
stepCount = 1 // Default to 1 if not set
}
if firstKeyPos == 0 || firstKeyPos >= len(args) {
return fmt.Errorf("redis: multi-shard command %s has no key arguments", cmd.Name())
}
// Group keys by slot
slotMap := make(map[int][]string)
keyOrder := make([]string, 0)
for i := firstKeyPos; i < len(args); i += stepCount {
key, ok := args[i].(string)
if !ok {
return fmt.Errorf("redis: non-string key at position %d: %v", i, args[i])
}
slot := hashtag.Slot(key)
slotMap[slot] = append(slotMap[slot], key)
for j := 1; j < stepCount; j++ {
if i+j >= len(args) {
break
}
slotMap[slot] = append(slotMap[slot], args[i+j].(string))
}
keyOrder = append(keyOrder, key)
}
return c.executeMultiSlot(ctx, cmd, slotMap, keyOrder, policy)
}
// executeMultiSlot executes commands across multiple slots concurrently
func (c *ClusterClient) executeMultiSlot(ctx context.Context, cmd Cmder, slotMap map[int][]string, keyOrder []string, policy *routing.CommandPolicy) error {
results := make(chan slotResult, len(slotMap))
var wg sync.WaitGroup
// Execute on each slot concurrently
for slot, keys := range slotMap {
wg.Add(1)
go func(slot int, keys []string) {
defer wg.Done()
node, err := c.cmdNodeWithShardPicker(ctx, cmd.Name(), slot, c.opt.ShardPicker)
if err != nil {
results <- slotResult{nil, keys, err}
return
}
// Create a command for this specific slot's keys
subCmd := c.createSlotSpecificCommand(ctx, cmd, keys)
err = node.Client.Process(ctx, subCmd)
results <- slotResult{subCmd, keys, err}
}(slot, keys)
}
go func() {
wg.Wait()
close(results)
}()
return c.aggregateMultiSlotResults(ctx, cmd, results, keyOrder, policy)
}
// createSlotSpecificCommand creates a new command for a specific slot's keys
func (c *ClusterClient) createSlotSpecificCommand(ctx context.Context, originalCmd Cmder, keys []string) Cmder {
originalArgs := originalCmd.Args()
firstKeyPos := int(cmdFirstKeyPos(originalCmd))
// Build new args with only the specified keys
newArgs := make([]interface{}, 0, firstKeyPos+len(keys))
// Copy command name and arguments before the keys
newArgs = append(newArgs, originalArgs[:firstKeyPos]...)
// Add the slot-specific keys
for _, key := range keys {
newArgs = append(newArgs, key)
}
// Create a new command of the same type using the helper function
return createCommandByType(ctx, originalCmd.GetCmdType(), newArgs...)
}
// createCommandByType creates a new command of the specified type with the given arguments
func createCommandByType(ctx context.Context, cmdType CmdType, args ...interface{}) Cmder {
switch cmdType {
case CmdTypeString:
return NewStringCmd(ctx, args...)
case CmdTypeInt:
return NewIntCmd(ctx, args...)
case CmdTypeBool:
return NewBoolCmd(ctx, args...)
case CmdTypeFloat:
return NewFloatCmd(ctx, args...)
case CmdTypeStringSlice:
return NewStringSliceCmd(ctx, args...)
case CmdTypeIntSlice:
return NewIntSliceCmd(ctx, args...)
case CmdTypeFloatSlice:
return NewFloatSliceCmd(ctx, args...)
case CmdTypeBoolSlice:
return NewBoolSliceCmd(ctx, args...)
case CmdTypeStatus:
return NewStatusCmd(ctx, args...)
case CmdTypeTime:
return NewTimeCmd(ctx, args...)
case CmdTypeMapStringString:
return NewMapStringStringCmd(ctx, args...)
case CmdTypeMapStringInt:
return NewMapStringIntCmd(ctx, args...)
case CmdTypeMapStringInterface:
return NewMapStringInterfaceCmd(ctx, args...)
case CmdTypeMapStringInterfaceSlice:
return NewMapStringInterfaceSliceCmd(ctx, args...)
case CmdTypeSlice:
return NewSliceCmd(ctx, args...)
case CmdTypeStringStructMap:
return NewStringStructMapCmd(ctx, args...)
case CmdTypeXMessageSlice:
return NewXMessageSliceCmd(ctx, args...)
case CmdTypeXStreamSlice:
return NewXStreamSliceCmd(ctx, args...)
case CmdTypeXPending:
return NewXPendingCmd(ctx, args...)
case CmdTypeXPendingExt:
return NewXPendingExtCmd(ctx, args...)
case CmdTypeXAutoClaim:
return NewXAutoClaimCmd(ctx, args...)
case CmdTypeXAutoClaimJustID:
return NewXAutoClaimJustIDCmd(ctx, args...)
case CmdTypeXInfoStreamFull:
return NewXInfoStreamFullCmd(ctx, args...)
case CmdTypeZSlice:
return NewZSliceCmd(ctx, args...)
case CmdTypeZWithKey:
return NewZWithKeyCmd(ctx, args...)
case CmdTypeClusterSlots:
return NewClusterSlotsCmd(ctx, args...)
case CmdTypeGeoPos:
return NewGeoPosCmd(ctx, args...)
case CmdTypeCommandsInfo:
return NewCommandsInfoCmd(ctx, args...)
case CmdTypeSlowLog:
return NewSlowLogCmd(ctx, args...)
case CmdTypeKeyValues:
return NewKeyValuesCmd(ctx, args...)
case CmdTypeZSliceWithKey:
return NewZSliceWithKeyCmd(ctx, args...)
case CmdTypeFunctionList:
return NewFunctionListCmd(ctx, args...)
case CmdTypeFunctionStats:
return NewFunctionStatsCmd(ctx, args...)
case CmdTypeKeyFlags:
return NewKeyFlagsCmd(ctx, args...)
case CmdTypeDuration:
return NewDurationCmd(ctx, time.Millisecond, args...)
}
return NewCmd(ctx, args...)
}
// executeSpecialCommand handles commands with special routing requirements
func (c *ClusterClient) executeSpecialCommand(ctx context.Context, cmd Cmder, policy *routing.CommandPolicy, node *clusterNode) error {
switch cmd.Name() {
case "ft.cursor":
return c.executeCursorCommand(ctx, cmd)
default:
return c.executeDefault(ctx, cmd, policy, node)
}
}
// executeCursorCommand handles FT.CURSOR commands with sticky routing
func (c *ClusterClient) executeCursorCommand(ctx context.Context, cmd Cmder) error {
args := cmd.Args()
if len(args) < 4 {
return errInvalidCursorCmdArgsCount
}
cursorID, ok := args[3].(string)
if !ok {
return errInvalidCursorIdType
}
// Route based on cursor ID to maintain stickiness
slot := hashtag.Slot(cursorID)
node, err := c.cmdNodeWithShardPicker(ctx, cmd.Name(), slot, c.opt.ShardPicker)
if err != nil {
return err
}
return node.Client.Process(ctx, cmd)
}
// executeParallel executes a command on multiple nodes concurrently
func (c *ClusterClient) executeParallel(ctx context.Context, cmd Cmder, nodes []*clusterNode, policy *routing.CommandPolicy) error {
if len(nodes) == 0 {
return errClusterNoNodes
}
if len(nodes) == 1 {
return nodes[0].Client.Process(ctx, cmd)
}
type nodeResult struct {
cmd Cmder
err error
}
results := make(chan nodeResult, len(nodes))
var wg sync.WaitGroup
for _, node := range nodes {
wg.Add(1)
go func(n *clusterNode) {
defer wg.Done()
cmdCopy := cmd.Clone()
err := n.Client.Process(ctx, cmdCopy)
results <- nodeResult{cmdCopy, err}
}(node)
}
go func() {
wg.Wait()
close(results)
}()
// Collect results and check for errors
cmds := make([]Cmder, 0, len(nodes))
var firstErr error
for result := range results {
if result.err != nil && firstErr == nil {
firstErr = result.err
}
cmds = append(cmds, result.cmd)
}
// If there was an error and no policy specified, fail fast
if firstErr != nil && (policy == nil || policy.Response == routing.RespDefaultKeyless) {
cmd.SetErr(firstErr)
return firstErr
}
return c.aggregateResponses(cmd, cmds, policy)
}
// aggregateMultiSlotResults aggregates results from multi-slot execution
func (c *ClusterClient) aggregateMultiSlotResults(ctx context.Context, cmd Cmder, results <-chan slotResult, keyOrder []string, policy *routing.CommandPolicy) error {
keyedResults := make(map[string]routing.AggregatorResErr)
var firstErr error
for result := range results {
if result.err != nil && firstErr == nil {
firstErr = result.err
}
if result.cmd != nil && result.err == nil {
value, err := ExtractCommandValue(result.cmd)
// Check if the result is a slice (e.g., from MGET)
if sliceValue, ok := value.([]interface{}); ok {
// Map each element to its corresponding key
for i, key := range result.keys {
if i < len(sliceValue) {
keyedResults[key] = routing.AggregatorResErr{Result: sliceValue[i], Err: err}
} else {
keyedResults[key] = routing.AggregatorResErr{Result: nil, Err: err}
}
}
} else {
// For non-slice results, map the entire result to each key
for _, key := range result.keys {
keyedResults[key] = routing.AggregatorResErr{Result: value, Err: err}
}
}
}
// TODO: return multiple errors by order when we will implement multiple errors returning
if result.err != nil {
firstErr = result.err
}
}
return c.aggregateKeyedValues(cmd, keyedResults, keyOrder, policy)
}
// aggregateKeyedValues aggregates individual key-value pairs while preserving key order
func (c *ClusterClient) aggregateKeyedValues(cmd Cmder, keyedResults map[string]routing.AggregatorResErr, keyOrder []string, policy *routing.CommandPolicy) error {
if len(keyedResults) == 0 {
return errNoResToAggregate
}
aggregator := c.createAggregator(policy, cmd, true)
// Set key order for keyed aggregators
var keyedAgg *routing.DefaultKeyedAggregator
var isKeyedAgg bool
var err error
if keyedAgg, isKeyedAgg = aggregator.(*routing.DefaultKeyedAggregator); isKeyedAgg {
err = keyedAgg.BatchAddWithKeyOrder(keyedResults, keyOrder)
} else {
err = aggregator.BatchAdd(keyedResults)
}
if err != nil {
return err
}
return c.finishAggregation(cmd, aggregator)
}
// aggregateResponses aggregates multiple shard responses
func (c *ClusterClient) aggregateResponses(cmd Cmder, cmds []Cmder, policy *routing.CommandPolicy) error {
if len(cmds) == 0 {
return errNoCmdsToAggregate
}
if len(cmds) == 1 {
shardCmd := cmds[0]
if err := shardCmd.Err(); err != nil {
cmd.SetErr(err)
return err
}
value, _ := ExtractCommandValue(shardCmd)
return c.setCommandValue(cmd, value)
}
aggregator := c.createAggregator(policy, cmd, false)
batchWithErrs := []routing.AggregatorResErr{}
// Add all results to aggregator
for _, shardCmd := range cmds {
value, err := ExtractCommandValue(shardCmd)
batchWithErrs = append(batchWithErrs, routing.AggregatorResErr{
Result: value,
Err: err,
})
}
err := aggregator.BatchSlice(batchWithErrs)
if err != nil {
return err
}
return c.finishAggregation(cmd, aggregator)
}
// createAggregator creates the appropriate response aggregator
func (c *ClusterClient) createAggregator(policy *routing.CommandPolicy, cmd Cmder, isKeyed bool) routing.ResponseAggregator {
if policy != nil {
return routing.NewResponseAggregator(policy.Response, cmd.Name())
}
if !isKeyed {
firstKeyPos := cmdFirstKeyPos(cmd)
isKeyed = firstKeyPos > 0
}
return routing.NewDefaultAggregator(isKeyed)
}
// finishAggregation completes the aggregation process and sets the result
func (c *ClusterClient) finishAggregation(cmd Cmder, aggregator routing.ResponseAggregator) error {
finalValue, finalErr := aggregator.Result()
if finalErr != nil {
cmd.SetErr(finalErr)
return finalErr
}
return c.setCommandValue(cmd, finalValue)
}
// pickArbitraryNode selects a master or slave shard using the configured ShardPicker
func (c *ClusterClient) pickArbitraryNode(ctx context.Context) *clusterNode {
state, err := c.state.Get(ctx)
if err != nil || len(state.Masters) == 0 {
return nil
}
allNodes := append(state.Masters, state.Slaves...)
idx := c.opt.ShardPicker.Next(len(allNodes))
return allNodes[idx]
}
// hasKeys checks if a command operates on keys
func (c *ClusterClient) hasKeys(cmd Cmder) bool {
firstKeyPos := cmdFirstKeyPos(cmd)
return firstKeyPos > 0
}
func (c *ClusterClient) readOnlyEnabled() bool {
return c.opt.ReadOnly
}
// setCommandValue sets the aggregated value on a command using the enum-based approach
func (c *ClusterClient) setCommandValue(cmd Cmder, value interface{}) error {
// If value is nil, it might mean ExtractCommandValue couldn't extract the value
// but the command might have executed successfully. In this case, don't set an error.
if value == nil {
// ExtractCommandValue returned nil - this means the command type is not supported
// in the aggregation flow. This is a programming error, not a runtime error.
if cmd.Err() != nil {
// Command already has an error, preserve it
return cmd.Err()
}
// Command executed successfully but we can't extract/set the aggregated value
// This indicates the command type needs to be added to ExtractCommandValue
return fmt.Errorf("redis: cannot aggregate command %s: unsupported command type %d",
cmd.Name(), cmd.GetCmdType())
}
switch cmd.GetCmdType() {
case CmdTypeGeneric:
if c, ok := cmd.(*Cmd); ok {
c.SetVal(value)
}
case CmdTypeString:
if c, ok := cmd.(*StringCmd); ok {
if v, ok := value.(string); ok {
c.SetVal(v)
}
}
case CmdTypeInt:
if c, ok := cmd.(*IntCmd); ok {
if v, ok := value.(int64); ok {
c.SetVal(v)
} else if v, ok := value.(float64); ok {
c.SetVal(int64(v))
}
}
case CmdTypeBool:
if c, ok := cmd.(*BoolCmd); ok {
if v, ok := value.(bool); ok {
c.SetVal(v)
}
}
case CmdTypeFloat:
if c, ok := cmd.(*FloatCmd); ok {
if v, ok := value.(float64); ok {
c.SetVal(v)
}
}
case CmdTypeStringSlice:
if c, ok := cmd.(*StringSliceCmd); ok {
if v, ok := value.([]string); ok {
c.SetVal(v)
}
}
case CmdTypeIntSlice:
if c, ok := cmd.(*IntSliceCmd); ok {
if v, ok := value.([]int64); ok {
c.SetVal(v)
} else if v, ok := value.([]float64); ok {
els := len(v)
intSlc := make([]int, els)
for i := range v {
intSlc[i] = int(v[i])
}
}
}
case CmdTypeFloatSlice:
if c, ok := cmd.(*FloatSliceCmd); ok {
if v, ok := value.([]float64); ok {
c.SetVal(v)
}
}
case CmdTypeBoolSlice:
if c, ok := cmd.(*BoolSliceCmd); ok {
if v, ok := value.([]bool); ok {
c.SetVal(v)
}
}
case CmdTypeMapStringString:
if c, ok := cmd.(*MapStringStringCmd); ok {
if v, ok := value.(map[string]string); ok {
c.SetVal(v)
}
}
case CmdTypeMapStringInt:
if c, ok := cmd.(*MapStringIntCmd); ok {
if v, ok := value.(map[string]int64); ok {
c.SetVal(v)
}
}
case CmdTypeMapStringInterface:
if c, ok := cmd.(*MapStringInterfaceCmd); ok {
if v, ok := value.(map[string]interface{}); ok {
c.SetVal(v)
}
}
case CmdTypeSlice:
if c, ok := cmd.(*SliceCmd); ok {
if v, ok := value.([]interface{}); ok {
c.SetVal(v)
}
}
case CmdTypeStatus:
if c, ok := cmd.(*StatusCmd); ok {
if v, ok := value.(string); ok {
c.SetVal(v)
}
}
case CmdTypeDuration:
if c, ok := cmd.(*DurationCmd); ok {
if v, ok := value.(time.Duration); ok {
c.SetVal(v)
}
}
case CmdTypeTime:
if c, ok := cmd.(*TimeCmd); ok {
if v, ok := value.(time.Time); ok {
c.SetVal(v)
}
}
case CmdTypeKeyValueSlice:
if c, ok := cmd.(*KeyValueSliceCmd); ok {
if v, ok := value.([]KeyValue); ok {
c.SetVal(v)
}
}
case CmdTypeStringStructMap:
if c, ok := cmd.(*StringStructMapCmd); ok {
if v, ok := value.(map[string]struct{}); ok {
c.SetVal(v)
}
}
case CmdTypeXMessageSlice:
if c, ok := cmd.(*XMessageSliceCmd); ok {
if v, ok := value.([]XMessage); ok {
c.SetVal(v)
}
}
case CmdTypeXStreamSlice:
if c, ok := cmd.(*XStreamSliceCmd); ok {
if v, ok := value.([]XStream); ok {
c.SetVal(v)
}
}
case CmdTypeXPending:
if c, ok := cmd.(*XPendingCmd); ok {
if v, ok := value.(*XPending); ok {
c.SetVal(v)
}
}
case CmdTypeXPendingExt:
if c, ok := cmd.(*XPendingExtCmd); ok {
if v, ok := value.([]XPendingExt); ok {
c.SetVal(v)
}
}
case CmdTypeXAutoClaim:
if c, ok := cmd.(*XAutoClaimCmd); ok {
if v, ok := value.(CmdTypeXAutoClaimValue); ok {
c.SetVal(v.messages, v.start)
}
}
case CmdTypeXAutoClaimJustID:
if c, ok := cmd.(*XAutoClaimJustIDCmd); ok {
if v, ok := value.(CmdTypeXAutoClaimJustIDValue); ok {
c.SetVal(v.ids, v.start)
}
}
case CmdTypeXInfoConsumers:
if c, ok := cmd.(*XInfoConsumersCmd); ok {
if v, ok := value.([]XInfoConsumer); ok {
c.SetVal(v)
}
}
case CmdTypeXInfoGroups:
if c, ok := cmd.(*XInfoGroupsCmd); ok {
if v, ok := value.([]XInfoGroup); ok {
c.SetVal(v)
}
}
case CmdTypeXInfoStream:
if c, ok := cmd.(*XInfoStreamCmd); ok {
if v, ok := value.(*XInfoStream); ok {
c.SetVal(v)
}
}
case CmdTypeXInfoStreamFull:
if c, ok := cmd.(*XInfoStreamFullCmd); ok {
if v, ok := value.(*XInfoStreamFull); ok {
c.SetVal(v)
}
}
case CmdTypeZSlice:
if c, ok := cmd.(*ZSliceCmd); ok {
if v, ok := value.([]Z); ok {
c.SetVal(v)
}
}
case CmdTypeZWithKey:
if c, ok := cmd.(*ZWithKeyCmd); ok {
if v, ok := value.(*ZWithKey); ok {
c.SetVal(v)
}
}
case CmdTypeScan:
if c, ok := cmd.(*ScanCmd); ok {
if v, ok := value.(CmdTypeScanValue); ok {
c.SetVal(v.keys, v.cursor)
}
}
case CmdTypeClusterSlots:
if c, ok := cmd.(*ClusterSlotsCmd); ok {
if v, ok := value.([]ClusterSlot); ok {
c.SetVal(v)
}
}
case CmdTypeGeoLocation:
if c, ok := cmd.(*GeoLocationCmd); ok {
if v, ok := value.([]GeoLocation); ok {
c.SetVal(v)
}
}
case CmdTypeGeoSearchLocation:
if c, ok := cmd.(*GeoSearchLocationCmd); ok {
if v, ok := value.([]GeoLocation); ok {
c.SetVal(v)
}
}
case CmdTypeGeoPos:
if c, ok := cmd.(*GeoPosCmd); ok {
if v, ok := value.([]*GeoPos); ok {
c.SetVal(v)
}
}
case CmdTypeCommandsInfo:
if c, ok := cmd.(*CommandsInfoCmd); ok {
if v, ok := value.(map[string]*CommandInfo); ok {
c.SetVal(v)
}
}
case CmdTypeSlowLog:
if c, ok := cmd.(*SlowLogCmd); ok {
if v, ok := value.([]SlowLog); ok {
c.SetVal(v)
}
}
case CmdTypeMapStringStringSlice:
if c, ok := cmd.(*MapStringStringSliceCmd); ok {
if v, ok := value.([]map[string]string); ok {
c.SetVal(v)
}
}
case CmdTypeMapMapStringInterface:
if c, ok := cmd.(*MapMapStringInterfaceCmd); ok {
if v, ok := value.(map[string]interface{}); ok {
c.SetVal(v)
}
}
case CmdTypeMapStringInterfaceSlice:
if c, ok := cmd.(*MapStringInterfaceSliceCmd); ok {
if v, ok := value.([]map[string]interface{}); ok {
c.SetVal(v)
}
}
case CmdTypeKeyValues:
if c, ok := cmd.(*KeyValuesCmd); ok {
// KeyValuesCmd needs a key string and values slice
if v, ok := value.(CmdTypeKeyValuesValue); ok {
c.SetVal(v.key, v.values)
}
}
case CmdTypeZSliceWithKey:
if c, ok := cmd.(*ZSliceWithKeyCmd); ok {
// ZSliceWithKeyCmd needs a key string and Z slice
if v, ok := value.(CmdTypeZSliceWithKeyValue); ok {
c.SetVal(v.key, v.zSlice)
}
}
case CmdTypeFunctionList:
if c, ok := cmd.(*FunctionListCmd); ok {
if v, ok := value.([]Library); ok {
c.SetVal(v)
}
}
case CmdTypeFunctionStats:
if c, ok := cmd.(*FunctionStatsCmd); ok {
if v, ok := value.(FunctionStats); ok {
c.SetVal(v)
}
}
case CmdTypeLCS:
if c, ok := cmd.(*LCSCmd); ok {
if v, ok := value.(*LCSMatch); ok {
c.SetVal(v)
}
}
case CmdTypeKeyFlags:
if c, ok := cmd.(*KeyFlagsCmd); ok {
if v, ok := value.([]KeyFlags); ok {
c.SetVal(v)
}
}
case CmdTypeClusterLinks:
if c, ok := cmd.(*ClusterLinksCmd); ok {
if v, ok := value.([]ClusterLink); ok {
c.SetVal(v)
}
}
case CmdTypeClusterShards:
if c, ok := cmd.(*ClusterShardsCmd); ok {
if v, ok := value.([]ClusterShard); ok {
c.SetVal(v)
}
}
case CmdTypeRankWithScore:
if c, ok := cmd.(*RankWithScoreCmd); ok {
if v, ok := value.(RankScore); ok {
c.SetVal(v)
}
}
case CmdTypeClientInfo:
if c, ok := cmd.(*ClientInfoCmd); ok {
if v, ok := value.(*ClientInfo); ok {
c.SetVal(v)
}
}
case CmdTypeACLLog:
if c, ok := cmd.(*ACLLogCmd); ok {
if v, ok := value.([]*ACLLogEntry); ok {
c.SetVal(v)
}
}
case CmdTypeInfo:
if c, ok := cmd.(*InfoCmd); ok {
if v, ok := value.(map[string]map[string]string); ok {
c.SetVal(v)
}
}
case CmdTypeMonitor:
// MonitorCmd doesn't have SetVal method
// Skip setting value for MonitorCmd
case CmdTypeJSON:
if c, ok := cmd.(*JSONCmd); ok {
if v, ok := value.(string); ok {
c.SetVal(v)
}
}
case CmdTypeJSONSlice:
if c, ok := cmd.(*JSONSliceCmd); ok {
if v, ok := value.([]interface{}); ok {
c.SetVal(v)
}
}
case CmdTypeIntPointerSlice:
if c, ok := cmd.(*IntPointerSliceCmd); ok {
if v, ok := value.([]*int64); ok {
c.SetVal(v)
}
}
case CmdTypeScanDump:
if c, ok := cmd.(*ScanDumpCmd); ok {
if v, ok := value.(ScanDump); ok {
c.SetVal(v)
}
}
case CmdTypeBFInfo:
if c, ok := cmd.(*BFInfoCmd); ok {
if v, ok := value.(BFInfo); ok {
c.SetVal(v)
}
}
case CmdTypeCFInfo:
if c, ok := cmd.(*CFInfoCmd); ok {
if v, ok := value.(CFInfo); ok {
c.SetVal(v)
}
}
case CmdTypeCMSInfo:
if c, ok := cmd.(*CMSInfoCmd); ok {
if v, ok := value.(CMSInfo); ok {
c.SetVal(v)
}
}
case CmdTypeTopKInfo:
if c, ok := cmd.(*TopKInfoCmd); ok {
if v, ok := value.(TopKInfo); ok {
c.SetVal(v)
}
}
case CmdTypeTDigestInfo:
if c, ok := cmd.(*TDigestInfoCmd); ok {
if v, ok := value.(TDigestInfo); ok {
c.SetVal(v)
}
}
case CmdTypeFTSynDump:
if c, ok := cmd.(*FTSynDumpCmd); ok {
if v, ok := value.([]FTSynDumpResult); ok {
c.SetVal(v)
}
}
case CmdTypeAggregate:
if c, ok := cmd.(*AggregateCmd); ok {
if v, ok := value.(*FTAggregateResult); ok {
c.SetVal(v)
}
}
case CmdTypeFTInfo:
if c, ok := cmd.(*FTInfoCmd); ok {
if v, ok := value.(FTInfoResult); ok {
c.SetVal(v)
}
}
case CmdTypeFTSpellCheck:
if c, ok := cmd.(*FTSpellCheckCmd); ok {
if v, ok := value.([]SpellCheckResult); ok {
c.SetVal(v)
}
}
case CmdTypeFTSearch:
if c, ok := cmd.(*FTSearchCmd); ok {
if v, ok := value.(FTSearchResult); ok {
c.SetVal(v)
}
}
case CmdTypeTSTimestampValue:
if c, ok := cmd.(*TSTimestampValueCmd); ok {
if v, ok := value.(TSTimestampValue); ok {
c.SetVal(v)
}
}
case CmdTypeTSTimestampValueSlice:
if c, ok := cmd.(*TSTimestampValueSliceCmd); ok {
if v, ok := value.([]TSTimestampValue); ok {
c.SetVal(v)
}
}
default:
// Fallback to reflection for unknown types
return c.setCommandValueReflection(cmd, value)
}
return nil
}
// setCommandValueReflection is a fallback function that uses reflection
func (c *ClusterClient) setCommandValueReflection(cmd Cmder, value interface{}) error {
cmdValue := reflect.ValueOf(cmd)
if cmdValue.Kind() != reflect.Ptr || cmdValue.IsNil() {
return errInvalidCmdPointer
}
setValMethod := cmdValue.MethodByName("SetVal")
if !setValMethod.IsValid() {
return fmt.Errorf("redis: command %T does not have SetVal method", cmd)
}
args := []reflect.Value{reflect.ValueOf(value)}
switch cmd.(type) {
case *XAutoClaimCmd, *XAutoClaimJustIDCmd:
args = append(args, reflect.ValueOf(""))
case *ScanCmd:
args = append(args, reflect.ValueOf(uint64(0)))
case *KeyValuesCmd, *ZSliceWithKeyCmd:
if key, ok := value.(string); ok {
args = []reflect.Value{reflect.ValueOf(key)}
if _, ok := cmd.(*ZSliceWithKeyCmd); ok {
args = append(args, reflect.ValueOf([]Z{}))
} else {
args = append(args, reflect.ValueOf([]string{}))
}
}
}
defer func() {
if r := recover(); r != nil {
cmd.SetErr(fmt.Errorf("redis: failed to set command value: %v", r))
}
}()
setValMethod.Call(args)
return nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -225,8 +225,9 @@ type ScanDumpCmd struct {
func newScanDumpCmd(ctx context.Context, args ...interface{}) *ScanDumpCmd { func newScanDumpCmd(ctx context.Context, args ...interface{}) *ScanDumpCmd {
return &ScanDumpCmd{ return &ScanDumpCmd{
baseCmd: baseCmd{ baseCmd: baseCmd{
ctx: ctx, ctx: ctx,
args: args, args: args,
cmdType: CmdTypeScanDump,
}, },
} }
} }
@@ -270,6 +271,13 @@ func (cmd *ScanDumpCmd) readReply(rd *proto.Reader) (err error) {
return nil return nil
} }
func (cmd *ScanDumpCmd) Clone() Cmder {
return &ScanDumpCmd{
baseCmd: cmd.cloneBaseCmd(),
val: cmd.val, // ScanDump is a simple struct, can be copied directly
}
}
// Returns information about a Bloom filter. // Returns information about a Bloom filter.
// For more information - https://redis.io/commands/bf.info/ // For more information - https://redis.io/commands/bf.info/
func (c cmdable) BFInfo(ctx context.Context, key string) *BFInfoCmd { func (c cmdable) BFInfo(ctx context.Context, key string) *BFInfoCmd {
@@ -296,8 +304,9 @@ type BFInfoCmd struct {
func NewBFInfoCmd(ctx context.Context, args ...interface{}) *BFInfoCmd { func NewBFInfoCmd(ctx context.Context, args ...interface{}) *BFInfoCmd {
return &BFInfoCmd{ return &BFInfoCmd{
baseCmd: baseCmd{ baseCmd: baseCmd{
ctx: ctx, ctx: ctx,
args: args, args: args,
cmdType: CmdTypeBFInfo,
}, },
} }
} }
@@ -388,6 +397,13 @@ func (cmd *BFInfoCmd) readReply(rd *proto.Reader) (err error) {
return nil return nil
} }
func (cmd *BFInfoCmd) Clone() Cmder {
return &BFInfoCmd{
baseCmd: cmd.cloneBaseCmd(),
val: cmd.val, // BFInfo is a simple struct, can be copied directly
}
}
// BFInfoCapacity returns information about the capacity of a Bloom filter. // BFInfoCapacity returns information about the capacity of a Bloom filter.
// For more information - https://redis.io/commands/bf.info/ // For more information - https://redis.io/commands/bf.info/
func (c cmdable) BFInfoCapacity(ctx context.Context, key string) *BFInfoCmd { func (c cmdable) BFInfoCapacity(ctx context.Context, key string) *BFInfoCmd {
@@ -625,8 +641,9 @@ type CFInfoCmd struct {
func NewCFInfoCmd(ctx context.Context, args ...interface{}) *CFInfoCmd { func NewCFInfoCmd(ctx context.Context, args ...interface{}) *CFInfoCmd {
return &CFInfoCmd{ return &CFInfoCmd{
baseCmd: baseCmd{ baseCmd: baseCmd{
ctx: ctx, ctx: ctx,
args: args, args: args,
cmdType: CmdTypeCFInfo,
}, },
} }
} }
@@ -692,6 +709,13 @@ func (cmd *CFInfoCmd) readReply(rd *proto.Reader) (err error) {
return nil return nil
} }
func (cmd *CFInfoCmd) Clone() Cmder {
return &CFInfoCmd{
baseCmd: cmd.cloneBaseCmd(),
val: cmd.val, // CFInfo is a simple struct, can be copied directly
}
}
// CFInfo returns information about a Cuckoo filter. // CFInfo returns information about a Cuckoo filter.
// For more information - https://redis.io/commands/cf.info/ // For more information - https://redis.io/commands/cf.info/
func (c cmdable) CFInfo(ctx context.Context, key string) *CFInfoCmd { func (c cmdable) CFInfo(ctx context.Context, key string) *CFInfoCmd {
@@ -787,8 +811,9 @@ type CMSInfoCmd struct {
func NewCMSInfoCmd(ctx context.Context, args ...interface{}) *CMSInfoCmd { func NewCMSInfoCmd(ctx context.Context, args ...interface{}) *CMSInfoCmd {
return &CMSInfoCmd{ return &CMSInfoCmd{
baseCmd: baseCmd{ baseCmd: baseCmd{
ctx: ctx, ctx: ctx,
args: args, args: args,
cmdType: CmdTypeCMSInfo,
}, },
} }
} }
@@ -843,6 +868,13 @@ func (cmd *CMSInfoCmd) readReply(rd *proto.Reader) (err error) {
return nil return nil
} }
func (cmd *CMSInfoCmd) Clone() Cmder {
return &CMSInfoCmd{
baseCmd: cmd.cloneBaseCmd(),
val: cmd.val, // CMSInfo is a simple struct, can be copied directly
}
}
// CMSInfo returns information about a Count-Min Sketch filter. // CMSInfo returns information about a Count-Min Sketch filter.
// For more information - https://redis.io/commands/cms.info/ // For more information - https://redis.io/commands/cms.info/
func (c cmdable) CMSInfo(ctx context.Context, key string) *CMSInfoCmd { func (c cmdable) CMSInfo(ctx context.Context, key string) *CMSInfoCmd {
@@ -980,8 +1012,9 @@ type TopKInfoCmd struct {
func NewTopKInfoCmd(ctx context.Context, args ...interface{}) *TopKInfoCmd { func NewTopKInfoCmd(ctx context.Context, args ...interface{}) *TopKInfoCmd {
return &TopKInfoCmd{ return &TopKInfoCmd{
baseCmd: baseCmd{ baseCmd: baseCmd{
ctx: ctx, ctx: ctx,
args: args, args: args,
cmdType: CmdTypeTopKInfo,
}, },
} }
} }
@@ -1038,6 +1071,13 @@ func (cmd *TopKInfoCmd) readReply(rd *proto.Reader) (err error) {
return nil return nil
} }
func (cmd *TopKInfoCmd) Clone() Cmder {
return &TopKInfoCmd{
baseCmd: cmd.cloneBaseCmd(),
val: cmd.val, // TopKInfo is a simple struct, can be copied directly
}
}
// TopKInfo returns information about a Top-K filter. // TopKInfo returns information about a Top-K filter.
// For more information - https://redis.io/commands/topk.info/ // For more information - https://redis.io/commands/topk.info/
func (c cmdable) TopKInfo(ctx context.Context, key string) *TopKInfoCmd { func (c cmdable) TopKInfo(ctx context.Context, key string) *TopKInfoCmd {
@@ -1227,8 +1267,9 @@ type TDigestInfoCmd struct {
func NewTDigestInfoCmd(ctx context.Context, args ...interface{}) *TDigestInfoCmd { func NewTDigestInfoCmd(ctx context.Context, args ...interface{}) *TDigestInfoCmd {
return &TDigestInfoCmd{ return &TDigestInfoCmd{
baseCmd: baseCmd{ baseCmd: baseCmd{
ctx: ctx, ctx: ctx,
args: args, args: args,
cmdType: CmdTypeTDigestInfo,
}, },
} }
} }
@@ -1295,6 +1336,13 @@ func (cmd *TDigestInfoCmd) readReply(rd *proto.Reader) (err error) {
return nil return nil
} }
func (cmd *TDigestInfoCmd) Clone() Cmder {
return &TDigestInfoCmd{
baseCmd: cmd.cloneBaseCmd(),
val: cmd.val, // TDigestInfo is a simple struct, can be copied directly
}
}
// TDigestInfo returns information about a t-Digest data structure. // TDigestInfo returns information about a t-Digest data structure.
// For more information - https://redis.io/commands/tdigest.info/ // For more information - https://redis.io/commands/tdigest.info/
func (c cmdable) TDigestInfo(ctx context.Context, key string) *TDigestInfoCmd { func (c cmdable) TDigestInfo(ctx context.Context, key string) *TDigestInfoCmd {

View File

@@ -768,8 +768,9 @@ func ProcessAggregateResult(data []interface{}) (*FTAggregateResult, error) {
func NewAggregateCmd(ctx context.Context, args ...interface{}) *AggregateCmd { func NewAggregateCmd(ctx context.Context, args ...interface{}) *AggregateCmd {
return &AggregateCmd{ return &AggregateCmd{
baseCmd: baseCmd{ baseCmd: baseCmd{
ctx: ctx, ctx: ctx,
args: args, args: args,
cmdType: CmdTypeAggregate,
}, },
} }
} }
@@ -810,6 +811,31 @@ func (cmd *AggregateCmd) readReply(rd *proto.Reader) (err error) {
return nil return nil
} }
func (cmd *AggregateCmd) Clone() Cmder {
var val *FTAggregateResult
if cmd.val != nil {
val = &FTAggregateResult{
Total: cmd.val.Total,
}
if cmd.val.Rows != nil {
val.Rows = make([]AggregateRow, len(cmd.val.Rows))
for i, row := range cmd.val.Rows {
val.Rows[i] = AggregateRow{}
if row.Fields != nil {
val.Rows[i].Fields = make(map[string]interface{}, len(row.Fields))
for k, v := range row.Fields {
val.Rows[i].Fields[k] = v
}
}
}
}
}
return &AggregateCmd{
baseCmd: cmd.cloneBaseCmd(),
val: val,
}
}
// FTAggregateWithArgs - Performs a search query on an index and applies a series of aggregate transformations to the result. // FTAggregateWithArgs - Performs a search query on an index and applies a series of aggregate transformations to the result.
// The 'index' parameter specifies the index to search, and the 'query' parameter specifies the search query. // The 'index' parameter specifies the index to search, and the 'query' parameter specifies the search query.
// This function also allows for specifying additional options such as: Verbatim, LoadAll, Load, Timeout, GroupBy, SortBy, SortByMax, Apply, LimitOffset, Limit, Filter, WithCursor, Params, and DialectVersion. // This function also allows for specifying additional options such as: Verbatim, LoadAll, Load, Timeout, GroupBy, SortBy, SortByMax, Apply, LimitOffset, Limit, Filter, WithCursor, Params, and DialectVersion.
@@ -1597,8 +1623,9 @@ type FTInfoCmd struct {
func newFTInfoCmd(ctx context.Context, args ...interface{}) *FTInfoCmd { func newFTInfoCmd(ctx context.Context, args ...interface{}) *FTInfoCmd {
return &FTInfoCmd{ return &FTInfoCmd{
baseCmd: baseCmd{ baseCmd: baseCmd{
ctx: ctx, ctx: ctx,
args: args, args: args,
cmdType: CmdTypeFTInfo,
}, },
} }
} }
@@ -1660,6 +1687,68 @@ func (cmd *FTInfoCmd) readReply(rd *proto.Reader) (err error) {
return nil return nil
} }
func (cmd *FTInfoCmd) Clone() Cmder {
val := FTInfoResult{
IndexErrors: cmd.val.IndexErrors,
BytesPerRecordAvg: cmd.val.BytesPerRecordAvg,
Cleaning: cmd.val.Cleaning,
CursorStats: cmd.val.CursorStats,
DocTableSizeMB: cmd.val.DocTableSizeMB,
GCStats: cmd.val.GCStats,
GeoshapesSzMB: cmd.val.GeoshapesSzMB,
HashIndexingFailures: cmd.val.HashIndexingFailures,
IndexDefinition: cmd.val.IndexDefinition,
IndexName: cmd.val.IndexName,
Indexing: cmd.val.Indexing,
InvertedSzMB: cmd.val.InvertedSzMB,
KeyTableSizeMB: cmd.val.KeyTableSizeMB,
MaxDocID: cmd.val.MaxDocID,
NumDocs: cmd.val.NumDocs,
NumRecords: cmd.val.NumRecords,
NumTerms: cmd.val.NumTerms,
NumberOfUses: cmd.val.NumberOfUses,
OffsetBitsPerRecordAvg: cmd.val.OffsetBitsPerRecordAvg,
OffsetVectorsSzMB: cmd.val.OffsetVectorsSzMB,
OffsetsPerTermAvg: cmd.val.OffsetsPerTermAvg,
PercentIndexed: cmd.val.PercentIndexed,
RecordsPerDocAvg: cmd.val.RecordsPerDocAvg,
SortableValuesSizeMB: cmd.val.SortableValuesSizeMB,
TagOverheadSzMB: cmd.val.TagOverheadSzMB,
TextOverheadSzMB: cmd.val.TextOverheadSzMB,
TotalIndexMemorySzMB: cmd.val.TotalIndexMemorySzMB,
TotalIndexingTime: cmd.val.TotalIndexingTime,
TotalInvertedIndexBlocks: cmd.val.TotalInvertedIndexBlocks,
VectorIndexSzMB: cmd.val.VectorIndexSzMB,
}
// Clone slices and maps
if cmd.val.Attributes != nil {
val.Attributes = make([]FTAttribute, len(cmd.val.Attributes))
copy(val.Attributes, cmd.val.Attributes)
}
if cmd.val.DialectStats != nil {
val.DialectStats = make(map[string]int, len(cmd.val.DialectStats))
for k, v := range cmd.val.DialectStats {
val.DialectStats[k] = v
}
}
if cmd.val.FieldStatistics != nil {
val.FieldStatistics = make([]FieldStatistic, len(cmd.val.FieldStatistics))
copy(val.FieldStatistics, cmd.val.FieldStatistics)
}
if cmd.val.IndexOptions != nil {
val.IndexOptions = make([]string, len(cmd.val.IndexOptions))
copy(val.IndexOptions, cmd.val.IndexOptions)
}
if cmd.val.IndexDefinition.Prefixes != nil {
val.IndexDefinition.Prefixes = make([]string, len(cmd.val.IndexDefinition.Prefixes))
copy(val.IndexDefinition.Prefixes, cmd.val.IndexDefinition.Prefixes)
}
return &FTInfoCmd{
baseCmd: cmd.cloneBaseCmd(),
val: val,
}
}
// FTInfo - Retrieves information about an index. // FTInfo - Retrieves information about an index.
// The 'index' parameter specifies the index to retrieve information about. // The 'index' parameter specifies the index to retrieve information about.
// For more information, please refer to the Redis documentation: // For more information, please refer to the Redis documentation:
@@ -1716,8 +1805,9 @@ type FTSpellCheckCmd struct {
func newFTSpellCheckCmd(ctx context.Context, args ...interface{}) *FTSpellCheckCmd { func newFTSpellCheckCmd(ctx context.Context, args ...interface{}) *FTSpellCheckCmd {
return &FTSpellCheckCmd{ return &FTSpellCheckCmd{
baseCmd: baseCmd{ baseCmd: baseCmd{
ctx: ctx, ctx: ctx,
args: args, args: args,
cmdType: CmdTypeFTSpellCheck,
}, },
} }
} }
@@ -1813,6 +1903,26 @@ func parseFTSpellCheck(data []interface{}) ([]SpellCheckResult, error) {
return results, nil return results, nil
} }
func (cmd *FTSpellCheckCmd) Clone() Cmder {
var val []SpellCheckResult
if cmd.val != nil {
val = make([]SpellCheckResult, len(cmd.val))
for i, result := range cmd.val {
val[i] = SpellCheckResult{
Term: result.Term,
}
if result.Suggestions != nil {
val[i].Suggestions = make([]SpellCheckSuggestion, len(result.Suggestions))
copy(val[i].Suggestions, result.Suggestions)
}
}
}
return &FTSpellCheckCmd{
baseCmd: cmd.cloneBaseCmd(),
val: val,
}
}
func parseFTSearch(data []interface{}, noContent, withScores, withPayloads, withSortKeys bool) (FTSearchResult, error) { func parseFTSearch(data []interface{}, noContent, withScores, withPayloads, withSortKeys bool) (FTSearchResult, error) {
if len(data) < 1 { if len(data) < 1 {
return FTSearchResult{}, fmt.Errorf("unexpected search result format") return FTSearchResult{}, fmt.Errorf("unexpected search result format")
@@ -1909,8 +2019,9 @@ type FTSearchCmd struct {
func newFTSearchCmd(ctx context.Context, options *FTSearchOptions, args ...interface{}) *FTSearchCmd { func newFTSearchCmd(ctx context.Context, options *FTSearchOptions, args ...interface{}) *FTSearchCmd {
return &FTSearchCmd{ return &FTSearchCmd{
baseCmd: baseCmd{ baseCmd: baseCmd{
ctx: ctx, ctx: ctx,
args: args, args: args,
cmdType: CmdTypeFTSearch,
}, },
options: options, options: options,
} }
@@ -1952,6 +2063,89 @@ func (cmd *FTSearchCmd) readReply(rd *proto.Reader) (err error) {
return nil return nil
} }
func (cmd *FTSearchCmd) Clone() Cmder {
val := FTSearchResult{
Total: cmd.val.Total,
}
if cmd.val.Docs != nil {
val.Docs = make([]Document, len(cmd.val.Docs))
for i, doc := range cmd.val.Docs {
val.Docs[i] = Document{
ID: doc.ID,
Score: doc.Score,
Payload: doc.Payload,
SortKey: doc.SortKey,
}
if doc.Fields != nil {
val.Docs[i].Fields = make(map[string]string, len(doc.Fields))
for k, v := range doc.Fields {
val.Docs[i].Fields[k] = v
}
}
}
}
var options *FTSearchOptions
if cmd.options != nil {
options = &FTSearchOptions{
NoContent: cmd.options.NoContent,
Verbatim: cmd.options.Verbatim,
NoStopWords: cmd.options.NoStopWords,
WithScores: cmd.options.WithScores,
WithPayloads: cmd.options.WithPayloads,
WithSortKeys: cmd.options.WithSortKeys,
Slop: cmd.options.Slop,
Timeout: cmd.options.Timeout,
InOrder: cmd.options.InOrder,
Language: cmd.options.Language,
Expander: cmd.options.Expander,
Scorer: cmd.options.Scorer,
ExplainScore: cmd.options.ExplainScore,
Payload: cmd.options.Payload,
SortByWithCount: cmd.options.SortByWithCount,
LimitOffset: cmd.options.LimitOffset,
Limit: cmd.options.Limit,
CountOnly: cmd.options.CountOnly,
DialectVersion: cmd.options.DialectVersion,
}
// Clone slices and maps
if cmd.options.Filters != nil {
options.Filters = make([]FTSearchFilter, len(cmd.options.Filters))
copy(options.Filters, cmd.options.Filters)
}
if cmd.options.GeoFilter != nil {
options.GeoFilter = make([]FTSearchGeoFilter, len(cmd.options.GeoFilter))
copy(options.GeoFilter, cmd.options.GeoFilter)
}
if cmd.options.InKeys != nil {
options.InKeys = make([]interface{}, len(cmd.options.InKeys))
copy(options.InKeys, cmd.options.InKeys)
}
if cmd.options.InFields != nil {
options.InFields = make([]interface{}, len(cmd.options.InFields))
copy(options.InFields, cmd.options.InFields)
}
if cmd.options.Return != nil {
options.Return = make([]FTSearchReturn, len(cmd.options.Return))
copy(options.Return, cmd.options.Return)
}
if cmd.options.SortBy != nil {
options.SortBy = make([]FTSearchSortBy, len(cmd.options.SortBy))
copy(options.SortBy, cmd.options.SortBy)
}
if cmd.options.Params != nil {
options.Params = make(map[string]interface{}, len(cmd.options.Params))
for k, v := range cmd.options.Params {
options.Params[k] = v
}
}
}
return &FTSearchCmd{
baseCmd: cmd.cloneBaseCmd(),
val: val,
options: options,
}
}
// FTHybridResult represents the result of a hybrid search operation // FTHybridResult represents the result of a hybrid search operation
type FTHybridResult struct { type FTHybridResult struct {
TotalResults int TotalResults int
@@ -2153,6 +2347,111 @@ func (cmd *FTHybridCmd) readReply(rd *proto.Reader) (err error) {
return nil return nil
} }
func (cmd *FTHybridCmd) Clone() Cmder {
val := FTHybridResult{
TotalResults: cmd.val.TotalResults,
ExecutionTime: cmd.val.ExecutionTime,
}
if cmd.val.Results != nil {
val.Results = make([]map[string]interface{}, len(cmd.val.Results))
for i, result := range cmd.val.Results {
val.Results[i] = make(map[string]interface{}, len(result))
for k, v := range result {
val.Results[i][k] = v
}
}
}
if cmd.val.Warnings != nil {
val.Warnings = make([]string, len(cmd.val.Warnings))
copy(val.Warnings, cmd.val.Warnings)
}
var cursorVal *FTHybridCursorResult
if cmd.cursorVal != nil {
cursorVal = &FTHybridCursorResult{
SearchCursorID: cmd.cursorVal.SearchCursorID,
VsimCursorID: cmd.cursorVal.VsimCursorID,
}
}
var options *FTHybridOptions
if cmd.options != nil {
options = &FTHybridOptions{
CountExpressions: cmd.options.CountExpressions,
Load: cmd.options.Load,
Filter: cmd.options.Filter,
LimitOffset: cmd.options.LimitOffset,
Limit: cmd.options.Limit,
ExplainScore: cmd.options.ExplainScore,
Timeout: cmd.options.Timeout,
WithCursor: cmd.options.WithCursor,
}
// Clone slices and maps
if cmd.options.SearchExpressions != nil {
options.SearchExpressions = make([]FTHybridSearchExpression, len(cmd.options.SearchExpressions))
copy(options.SearchExpressions, cmd.options.SearchExpressions)
}
if cmd.options.VectorExpressions != nil {
options.VectorExpressions = make([]FTHybridVectorExpression, len(cmd.options.VectorExpressions))
copy(options.VectorExpressions, cmd.options.VectorExpressions)
}
if cmd.options.Combine != nil {
options.Combine = &FTHybridCombineOptions{
Method: cmd.options.Combine.Method,
Count: cmd.options.Combine.Count,
Window: cmd.options.Combine.Window,
Constant: cmd.options.Combine.Constant,
Alpha: cmd.options.Combine.Alpha,
Beta: cmd.options.Combine.Beta,
YieldScoreAs: cmd.options.Combine.YieldScoreAs,
}
}
if cmd.options.GroupBy != nil {
options.GroupBy = &FTHybridGroupBy{
Count: cmd.options.GroupBy.Count,
ReduceFunc: cmd.options.GroupBy.ReduceFunc,
ReduceCount: cmd.options.GroupBy.ReduceCount,
}
if cmd.options.GroupBy.Fields != nil {
options.GroupBy.Fields = make([]string, len(cmd.options.GroupBy.Fields))
copy(options.GroupBy.Fields, cmd.options.GroupBy.Fields)
}
if cmd.options.GroupBy.ReduceParams != nil {
options.GroupBy.ReduceParams = make([]interface{}, len(cmd.options.GroupBy.ReduceParams))
copy(options.GroupBy.ReduceParams, cmd.options.GroupBy.ReduceParams)
}
}
if cmd.options.Apply != nil {
options.Apply = make([]FTHybridApply, len(cmd.options.Apply))
copy(options.Apply, cmd.options.Apply)
}
if cmd.options.SortBy != nil {
options.SortBy = make([]FTSearchSortBy, len(cmd.options.SortBy))
copy(options.SortBy, cmd.options.SortBy)
}
if cmd.options.Params != nil {
options.Params = make(map[string]interface{}, len(cmd.options.Params))
for k, v := range cmd.options.Params {
options.Params[k] = v
}
}
if cmd.options.WithCursorOptions != nil {
options.WithCursorOptions = &FTHybridWithCursor{
MaxIdle: cmd.options.WithCursorOptions.MaxIdle,
Count: cmd.options.WithCursorOptions.Count,
}
}
}
return &FTHybridCmd{
baseCmd: cmd.cloneBaseCmd(),
val: val,
cursorVal: cursorVal,
options: options,
withCursor: cmd.withCursor,
}
}
// FTSearch - Executes a search query on an index. // FTSearch - Executes a search query on an index.
// The 'index' parameter specifies the index to search, and the 'query' parameter specifies the search query. // The 'index' parameter specifies the index to search, and the 'query' parameter specifies the search query.
// For more information, please refer to the Redis documentation about [FT.SEARCH]. // For more information, please refer to the Redis documentation about [FT.SEARCH].
@@ -2412,8 +2711,9 @@ func (c cmdable) FTSearchWithArgs(ctx context.Context, index string, query strin
func NewFTSynDumpCmd(ctx context.Context, args ...interface{}) *FTSynDumpCmd { func NewFTSynDumpCmd(ctx context.Context, args ...interface{}) *FTSynDumpCmd {
return &FTSynDumpCmd{ return &FTSynDumpCmd{
baseCmd: baseCmd{ baseCmd: baseCmd{
ctx: ctx, ctx: ctx,
args: args, args: args,
cmdType: CmdTypeFTSynDump,
}, },
} }
} }
@@ -2479,6 +2779,26 @@ func (cmd *FTSynDumpCmd) readReply(rd *proto.Reader) error {
return nil return nil
} }
func (cmd *FTSynDumpCmd) Clone() Cmder {
var val []FTSynDumpResult
if cmd.val != nil {
val = make([]FTSynDumpResult, len(cmd.val))
for i, result := range cmd.val {
val[i] = FTSynDumpResult{
Term: result.Term,
}
if result.Synonyms != nil {
val[i].Synonyms = make([]string, len(result.Synonyms))
copy(val[i].Synonyms, result.Synonyms)
}
}
}
return &FTSynDumpCmd{
baseCmd: cmd.cloneBaseCmd(),
val: val,
}
}
// FTSynDump - Dumps the contents of a synonym group. // FTSynDump - Dumps the contents of a synonym group.
// The 'index' parameter specifies the index to dump. // The 'index' parameter specifies the index to dump.
// For more information, please refer to the Redis documentation: // For more information, please refer to the Redis documentation:

View File

@@ -486,8 +486,9 @@ type TSTimestampValueCmd struct {
func newTSTimestampValueCmd(ctx context.Context, args ...interface{}) *TSTimestampValueCmd { func newTSTimestampValueCmd(ctx context.Context, args ...interface{}) *TSTimestampValueCmd {
return &TSTimestampValueCmd{ return &TSTimestampValueCmd{
baseCmd: baseCmd{ baseCmd: baseCmd{
ctx: ctx, ctx: ctx,
args: args, args: args,
cmdType: CmdTypeTSTimestampValue,
}, },
} }
} }
@@ -533,6 +534,13 @@ func (cmd *TSTimestampValueCmd) readReply(rd *proto.Reader) (err error) {
return nil return nil
} }
func (cmd *TSTimestampValueCmd) Clone() Cmder {
return &TSTimestampValueCmd{
baseCmd: cmd.cloneBaseCmd(),
val: cmd.val, // TSTimestampValue is a simple struct, can be copied directly
}
}
// TSInfo - Returns information about a time-series key. // TSInfo - Returns information about a time-series key.
// For more information - https://redis.io/commands/ts.info/ // For more information - https://redis.io/commands/ts.info/
func (c cmdable) TSInfo(ctx context.Context, key string) *MapStringInterfaceCmd { func (c cmdable) TSInfo(ctx context.Context, key string) *MapStringInterfaceCmd {
@@ -704,8 +712,9 @@ type TSTimestampValueSliceCmd struct {
func newTSTimestampValueSliceCmd(ctx context.Context, args ...interface{}) *TSTimestampValueSliceCmd { func newTSTimestampValueSliceCmd(ctx context.Context, args ...interface{}) *TSTimestampValueSliceCmd {
return &TSTimestampValueSliceCmd{ return &TSTimestampValueSliceCmd{
baseCmd: baseCmd{ baseCmd: baseCmd{
ctx: ctx, ctx: ctx,
args: args, args: args,
cmdType: CmdTypeTSTimestampValueSlice,
}, },
} }
} }
@@ -752,6 +761,18 @@ func (cmd *TSTimestampValueSliceCmd) readReply(rd *proto.Reader) (err error) {
return nil return nil
} }
func (cmd *TSTimestampValueSliceCmd) Clone() Cmder {
var val []TSTimestampValue
if cmd.val != nil {
val = make([]TSTimestampValue, len(cmd.val))
copy(val, cmd.val)
}
return &TSTimestampValueSliceCmd{
baseCmd: cmd.cloneBaseCmd(),
val: val,
}
}
// TSMRange - Returns a range of samples from multiple time-series keys. // TSMRange - Returns a range of samples from multiple time-series keys.
// For more information - https://redis.io/commands/ts.mrange/ // For more information - https://redis.io/commands/ts.mrange/
func (c cmdable) TSMRange(ctx context.Context, fromTimestamp int, toTimestamp int, filterExpr []string) *MapStringSliceInterfaceCmd { func (c cmdable) TSMRange(ctx context.Context, fromTimestamp int, toTimestamp int, filterExpr []string) *MapStringSliceInterfaceCmd {