Skip to content

[WIP] hitless #3423

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: ndyakov/CAE-1088-resp3-notification-handlers
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
416 changes: 416 additions & 0 deletions hitless.go

Large diffs are not rendered by default.

197 changes: 197 additions & 0 deletions hitless_config_defaults_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package redis

import (
"testing"
"time"
)

func TestHitlessUpgradeConfig_DefaultValues(t *testing.T) {
tests := []struct {
name string
inputConfig *HitlessUpgradeConfig
expectedConfig *HitlessUpgradeConfig
}{
{
name: "nil config should use all defaults",
inputConfig: nil,
expectedConfig: &HitlessUpgradeConfig{
Enabled: true,
TransitionTimeout: 60 * time.Second,
CleanupInterval: 30 * time.Second,
},
},
{
name: "zero TransitionTimeout should use default",
inputConfig: &HitlessUpgradeConfig{
Enabled: false,
TransitionTimeout: 0, // Zero value
CleanupInterval: 45 * time.Second,
},
expectedConfig: &HitlessUpgradeConfig{
Enabled: false,
TransitionTimeout: 60 * time.Second, // Should use default
CleanupInterval: 45 * time.Second,
},
},
{
name: "zero CleanupInterval should use default",
inputConfig: &HitlessUpgradeConfig{
Enabled: true,
TransitionTimeout: 90 * time.Second,
CleanupInterval: 0, // Zero value
},
expectedConfig: &HitlessUpgradeConfig{
Enabled: true,
TransitionTimeout: 90 * time.Second,
CleanupInterval: 30 * time.Second, // Should use default
},
},
{
name: "both timeouts zero should use defaults",
inputConfig: &HitlessUpgradeConfig{
Enabled: true,
TransitionTimeout: 0, // Zero value
CleanupInterval: 0, // Zero value
},
expectedConfig: &HitlessUpgradeConfig{
Enabled: true,
TransitionTimeout: 60 * time.Second, // Should use default
CleanupInterval: 30 * time.Second, // Should use default
},
},
{
name: "all values set should be preserved",
inputConfig: &HitlessUpgradeConfig{
Enabled: false,
TransitionTimeout: 120 * time.Second,
CleanupInterval: 60 * time.Second,
},
expectedConfig: &HitlessUpgradeConfig{
Enabled: false,
TransitionTimeout: 120 * time.Second,
CleanupInterval: 60 * time.Second,
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test with a mock client that has hitless upgrades enabled
opt := &Options{
Addr: "127.0.0.1:6379",
Protocol: 3,
HitlessUpgrades: true,
HitlessUpgradeConfig: tt.inputConfig,
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
}

// Test the integration creation using the internal method directly
// since initializeHitlessIntegration requires a push processor
integration := newHitlessIntegrationWithTimeouts(tt.inputConfig, opt.ReadTimeout, opt.WriteTimeout)
if integration == nil {
t.Fatal("Integration should not be nil")
}

// Get the config from the integration
actualConfig := integration.GetConfig()
if actualConfig == nil {
t.Fatal("Config should not be nil")
}

// Verify all fields match expected values
if actualConfig.Enabled != tt.expectedConfig.Enabled {
t.Errorf("Enabled: expected %v, got %v", tt.expectedConfig.Enabled, actualConfig.Enabled)
}
if actualConfig.TransitionTimeout != tt.expectedConfig.TransitionTimeout {
t.Errorf("TransitionTimeout: expected %v, got %v", tt.expectedConfig.TransitionTimeout, actualConfig.TransitionTimeout)
}
if actualConfig.CleanupInterval != tt.expectedConfig.CleanupInterval {
t.Errorf("CleanupInterval: expected %v, got %v", tt.expectedConfig.CleanupInterval, actualConfig.CleanupInterval)
}

// Test UpdateConfig as well
newConfig := &HitlessUpgradeConfig{
Enabled: !tt.expectedConfig.Enabled,
TransitionTimeout: 0, // Zero value should use default
CleanupInterval: 0, // Zero value should use default
}

err := integration.UpdateConfig(newConfig)
if err != nil {
t.Fatalf("Failed to update config: %v", err)
}

// Verify updated config has defaults applied
updatedConfig := integration.GetConfig()
if updatedConfig.Enabled == tt.expectedConfig.Enabled {
t.Error("Enabled should have been toggled")
}
if updatedConfig.TransitionTimeout != 60*time.Second {
t.Errorf("TransitionTimeout should use default (60s), got %v", updatedConfig.TransitionTimeout)
}
if updatedConfig.CleanupInterval != 30*time.Second {
t.Errorf("CleanupInterval should use default (30s), got %v", updatedConfig.CleanupInterval)
}
})
}
}

func TestDefaultHitlessUpgradeConfig(t *testing.T) {
config := DefaultHitlessUpgradeConfig()

if config == nil {
t.Fatal("Default config should not be nil")
}

if !config.Enabled {
t.Error("Default config should have Enabled=true")
}

if config.TransitionTimeout != 60*time.Second {
t.Errorf("Default TransitionTimeout should be 60s, got %v", config.TransitionTimeout)
}

if config.CleanupInterval != 30*time.Second {
t.Errorf("Default CleanupInterval should be 30s, got %v", config.CleanupInterval)
}
}

func TestHitlessUpgradeConfig_ZeroValueHandling(t *testing.T) {
// Test that zero values are properly handled in various scenarios

// Test 1: Partial config with some zero values
partialConfig := &HitlessUpgradeConfig{
Enabled: true,
// TransitionTimeout and CleanupInterval are zero values
}

integration := newHitlessIntegrationWithTimeouts(partialConfig, 3*time.Second, 3*time.Second)
if integration == nil {
t.Fatal("Integration should not be nil")
}

config := integration.GetConfig()
if config.TransitionTimeout == 0 {
t.Error("Zero TransitionTimeout should have been replaced with default")
}
if config.CleanupInterval == 0 {
t.Error("Zero CleanupInterval should have been replaced with default")
}

// Test 2: Empty struct
emptyConfig := &HitlessUpgradeConfig{}

integration2 := newHitlessIntegrationWithTimeouts(emptyConfig, 3*time.Second, 3*time.Second)
if integration2 == nil {
t.Fatal("Integration should not be nil")
}

config2 := integration2.GetConfig()
if config2.TransitionTimeout == 0 {
t.Error("Zero TransitionTimeout in empty config should have been replaced with default")
}
if config2.CleanupInterval == 0 {
t.Error("Zero CleanupInterval in empty config should have been replaced with default")
}
}
23 changes: 23 additions & 0 deletions internal/hitless/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Hitless Upgrade Package

This package implements hitless upgrade functionality for Redis cluster clients using the push notification architecture. It provides handlers for managing connection and pool state during Redis cluster upgrades.

## Quick Start

To enable hitless upgrades in your Redis client, simply set the configuration option:

```go
import "github.com/redis/go-redis/v9"

// Enable hitless upgrades with a simple configuration option
client := redis.NewClusterClient(&redis.ClusterOptions{
Addrs: []string{"127.0.0.1:7000", "127.0.0.1:7001", "127.0.0.1:7002"},
Protocol: 3, // RESP3 required for push notifications
HitlessUpgrades: true, // Enable hitless upgrades
})
defer client.Close()

// That's it! Use your client normally - hitless upgrades work automatically
ctx := context.Background()
client.Set(ctx, "key", "value", 0)
```
Loading
Loading