mirror of
https://github.com/hanzokv/go.git
synced 2026-09-24 00:31:51 +00:00
* [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]>
435 lines
14 KiB
Go
435 lines
14 KiB
Go
package e2e
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
"github.com/redis/go-redis/v9/maintnotifications"
|
|
)
|
|
|
|
// TestUnifiedInjector_SMIGRATING demonstrates using the unified notification injector
|
|
// This test requires the proxy-based mock to directly inject notifications
|
|
func TestUnifiedInjector_SMIGRATING(t *testing.T) {
|
|
// Skip if using real fault injector (these tests require proxy mock for direct notification injection)
|
|
if os.Getenv("FAULT_INJECTOR_URL") != "" {
|
|
t.Skip("Skipping unified injector test - requires proxy mock, not real fault injector")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
// Create notification injector (automatically chooses based on environment)
|
|
injector, err := NewNotificationInjector()
|
|
if err != nil {
|
|
t.Fatalf("[ERROR] Failed to create notification injector: %v", err)
|
|
}
|
|
|
|
// Start the injector - skip if proxy not available
|
|
if err := injector.Start(); err != nil {
|
|
t.Skipf("Skipping test - proxy not available: %v", err)
|
|
}
|
|
defer injector.Stop()
|
|
|
|
// Get test mode configuration
|
|
testMode := injector.GetTestModeConfig()
|
|
t.Logf("Using %s mode", testMode.Mode)
|
|
t.Logf("Cluster addresses: %v", injector.GetClusterAddrs())
|
|
|
|
// Create cluster client with maintnotifications enabled
|
|
client := redis.NewClusterClient(&redis.ClusterOptions{
|
|
Addrs: injector.GetClusterAddrs(),
|
|
Protocol: 3, // RESP3 required for push notifications
|
|
|
|
MaintNotificationsConfig: &maintnotifications.Config{
|
|
Mode: maintnotifications.ModeEnabled,
|
|
RelaxedTimeout: 30 * time.Second,
|
|
},
|
|
})
|
|
defer client.Close()
|
|
|
|
// Set up notification tracking
|
|
tracker := NewTrackingNotificationsHook()
|
|
setupNotificationHook(client, tracker)
|
|
|
|
// Verify connection
|
|
if err := client.Ping(ctx).Err(); err != nil {
|
|
t.Fatalf("[ERROR] Failed to connect to cluster: %v", err)
|
|
}
|
|
|
|
// Perform some operations to establish connections
|
|
for i := 0; i < 10; i++ {
|
|
if err := client.Set(ctx, fmt.Sprintf("key%d", i), "value", 0).Err(); err != nil {
|
|
t.Logf("Warning: Failed to set key: %v", err)
|
|
}
|
|
}
|
|
|
|
// Start a blocking operation in background to keep connection active
|
|
// This ensures the proxy has an active connection to send notifications to
|
|
blockingDone := make(chan error, 1)
|
|
go func() {
|
|
// BLPOP with 10 second timeout - keeps connection active
|
|
_, err := client.BLPop(ctx, 10*time.Second, "notification-test-list").Result()
|
|
blockingDone <- err
|
|
}()
|
|
|
|
// Wait for blocking command to start (mode-aware)
|
|
time.Sleep(testMode.ConnectionEstablishDelay)
|
|
|
|
// Inject SMIGRATING notification while connection is active
|
|
t.Log("Injecting SMIGRATING notification...")
|
|
if err := injector.InjectSMIGRATING(ctx, 12345, "1000-2000", "3000"); err != nil {
|
|
t.Fatalf("[ERROR] Failed to inject SMIGRATING: %v", err)
|
|
}
|
|
|
|
// Wait for notification processing (mode-aware)
|
|
time.Sleep(testMode.NotificationDelay)
|
|
|
|
// Verify notification was received
|
|
analysis := tracker.GetAnalysis()
|
|
if analysis.SMigratingCount == 0 {
|
|
t.Errorf("[ERROR] Expected to receive SMIGRATING notification, got 0")
|
|
} else {
|
|
t.Logf("✓ Received %d SMIGRATING notification(s)", analysis.SMigratingCount)
|
|
}
|
|
|
|
// Verify operations still work (timeouts should be relaxed)
|
|
if err := client.Set(ctx, "test-key-during-migration", "value", 0).Err(); err != nil {
|
|
t.Errorf("[ERROR] Expected operations to work during migration, got error: %v", err)
|
|
}
|
|
|
|
// Print analysis
|
|
analysis.Print(t)
|
|
|
|
t.Log("✓ SMIGRATING test passed")
|
|
}
|
|
|
|
// TestUnifiedInjector_SMIGRATED demonstrates SMIGRATED notification handling
|
|
// This test requires the proxy-based mock to directly inject notifications
|
|
func TestUnifiedInjector_SMIGRATED(t *testing.T) {
|
|
// Skip if using real fault injector (these tests require proxy mock for direct notification injection)
|
|
if os.Getenv("FAULT_INJECTOR_URL") != "" {
|
|
t.Skip("Skipping unified injector test - requires proxy mock, not real fault injector")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
injector, err := NewNotificationInjector()
|
|
if err != nil {
|
|
t.Fatalf("[ERROR] Failed to create notification injector: %v", err)
|
|
}
|
|
|
|
// Start the injector - skip if proxy not available
|
|
if err := injector.Start(); err != nil {
|
|
t.Skipf("Skipping test - proxy not available: %v", err)
|
|
}
|
|
defer injector.Stop()
|
|
|
|
// Get test mode configuration
|
|
testMode := injector.GetTestModeConfig()
|
|
t.Logf("Using %s mode", testMode.Mode)
|
|
|
|
// Track cluster state reloads
|
|
var reloadCount atomic.Int32
|
|
|
|
client := redis.NewClusterClient(&redis.ClusterOptions{
|
|
Addrs: injector.GetClusterAddrs(),
|
|
Protocol: 3,
|
|
|
|
MaintNotificationsConfig: &maintnotifications.Config{
|
|
Mode: maintnotifications.ModeEnabled,
|
|
RelaxedTimeout: 30 * time.Second,
|
|
},
|
|
})
|
|
defer client.Close()
|
|
|
|
// Set up notification tracking
|
|
tracker := NewTrackingNotificationsHook()
|
|
setupNotificationHook(client, tracker)
|
|
|
|
if err := client.Ping(ctx).Err(); err != nil {
|
|
t.Fatalf("[ERROR] Failed to connect: %v", err)
|
|
}
|
|
|
|
// Set up reload callback on existing nodes
|
|
client.ForEachShard(ctx, func(ctx context.Context, nodeClient *redis.Client) error {
|
|
manager := nodeClient.GetMaintNotificationsManager()
|
|
if manager != nil {
|
|
manager.SetClusterStateReloadCallback(func(ctx context.Context, hostPort string, slotRanges []string) {
|
|
reloadCount.Add(1)
|
|
t.Logf("Cluster state reload triggered for %s, slots: %v", hostPort, slotRanges)
|
|
})
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// Set up reload callback for new nodes
|
|
client.OnNewNode(func(nodeClient *redis.Client) {
|
|
manager := nodeClient.GetMaintNotificationsManager()
|
|
if manager != nil {
|
|
manager.SetClusterStateReloadCallback(func(ctx context.Context, hostPort string, slotRanges []string) {
|
|
reloadCount.Add(1)
|
|
t.Logf("Cluster state reload triggered for %s, slots: %v", hostPort, slotRanges)
|
|
})
|
|
}
|
|
})
|
|
|
|
// Perform some operations to establish connections
|
|
for i := 0; i < 10; i++ {
|
|
if err := client.Set(ctx, fmt.Sprintf("key%d", i), "value", 0).Err(); err != nil {
|
|
t.Logf("Warning: Failed to set key: %v", err)
|
|
}
|
|
}
|
|
|
|
initialReloads := reloadCount.Load()
|
|
|
|
// Start a blocking operation in background to keep connection active
|
|
blockingDone := make(chan error, 1)
|
|
go func() {
|
|
// BLPOP with 10 second timeout - keeps connection active
|
|
_, err := client.BLPop(ctx, 10*time.Second, "notification-test-list").Result()
|
|
blockingDone <- err
|
|
}()
|
|
|
|
// Wait for blocking command to start (mode-aware)
|
|
time.Sleep(testMode.ConnectionEstablishDelay)
|
|
|
|
// Get all node addresses - needed for both modes
|
|
addrs := injector.GetClusterAddrs()
|
|
var newNodeAddr string
|
|
if len(addrs) >= 4 {
|
|
newNodeAddr = addrs[3] // Node 3: 127.0.0.1:17003
|
|
} else {
|
|
// Fallback to first node if we don't have 4 nodes
|
|
newNodeAddr = addrs[0]
|
|
}
|
|
|
|
// Mode-specific behavior for triggering SMIGRATED
|
|
if testMode.IsProxyMock() {
|
|
// Proxy mock: Directly inject SMIGRATED notification
|
|
t.Log("Injecting SMIGRATED notification to swap node 2 for node 3...")
|
|
t.Logf("Using new node address: %s", newNodeAddr)
|
|
|
|
if err := injector.InjectSMIGRATED(ctx, 12346, newNodeAddr, "1000-2000", "3000"); err != nil {
|
|
t.Fatalf("[ERROR] Failed to inject SMIGRATED: %v", err)
|
|
}
|
|
} else {
|
|
// Real fault injector: Trigger slot migration which generates both SMIGRATING and SMIGRATED
|
|
t.Log("Triggering slot migration (will generate SMIGRATING and SMIGRATED)...")
|
|
|
|
// First inject SMIGRATING (this triggers the actual migration)
|
|
if err := injector.InjectSMIGRATING(ctx, 12345, "1000-2000", "3000"); err != nil {
|
|
t.Fatalf("[ERROR] Failed to trigger slot migration: %v", err)
|
|
}
|
|
|
|
// Wait for migration to complete (real FI takes longer)
|
|
t.Log("Waiting for migration to complete...")
|
|
}
|
|
|
|
// Wait for notification processing (mode-aware)
|
|
time.Sleep(testMode.NotificationDelay)
|
|
|
|
// Wait for blocking operation to complete
|
|
<-blockingDone
|
|
|
|
// Verify notification was received
|
|
analysis := tracker.GetAnalysis()
|
|
|
|
if testMode.IsProxyMock() {
|
|
// Proxy mock: SMIGRATED notifications may not always be received
|
|
// because they're sent to all connections, but the client might not be actively
|
|
// listening on all of them. This is expected behavior.
|
|
if analysis.MigratedCount > 0 {
|
|
t.Logf("✓ Received %d SMIGRATED notification(s)", analysis.MigratedCount)
|
|
} else {
|
|
t.Logf("Note: No SMIGRATED notifications received (expected in proxy mock mode)")
|
|
}
|
|
} else {
|
|
// Real FI: Should receive both SMIGRATING and SMIGRATED
|
|
if analysis.MigratingCount == 0 {
|
|
t.Errorf("[ERROR] Expected to receive SMIGRATING notification with real FI, got 0")
|
|
} else {
|
|
t.Logf("✓ Received %d SMIGRATING notification(s)", analysis.MigratingCount)
|
|
}
|
|
|
|
if analysis.MigratedCount > 0 {
|
|
t.Logf("✓ Received %d SMIGRATED notification(s)", analysis.MigratedCount)
|
|
} else {
|
|
t.Logf("Note: SMIGRATED notification not yet received (migration may still be in progress)")
|
|
}
|
|
}
|
|
|
|
// Verify cluster state reload callback
|
|
// Note: SMIGRATED notifications trigger cluster reload via the endpoint information
|
|
// However, in proxy mock mode, the callback might not be triggered because
|
|
// the notification is sent to all connections, not just the active one
|
|
finalReloads := reloadCount.Load()
|
|
if finalReloads > initialReloads {
|
|
t.Logf("✓ Cluster state reloaded %d time(s)", finalReloads-initialReloads)
|
|
} else {
|
|
t.Logf("Note: Cluster state reload callback not triggered (expected in proxy mock mode)")
|
|
}
|
|
|
|
// Verify the client discovered the new node topology
|
|
// After SMIGRATED, the client should reload cluster slots
|
|
// Give it a moment to process
|
|
time.Sleep(2 * time.Second)
|
|
|
|
// Count how many nodes the client knows about
|
|
var nodeAddrsMu sync.Mutex
|
|
nodeAddrs := make(map[string]bool)
|
|
client.ForEachShard(ctx, func(ctx context.Context, nodeClient *redis.Client) error {
|
|
addr := nodeClient.Options().Addr
|
|
nodeAddrsMu.Lock()
|
|
nodeAddrs[addr] = true
|
|
nodeAddrsMu.Unlock()
|
|
t.Logf("Client knows about node: %s", addr)
|
|
return nil
|
|
})
|
|
|
|
// We should have 3 nodes (0, 1, 3) after the swap
|
|
if len(nodeAddrs) < 3 {
|
|
t.Logf("Warning: Expected client to discover 3 nodes after SMIGRATED, got %d", len(nodeAddrs))
|
|
} else {
|
|
t.Logf("✓ Client discovered %d node(s) after SMIGRATED", len(nodeAddrs))
|
|
}
|
|
|
|
// Verify the new node (17003) is in the list
|
|
if len(addrs) >= 4 && !nodeAddrs[newNodeAddr] {
|
|
t.Logf("Warning: Client did not discover new node %s", newNodeAddr)
|
|
} else if len(addrs) >= 4 {
|
|
t.Logf("✓ Client discovered new node %s", newNodeAddr)
|
|
}
|
|
|
|
// Verify we can still perform operations after SMIGRATED
|
|
// The client should have reloaded cluster topology
|
|
successCount := 0
|
|
for i := 0; i < 10; i++ {
|
|
key := fmt.Sprintf("post-migration-key-%d", i)
|
|
if err := client.Set(ctx, key, "value", 0).Err(); err == nil {
|
|
successCount++
|
|
} else {
|
|
t.Logf("Warning: Failed to set key after SMIGRATED: %v", err)
|
|
}
|
|
}
|
|
|
|
if successCount < 8 {
|
|
t.Errorf("[ERROR] Expected most operations to succeed after SMIGRATED, got %d/10", successCount)
|
|
} else {
|
|
t.Logf("✓ Successfully performed %d/10 operations after SMIGRATED", successCount)
|
|
}
|
|
|
|
// Print analysis
|
|
analysis.Print(t)
|
|
|
|
t.Log("✓ SMIGRATED test passed")
|
|
}
|
|
|
|
// TestUnifiedInjector_ComplexScenario demonstrates a complex migration scenario
|
|
// This test requires the proxy-based mock to directly inject notifications
|
|
func TestUnifiedInjector_ComplexScenario(t *testing.T) {
|
|
// Skip if using real fault injector (these tests require proxy mock for direct notification injection)
|
|
if os.Getenv("FAULT_INJECTOR_URL") != "" {
|
|
t.Skip("Skipping unified injector test - requires proxy mock, not real fault injector")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
injector, err := NewNotificationInjector()
|
|
if err != nil {
|
|
t.Fatalf("[ERROR] Failed to create notification injector: %v", err)
|
|
}
|
|
|
|
// Start the injector - skip if proxy not available
|
|
if err := injector.Start(); err != nil {
|
|
t.Skipf("Skipping test - proxy not available: %v", err)
|
|
}
|
|
defer injector.Stop()
|
|
|
|
// Get test mode configuration
|
|
testMode := injector.GetTestModeConfig()
|
|
t.Logf("Using %s mode", testMode.Mode)
|
|
|
|
var reloadCount atomic.Int32
|
|
|
|
client := redis.NewClusterClient(&redis.ClusterOptions{
|
|
Addrs: injector.GetClusterAddrs(),
|
|
Protocol: 3,
|
|
})
|
|
defer client.Close()
|
|
|
|
tracker := NewTrackingNotificationsHook()
|
|
setupNotificationHook(client, tracker)
|
|
|
|
client.OnNewNode(func(nodeClient *redis.Client) {
|
|
manager := nodeClient.GetMaintNotificationsManager()
|
|
if manager != nil {
|
|
manager.SetClusterStateReloadCallback(func(ctx context.Context, hostPort string, slotRanges []string) {
|
|
reloadCount.Add(1)
|
|
})
|
|
}
|
|
})
|
|
|
|
if err := client.Ping(ctx).Err(); err != nil {
|
|
t.Fatalf("[ERROR] Failed to connect: %v", err)
|
|
}
|
|
|
|
// Perform operations
|
|
for i := 0; i < 20; i++ {
|
|
client.Set(ctx, fmt.Sprintf("key%d", i), "value", 0)
|
|
}
|
|
|
|
// Wait for connections to establish (mode-aware)
|
|
time.Sleep(testMode.ConnectionEstablishDelay)
|
|
|
|
// Simulate a multi-step migration scenario
|
|
t.Log("Step 1: Injecting SMIGRATING for slots 0-5000...")
|
|
if err := injector.InjectSMIGRATING(ctx, 10001, "0-5000"); err != nil {
|
|
t.Fatalf("[ERROR] Failed to inject SMIGRATING: %v", err)
|
|
}
|
|
|
|
// Wait for notification processing (mode-aware)
|
|
time.Sleep(testMode.NotificationDelay)
|
|
|
|
// Verify operations work during migration
|
|
for i := 0; i < 5; i++ {
|
|
if err := client.Set(ctx, fmt.Sprintf("migration-key%d", i), "value", 0).Err(); err != nil {
|
|
t.Logf("Warning: Operation failed during migration: %v", err)
|
|
}
|
|
}
|
|
|
|
if testMode.IsProxyMock() {
|
|
// Only inject SMIGRATED with mock injector
|
|
t.Log("Step 2: Injecting SMIGRATED for completed migration...")
|
|
addrs := injector.GetClusterAddrs()
|
|
hostPort := addrs[0]
|
|
|
|
if err := injector.InjectSMIGRATED(ctx, 10002, hostPort, "0-5000"); err != nil {
|
|
t.Fatalf("[ERROR] Failed to inject SMIGRATED: %v", err)
|
|
}
|
|
|
|
// Wait for notification processing (mode-aware)
|
|
time.Sleep(testMode.NotificationDelay)
|
|
}
|
|
|
|
// Verify operations still work
|
|
for i := 0; i < 5; i++ {
|
|
if err := client.Set(ctx, fmt.Sprintf("post-migration-key%d", i), "value", 0).Err(); err != nil {
|
|
t.Errorf("[ERROR] Operations failed after migration: %v", err)
|
|
}
|
|
}
|
|
|
|
// Print final analysis
|
|
analysis := tracker.GetAnalysis()
|
|
analysis.Print(t)
|
|
|
|
t.Logf("✓ Complex scenario test passed")
|
|
t.Logf(" - SMIGRATING notifications: %d", analysis.MigratingCount)
|
|
t.Logf(" - SMIGRATED notifications: %d", analysis.MigratedCount)
|
|
t.Logf(" - Cluster state reloads: %d", reloadCount.Load())
|
|
}
|