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]>
680 lines
26 KiB
Go
680 lines
26 KiB
Go
package e2e
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
logs2 "github.com/redis/go-redis/v9/internal/maintnotifications/logs"
|
|
"github.com/redis/go-redis/v9/logging"
|
|
"github.com/redis/go-redis/v9/maintnotifications"
|
|
)
|
|
|
|
// TestPushNotifications tests Redis Enterprise push notifications (MOVING, MIGRATING, MIGRATED, FAILING_OVER, FAILED_OVER)
|
|
// This test now works with BOTH the real fault injector and the proxy mock
|
|
func TestPushNotifications(t *testing.T) {
|
|
if os.Getenv("E2E_SCENARIO_TESTS") != "true" {
|
|
t.Skip("Scenario tests require E2E_SCENARIO_TESTS=true")
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
|
defer cancel()
|
|
|
|
// Setup: Create fresh database and client factory for this test
|
|
bdbID, factory, testMode, cleanup := SetupTestDatabaseAndFactory(t, ctx, "standalone")
|
|
defer cleanup()
|
|
t.Logf("[PUSH-NOTIFICATIONS] Created test database with bdb_id: %d (mode: %s)", bdbID, testMode.Mode)
|
|
|
|
// Wait for database to be fully ready (mode-aware)
|
|
time.Sleep(testMode.DatabaseReadyDelay)
|
|
|
|
var dump = true
|
|
var seqIDToObserve int64
|
|
var connIDToObserve uint64
|
|
|
|
var match string
|
|
var found bool
|
|
|
|
var status *ActionStatusResponse
|
|
var bindStatus *ActionStatusResponse
|
|
var movingNotification []interface{}
|
|
var commandsRunner2, commandsRunner3 *CommandRunner
|
|
var errorsDetected = false
|
|
|
|
var p = func(format string, args ...interface{}) {
|
|
printLog("PUSH-NOTIFICATIONS", false, format, args...)
|
|
}
|
|
|
|
var e = func(format string, args ...interface{}) {
|
|
errorsDetected = true
|
|
printLog("PUSH-NOTIFICATIONS", true, format, args...)
|
|
}
|
|
|
|
var ef = func(format string, args ...interface{}) {
|
|
printLog("PUSH-NOTIFICATIONS", true, format, args...)
|
|
t.FailNow()
|
|
}
|
|
|
|
logCollector.ClearLogs()
|
|
defer func() {
|
|
logCollector.Clear()
|
|
}()
|
|
|
|
// Get endpoint config from factory (now connected to new database)
|
|
endpointConfig := factory.GetConfig()
|
|
|
|
// Create notification injector (works with both proxy mock and real FI)
|
|
injector, err := NewNotificationInjector()
|
|
if err != nil {
|
|
ef("Failed to create notification injector: %v", err)
|
|
}
|
|
defer injector.Stop()
|
|
|
|
// For real fault injector, we also need the FaultInjectorClient for actions
|
|
var faultInjector *FaultInjectorClient
|
|
if !testMode.IsProxyMock() {
|
|
faultInjector, err = CreateTestFaultInjector()
|
|
if err != nil {
|
|
ef("Failed to create fault injector: %v", err)
|
|
}
|
|
}
|
|
|
|
minIdleConns := 5
|
|
poolSize := 10
|
|
maxConnections := 15
|
|
// Create Redis client with push notifications enabled
|
|
client, err := factory.Create("push-notification-client", &CreateClientOptions{
|
|
Protocol: 3, // RESP3 required for push notifications
|
|
PoolSize: poolSize,
|
|
MinIdleConns: minIdleConns,
|
|
MaxActiveConns: maxConnections,
|
|
MaintNotificationsConfig: &maintnotifications.Config{
|
|
Mode: maintnotifications.ModeEnabled,
|
|
HandoffTimeout: 40 * time.Second, // 30 seconds
|
|
RelaxedTimeout: 10 * time.Second, // 10 seconds relaxed timeout
|
|
PostHandoffRelaxedDuration: 2 * time.Second, // 2 seconds post-handoff relaxed duration
|
|
MaxWorkers: 20,
|
|
EndpointType: maintnotifications.EndpointTypeExternalIP, // Use external IP for enterprise
|
|
},
|
|
ClientName: "push-notification-test-client",
|
|
})
|
|
if err != nil {
|
|
ef("Failed to create client: %v", err)
|
|
}
|
|
|
|
defer func() {
|
|
factory.DestroyAll()
|
|
}()
|
|
|
|
// Create timeout tracker
|
|
tracker := NewTrackingNotificationsHook()
|
|
logger := maintnotifications.NewLoggingHook(int(logging.LogLevelDebug))
|
|
setupNotificationHooks(client, tracker, logger)
|
|
defer func() {
|
|
tracker.Clear()
|
|
}()
|
|
|
|
// Verify initial connectivity
|
|
err = client.Ping(ctx).Err()
|
|
if err != nil {
|
|
ef("Failed to ping Redis: %v", err)
|
|
}
|
|
|
|
p("Client connected successfully, starting push notification test")
|
|
|
|
commandsRunner, _ := NewCommandRunner(client)
|
|
defer func() {
|
|
if dump {
|
|
p("Command runner stats:")
|
|
p("Operations: %d, Errors: %d, Timeout Errors: %d",
|
|
commandsRunner.GetStats().Operations, commandsRunner.GetStats().Errors, commandsRunner.GetStats().TimeoutErrors)
|
|
}
|
|
p("Stopping command runner...")
|
|
commandsRunner.Stop()
|
|
}()
|
|
|
|
p("Starting FAILING_OVER / FAILED_OVER notifications test...")
|
|
|
|
// Mode-aware: Proxy mock directly injects notifications, real FI triggers actions
|
|
var failoverResp *ActionResponse
|
|
if testMode.IsProxyMock() {
|
|
// Proxy mock: Directly inject FAILING_OVER notification
|
|
p("Injecting FAILING_OVER notification (proxy mock mode)...")
|
|
if err := injector.InjectFAILING_OVER(ctx, 1000); err != nil {
|
|
ef("Failed to inject FAILING_OVER: %v", err)
|
|
}
|
|
time.Sleep(testMode.NotificationDelay)
|
|
} else {
|
|
// Real FI: Trigger failover action to generate FAILING_OVER, FAILED_OVER notifications
|
|
p("Triggering failover action to generate push notifications...")
|
|
failoverResp, err = faultInjector.TriggerAction(ctx, ActionRequest{
|
|
Type: "failover",
|
|
Parameters: map[string]interface{}{
|
|
"bdb_id": endpointConfig.BdbID,
|
|
},
|
|
})
|
|
if err != nil {
|
|
ef("Failed to trigger failover action: %v", err)
|
|
}
|
|
}
|
|
go func() {
|
|
p("Waiting for FAILING_OVER notification")
|
|
match, found = logCollector.MatchOrWaitForLogMatchFunc(func(s string) bool {
|
|
return strings.Contains(s, logs2.ProcessingNotificationMessage) && notificationType(s, "FAILING_OVER")
|
|
}, 3*time.Minute)
|
|
commandsRunner.Stop()
|
|
}()
|
|
commandsRunner.FireCommandsUntilStop(ctx)
|
|
if !found {
|
|
ef("FAILING_OVER notification was not received within 3 minutes")
|
|
}
|
|
failingOverData := logs2.ExtractDataFromLogMessage(match)
|
|
p("FAILING_OVER notification received. %v", failingOverData)
|
|
seqIDToObserve = int64(failingOverData["seqID"].(float64))
|
|
connIDToObserve = uint64(failingOverData["connID"].(float64))
|
|
|
|
// Inject FAILED_OVER in proxy mock mode
|
|
if testMode.IsProxyMock() {
|
|
p("Injecting FAILED_OVER notification (proxy mock mode)...")
|
|
if err := injector.InjectFAILED_OVER(ctx, 1001); err != nil {
|
|
ef("Failed to inject FAILED_OVER: %v", err)
|
|
}
|
|
time.Sleep(testMode.NotificationDelay)
|
|
}
|
|
|
|
go func() {
|
|
// Note: Redis Enterprise may not send FAILED_OVER with seqID = FAILING_OVER.seqID + 1
|
|
// We wait for any FAILED_OVER on the same connection
|
|
p("Waiting for FAILED_OVER notification on conn %d...", connIDToObserve)
|
|
match, found = logCollector.MatchOrWaitForLogMatchFunc(func(s string) bool {
|
|
return strings.Contains(s, logs2.ProcessingNotificationMessage) && notificationType(s, "FAILED_OVER") && connID(s, connIDToObserve)
|
|
}, 3*time.Minute)
|
|
commandsRunner.Stop()
|
|
}()
|
|
commandsRunner.FireCommandsUntilStop(ctx)
|
|
if !found {
|
|
ef("FAILED_OVER notification was not received within 3 minutes")
|
|
}
|
|
failedOverData := logs2.ExtractDataFromLogMessage(match)
|
|
p("FAILED_OVER notification received. %v", failedOverData)
|
|
|
|
// Wait for action to complete (real FI only)
|
|
if !testMode.IsProxyMock() {
|
|
status, err = faultInjector.WaitForAction(ctx, failoverResp.ActionID,
|
|
WithMaxWaitTime(240*time.Second),
|
|
WithPollInterval(2*time.Second),
|
|
)
|
|
if err != nil {
|
|
ef("[FI] Failover action failed: %v", err)
|
|
}
|
|
p("Failover action completed: %s %s", status.Status, actionOutputIfFailed(status))
|
|
}
|
|
|
|
p("FAILING_OVER / FAILED_OVER notifications test completed successfully")
|
|
|
|
// Test: Trigger migrate action to generate MOVING, MIGRATING, MIGRATED notifications
|
|
var migrateResp *ActionResponse
|
|
if testMode.IsProxyMock() {
|
|
// Proxy mock: Directly inject MIGRATING notification
|
|
p("Injecting MIGRATING notification (proxy mock mode)...")
|
|
if err := injector.InjectMIGRATING(ctx, 2000, 5000); err != nil {
|
|
ef("Failed to inject MIGRATING: %v", err)
|
|
}
|
|
time.Sleep(testMode.NotificationDelay)
|
|
} else {
|
|
// Real FI: Trigger migrate action
|
|
p("Triggering migrate action to generate push notifications...")
|
|
migrateResp, err = faultInjector.TriggerAction(ctx, ActionRequest{
|
|
Type: "migrate",
|
|
Parameters: map[string]interface{}{
|
|
"bdb_id": endpointConfig.BdbID,
|
|
},
|
|
})
|
|
if err != nil {
|
|
ef("Failed to trigger migrate action: %v", err)
|
|
}
|
|
}
|
|
go func() {
|
|
if DebugE2E() {
|
|
fmt.Printf("[DEBUG] Goroutine started: waiting for MIGRATING notification...\n")
|
|
}
|
|
match, found = logCollector.MatchOrWaitForLogMatchFunc(func(s string) bool {
|
|
return strings.Contains(s, logs2.ProcessingNotificationMessage) && notificationType(s, "MIGRATING")
|
|
}, 60*time.Second)
|
|
if DebugE2E() {
|
|
fmt.Printf("[DEBUG] Goroutine: MatchOrWaitForLogMatchFunc returned, found=%v, calling Stop()...\n", found)
|
|
}
|
|
commandsRunner.Stop()
|
|
if DebugE2E() {
|
|
fmt.Printf("[DEBUG] Goroutine: Stop() returned\n")
|
|
}
|
|
}()
|
|
if DebugE2E() {
|
|
fmt.Printf("[DEBUG] Main thread: calling FireCommandsUntilStop...\n")
|
|
}
|
|
commandsRunner.FireCommandsUntilStop(ctx)
|
|
if DebugE2E() {
|
|
fmt.Printf("[DEBUG] Main thread: FireCommandsUntilStop returned\n")
|
|
}
|
|
if !found {
|
|
if !testMode.IsProxyMock() {
|
|
status, err = faultInjector.WaitForAction(ctx, migrateResp.ActionID,
|
|
WithMaxWaitTime(240*time.Second),
|
|
WithPollInterval(2*time.Second),
|
|
)
|
|
if err != nil {
|
|
ef("[FI] Migrate action failed: %v", err)
|
|
}
|
|
p("[FI] Migrate action completed: %s %s", status.Status, actionOutputIfFailed(status))
|
|
}
|
|
ef("MIGRATING notification for migrate action was not received within 60 seconds")
|
|
}
|
|
migrateData := logs2.ExtractDataFromLogMessage(match)
|
|
seqIDToObserve = int64(migrateData["seqID"].(float64))
|
|
connIDToObserve = uint64(migrateData["connID"].(float64))
|
|
p("MIGRATING notification received: seqID: %d, connID: %d", seqIDToObserve, connIDToObserve)
|
|
|
|
// Wait for action to complete and inject MIGRATED (mode-aware)
|
|
if testMode.IsProxyMock() {
|
|
// Proxy mock: Directly inject MIGRATED notification
|
|
p("Injecting MIGRATED notification (proxy mock mode)...")
|
|
if err := injector.InjectMIGRATED(ctx, 2001, 5000); err != nil {
|
|
ef("Failed to inject MIGRATED: %v", err)
|
|
}
|
|
time.Sleep(testMode.NotificationDelay)
|
|
} else {
|
|
// Real FI: Wait for action to complete (generates MIGRATED automatically)
|
|
status, err = faultInjector.WaitForAction(ctx, migrateResp.ActionID,
|
|
WithMaxWaitTime(240*time.Second),
|
|
WithPollInterval(2*time.Second),
|
|
)
|
|
if err != nil {
|
|
ef("[FI] Migrate action failed: %v", err)
|
|
}
|
|
p("[FI] Migrate action completed: %s %s", status.Status, actionOutputIfFailed(status))
|
|
}
|
|
|
|
go func() {
|
|
// Note: During a real migration, the connection that received MIGRATING might be closed.
|
|
// MIGRATED might arrive on a different connection.
|
|
// We accept any MIGRATED notification rather than requiring same connection/seqID.
|
|
p("Waiting for MIGRATED notification (preferring conn %d with seqID %d)...", connIDToObserve, seqIDToObserve+1)
|
|
match, found = logCollector.MatchOrWaitForLogMatchFunc(func(s string) bool {
|
|
return strings.Contains(s, logs2.ProcessingNotificationMessage) && notificationType(s, "MIGRATED")
|
|
}, 3*time.Minute)
|
|
commandsRunner.Stop()
|
|
}()
|
|
commandsRunner.FireCommandsUntilStop(ctx)
|
|
if !found {
|
|
ef("MIGRATED notification was not received within 3 minutes")
|
|
}
|
|
migratedData := logs2.ExtractDataFromLogMessage(match)
|
|
p("MIGRATED notification received. %v", migratedData)
|
|
|
|
p("MIGRATING / MIGRATED notifications test completed successfully")
|
|
|
|
// Trigger bind action to complete the migration process (or inject MOVING in proxy mock mode)
|
|
var bindResp *ActionResponse
|
|
if testMode.IsProxyMock() {
|
|
// Proxy mock: We need connections to receive the MOVING notification,
|
|
// but we must stop traffic before handoffs start to avoid data race.
|
|
// The handoff worker reinitializes connections, which races with CommandRunner.
|
|
p("Starting commands before MOVING injection (proxy mock mode)...")
|
|
go commandsRunner.FireCommandsUntilStop(ctx)
|
|
// Give commands time to establish connections
|
|
time.Sleep(1 * time.Second)
|
|
|
|
// Proxy mock: Directly inject MOVING notification
|
|
// Format: ["MOVING", seqID, timeS, endpoint]
|
|
// timeS is the time in seconds until the connection should be handed off
|
|
p("Injecting MOVING notification (proxy mock mode)...")
|
|
if err := injector.InjectMOVING(ctx, 3000, 30, ""); err != nil {
|
|
ef("Failed to inject MOVING: %v", err)
|
|
}
|
|
time.Sleep(testMode.NotificationDelay)
|
|
|
|
// Wait for MOVING notification to be received before stopping CommandRunner
|
|
p("Waiting for MOVING notification to be received (proxy mock mode)...")
|
|
match, found := logCollector.MatchOrWaitForLogMatchFunc(func(s string) bool {
|
|
return strings.Contains(s, logs2.ProcessingNotificationMessage) && notificationType(s, "MOVING")
|
|
}, 30*time.Second)
|
|
if !found {
|
|
ef("MOVING notification was not received in proxy mock mode")
|
|
}
|
|
movingData := logs2.ExtractDataFromLogMessage(match)
|
|
p("MOVING notification received (proxy mock mode). %v", movingData)
|
|
|
|
// Stop command runner before handoffs start to avoid data race
|
|
// Handoffs are scheduled at timeS/2 = 15 seconds, so stop before that
|
|
p("Stopping command runner before handoffs start (proxy mock mode)...")
|
|
commandsRunner.Stop()
|
|
} else {
|
|
// Real FI: Trigger bind action
|
|
p("Triggering bind action to complete migration...")
|
|
bindResp, err = faultInjector.TriggerAction(ctx, ActionRequest{
|
|
Type: "bind",
|
|
Parameters: map[string]interface{}{
|
|
"bdb_id": endpointConfig.BdbID,
|
|
},
|
|
})
|
|
if err != nil {
|
|
ef("Failed to trigger bind action: %v", err)
|
|
}
|
|
}
|
|
|
|
// Multi-client tests - only run if not using proxy mock
|
|
if !testMode.SkipMultiClientTests {
|
|
// start a second client but don't execute any commands on it
|
|
p("Starting a second client to observe notification during moving...")
|
|
client2, err := factory.Create("push-notification-client-2", &CreateClientOptions{
|
|
Protocol: 3, // RESP3 required for push notifications
|
|
PoolSize: poolSize,
|
|
MinIdleConns: minIdleConns,
|
|
MaxActiveConns: maxConnections,
|
|
MaintNotificationsConfig: &maintnotifications.Config{
|
|
Mode: maintnotifications.ModeEnabled,
|
|
HandoffTimeout: 40 * time.Second, // 30 seconds
|
|
RelaxedTimeout: 30 * time.Minute, // 30 minutes relaxed timeout for second client
|
|
PostHandoffRelaxedDuration: 2 * time.Second, // 2 seconds post-handoff relaxed duration
|
|
MaxWorkers: 20,
|
|
EndpointType: maintnotifications.EndpointTypeExternalIP, // Use external IP for enterprise
|
|
},
|
|
ClientName: "push-notification-test-client-2",
|
|
})
|
|
|
|
if err != nil {
|
|
ef("failed to create client: %v", err)
|
|
}
|
|
// setup tracking for second client
|
|
tracker2 := NewTrackingNotificationsHook()
|
|
logger2 := maintnotifications.NewLoggingHook(int(logging.LogLevelDebug))
|
|
setupNotificationHooks(client2, tracker2, logger2)
|
|
commandsRunner2, _ = NewCommandRunner(client2)
|
|
p("Second client created")
|
|
|
|
// Use a channel to communicate errors from the goroutine
|
|
errChan := make(chan error, 1)
|
|
|
|
go func() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
errChan <- fmt.Errorf("goroutine panic: %v", r)
|
|
}
|
|
}()
|
|
|
|
p("Waiting for MOVING notification on first client")
|
|
match, found = logCollector.MatchOrWaitForLogMatchFunc(func(s string) bool {
|
|
return strings.Contains(s, logs2.ProcessingNotificationMessage) && notificationType(s, "MOVING")
|
|
}, 3*time.Minute)
|
|
commandsRunner.Stop()
|
|
if !found {
|
|
errChan <- fmt.Errorf("MOVING notification was not received within 3 minutes ON A FIRST CLIENT")
|
|
return
|
|
}
|
|
|
|
// once moving is received, start a second client commands runner
|
|
p("Starting commands on second client")
|
|
go commandsRunner2.FireCommandsUntilStop(ctx)
|
|
|
|
p("Waiting for MOVING notification on second client")
|
|
matchNotif, fnd := tracker2.FindOrWaitForNotification("MOVING", 3*time.Minute)
|
|
if !fnd {
|
|
errChan <- fmt.Errorf("MOVING notification was not received within 3 minutes ON A SECOND CLIENT")
|
|
return
|
|
} else {
|
|
p("MOVING notification received on second client %v", matchNotif)
|
|
}
|
|
|
|
// Signal success
|
|
errChan <- nil
|
|
}()
|
|
commandsRunner.FireCommandsUntilStop(ctx)
|
|
// wait for moving on first client
|
|
// once the commandRunner stops, it means a waiting
|
|
// on the logCollector match has completed and we can proceed
|
|
if !found {
|
|
ef("MOVING notification was not received within 3 minutes")
|
|
}
|
|
movingData := logs2.ExtractDataFromLogMessage(match)
|
|
p("MOVING notification received. %v", movingData)
|
|
seqIDToObserve = int64(movingData["seqID"].(float64))
|
|
connIDToObserve = uint64(movingData["connID"].(float64))
|
|
|
|
time.Sleep(3 * time.Second)
|
|
// start a third client but don't execute any commands on it
|
|
p("Starting a third client to observe notification during moving...")
|
|
client3, err := factory.Create("push-notification-test-client-3", &CreateClientOptions{
|
|
Protocol: 3, // RESP3 required for push notifications
|
|
PoolSize: poolSize,
|
|
MinIdleConns: minIdleConns,
|
|
MaxActiveConns: maxConnections,
|
|
MaintNotificationsConfig: &maintnotifications.Config{
|
|
Mode: maintnotifications.ModeEnabled,
|
|
HandoffTimeout: 40 * time.Second, // 30 seconds
|
|
RelaxedTimeout: 30 * time.Minute, // 30 minutes relaxed timeout for second client
|
|
PostHandoffRelaxedDuration: 2 * time.Second, // 2 seconds post-handoff relaxed duration
|
|
MaxWorkers: 20,
|
|
EndpointType: maintnotifications.EndpointTypeExternalIP, // Use external IP for enterprise
|
|
},
|
|
ClientName: "push-notification-test-client-3",
|
|
})
|
|
|
|
if err != nil {
|
|
ef("failed to create client: %v", err)
|
|
}
|
|
// setup tracking for third client
|
|
tracker3 := NewTrackingNotificationsHook()
|
|
logger3 := maintnotifications.NewLoggingHook(int(logging.LogLevelDebug))
|
|
setupNotificationHooks(client3, tracker3, logger3)
|
|
commandsRunner3, _ = NewCommandRunner(client3)
|
|
p("Third client created")
|
|
go commandsRunner3.FireCommandsUntilStop(ctx)
|
|
// wait for moving on third client
|
|
movingNotification, found = tracker3.FindOrWaitForNotification("MOVING", 3*time.Minute)
|
|
if !found {
|
|
p("[NOTICE] MOVING notification was not received within 3 minutes ON A THIRD CLIENT")
|
|
} else {
|
|
p("MOVING notification received on third client. %v", movingNotification)
|
|
if len(movingNotification) != 4 {
|
|
p("[NOTICE] Invalid MOVING notification format: %s", movingNotification)
|
|
}
|
|
mNotifTimeS, ok := movingNotification[2].(int64)
|
|
if !ok {
|
|
p("[NOTICE] Invalid timeS in MOVING notification: %s", movingNotification)
|
|
}
|
|
// expect timeS to be less than 15
|
|
if mNotifTimeS < 15 {
|
|
p("[NOTICE] Expected timeS < 15, got %d", mNotifTimeS)
|
|
}
|
|
}
|
|
commandsRunner3.Stop()
|
|
commandsRunner2.Stop() // Stop commandsRunner2 before the final section
|
|
// Wait for the goroutine to complete and check for errors
|
|
if err := <-errChan; err != nil {
|
|
ef("Second client goroutine error: %v", err)
|
|
}
|
|
|
|
// Wait for bind action to complete
|
|
bindStatus, err = faultInjector.WaitForAction(ctx, bindResp.ActionID,
|
|
WithMaxWaitTime(240*time.Second),
|
|
WithPollInterval(2*time.Second))
|
|
if err != nil {
|
|
ef("Bind action failed: %v", err)
|
|
}
|
|
|
|
p("Bind action completed: %s %s", bindStatus.Status, actionOutputIfFailed(bindStatus))
|
|
|
|
p("MOVING notification test completed successfully")
|
|
} else {
|
|
p("Skipping multi-client MOVING test (proxy mock mode)")
|
|
}
|
|
|
|
p("Executing commands and collecting logs for analysis... ")
|
|
|
|
// Run commands based on mode
|
|
if testMode.SkipMultiClientTests {
|
|
// Single client mode (proxy mock)
|
|
// CommandRunner was already stopped before handoffs started (to avoid data race)
|
|
// Wait for handoffs to complete:
|
|
// 1. MOVING notification was already processed
|
|
// 2. Handoff is scheduled at timeS/2 = 15 seconds
|
|
// 3. Handoff executes
|
|
// Total: ~18 seconds for handoffs to complete
|
|
p("Waiting for handoffs to complete (proxy mock mode)...")
|
|
time.Sleep(18 * time.Second)
|
|
|
|
// Restart command runner to observe post-handoff behavior
|
|
p("Restarting command runner to observe post-handoff behavior...")
|
|
commandsRunner, _ = NewCommandRunner(client)
|
|
go commandsRunner.FireCommandsUntilStop(ctx)
|
|
|
|
// Run traffic for a bit to observe post-handoff behavior
|
|
time.Sleep(5 * time.Second)
|
|
commandsRunner.Stop()
|
|
} else {
|
|
// Multi-client mode (real FI)
|
|
go commandsRunner.FireCommandsUntilStop(ctx)
|
|
go commandsRunner2.FireCommandsUntilStop(ctx)
|
|
go commandsRunner3.FireCommandsUntilStop(ctx)
|
|
time.Sleep(2 * time.Minute)
|
|
commandsRunner.Stop()
|
|
commandsRunner2.Stop()
|
|
commandsRunner3.Stop()
|
|
}
|
|
|
|
time.Sleep(2 * testMode.NotificationDelay)
|
|
allLogsAnalysis := logCollector.GetAnalysis()
|
|
trackerAnalysis := tracker.GetAnalysis()
|
|
|
|
if allLogsAnalysis.TimeoutErrorsCount > 0 {
|
|
e("Unexpected timeout errors: %d", allLogsAnalysis.TimeoutErrorsCount)
|
|
}
|
|
if trackerAnalysis.UnexpectedNotificationCount > 0 {
|
|
e("Unexpected notifications: %d", trackerAnalysis.UnexpectedNotificationCount)
|
|
}
|
|
if trackerAnalysis.NotificationProcessingErrors > 0 {
|
|
e("Notification processing errors: %d", trackerAnalysis.NotificationProcessingErrors)
|
|
}
|
|
if allLogsAnalysis.RelaxedTimeoutCount == 0 {
|
|
e("Expected relaxed timeouts, got none")
|
|
}
|
|
if allLogsAnalysis.UnrelaxedTimeoutCount == 0 {
|
|
e("Expected unrelaxed timeouts, got none")
|
|
}
|
|
if allLogsAnalysis.UnrelaxedAfterMoving == 0 {
|
|
e("Expected unrelaxed timeouts after moving, got none")
|
|
}
|
|
if allLogsAnalysis.RelaxedPostHandoffCount == 0 {
|
|
e("Expected relaxed timeouts after post-handoff, got none")
|
|
}
|
|
// validate number of connections we do not exceed max connections
|
|
// Adjust expected connections based on mode
|
|
// During handoffs, new connections are created before old ones are closed,
|
|
// so the connection count can temporarily exceed the pool size.
|
|
// We use a multiplier to account for this.
|
|
expectedMaxConns := int64(maxConnections)
|
|
if !testMode.SkipMultiClientTests {
|
|
// We started three clients, and during handoffs each connection can temporarily
|
|
// have both old and new connections active. We allow 4x the base connections
|
|
// to account for handoff overhead across all clients.
|
|
expectedMaxConns = int64(maxConnections) * 4
|
|
} else {
|
|
// In proxy mock mode, we test with a standalone client,
|
|
// During handoffs, new connections are created before old ones are closed,
|
|
expectedMaxConns = int64(maxConnections) * 2
|
|
}
|
|
|
|
if allLogsAnalysis.ConnectionCount > expectedMaxConns {
|
|
e("Expected no more than %d connections, got %d", expectedMaxConns, allLogsAnalysis.ConnectionCount)
|
|
}
|
|
|
|
if allLogsAnalysis.ConnectionCount < int64(minIdleConns) {
|
|
e("Expected at least %d connections, got %d", minIdleConns, allLogsAnalysis.ConnectionCount)
|
|
}
|
|
|
|
// validate logs are present for all connections
|
|
for connID := range trackerAnalysis.connIds {
|
|
if len(allLogsAnalysis.connLogs[connID]) == 0 {
|
|
e("No logs found for connection %d", connID)
|
|
}
|
|
}
|
|
// checks are tracker >= logs since the tracker only tracks client1
|
|
// logs include all clients (and some of them start logging even before all hooks are setup)
|
|
// for example for idle connections if they receive a notification before the hook is setup
|
|
// the action (i.e. relaxing timeouts) will be logged, but the notification will not be tracked and maybe wont be logged
|
|
|
|
// validate number of notifications in tracker matches number of notifications in logs
|
|
// allow for more moving in the logs since we started a second client
|
|
if trackerAnalysis.TotalNotifications > allLogsAnalysis.TotalNotifications {
|
|
e("Expected at least %d or more notifications, got %d", trackerAnalysis.TotalNotifications, allLogsAnalysis.TotalNotifications)
|
|
}
|
|
|
|
if trackerAnalysis.MovingCount > allLogsAnalysis.MovingCount {
|
|
e("Expected at least %d or more MOVING notifications, got %d", trackerAnalysis.MovingCount, allLogsAnalysis.MovingCount)
|
|
}
|
|
|
|
if trackerAnalysis.MigratingCount > allLogsAnalysis.MigratingCount {
|
|
e("Expected at least %d MIGRATING notifications, got %d", trackerAnalysis.MigratingCount, allLogsAnalysis.MigratingCount)
|
|
}
|
|
|
|
if trackerAnalysis.MigratedCount > allLogsAnalysis.MigratedCount {
|
|
e("Expected at least %d MIGRATED notifications, got %d", trackerAnalysis.MigratedCount, allLogsAnalysis.MigratedCount)
|
|
}
|
|
|
|
if trackerAnalysis.FailingOverCount > allLogsAnalysis.FailingOverCount {
|
|
e("Expected at least %d FAILING_OVER notifications, got %d", trackerAnalysis.FailingOverCount, allLogsAnalysis.FailingOverCount)
|
|
}
|
|
|
|
if trackerAnalysis.FailedOverCount > allLogsAnalysis.FailedOverCount {
|
|
e("Expected at least %d FAILED_OVER notifications, got %d", trackerAnalysis.FailedOverCount, allLogsAnalysis.FailedOverCount)
|
|
}
|
|
|
|
if trackerAnalysis.UnexpectedNotificationCount != allLogsAnalysis.UnexpectedCount {
|
|
e("Expected %d unexpected notifications, got %d", trackerAnalysis.UnexpectedNotificationCount, allLogsAnalysis.UnexpectedCount)
|
|
}
|
|
|
|
// unrelaxed (and relaxed) after moving wont be tracked by the hook, so we have to exclude it
|
|
if trackerAnalysis.UnrelaxedTimeoutCount > allLogsAnalysis.UnrelaxedTimeoutCount-allLogsAnalysis.UnrelaxedAfterMoving {
|
|
e("Expected at least %d unrelaxed timeouts, got %d", trackerAnalysis.UnrelaxedTimeoutCount, allLogsAnalysis.UnrelaxedTimeoutCount-allLogsAnalysis.UnrelaxedAfterMoving)
|
|
}
|
|
if trackerAnalysis.RelaxedTimeoutCount > allLogsAnalysis.RelaxedTimeoutCount-allLogsAnalysis.RelaxedPostHandoffCount {
|
|
e("Expected at least %d relaxed timeouts, got %d", trackerAnalysis.RelaxedTimeoutCount, allLogsAnalysis.RelaxedTimeoutCount-allLogsAnalysis.RelaxedPostHandoffCount)
|
|
}
|
|
|
|
// validate all handoffs succeeded
|
|
if allLogsAnalysis.FailedHandoffCount > 0 {
|
|
e("Expected no failed handoffs, got %d", allLogsAnalysis.FailedHandoffCount)
|
|
}
|
|
if allLogsAnalysis.SucceededHandoffCount == 0 {
|
|
e("Expected at least one successful handoff, got none")
|
|
}
|
|
if allLogsAnalysis.TotalHandoffCount != allLogsAnalysis.SucceededHandoffCount {
|
|
e("Expected total handoffs to match successful handoffs, got %d != %d", allLogsAnalysis.TotalHandoffCount, allLogsAnalysis.SucceededHandoffCount)
|
|
}
|
|
|
|
// no additional retries
|
|
if allLogsAnalysis.TotalHandoffRetries != allLogsAnalysis.TotalHandoffCount {
|
|
e("Expected no additional handoff retries, got %d", allLogsAnalysis.TotalHandoffRetries-allLogsAnalysis.TotalHandoffCount)
|
|
}
|
|
|
|
if errorsDetected {
|
|
logCollector.DumpLogs()
|
|
trackerAnalysis.Print(t)
|
|
logCollector.Clear()
|
|
tracker.Clear()
|
|
ef("[FAIL] Errors detected in push notification test")
|
|
}
|
|
|
|
p("Analysis complete, no errors found")
|
|
allLogsAnalysis.Print(t)
|
|
trackerAnalysis.Print(t)
|
|
p("Command runner stats:")
|
|
p("Operations: %d, Errors: %d, Timeout Errors: %d",
|
|
commandsRunner.GetStats().Operations, commandsRunner.GetStats().Errors, commandsRunner.GetStats().TimeoutErrors)
|
|
|
|
p("Push notification test completed successfully")
|
|
}
|