1
0
mirror of https://github.com/redis/go-redis.git synced 2026-10-24 07:59:20 +03:00
Files
go-redis/osscluster_router_test.go
ofekshenawa 48d65e5641 Metadata driven cluster routing (#4093)
* 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
2026-10-21 17:34:41 +03:00

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()
}