Files
go/maintnotifications/e2e/scenario_stress_test.go
T
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

317 lines
9.4 KiB
Go

package e2e
import (
"context"
"fmt"
"os"
"sync"
"testing"
"time"
"github.com/redis/go-redis/v9"
"github.com/redis/go-redis/v9/logging"
"github.com/redis/go-redis/v9/maintnotifications"
)
// TestStressPushNotifications tests push notifications under extreme stress conditions
func TestStressPushNotifications(t *testing.T) {
if os.Getenv("E2E_SCENARIO_TESTS") != "true" {
t.Skip("[STRESS][SKIP] Scenario tests require E2E_SCENARIO_TESTS=true")
}
ctx, cancel := context.WithTimeout(context.Background(), 40*time.Minute)
defer cancel()
// Setup: Create fresh database and client factory for this test
bdbID, factory, testMode, cleanup := SetupTestDatabaseAndFactory(t, ctx, "standalone")
defer cleanup()
t.Logf("[STRESS] Created test database with bdb_id: %d (mode: %s)", bdbID, testMode.Mode)
// Skip this test if using proxy mock (stress test requires real fault injector)
if testMode.IsProxyMock() {
t.Skip("Skipping stress test - requires real fault injector")
}
// Wait for database to be fully ready (mode-aware)
time.Sleep(testMode.DatabaseReadyDelay)
var dump = true
var errorsDetected = false
var p = func(format string, args ...interface{}) {
printLog("STRESS", false, format, args...)
}
var e = func(format string, args ...interface{}) {
errorsDetected = true
printLog("STRESS", true, format, args...)
}
var ef = func(format string, args ...interface{}) {
printLog("STRESS", 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)
}
// Extreme stress configuration
minIdleConns := 50
poolSize := 150
maxConnections := 200
numClients := 4
var clients []redis.UniversalClient
var trackers []*TrackingNotificationsHook
var commandRunners []*CommandRunner
// Create multiple clients for extreme stress
for i := 0; i < numClients; i++ {
client, err := factory.Create(fmt.Sprintf("stress-client-%d", i), &CreateClientOptions{
Protocol: 3, // RESP3 required for push notifications
PoolSize: poolSize,
MinIdleConns: minIdleConns,
MaxActiveConns: maxConnections,
MaintNotificationsConfig: &maintnotifications.Config{
Mode: maintnotifications.ModeEnabled,
HandoffTimeout: 60 * time.Second, // Longer timeout for stress
RelaxedTimeout: 20 * time.Second, // Longer relaxed timeout
PostHandoffRelaxedDuration: 5 * time.Second, // Longer post-handoff duration
MaxWorkers: 50, // Maximum workers for stress
HandoffQueueSize: 1000, // Large queue for stress
EndpointType: maintnotifications.EndpointTypeExternalIP,
},
ClientName: fmt.Sprintf("stress-test-client-%d", i),
})
if err != nil {
ef("Failed to create stress client %d: %v", i, err)
}
clients = append(clients, client)
// Setup tracking for each client
tracker := NewTrackingNotificationsHook()
logger := maintnotifications.NewLoggingHook(int(logging.LogLevelWarn)) // Minimal logging for stress
setupNotificationHooks(client, tracker, logger)
trackers = append(trackers, tracker)
// Create command runner for each client
commandRunner, _ := NewCommandRunner(client)
commandRunners = append(commandRunners, commandRunner)
}
defer func() {
if dump {
p("Pool stats:")
factory.PrintPoolStats(t)
}
for _, runner := range commandRunners {
runner.Stop()
}
factory.DestroyAll()
}()
// Verify initial connectivity for all clients
for i, client := range clients {
err = client.Ping(ctx).Err()
if err != nil {
ef("Failed to ping Redis with stress client %d: %v", i, err)
}
}
p("All %d stress clients connected successfully", numClients)
// Start extreme traffic load on all clients
var trafficWg sync.WaitGroup
for i, runner := range commandRunners {
trafficWg.Add(1)
go func(clientID int, r *CommandRunner) {
defer trafficWg.Done()
p("Starting extreme traffic load on stress client %d", clientID)
r.FireCommandsUntilStop(ctx)
}(i, runner)
}
// Wait for traffic to stabilize
time.Sleep(10 * time.Second)
// Trigger multiple concurrent fault injection actions
var actionWg sync.WaitGroup
var actionResults []string
var actionMutex sync.Mutex
actions := []struct {
name string
action string
delay time.Duration
}{
{"failover-1", "failover", 0},
{"migrate-1", "migrate", 5 * time.Second},
{"failover-2", "failover", 10 * time.Second},
}
p("Starting %d concurrent fault injection actions under extreme stress...", len(actions))
for _, action := range actions {
actionWg.Add(1)
go func(actionName, actionType string, delay time.Duration) {
defer actionWg.Done()
if delay > 0 {
time.Sleep(delay)
}
p("Triggering %s action under extreme stress...", actionName)
var resp *ActionResponse
var err error
switch actionType {
case "failover":
resp, err = faultInjector.TriggerAction(ctx, ActionRequest{
Type: "failover",
Parameters: map[string]interface{}{
"bdb_id": endpointConfig.BdbID,
},
})
case "migrate":
resp, err = faultInjector.TriggerAction(ctx, ActionRequest{
Type: "migrate",
Parameters: map[string]interface{}{
"bdb_id": endpointConfig.BdbID,
},
})
}
if err != nil {
e("Failed to trigger %s action: %v", actionName, err)
return
}
// Wait for action to complete
status, err := faultInjector.WaitForAction(ctx, resp.ActionID,
WithMaxWaitTime(360*time.Second), // Longer wait time for stress
WithPollInterval(2*time.Second),
)
if err != nil {
e("[FI] %s action failed: %v", actionName, err)
return
}
actionMutex.Lock()
actionResults = append(actionResults, fmt.Sprintf("%s: %s %s", actionName, status.Status, actionOutputIfFailed(status)))
actionMutex.Unlock()
p("[FI] %s action completed: %s %s", actionName, status.Status, actionOutputIfFailed(status))
}(action.name, action.action, action.delay)
}
// Wait for all actions to complete
actionWg.Wait()
// Continue stress for a bit longer
p("All fault injection actions completed, continuing stress for 2 more minutes...")
time.Sleep(2 * time.Minute)
// Stop all command runners
for _, runner := range commandRunners {
runner.Stop()
}
trafficWg.Wait()
// Analyze stress test results
allLogsAnalysis := logCollector.GetAnalysis()
totalOperations := int64(0)
totalErrors := int64(0)
totalTimeoutErrors := int64(0)
for i, runner := range commandRunners {
stats := runner.GetStats()
p("Stress client %d stats: Operations: %d, Errors: %d, Timeout Errors: %d",
i, stats.Operations, stats.Errors, stats.TimeoutErrors)
totalOperations += stats.Operations
totalErrors += stats.Errors
totalTimeoutErrors += stats.TimeoutErrors
}
p("STRESS TEST RESULTS:")
p("Total operations across all clients: %d", totalOperations)
p("Total errors: %d (%.2f%%)", totalErrors, float64(totalErrors)/float64(totalOperations)*100)
p("Total timeout errors: %d (%.2f%%)", totalTimeoutErrors, float64(totalTimeoutErrors)/float64(totalOperations)*100)
p("Total connections used: %d", allLogsAnalysis.ConnectionCount)
// Print action results
actionMutex.Lock()
p("Fault injection action results:")
for _, result := range actionResults {
p(" %s", result)
}
actionMutex.Unlock()
// Validate stress test results
if totalOperations < 1000 {
e("Expected at least 1000 operations under stress, got %d", totalOperations)
}
// Allow higher error rates under extreme stress (up to 20%)
errorRate := float64(totalErrors) / float64(totalOperations) * 100
if errorRate > 20.0 {
e("Error rate too high under stress: %.2f%% (max allowed: 20%%)", errorRate)
}
// Validate connection limits weren't exceeded
expectedMaxConnections := int64(numClients * maxConnections)
if allLogsAnalysis.ConnectionCount > expectedMaxConnections {
e("Connection count exceeded limit: %d > %d", allLogsAnalysis.ConnectionCount, expectedMaxConnections)
}
// Validate notifications were processed
totalTrackerNotifications := int64(0)
totalProcessingErrors := int64(0)
for _, tracker := range trackers {
analysis := tracker.GetAnalysis()
totalTrackerNotifications += analysis.TotalNotifications
totalProcessingErrors += analysis.NotificationProcessingErrors
}
if totalProcessingErrors > totalTrackerNotifications/10 { // Allow up to 10% processing errors under stress
e("Too many notification processing errors under stress: %d/%d", totalProcessingErrors, totalTrackerNotifications)
}
if errorsDetected {
ef("Errors detected under stress")
logCollector.DumpLogs()
for i, tracker := range trackers {
p("=== Stress Client %d Analysis ===", i)
tracker.GetAnalysis().Print(t)
}
logCollector.Clear()
for _, tracker := range trackers {
tracker.Clear()
}
}
dump = false
p("[SUCCESS] Stress test completed successfully!")
p("Processed %d operations across %d clients with %d connections",
totalOperations, numClients, allLogsAnalysis.ConnectionCount)
p("Error rate: %.2f%%, Notification processing errors: %d/%d",
errorRate, totalProcessingErrors, totalTrackerNotifications)
// Print final analysis (summary only for stress tests to reduce output)
allLogsAnalysis.PrintSummary(t)
for i, tracker := range trackers {
p("=== Stress Client %d ===", i)
tracker.GetAnalysis().PrintSummary(t)
}
}