1
0
mirror of https://github.com/redis/go-redis.git synced 2025-11-26 06:23:09 +03:00
Files
go-redis/maintnotifications/e2e/scenario_push_notifications_test.go
Nedyalko Dyakov 042610b79d fix(conn): conn to have state machine (#3559)
* wip

* wip, used and unusable states

* polish state machine

* correct handling OnPut

* better errors for tests, hook should work now

* fix linter

* improve reauth state management. fix tests

* Update internal/pool/conn.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update internal/pool/conn.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* better timeouts

* empty endpoint handoff case

* fix handoff state when queued for handoff

* try to detect the deadlock

* try to detect the deadlock x2

* delete should be called

* improve tests

* fix mark on uninitialized connection

* Update internal/pool/conn_state_test.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update internal/pool/conn_state_test.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update internal/pool/pool.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update internal/pool/conn_state.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update internal/pool/conn.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix error from copilot

* address copilot comment

* fix(pool): pool performance  (#3565)

* perf(pool): replace hookManager RWMutex with atomic.Pointer and add predefined state slices

- Replace hookManager RWMutex with atomic.Pointer for lock-free reads in hot paths
- Add predefined state slices to avoid allocations (validFromInUse, validFromCreatedOrIdle, etc.)
- Add Clone() method to PoolHookManager for atomic updates
- Update AddPoolHook/RemovePoolHook to use copy-on-write pattern
- Update all hookManager access points to use atomic Load()

Performance improvements:
- Eliminates RWMutex contention in Get/Put/Remove hot paths
- Reduces allocations by reusing predefined state slices
- Lock-free reads allow better CPU cache utilization

* perf(pool): eliminate mutex overhead in state machine hot path

The state machine was calling notifyWaiters() on EVERY Get/Put operation,
which acquired a mutex even when no waiters were present (the common case).

Fix: Use atomic waiterCount to check for waiters BEFORE acquiring mutex.
This eliminates mutex contention in the hot path (Get/Put operations).

Implementation:
- Added atomic.Int32 waiterCount field to ConnStateMachine
- Increment when adding waiter, decrement when removing
- Check waiterCount atomically before acquiring mutex in notifyWaiters()

Performance impact:
- Before: mutex lock/unlock on every Get/Put (even with no waiters)
- After: lock-free atomic check, only acquire mutex if waiters exist
- Expected improvement: ~30-50% for Get/Put operations

* perf(pool): use predefined state slices to eliminate allocations in hot path

The pool was creating new slice literals on EVERY Get/Put operation:
- popIdle(): []ConnState{StateCreated, StateIdle}
- putConn(): []ConnState{StateInUse}
- CompareAndSwapUsed(): []ConnState{StateIdle} and []ConnState{StateInUse}
- MarkUnusableForHandoff(): []ConnState{StateInUse, StateIdle, StateCreated}

These allocations were happening millions of times per second in the hot path.

Fix: Use predefined global slices defined in conn_state.go:
- validFromInUse
- validFromCreatedOrIdle
- validFromCreatedInUseOrIdle

Performance impact:
- Before: 4 slice allocations per Get/Put cycle
- After: 0 allocations (use predefined slices)
- Expected improvement: ~30-40% reduction in allocations and GC pressure

* perf(pool): optimize TryTransition to reduce atomic operations

Further optimize the hot path by:
1. Remove redundant GetState() call in the loop
2. Only check waiterCount after successful CAS (not before loop)
3. Inline the waiterCount check to avoid notifyWaiters() call overhead

This reduces atomic operations from 4-5 per Get/Put to 2-3:
- Before: GetState() + CAS + waiterCount.Load() + notifyWaiters mutex check
- After: CAS + waiterCount.Load() (only if CAS succeeds)

Performance impact:
- Eliminates 1-2 atomic operations per Get/Put
- Expected improvement: ~10-15% for Get/Put operations

* perf(pool): add fast path for Get/Put to match master performance

Introduced TryTransitionFast() for the hot path (Get/Put operations):
- Single CAS operation (same as master's atomic bool)
- No waiter notification overhead
- No loop through valid states
- No error allocation

Hot path flow:
1. popIdle(): Try IDLE → IN_USE (fast), fallback to CREATED → IN_USE
2. putConn(): Try IN_USE → IDLE (fast)

This matches master's performance while preserving state machine for:
- Background operations (handoff/reauth use UNUSABLE state)
- State validation (TryTransition still available)
- Waiter notification (AwaitAndTransition for blocking)

Performance comparison per Get/Put cycle:
- Master: 2 atomic CAS operations
- State machine (before): 5 atomic operations (2.5x slower)
- State machine (after): 2 atomic CAS operations (same as master!)

Expected improvement: Restore to baseline ~11,373 ops/sec

* combine cas

* fix linter

* try faster approach

* fast semaphore

* better inlining for hot path

* fix linter issues

* use new semaphore in auth as well

* linter should be happy now

* add comments

* Update internal/pool/conn_state.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* address comment

* slight reordering

* try to cache time if for non-critical calculation

* fix wrong benchmark

* add concurrent test

* fix benchmark report

* add additional expect to check output

* comment and variable rename

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* initConn sets IDLE state

- Handle unexpected conn state changes

* fix precision of time cache and usedAt

* allow e2e tests to run longer

* Fix broken initialization of idle connections

* optimize push notif

* 100ms -> 50ms

* use correct timer for last health check

* verify pass auth on conn creation

* fix assertion

* fix unsafe test

* fix benchmark test

* improve remove conn

* re doesn't support requirepass

* wait more in e2e test

* flaky test

* add missed method in interface

* fix test assertions

* silence logs and faster hooks manager

* address linter comment

* fix flaky test

* use read instad of control

* use pool size for semsize

* CAS instead of reading the state

* preallocate errors and states

* preallocate state slices

* fix flaky test

* fix fast semaphore that could have been starved

* try to fix the semaphore

* should properly notify the waiters

- this way a waiter that timesout at the same time
a releaser is releasing, won't throw token. the releaser
will fail to notify and will pick another waiter.

this hybrid approach should be faster than channels and maintains FIFO

* waiter may double-release (if closed/times out)

* priority of operations

* use simple approach of fifo waiters

* use simple channel based semaphores

* address linter and tests

* remove unused benchs

* change log message

* address pr comments

* address pr comments

* fix data race

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-11 17:38:29 +02:00

515 lines
19 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)
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, cleanup := SetupTestDatabaseAndFactory(t, ctx, "standalone")
defer cleanup()
t.Logf("[PUSH-NOTIFICATIONS] Created test database with bdb_id: %d", bdbID)
// Wait for database to be fully ready
time.Sleep(10 * time.Second)
var dump = true
var seqIDToObserve int64
var connIDToObserve uint64
var match string
var found bool
var status *ActionStatusResponse
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 fault injector
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...")
// Test: 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))
go func() {
p("Waiting for FAILED_OVER notification on conn %d with seqID %d...", connIDToObserve, seqIDToObserve+1)
match, found = logCollector.MatchOrWaitForLogMatchFunc(func(s string) bool {
return notificationType(s, "FAILED_OVER") && connID(s, connIDToObserve) && seqID(s, seqIDToObserve+1)
}, 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)
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("[FI] Failover action completed: %v %s", status.Status, actionOutputIfFailed(status))
p("FAILING_OVER / FAILED_OVER notifications test completed successfully")
// Test: Trigger migrate action to generate MOVING, MIGRATING, MIGRATED notifications
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() {
match, found = logCollector.MatchOrWaitForLogMatchFunc(func(s string) bool {
return strings.Contains(s, logs2.ProcessingNotificationMessage) && strings.Contains(s, "MIGRATING")
}, 60*time.Second)
commandsRunner.Stop()
}()
commandsRunner.FireCommandsUntilStop(ctx)
if !found {
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)
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() {
p("Waiting for MIGRATED notification on conn %d with seqID %d...", connIDToObserve, seqIDToObserve+1)
match, found = logCollector.MatchOrWaitForLogMatchFunc(func(s string) bool {
return notificationType(s, "MIGRATED") && connID(s, connIDToObserve) && seqID(s, seqIDToObserve+1)
}, 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
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)
}
// 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-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-3",
})
if err != nil {
ef("failed to create client: %v", err)
}
// setup tracking for second 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()
// 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")
p("Executing commands and collecting logs for analysis... ")
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(1 * time.Minute)
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
// we started three clients, so we expect 3x the connections
if allLogsAnalysis.ConnectionCount > int64(maxConnections)*3 {
e("Expected no more than %d connections, got %d", maxConnections*3, 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")
}