mirror of
https://github.com/redis/go-redis.git
synced 2026-10-24 07:59:20 +03:00
* feat(command): add metadata-driven routing * fix(cluster): preserve routing compatibility * fix spellcheck ci * fix(cluster): harden metadata routing - Preserve legacy routing, hook order and typed results. - Handle topology health and invalid metadata safely. - Reject fan-out from ordered async pipelines before dispatch. - Reduce allocations and duplicate routing work. - Add regression coverage and document remaining limitations. * fix(cluster): correct routing and cursor ownership - Handle unavailable nodes and malformed topology replies - Bound metadata verification and retry refreshes in the background - Bind client-local cursor tokens to their originating nodes - Validate routing policies and refresh topology after redirects - Reduce routing allocations and extend tests and documentation * fix spellcheck ci * fix ci * fix ci * refactor(cluster): split cursor ownership * fix cursor bug * fix ci
64 lines
1.9 KiB
Go
64 lines
1.9 KiB
Go
package redis
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/redis/go-redis/v9/internal/routing"
|
|
)
|
|
|
|
// clusterState is published as an immutable snapshot shared by every concurrent
|
|
// caller. Building the routed node list with append(state.Masters,
|
|
// state.Slaves...) reuses the spare capacity of the shared Masters backing array
|
|
// (newClusterState grows it via appendIfNotExist, so cap > len is common), so two
|
|
// goroutines routing commands at the same time write the same slots. Run with
|
|
// -race against the pre-fix tree to see the report at osscluster_router.go.
|
|
func TestPickKeylessReadOnlyNodeConcurrent(t *testing.T) {
|
|
nodes := make([]*clusterNode, 4)
|
|
for i := range nodes {
|
|
client := NewClient(&Options{})
|
|
t.Cleanup(func() { _ = client.Close() })
|
|
nodes[i] = &clusterNode{Client: client}
|
|
nodes[i].loaded.Store(1)
|
|
}
|
|
// len 3, cap 4 mirrors the appendIfNotExist growth for a 3-master cluster;
|
|
// one replica fits the spare slot, so append would alias without the fix.
|
|
masters := make([]*clusterNode, 0, 4)
|
|
masters = append(masters, nodes[:3]...)
|
|
state := &clusterState{
|
|
Masters: masters,
|
|
Slaves: nodes[3:],
|
|
createdAt: time.Now(),
|
|
}
|
|
|
|
c := &ClusterClient{
|
|
opt: &ClusterOptions{ShardPicker: &routing.RoundRobinPicker{}, ReadOnly: true},
|
|
state: newClusterStateHolder(
|
|
func(ctx context.Context) (*clusterState, error) { return state, nil },
|
|
time.Hour,
|
|
),
|
|
}
|
|
c.state.state.Store(state)
|
|
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 32; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for j := 0; j < 200; j++ {
|
|
node, err := c.cmdNodeWithShardPickerAndDecision(
|
|
context.Background(), -1, c.opt.ShardPicker,
|
|
&clusterRoutingDecision{readOnly: true, policyFromMetadata: true},
|
|
)
|
|
if err != nil || node == nil {
|
|
t.Errorf("keyless readonly selection node=%v err=%v", node, err)
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
}
|