mirror of
https://github.com/redis/go-redis.git
synced 2025-07-16 13:21:51 +03:00
417 lines
13 KiB
Go
417 lines
13 KiB
Go
package redis
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/redis/go-redis/v9/internal"
|
|
"github.com/redis/go-redis/v9/internal/hitless"
|
|
"github.com/redis/go-redis/v9/internal/pool"
|
|
)
|
|
|
|
// HitlessUpgradeConfig provides configuration for hitless upgrades
|
|
type HitlessUpgradeConfig struct {
|
|
// Enabled controls whether hitless upgrades are active
|
|
Enabled bool
|
|
|
|
// TransitionTimeout is the increased timeout for connections during transitions
|
|
// (MIGRATING/FAILING_OVER). This should be longer than normal operation timeouts
|
|
// to account for the time needed to complete the transition.
|
|
// Default: 60 seconds
|
|
TransitionTimeout time.Duration
|
|
|
|
// CleanupInterval controls how often expired states are cleaned up
|
|
// Default: 30 seconds
|
|
CleanupInterval time.Duration
|
|
}
|
|
|
|
// DefaultHitlessUpgradeConfig returns the default configuration for hitless upgrades
|
|
func DefaultHitlessUpgradeConfig() *HitlessUpgradeConfig {
|
|
return &HitlessUpgradeConfig{
|
|
Enabled: true,
|
|
TransitionTimeout: 60 * time.Second, // Longer timeout for transitioning connections
|
|
CleanupInterval: 30 * time.Second, // How often to clean up expired states
|
|
}
|
|
}
|
|
|
|
// HitlessUpgradeStatistics provides statistics about ongoing upgrade operations
|
|
type HitlessUpgradeStatistics struct {
|
|
ActiveConnections int // Total connections in transition
|
|
IsMoving bool // Whether pool is currently moving
|
|
MigratingConnections int // Connections in MIGRATING state
|
|
FailingOverConnections int // Connections in FAILING_OVER state
|
|
Timestamp time.Time // When these statistics were collected
|
|
}
|
|
|
|
// HitlessUpgradeStatus provides detailed status of all ongoing upgrades
|
|
type HitlessUpgradeStatus struct {
|
|
ConnectionStates map[interface{}]interface{}
|
|
IsMoving bool
|
|
NewEndpoint string
|
|
Timestamp time.Time
|
|
}
|
|
|
|
// HitlessIntegration provides the interface for hitless upgrade functionality
|
|
type HitlessIntegration interface {
|
|
// IsEnabled returns whether hitless upgrades are currently enabled
|
|
IsEnabled() bool
|
|
|
|
// EnableHitlessUpgrades enables hitless upgrade functionality
|
|
EnableHitlessUpgrades()
|
|
|
|
// DisableHitlessUpgrades disables hitless upgrade functionality
|
|
DisableHitlessUpgrades()
|
|
|
|
// GetConnectionTimeout returns the appropriate timeout for a connection
|
|
// If the connection is transitioning, returns the longer TransitionTimeout
|
|
GetConnectionTimeout(conn interface{}, defaultTimeout time.Duration) time.Duration
|
|
|
|
// GetConnectionTimeouts returns both read and write timeouts for a connection
|
|
// If the connection is transitioning, returns increased timeouts
|
|
GetConnectionTimeouts(conn interface{}, defaultReadTimeout, defaultWriteTimeout time.Duration) (time.Duration, time.Duration)
|
|
|
|
// MarkConnectionAsBlocking marks a connection as having blocking commands
|
|
MarkConnectionAsBlocking(conn interface{}, isBlocking bool)
|
|
|
|
// IsConnectionMarkedForClosing checks if a connection should be closed
|
|
IsConnectionMarkedForClosing(conn interface{}) bool
|
|
|
|
// ShouldRedirectBlockingConnection checks if a blocking connection should be redirected
|
|
ShouldRedirectBlockingConnection(conn interface{}) (bool, string)
|
|
|
|
// GetUpgradeStatistics returns current upgrade statistics
|
|
GetUpgradeStatistics() *HitlessUpgradeStatistics
|
|
|
|
// GetUpgradeStatus returns detailed upgrade status
|
|
GetUpgradeStatus() *HitlessUpgradeStatus
|
|
|
|
// UpdateConfig updates the hitless upgrade configuration
|
|
UpdateConfig(config *HitlessUpgradeConfig) error
|
|
|
|
// GetConfig returns the current configuration
|
|
GetConfig() *HitlessUpgradeConfig
|
|
|
|
// Close shuts down the hitless integration
|
|
Close() error
|
|
}
|
|
|
|
// hitlessIntegrationImpl implements the HitlessIntegration interface
|
|
type hitlessIntegrationImpl struct {
|
|
integration *hitless.RedisClientIntegration
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// newHitlessIntegration creates a new hitless integration instance
|
|
func newHitlessIntegration(config *HitlessUpgradeConfig) *hitlessIntegrationImpl {
|
|
if config == nil {
|
|
config = DefaultHitlessUpgradeConfig()
|
|
}
|
|
|
|
// Convert to internal config format
|
|
internalConfig := &hitless.HitlessUpgradeConfig{
|
|
Enabled: config.Enabled,
|
|
TransitionTimeout: config.TransitionTimeout,
|
|
CleanupInterval: config.CleanupInterval,
|
|
}
|
|
|
|
integration := hitless.NewRedisClientIntegration(internalConfig, 3*time.Second, 3*time.Second)
|
|
|
|
return &hitlessIntegrationImpl{
|
|
integration: integration,
|
|
}
|
|
}
|
|
|
|
// newHitlessIntegrationWithTimeouts creates a new hitless integration instance with timeout configuration
|
|
func newHitlessIntegrationWithTimeouts(config *HitlessUpgradeConfig, defaultReadTimeout, defaultWriteTimeout time.Duration) *hitlessIntegrationImpl {
|
|
// Start with defaults
|
|
defaults := DefaultHitlessUpgradeConfig()
|
|
|
|
// If config is nil, use all defaults
|
|
if config == nil {
|
|
config = defaults
|
|
}
|
|
|
|
// Ensure all fields are set with defaults if they are zero values
|
|
enabled := config.Enabled
|
|
transitionTimeout := config.TransitionTimeout
|
|
cleanupInterval := config.CleanupInterval
|
|
|
|
// Apply defaults for zero values
|
|
if transitionTimeout == 0 {
|
|
transitionTimeout = defaults.TransitionTimeout
|
|
}
|
|
if cleanupInterval == 0 {
|
|
cleanupInterval = defaults.CleanupInterval
|
|
}
|
|
|
|
// Convert to internal config format with all fields properly set
|
|
internalConfig := &hitless.HitlessUpgradeConfig{
|
|
Enabled: enabled,
|
|
TransitionTimeout: transitionTimeout,
|
|
CleanupInterval: cleanupInterval,
|
|
}
|
|
|
|
integration := hitless.NewRedisClientIntegration(internalConfig, defaultReadTimeout, defaultWriteTimeout)
|
|
|
|
return &hitlessIntegrationImpl{
|
|
integration: integration,
|
|
}
|
|
}
|
|
|
|
// IsEnabled returns whether hitless upgrades are currently enabled
|
|
func (h *hitlessIntegrationImpl) IsEnabled() bool {
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
return h.integration.IsEnabled()
|
|
}
|
|
|
|
// EnableHitlessUpgrades enables hitless upgrade functionality
|
|
func (h *hitlessIntegrationImpl) EnableHitlessUpgrades() {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
h.integration.EnableHitlessUpgrades()
|
|
}
|
|
|
|
// DisableHitlessUpgrades disables hitless upgrade functionality
|
|
func (h *hitlessIntegrationImpl) DisableHitlessUpgrades() {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
h.integration.DisableHitlessUpgrades()
|
|
}
|
|
|
|
// GetConnectionTimeout returns the appropriate timeout for a connection
|
|
func (h *hitlessIntegrationImpl) GetConnectionTimeout(conn interface{}, defaultTimeout time.Duration) time.Duration {
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
|
|
// Convert interface{} to *pool.Conn
|
|
if poolConn, ok := conn.(*pool.Conn); ok {
|
|
return h.integration.GetConnectionTimeout(poolConn, defaultTimeout)
|
|
}
|
|
|
|
// If not a pool connection, return default timeout
|
|
return defaultTimeout
|
|
}
|
|
|
|
// GetConnectionTimeouts returns both read and write timeouts for a connection
|
|
func (h *hitlessIntegrationImpl) GetConnectionTimeouts(conn interface{}, defaultReadTimeout, defaultWriteTimeout time.Duration) (time.Duration, time.Duration) {
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
|
|
// Convert interface{} to *pool.Conn
|
|
if poolConn, ok := conn.(*pool.Conn); ok {
|
|
return h.integration.GetConnectionTimeouts(poolConn, defaultReadTimeout, defaultWriteTimeout)
|
|
}
|
|
|
|
// If not a pool connection, return default timeouts
|
|
return defaultReadTimeout, defaultWriteTimeout
|
|
}
|
|
|
|
// MarkConnectionAsBlocking marks a connection as having blocking commands
|
|
func (h *hitlessIntegrationImpl) MarkConnectionAsBlocking(conn interface{}, isBlocking bool) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
// Convert interface{} to *pool.Conn
|
|
if poolConn, ok := conn.(*pool.Conn); ok {
|
|
h.integration.MarkConnectionAsBlocking(poolConn, isBlocking)
|
|
}
|
|
}
|
|
|
|
// IsConnectionMarkedForClosing checks if a connection should be closed
|
|
func (h *hitlessIntegrationImpl) IsConnectionMarkedForClosing(conn interface{}) bool {
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
|
|
// Convert interface{} to *pool.Conn
|
|
if poolConn, ok := conn.(*pool.Conn); ok {
|
|
return h.integration.IsConnectionMarkedForClosing(poolConn)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// ShouldRedirectBlockingConnection checks if a blocking connection should be redirected
|
|
func (h *hitlessIntegrationImpl) ShouldRedirectBlockingConnection(conn interface{}) (bool, string) {
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
|
|
// Convert interface{} to *pool.Conn (can be nil for checking pool state)
|
|
var poolConn *pool.Conn
|
|
if conn != nil {
|
|
if pc, ok := conn.(*pool.Conn); ok {
|
|
poolConn = pc
|
|
}
|
|
}
|
|
|
|
return h.integration.ShouldRedirectBlockingConnection(poolConn)
|
|
}
|
|
|
|
// GetUpgradeStatistics returns current upgrade statistics
|
|
func (h *hitlessIntegrationImpl) GetUpgradeStatistics() *HitlessUpgradeStatistics {
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
|
|
stats := h.integration.GetUpgradeStatistics()
|
|
if stats == nil {
|
|
return &HitlessUpgradeStatistics{Timestamp: time.Now()}
|
|
}
|
|
|
|
return &HitlessUpgradeStatistics{
|
|
ActiveConnections: stats.ActiveConnections,
|
|
IsMoving: stats.IsMoving,
|
|
MigratingConnections: stats.MigratingConnections,
|
|
FailingOverConnections: stats.FailingOverConnections,
|
|
Timestamp: stats.Timestamp,
|
|
}
|
|
}
|
|
|
|
// GetUpgradeStatus returns detailed upgrade status
|
|
func (h *hitlessIntegrationImpl) GetUpgradeStatus() *HitlessUpgradeStatus {
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
|
|
status := h.integration.GetUpgradeStatus()
|
|
if status == nil {
|
|
return &HitlessUpgradeStatus{
|
|
ConnectionStates: make(map[interface{}]interface{}),
|
|
IsMoving: false,
|
|
NewEndpoint: "",
|
|
Timestamp: time.Now(),
|
|
}
|
|
}
|
|
|
|
return &HitlessUpgradeStatus{
|
|
ConnectionStates: convertToInterfaceMap(status.ConnectionStates),
|
|
IsMoving: status.IsMoving,
|
|
NewEndpoint: status.NewEndpoint,
|
|
Timestamp: status.Timestamp,
|
|
}
|
|
}
|
|
|
|
// UpdateConfig updates the hitless upgrade configuration
|
|
func (h *hitlessIntegrationImpl) UpdateConfig(config *HitlessUpgradeConfig) error {
|
|
if config == nil {
|
|
return fmt.Errorf("config cannot be nil")
|
|
}
|
|
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
// Start with defaults for any zero values
|
|
defaults := DefaultHitlessUpgradeConfig()
|
|
|
|
// Ensure all fields are set with defaults if they are zero values
|
|
enabled := config.Enabled
|
|
transitionTimeout := config.TransitionTimeout
|
|
cleanupInterval := config.CleanupInterval
|
|
|
|
// Apply defaults for zero values
|
|
if transitionTimeout == 0 {
|
|
transitionTimeout = defaults.TransitionTimeout
|
|
}
|
|
if cleanupInterval == 0 {
|
|
cleanupInterval = defaults.CleanupInterval
|
|
}
|
|
|
|
// Convert to internal config format with all fields properly set
|
|
internalConfig := &hitless.HitlessUpgradeConfig{
|
|
Enabled: enabled,
|
|
TransitionTimeout: transitionTimeout,
|
|
CleanupInterval: cleanupInterval,
|
|
}
|
|
|
|
return h.integration.UpdateConfig(internalConfig)
|
|
}
|
|
|
|
// GetConfig returns the current configuration
|
|
func (h *hitlessIntegrationImpl) GetConfig() *HitlessUpgradeConfig {
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
|
|
internalConfig := h.integration.GetConfig()
|
|
if internalConfig == nil {
|
|
return DefaultHitlessUpgradeConfig()
|
|
}
|
|
|
|
return &HitlessUpgradeConfig{
|
|
Enabled: internalConfig.Enabled,
|
|
TransitionTimeout: internalConfig.TransitionTimeout,
|
|
CleanupInterval: internalConfig.CleanupInterval,
|
|
}
|
|
}
|
|
|
|
// Close shuts down the hitless integration
|
|
func (h *hitlessIntegrationImpl) Close() error {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
return h.integration.Close()
|
|
}
|
|
|
|
// getInternalIntegration returns the internal integration for use by Redis clients
|
|
func (h *hitlessIntegrationImpl) getInternalIntegration() *hitless.RedisClientIntegration {
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
return h.integration
|
|
}
|
|
|
|
// ClientTimeoutProvider interface for extracting timeout configuration from client options
|
|
type ClientTimeoutProvider interface {
|
|
GetReadTimeout() time.Duration
|
|
GetWriteTimeout() time.Duration
|
|
}
|
|
|
|
// optionsTimeoutProvider implements ClientTimeoutProvider for Options struct
|
|
type optionsTimeoutProvider struct {
|
|
readTimeout time.Duration
|
|
writeTimeout time.Duration
|
|
}
|
|
|
|
func (p *optionsTimeoutProvider) GetReadTimeout() time.Duration {
|
|
return p.readTimeout
|
|
}
|
|
|
|
func (p *optionsTimeoutProvider) GetWriteTimeout() time.Duration {
|
|
return p.writeTimeout
|
|
}
|
|
|
|
// newOptionsTimeoutProvider creates a timeout provider from Options
|
|
func newOptionsTimeoutProvider(readTimeout, writeTimeout time.Duration) ClientTimeoutProvider {
|
|
return &optionsTimeoutProvider{
|
|
readTimeout: readTimeout,
|
|
writeTimeout: writeTimeout,
|
|
}
|
|
}
|
|
|
|
// initializeHitlessIntegration initializes hitless integration for a client
|
|
func initializeHitlessIntegration(client interface{}, config *HitlessUpgradeConfig, timeoutProvider ClientTimeoutProvider) (*hitlessIntegrationImpl, error) {
|
|
if config == nil || !config.Enabled {
|
|
return nil, nil
|
|
}
|
|
|
|
// Extract timeout configuration from client options
|
|
defaultReadTimeout := timeoutProvider.GetReadTimeout()
|
|
defaultWriteTimeout := timeoutProvider.GetWriteTimeout()
|
|
|
|
// Create hitless integration - each client gets its own instance
|
|
integration := newHitlessIntegrationWithTimeouts(config, defaultReadTimeout, defaultWriteTimeout)
|
|
|
|
// Push notification handlers are registered directly by the client
|
|
// No separate registration needed in simplified implementation
|
|
|
|
internal.Logger.Printf(context.Background(), "hitless: initialized hitless upgrades for client")
|
|
|
|
return integration, nil
|
|
}
|
|
|
|
// convertToInterfaceMap converts a typed map to interface{} map for public API
|
|
func convertToInterfaceMap(input map[*pool.Conn]*hitless.ConnectionState) map[interface{}]interface{} {
|
|
result := make(map[interface{}]interface{})
|
|
for k, v := range input {
|
|
result[k] = v
|
|
}
|
|
return result
|
|
}
|