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]>
317 lines
9.4 KiB
Go
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)
|
|
}
|
|
}
|