Files
go/osscluster_lazy_reload_test.go
b53f2b0c9d feat(sch): MaintNotifications for ClusterClient (#3643)
* [maintnotif] Cluster specific handlers (#3613)

* maint notification handlers for cluster messages

* unrelax all conns

* trigger ci on feature branches

* feat(maintnotif): lazy cluster topology reload (#3614)

* lazy cluster topology reload

* fix discrepancies between options structs

* Update osscluster_lazy_reload_test.go

Co-authored-by: Copilot <[email protected]>

* Update osscluster.go

Co-authored-by: Copilot <[email protected]>

---------

Co-authored-by: Copilot <[email protected]>

* feat(e2e): mock maintnotif e2e tests (#3639)

* lazy cluster topology reload

* fix discrepancies between options structs

* Update osscluster_lazy_reload_test.go

Co-authored-by: Copilot <[email protected]>

* Update osscluster.go

Co-authored-by: Copilot <[email protected]>

* wip fault with mock proxy

* make lint happy

* fix linter issues

* faster tests with mocks

* linter once again

* add complex node test

* add ci e2e

* use correct redis container

* e2e fix

---------

Co-authored-by: Copilot <[email protected]>

* fix(retry): Add retry mechanism for NOREPLICAS error (#3647)

* fix(queuedNewConn): protect against nil context (#3649)

* fix(maintnotif): fix smigrated parser and add cluster state reload interval option (#3663)

* fix SMIGRATED parsing

* fix smigrated parser

* add ClusterStateReloadInterval to ClusterOptions

* fix tests

* set default cluster reload interval to 10s

* chore(lint): format

* feat(smigrated): new format & remember original host:port (#3697)

* lazy cluster topology reload

* fix discrepancies between options structs

* Update osscluster_lazy_reload_test.go

Co-authored-by: Copilot <[email protected]>

* Update osscluster.go

Co-authored-by: Copilot <[email protected]>

* wip fault with mock proxy

* make lint happy

* fix linter issues

* faster tests with mocks

* linter once again

* add complex node test

* add ci e2e

* use correct redis container

* e2e fix

* additional e2e tests

* fix data race

* fix random shard picker

* fix e2e tests

* fix for empty endpoint

* fix case when semaphore is full, but still need to check idle

* scenario tests

* create database from config

* wip

* feat(client): store original addrs for later use

* fix(notif): change smigrated notification

* fix(lint): fix linter

* fix(smigrated): use array

* fix(e2e): wip

* Update options.go

Co-authored-by: Copilot <[email protected]>

* Update redis.go

Co-authored-by: Copilot <[email protected]>

* fix(notif): if the conn has no original addr, trigger reload with first target

* chore(e2e): wip cluster e2e

* chore(e2e): fix nil pointer from e2e tests

* chore(e2e): fix tests and reports

* chore(e2e): proper logging in e2e

* chore(e2e): add pubsub in the tests as well

* chore(e2e): mockproxy fixes

* chore(e2e): mockproxy fixes

* chore(e2e): mockproxy fixes

* chore(e2e): mockproxy fixes v3

* stop background routines

* fix(e2e): tests

---------

Co-authored-by: Copilot <[email protected]>

* chore(docs): add example app (#3651)

* chore(lint): fmt

* chore(docs): improve docs

* chore(docs): update features.md

* chore(github): remove example

* chore(maintnotif): rename option and address pr comments

* fix(e2e): command runner should use client timeout

* chore(e2e): refactor tests

* fix(e2e): set default timeout to 90m

* fix(e2e): skip tests if proxy cannot be started

---------

Co-authored-by: Copilot <[email protected]>
Co-authored-by: ofekshenawa <[email protected]>
Co-authored-by: Elena Kolevska <[email protected]>
2026-02-13 13:25:07 +02:00

202 lines
5.6 KiB
Go

package redis
import (
"context"
"sync/atomic"
"testing"
"time"
)
// TestLazyReloadQueueBehavior tests that LazyReload properly queues reload requests
func TestLazyReloadQueueBehavior(t *testing.T) {
t.Run("SingleReload", func(t *testing.T) {
var reloadCount atomic.Int32
holder := newClusterStateHolder(func(ctx context.Context) (*clusterState, error) {
reloadCount.Add(1)
time.Sleep(50 * time.Millisecond) // Simulate reload work
return &clusterState{}, nil
}, 10*time.Second)
// Trigger one reload
holder.LazyReload()
// Wait for reload to complete
time.Sleep(300 * time.Millisecond)
if count := reloadCount.Load(); count != 1 {
t.Errorf("Expected 1 reload, got %d", count)
}
})
t.Run("ConcurrentReloadsDeduplication", func(t *testing.T) {
var reloadCount atomic.Int32
holder := newClusterStateHolder(func(ctx context.Context) (*clusterState, error) {
reloadCount.Add(1)
time.Sleep(50 * time.Millisecond) // Simulate reload work
return &clusterState{}, nil
}, 10*time.Second)
// Trigger multiple reloads concurrently
for i := 0; i < 10; i++ {
go holder.LazyReload()
}
// Wait for all to complete
time.Sleep(100 * time.Millisecond)
// Should only reload once (all concurrent calls deduplicated)
if count := reloadCount.Load(); count != 1 {
t.Errorf("Expected 1 reload (deduplication), got %d", count)
}
})
t.Run("PendingReloadDuringCooldown", func(t *testing.T) {
var reloadCount atomic.Int32
holder := newClusterStateHolder(func(ctx context.Context) (*clusterState, error) {
reloadCount.Add(1)
time.Sleep(10 * time.Millisecond) // Simulate reload work
return &clusterState{}, nil
}, 10*time.Second)
// Trigger first reload
holder.LazyReload()
// Wait for reload to complete but still in cooldown
time.Sleep(50 * time.Millisecond)
// Trigger second reload during cooldown period
holder.LazyReload()
// Wait for second reload to complete
time.Sleep(300 * time.Millisecond)
// Should have reloaded twice (second request queued and executed)
if count := reloadCount.Load(); count != 2 {
t.Errorf("Expected 2 reloads (queued during cooldown), got %d", count)
}
})
t.Run("MultiplePendingReloadsCollapsed", func(t *testing.T) {
var reloadCount atomic.Int32
holder := newClusterStateHolder(func(ctx context.Context) (*clusterState, error) {
reloadCount.Add(1)
time.Sleep(10 * time.Millisecond) // Simulate reload work
return &clusterState{}, nil
}, 10*time.Second)
// Trigger first reload
holder.LazyReload()
// Wait for reload to start
time.Sleep(5 * time.Millisecond)
// Trigger multiple reloads during active reload + cooldown
for i := 0; i < 10; i++ {
holder.LazyReload()
time.Sleep(5 * time.Millisecond)
}
// Wait for all to complete
time.Sleep(400 * time.Millisecond)
// Should have reloaded exactly twice:
// 1. Initial reload
// 2. One more reload for all the pending requests (collapsed into one)
if count := reloadCount.Load(); count != 2 {
t.Errorf("Expected 2 reloads (initial + collapsed pending), got %d", count)
}
})
t.Run("ReloadAfterCooldownPeriod", func(t *testing.T) {
var reloadCount atomic.Int32
holder := newClusterStateHolder(func(ctx context.Context) (*clusterState, error) {
reloadCount.Add(1)
time.Sleep(10 * time.Millisecond) // Simulate reload work
return &clusterState{}, nil
}, 10*time.Second)
// Trigger first reload
holder.LazyReload()
// Wait for reload + cooldown to complete
time.Sleep(300 * time.Millisecond)
// Trigger second reload after cooldown
holder.LazyReload()
// Wait for second reload to complete
time.Sleep(300 * time.Millisecond)
// Should have reloaded twice (separate reload cycles)
if count := reloadCount.Load(); count != 2 {
t.Errorf("Expected 2 reloads (separate cycles), got %d", count)
}
})
t.Run("ErrorDuringReload", func(t *testing.T) {
var reloadCount atomic.Int32
var shouldFail atomic.Bool
shouldFail.Store(true)
holder := newClusterStateHolder(func(ctx context.Context) (*clusterState, error) {
reloadCount.Add(1)
if shouldFail.Load() {
return nil, context.DeadlineExceeded
}
return &clusterState{}, nil
}, 10*time.Second)
// Trigger reload that will fail
holder.LazyReload()
// Wait for failed reload
time.Sleep(50 * time.Millisecond)
// Trigger another reload (should succeed now)
shouldFail.Store(false)
holder.LazyReload()
// Wait for successful reload
time.Sleep(300 * time.Millisecond)
// Should have attempted reload twice (first failed, second succeeded)
if count := reloadCount.Load(); count != 2 {
t.Errorf("Expected 2 reload attempts, got %d", count)
}
})
t.Run("CascadingSMIGRATEDScenario", func(t *testing.T) {
// Simulate the real-world scenario: multiple SMIGRATED notifications
// arriving in quick succession from different node clients
var reloadCount atomic.Int32
holder := newClusterStateHolder(func(ctx context.Context) (*clusterState, error) {
reloadCount.Add(1)
time.Sleep(20 * time.Millisecond) // Simulate realistic reload time
return &clusterState{}, nil
}, 10*time.Second)
// Simulate 5 SMIGRATED notifications arriving within 100ms
for i := 0; i < 5; i++ {
go holder.LazyReload()
time.Sleep(20 * time.Millisecond)
}
// Wait for all reloads to complete
time.Sleep(500 * time.Millisecond)
// Should reload at most 2 times:
// 1. First notification triggers reload
// 2. Notifications 2-5 collapse into one pending reload
count := reloadCount.Load()
if count < 1 || count > 2 {
t.Errorf("Expected 1-2 reloads for cascading scenario, got %d", count)
}
})
}