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:
1819
command.go
1819
command.go
File diff suppressed because it is too large
Load Diff
209
command_policy_resolver.go
Normal file
209
command_policy_resolver.go
Normal 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
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/redis/go-redis/v9/internal/proto"
|
||||
"github.com/redis/go-redis/v9/internal/routing"
|
||||
)
|
||||
|
||||
type TimeValue struct {
|
||||
@@ -680,6 +681,22 @@ var _ = Describe("Commands", func() {
|
||||
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() {
|
||||
cmdList := client.CommandList(ctx, nil)
|
||||
Expect(cmdList.Err()).NotTo(HaveOccurred())
|
||||
|
||||
@@ -12,6 +12,6 @@ require (
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // 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
|
||||
)
|
||||
|
||||
@@ -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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
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.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
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.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/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
|
||||
@@ -13,4 +13,5 @@ require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
)
|
||||
|
||||
@@ -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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/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/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/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/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||
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=
|
||||
|
||||
@@ -9,4 +9,5 @@ require github.com/redis/go-redis/v9 v9.7.0
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
)
|
||||
|
||||
@@ -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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
|
||||
@@ -9,4 +9,5 @@ require github.com/redis/go-redis/v9 v9.17.1
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
)
|
||||
|
||||
@@ -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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/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=
|
||||
|
||||
@@ -12,4 +12,5 @@ require (
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
)
|
||||
|
||||
@@ -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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/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=
|
||||
|
||||
@@ -9,4 +9,5 @@ require github.com/redis/go-redis/v9 v9.17.1
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
)
|
||||
|
||||
@@ -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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/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=
|
||||
|
||||
@@ -9,4 +9,5 @@ require github.com/redis/go-redis/v9 v9.11.0
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
)
|
||||
|
||||
@@ -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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/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=
|
||||
|
||||
@@ -36,6 +36,7 @@ require (
|
||||
go.opentelemetry.io/otel/sdk/metric v1.21.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.22.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/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
|
||||
@@ -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/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
|
||||
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/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
|
||||
|
||||
@@ -9,4 +9,5 @@ require github.com/redis/go-redis/v9 v9.17.1
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
)
|
||||
|
||||
@@ -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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/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=
|
||||
|
||||
@@ -12,4 +12,5 @@ require (
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
)
|
||||
|
||||
@@ -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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/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=
|
||||
|
||||
@@ -16,6 +16,7 @@ require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
)
|
||||
|
||||
retract (
|
||||
|
||||
@@ -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/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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
@@ -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.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
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/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=
|
||||
@@ -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/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.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
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/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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
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=
|
||||
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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
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-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
||||
@@ -13,6 +13,7 @@ require (
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
)
|
||||
|
||||
retract (
|
||||
|
||||
@@ -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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/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=
|
||||
|
||||
@@ -20,6 +20,7 @@ require (
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/go-logr/logr v1.4.1 // 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
|
||||
)
|
||||
|
||||
|
||||
@@ -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/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0=
|
||||
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/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -18,8 +18,10 @@ require (
|
||||
github.com/prometheus/client_model v0.3.0 // indirect
|
||||
github.com/prometheus/common v0.39.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
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
retract (
|
||||
|
||||
@@ -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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/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/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/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/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/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
|
||||
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/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
|
||||
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/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
|
||||
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.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
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
2
go.mod
@@ -9,6 +9,8 @@ require (
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f
|
||||
)
|
||||
|
||||
require go.uber.org/atomic v1.11.0
|
||||
|
||||
retract (
|
||||
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
|
||||
|
||||
5
go.sum
5
go.sum
@@ -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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/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=
|
||||
|
||||
1002
internal/routing/aggregator.go
Normal file
1002
internal/routing/aggregator.go
Normal file
File diff suppressed because it is too large
Load Diff
144
internal/routing/policy.go
Normal file
144
internal/routing/policy.go
Normal 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
|
||||
}
|
||||
57
internal/routing/shard_picker.go
Normal file
57
internal/routing/shard_picker.go
Normal 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)
|
||||
}
|
||||
97
internal/util/atomic_max.go
Normal file
97
internal/util/atomic_max.go
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
© 2023–present 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() }
|
||||
96
internal/util/atomic_min.go
Normal file
96
internal/util/atomic_min.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package util
|
||||
|
||||
/*
|
||||
© 2023–present 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() }
|
||||
35
json.go
35
json.go
@@ -70,6 +70,7 @@ func newJSONCmd(ctx context.Context, args ...interface{}) *JSONCmd {
|
||||
baseCmd: baseCmd{
|
||||
ctx: ctx,
|
||||
args: args,
|
||||
cmdType: CmdTypeJSON,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -165,6 +166,14 @@ func (cmd *JSONCmd) readReply(rd *proto.Reader) error {
|
||||
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 {
|
||||
@@ -177,6 +186,7 @@ func NewJSONSliceCmd(ctx context.Context, args ...interface{}) *JSONSliceCmd {
|
||||
baseCmd: baseCmd{
|
||||
ctx: ctx,
|
||||
args: args,
|
||||
cmdType: CmdTypeJSONSlice,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -233,6 +243,18 @@ func (cmd *JSONSliceCmd) readReply(rd *proto.Reader) error {
|
||||
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
|
||||
@@ -251,6 +273,7 @@ func NewIntPointerSliceCmd(ctx context.Context, args ...interface{}) *IntPointer
|
||||
baseCmd: baseCmd{
|
||||
ctx: ctx,
|
||||
args: args,
|
||||
cmdType: CmdTypeIntPointerSlice,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -290,6 +313,18 @@ func (cmd *IntPointerSliceCmd) readReply(rd *proto.Reader) error {
|
||||
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.
|
||||
|
||||
200
osscluster.go
200
osscluster.go
@@ -3,6 +3,7 @@ package redis
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
@@ -20,6 +21,7 @@ import (
|
||||
"github.com/redis/go-redis/v9/internal/pool"
|
||||
"github.com/redis/go-redis/v9/internal/proto"
|
||||
"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/push"
|
||||
)
|
||||
@@ -28,7 +30,11 @@ const (
|
||||
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
|
||||
// passed to NewClusterClient.
|
||||
@@ -115,6 +121,11 @@ type ClusterOptions struct {
|
||||
|
||||
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.
|
||||
//
|
||||
// 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.
|
||||
// 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
|
||||
// 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() {
|
||||
@@ -208,6 +222,10 @@ func (opt *ClusterOptions) init() {
|
||||
if opt.FailingTimeoutSeconds == 0 {
|
||||
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.
|
||||
@@ -926,6 +944,29 @@ func (c *clusterState) slotRandomNode(slot int) (*clusterNode, error) {
|
||||
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 {
|
||||
i := sort.Search(len(c.slots), func(i int) bool {
|
||||
return c.slots[i].end >= slot
|
||||
@@ -944,14 +985,13 @@ func (c *clusterState) slotNodes(slot int) []*clusterNode {
|
||||
|
||||
type clusterStateHolder struct {
|
||||
load func(ctx context.Context) (*clusterState, error)
|
||||
|
||||
state atomic.Value
|
||||
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{
|
||||
load: fn,
|
||||
load: load,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1010,6 +1050,7 @@ type ClusterClient struct {
|
||||
nodes *clusterNodes
|
||||
state *clusterStateHolder
|
||||
cmdsInfoCache *cmdsInfoCache
|
||||
cmdInfoResolver *commandInfoResolver
|
||||
cmdable
|
||||
hooksMixin
|
||||
}
|
||||
@@ -1017,9 +1058,6 @@ type ClusterClient struct {
|
||||
// NewClusterClient returns a Redis Cluster client as described in
|
||||
// http://redis.io/topics/cluster-spec.
|
||||
func NewClusterClient(opt *ClusterOptions) *ClusterClient {
|
||||
if opt == nil {
|
||||
panic("redis: NewClusterClient nil options")
|
||||
}
|
||||
opt.init()
|
||||
|
||||
c := &ClusterClient{
|
||||
@@ -1027,10 +1065,13 @@ func NewClusterClient(opt *ClusterOptions) *ClusterClient {
|
||||
nodes: newClusterNodes(opt),
|
||||
}
|
||||
|
||||
c.state = newClusterStateHolder(c.loadState)
|
||||
c.cmdsInfoCache = newCmdsInfoCache(c.cmdsInfo)
|
||||
c.cmdable = c.Process
|
||||
|
||||
c.state = newClusterStateHolder(c.loadState)
|
||||
|
||||
c.SetCommandInfoResolver(NewDefaultCommandPolicyResolver())
|
||||
|
||||
c.cmdable = c.Process
|
||||
c.initHooks(hooks{
|
||||
dial: nil,
|
||||
process: c.process,
|
||||
@@ -1083,7 +1124,11 @@ func (c *ClusterClient) process(ctx context.Context, cmd Cmder) error {
|
||||
|
||||
if node == nil {
|
||||
var err error
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -1091,14 +1136,17 @@ func (c *ClusterClient) process(ctx context.Context, cmd Cmder) error {
|
||||
|
||||
if ask {
|
||||
ask = false
|
||||
|
||||
pipe := node.Client.Pipeline()
|
||||
_ = pipe.Process(ctx, NewCmd(ctx, "asking"))
|
||||
_ = pipe.Process(ctx, cmd)
|
||||
_, lastErr = pipe.Exec(ctx)
|
||||
} else {
|
||||
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 lastErr == nil {
|
||||
@@ -1413,13 +1461,18 @@ func (c *ClusterClient) mapCmdsByNode(ctx context.Context, cmdsMap *cmdsMap, cmd
|
||||
return err
|
||||
}
|
||||
|
||||
preferredRandomSlot := -1
|
||||
if c.opt.ReadOnly && c.cmdsAreReadOnly(ctx, cmds) {
|
||||
for _, cmd := range cmds {
|
||||
slot := c.cmdSlot(cmd, preferredRandomSlot)
|
||||
if preferredRandomSlot == -1 {
|
||||
preferredRandomSlot = slot
|
||||
var policy *routing.CommandPolicy
|
||||
if c.cmdInfoResolver != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1430,10 +1483,16 @@ func (c *ClusterClient) mapCmdsByNode(ctx context.Context, cmdsMap *cmdsMap, cmd
|
||||
}
|
||||
|
||||
for _, cmd := range cmds {
|
||||
slot := c.cmdSlot(cmd, preferredRandomSlot)
|
||||
if preferredRandomSlot == -1 {
|
||||
preferredRandomSlot = slot
|
||||
var policy *routing.CommandPolicy
|
||||
if c.cmdInfoResolver != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1595,7 +1654,7 @@ func (c *ClusterClient) processTxPipeline(ctx context.Context, cmds []Cmder) err
|
||||
return err
|
||||
}
|
||||
|
||||
keyedCmdsBySlot := c.slottedKeyedCommands(cmds)
|
||||
keyedCmdsBySlot := c.slottedKeyedCommands(ctx, cmds)
|
||||
slot := -1
|
||||
switch len(keyedCmdsBySlot) {
|
||||
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
|
||||
// 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{}
|
||||
|
||||
preferredRandomSlot := -1
|
||||
prefferedRandomSlot := -1
|
||||
for _, cmd := range cmds {
|
||||
if cmdFirstKeyPos(cmd) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
slot := c.cmdSlot(cmd, preferredRandomSlot)
|
||||
if preferredRandomSlot == -1 {
|
||||
preferredRandomSlot = slot
|
||||
slot := c.cmdSlot(cmd, prefferedRandomSlot)
|
||||
if prefferedRandomSlot == -1 {
|
||||
prefferedRandomSlot = slot
|
||||
}
|
||||
|
||||
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 {
|
||||
if len(keys) == 0 {
|
||||
return fmt.Errorf("redis: Watch requires at least one key")
|
||||
return errNoWatchKeys
|
||||
}
|
||||
|
||||
slot := hashtag.Slot(keys[0])
|
||||
for _, key := range keys[1:] {
|
||||
if hashtag.Slot(key) != slot {
|
||||
err := fmt.Errorf("redis: Watch requires all keys to be in the same slot")
|
||||
return err
|
||||
return errWatchCrosslot
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1995,7 +2053,6 @@ func (c *ClusterClient) cmdsInfo(ctx context.Context) (map[string]*CommandInfo,
|
||||
|
||||
for _, idx := range perm {
|
||||
addr := addrs[idx]
|
||||
|
||||
node, err := c.nodes.GetOrCreate(addr)
|
||||
if err != nil {
|
||||
if firstErr == nil {
|
||||
@@ -2008,6 +2065,7 @@ func (c *ClusterClient) cmdsInfo(ctx context.Context) (map[string]*CommandInfo,
|
||||
if err == nil {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
if firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
@@ -2019,33 +2077,45 @@ func (c *ClusterClient) cmdsInfo(ctx context.Context) (map[string]*CommandInfo,
|
||||
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 {
|
||||
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 {
|
||||
internal.Logger.Printf(context.TODO(), "getting command info: %s", err)
|
||||
internal.Logger.Printf(cmdInfoCtx, "getting command info: %s", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
info := cmdsInfo[name]
|
||||
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
|
||||
}
|
||||
|
||||
func (c *ClusterClient) cmdSlot(cmd Cmder, preferredRandomSlot int) int {
|
||||
func (c *ClusterClient) cmdSlot(cmd Cmder, prefferedSlot int) int {
|
||||
args := cmd.Args()
|
||||
if args[0] == "cluster" && (args[1] == "getkeysinslot" || args[1] == "countkeysinslot") {
|
||||
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 preferredRandomSlot != -1 {
|
||||
return preferredRandomSlot
|
||||
if prefferedRandomSlot != -1 {
|
||||
return prefferedRandomSlot
|
||||
}
|
||||
return hashtag.RandomSlot()
|
||||
}
|
||||
@@ -2072,6 +2142,36 @@ func (c *ClusterClient) cmdNode(
|
||||
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) {
|
||||
if c.opt.RouteByLatency {
|
||||
return state.slotClosestNode(slot)
|
||||
@@ -2079,6 +2179,11 @@ func (c *ClusterClient) slotReadOnlyNode(state *clusterState, slot int) (*cluste
|
||||
if c.opt.RouteRandomly {
|
||||
return state.slotRandomNode(slot)
|
||||
}
|
||||
|
||||
if c.opt.ShardPicker != nil {
|
||||
return state.slotShardPickerSlaveNode(slot, c.opt.ShardPicker)
|
||||
}
|
||||
|
||||
return state.slotSlaveNode(slot)
|
||||
}
|
||||
|
||||
@@ -2126,6 +2231,31 @@ func (c *ClusterClient) context(ctx context.Context) context.Context {
|
||||
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 {
|
||||
for _, v := range vals {
|
||||
if v == newVal {
|
||||
|
||||
992
osscluster_router.go
Normal file
992
osscluster_router.go
Normal 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
|
||||
}
|
||||
1702
osscluster_test.go
1702
osscluster_test.go
File diff suppressed because it is too large
Load Diff
@@ -227,6 +227,7 @@ func newScanDumpCmd(ctx context.Context, args ...interface{}) *ScanDumpCmd {
|
||||
baseCmd: baseCmd{
|
||||
ctx: ctx,
|
||||
args: args,
|
||||
cmdType: CmdTypeScanDump,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -270,6 +271,13 @@ func (cmd *ScanDumpCmd) readReply(rd *proto.Reader) (err error) {
|
||||
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.
|
||||
// For more information - https://redis.io/commands/bf.info/
|
||||
func (c cmdable) BFInfo(ctx context.Context, key string) *BFInfoCmd {
|
||||
@@ -298,6 +306,7 @@ func NewBFInfoCmd(ctx context.Context, args ...interface{}) *BFInfoCmd {
|
||||
baseCmd: baseCmd{
|
||||
ctx: ctx,
|
||||
args: args,
|
||||
cmdType: CmdTypeBFInfo,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -388,6 +397,13 @@ func (cmd *BFInfoCmd) readReply(rd *proto.Reader) (err error) {
|
||||
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.
|
||||
// For more information - https://redis.io/commands/bf.info/
|
||||
func (c cmdable) BFInfoCapacity(ctx context.Context, key string) *BFInfoCmd {
|
||||
@@ -627,6 +643,7 @@ func NewCFInfoCmd(ctx context.Context, args ...interface{}) *CFInfoCmd {
|
||||
baseCmd: baseCmd{
|
||||
ctx: ctx,
|
||||
args: args,
|
||||
cmdType: CmdTypeCFInfo,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -692,6 +709,13 @@ func (cmd *CFInfoCmd) readReply(rd *proto.Reader) (err error) {
|
||||
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.
|
||||
// For more information - https://redis.io/commands/cf.info/
|
||||
func (c cmdable) CFInfo(ctx context.Context, key string) *CFInfoCmd {
|
||||
@@ -789,6 +813,7 @@ func NewCMSInfoCmd(ctx context.Context, args ...interface{}) *CMSInfoCmd {
|
||||
baseCmd: baseCmd{
|
||||
ctx: ctx,
|
||||
args: args,
|
||||
cmdType: CmdTypeCMSInfo,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -843,6 +868,13 @@ func (cmd *CMSInfoCmd) readReply(rd *proto.Reader) (err error) {
|
||||
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.
|
||||
// For more information - https://redis.io/commands/cms.info/
|
||||
func (c cmdable) CMSInfo(ctx context.Context, key string) *CMSInfoCmd {
|
||||
@@ -982,6 +1014,7 @@ func NewTopKInfoCmd(ctx context.Context, args ...interface{}) *TopKInfoCmd {
|
||||
baseCmd: baseCmd{
|
||||
ctx: ctx,
|
||||
args: args,
|
||||
cmdType: CmdTypeTopKInfo,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1038,6 +1071,13 @@ func (cmd *TopKInfoCmd) readReply(rd *proto.Reader) (err error) {
|
||||
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.
|
||||
// For more information - https://redis.io/commands/topk.info/
|
||||
func (c cmdable) TopKInfo(ctx context.Context, key string) *TopKInfoCmd {
|
||||
@@ -1229,6 +1269,7 @@ func NewTDigestInfoCmd(ctx context.Context, args ...interface{}) *TDigestInfoCmd
|
||||
baseCmd: baseCmd{
|
||||
ctx: ctx,
|
||||
args: args,
|
||||
cmdType: CmdTypeTDigestInfo,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1295,6 +1336,13 @@ func (cmd *TDigestInfoCmd) readReply(rd *proto.Reader) (err error) {
|
||||
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.
|
||||
// For more information - https://redis.io/commands/tdigest.info/
|
||||
func (c cmdable) TDigestInfo(ctx context.Context, key string) *TDigestInfoCmd {
|
||||
|
||||
@@ -770,6 +770,7 @@ func NewAggregateCmd(ctx context.Context, args ...interface{}) *AggregateCmd {
|
||||
baseCmd: baseCmd{
|
||||
ctx: ctx,
|
||||
args: args,
|
||||
cmdType: CmdTypeAggregate,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -810,6 +811,31 @@ func (cmd *AggregateCmd) readReply(rd *proto.Reader) (err error) {
|
||||
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.
|
||||
// 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.
|
||||
@@ -1599,6 +1625,7 @@ func newFTInfoCmd(ctx context.Context, args ...interface{}) *FTInfoCmd {
|
||||
baseCmd: baseCmd{
|
||||
ctx: ctx,
|
||||
args: args,
|
||||
cmdType: CmdTypeFTInfo,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1660,6 +1687,68 @@ func (cmd *FTInfoCmd) readReply(rd *proto.Reader) (err error) {
|
||||
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.
|
||||
// The 'index' parameter specifies the index to retrieve information about.
|
||||
// For more information, please refer to the Redis documentation:
|
||||
@@ -1718,6 +1807,7 @@ func newFTSpellCheckCmd(ctx context.Context, args ...interface{}) *FTSpellCheckC
|
||||
baseCmd: baseCmd{
|
||||
ctx: ctx,
|
||||
args: args,
|
||||
cmdType: CmdTypeFTSpellCheck,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1813,6 +1903,26 @@ func parseFTSpellCheck(data []interface{}) ([]SpellCheckResult, error) {
|
||||
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) {
|
||||
if len(data) < 1 {
|
||||
return FTSearchResult{}, fmt.Errorf("unexpected search result format")
|
||||
@@ -1911,6 +2021,7 @@ func newFTSearchCmd(ctx context.Context, options *FTSearchOptions, args ...inter
|
||||
baseCmd: baseCmd{
|
||||
ctx: ctx,
|
||||
args: args,
|
||||
cmdType: CmdTypeFTSearch,
|
||||
},
|
||||
options: options,
|
||||
}
|
||||
@@ -1952,6 +2063,89 @@ func (cmd *FTSearchCmd) readReply(rd *proto.Reader) (err error) {
|
||||
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
|
||||
type FTHybridResult struct {
|
||||
TotalResults int
|
||||
@@ -2153,6 +2347,111 @@ func (cmd *FTHybridCmd) readReply(rd *proto.Reader) (err error) {
|
||||
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.
|
||||
// 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].
|
||||
@@ -2414,6 +2713,7 @@ func NewFTSynDumpCmd(ctx context.Context, args ...interface{}) *FTSynDumpCmd {
|
||||
baseCmd: baseCmd{
|
||||
ctx: ctx,
|
||||
args: args,
|
||||
cmdType: CmdTypeFTSynDump,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -2479,6 +2779,26 @@ func (cmd *FTSynDumpCmd) readReply(rd *proto.Reader) error {
|
||||
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.
|
||||
// The 'index' parameter specifies the index to dump.
|
||||
// For more information, please refer to the Redis documentation:
|
||||
|
||||
@@ -488,6 +488,7 @@ func newTSTimestampValueCmd(ctx context.Context, args ...interface{}) *TSTimesta
|
||||
baseCmd: baseCmd{
|
||||
ctx: ctx,
|
||||
args: args,
|
||||
cmdType: CmdTypeTSTimestampValue,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -533,6 +534,13 @@ func (cmd *TSTimestampValueCmd) readReply(rd *proto.Reader) (err error) {
|
||||
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.
|
||||
// For more information - https://redis.io/commands/ts.info/
|
||||
func (c cmdable) TSInfo(ctx context.Context, key string) *MapStringInterfaceCmd {
|
||||
@@ -706,6 +714,7 @@ func newTSTimestampValueSliceCmd(ctx context.Context, args ...interface{}) *TSTi
|
||||
baseCmd: baseCmd{
|
||||
ctx: ctx,
|
||||
args: args,
|
||||
cmdType: CmdTypeTSTimestampValueSlice,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -752,6 +761,18 @@ func (cmd *TSTimestampValueSliceCmd) readReply(rd *proto.Reader) (err error) {
|
||||
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.
|
||||
// For more information - https://redis.io/commands/ts.mrange/
|
||||
func (c cmdable) TSMRange(ctx context.Context, fromTimestamp int, toTimestamp int, filterExpr []string) *MapStringSliceInterfaceCmd {
|
||||
|
||||
Reference in New Issue
Block a user