Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

321 rader
10 KiB

  1. package maintnotifications
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "net"
  7. "sync"
  8. "sync/atomic"
  9. "time"
  10. "github.com/redis/go-redis/v9/internal"
  11. "github.com/redis/go-redis/v9/internal/interfaces"
  12. "github.com/redis/go-redis/v9/internal/maintnotifications/logs"
  13. "github.com/redis/go-redis/v9/internal/pool"
  14. "github.com/redis/go-redis/v9/push"
  15. )
  16. // Push notification type constants for maintenance
  17. const (
  18. NotificationMoving = "MOVING"
  19. NotificationMigrating = "MIGRATING"
  20. NotificationMigrated = "MIGRATED"
  21. NotificationFailingOver = "FAILING_OVER"
  22. NotificationFailedOver = "FAILED_OVER"
  23. )
  24. // maintenanceNotificationTypes contains all notification types that maintenance handles
  25. var maintenanceNotificationTypes = []string{
  26. NotificationMoving,
  27. NotificationMigrating,
  28. NotificationMigrated,
  29. NotificationFailingOver,
  30. NotificationFailedOver,
  31. }
  32. // NotificationHook is called before and after notification processing
  33. // PreHook can modify the notification and return false to skip processing
  34. // PostHook is called after successful processing
  35. type NotificationHook interface {
  36. PreHook(ctx context.Context, notificationCtx push.NotificationHandlerContext, notificationType string, notification []interface{}) ([]interface{}, bool)
  37. PostHook(ctx context.Context, notificationCtx push.NotificationHandlerContext, notificationType string, notification []interface{}, result error)
  38. }
  39. // MovingOperationKey provides a unique key for tracking MOVING operations
  40. // that combines sequence ID with connection identifier to handle duplicate
  41. // sequence IDs across multiple connections to the same node.
  42. type MovingOperationKey struct {
  43. SeqID int64 // Sequence ID from MOVING notification
  44. ConnID uint64 // Unique connection identifier
  45. }
  46. // String returns a string representation of the key for debugging
  47. func (k MovingOperationKey) String() string {
  48. return fmt.Sprintf("seq:%d-conn:%d", k.SeqID, k.ConnID)
  49. }
  50. // Manager provides a simplified upgrade functionality with hooks and atomic state.
  51. type Manager struct {
  52. client interfaces.ClientInterface
  53. config *Config
  54. options interfaces.OptionsInterface
  55. pool pool.Pooler
  56. // MOVING operation tracking - using sync.Map for better concurrent performance
  57. activeMovingOps sync.Map // map[MovingOperationKey]*MovingOperation
  58. // Atomic state tracking - no locks needed for state queries
  59. activeOperationCount atomic.Int64 // Number of active operations
  60. closed atomic.Bool // Manager closed state
  61. // Notification hooks for extensibility
  62. hooks []NotificationHook
  63. hooksMu sync.RWMutex // Protects hooks slice
  64. poolHooksRef *PoolHook
  65. }
  66. // MovingOperation tracks an active MOVING operation.
  67. type MovingOperation struct {
  68. SeqID int64
  69. NewEndpoint string
  70. StartTime time.Time
  71. Deadline time.Time
  72. }
  73. // NewManager creates a new simplified manager.
  74. func NewManager(client interfaces.ClientInterface, pool pool.Pooler, config *Config) (*Manager, error) {
  75. if client == nil {
  76. return nil, ErrInvalidClient
  77. }
  78. hm := &Manager{
  79. client: client,
  80. pool: pool,
  81. options: client.GetOptions(),
  82. config: config.Clone(),
  83. hooks: make([]NotificationHook, 0),
  84. }
  85. // Set up push notification handling
  86. if err := hm.setupPushNotifications(); err != nil {
  87. return nil, err
  88. }
  89. return hm, nil
  90. }
  91. // GetPoolHook creates a pool hook with a custom dialer.
  92. func (hm *Manager) InitPoolHook(baseDialer func(context.Context, string, string) (net.Conn, error)) {
  93. poolHook := hm.createPoolHook(baseDialer)
  94. hm.pool.AddPoolHook(poolHook)
  95. }
  96. // setupPushNotifications sets up push notification handling by registering with the client's processor.
  97. func (hm *Manager) setupPushNotifications() error {
  98. processor := hm.client.GetPushProcessor()
  99. if processor == nil {
  100. return ErrInvalidClient // Client doesn't support push notifications
  101. }
  102. // Create our notification handler
  103. handler := &NotificationHandler{manager: hm, operationsManager: hm}
  104. // Register handlers for all upgrade notifications with the client's processor
  105. for _, notificationType := range maintenanceNotificationTypes {
  106. if err := processor.RegisterHandler(notificationType, handler, true); err != nil {
  107. return errors.New(logs.FailedToRegisterHandler(notificationType, err))
  108. }
  109. }
  110. return nil
  111. }
  112. // TrackMovingOperationWithConnID starts a new MOVING operation with a specific connection ID.
  113. func (hm *Manager) TrackMovingOperationWithConnID(ctx context.Context, newEndpoint string, deadline time.Time, seqID int64, connID uint64) error {
  114. // Create composite key
  115. key := MovingOperationKey{
  116. SeqID: seqID,
  117. ConnID: connID,
  118. }
  119. // Create MOVING operation record
  120. movingOp := &MovingOperation{
  121. SeqID: seqID,
  122. NewEndpoint: newEndpoint,
  123. StartTime: time.Now(),
  124. Deadline: deadline,
  125. }
  126. // Use LoadOrStore for atomic check-and-set operation
  127. if _, loaded := hm.activeMovingOps.LoadOrStore(key, movingOp); loaded {
  128. // Duplicate MOVING notification, ignore
  129. if internal.LogLevel.DebugOrAbove() { // Debug level
  130. internal.Logger.Printf(context.Background(), logs.DuplicateMovingOperation(connID, newEndpoint, seqID))
  131. }
  132. return nil
  133. }
  134. if internal.LogLevel.DebugOrAbove() { // Debug level
  135. internal.Logger.Printf(context.Background(), logs.TrackingMovingOperation(connID, newEndpoint, seqID))
  136. }
  137. // Increment active operation count atomically
  138. hm.activeOperationCount.Add(1)
  139. return nil
  140. }
  141. // UntrackOperationWithConnID completes a MOVING operation with a specific connection ID.
  142. func (hm *Manager) UntrackOperationWithConnID(seqID int64, connID uint64) {
  143. // Create composite key
  144. key := MovingOperationKey{
  145. SeqID: seqID,
  146. ConnID: connID,
  147. }
  148. // Remove from active operations atomically
  149. if _, loaded := hm.activeMovingOps.LoadAndDelete(key); loaded {
  150. if internal.LogLevel.DebugOrAbove() { // Debug level
  151. internal.Logger.Printf(context.Background(), logs.UntrackingMovingOperation(connID, seqID))
  152. }
  153. // Decrement active operation count only if operation existed
  154. hm.activeOperationCount.Add(-1)
  155. } else {
  156. if internal.LogLevel.DebugOrAbove() { // Debug level
  157. internal.Logger.Printf(context.Background(), logs.OperationNotTracked(connID, seqID))
  158. }
  159. }
  160. }
  161. // GetActiveMovingOperations returns active operations with composite keys.
  162. // WARNING: This method creates a new map and copies all operations on every call.
  163. // Use sparingly, especially in hot paths or high-frequency logging.
  164. func (hm *Manager) GetActiveMovingOperations() map[MovingOperationKey]*MovingOperation {
  165. result := make(map[MovingOperationKey]*MovingOperation)
  166. // Iterate over sync.Map to build result
  167. hm.activeMovingOps.Range(func(key, value interface{}) bool {
  168. k := key.(MovingOperationKey)
  169. op := value.(*MovingOperation)
  170. // Create a copy to avoid sharing references
  171. result[k] = &MovingOperation{
  172. SeqID: op.SeqID,
  173. NewEndpoint: op.NewEndpoint,
  174. StartTime: op.StartTime,
  175. Deadline: op.Deadline,
  176. }
  177. return true // Continue iteration
  178. })
  179. return result
  180. }
  181. // IsHandoffInProgress returns true if any handoff is in progress.
  182. // Uses atomic counter for lock-free operation.
  183. func (hm *Manager) IsHandoffInProgress() bool {
  184. return hm.activeOperationCount.Load() > 0
  185. }
  186. // GetActiveOperationCount returns the number of active operations.
  187. // Uses atomic counter for lock-free operation.
  188. func (hm *Manager) GetActiveOperationCount() int64 {
  189. return hm.activeOperationCount.Load()
  190. }
  191. // Close closes the manager.
  192. func (hm *Manager) Close() error {
  193. // Use atomic operation for thread-safe close check
  194. if !hm.closed.CompareAndSwap(false, true) {
  195. return nil // Already closed
  196. }
  197. // Shutdown the pool hook if it exists
  198. if hm.poolHooksRef != nil {
  199. // Use a timeout to prevent hanging indefinitely
  200. shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  201. defer cancel()
  202. err := hm.poolHooksRef.Shutdown(shutdownCtx)
  203. if err != nil {
  204. // was not able to close pool hook, keep closed state false
  205. hm.closed.Store(false)
  206. return err
  207. }
  208. // Remove the pool hook from the pool
  209. if hm.pool != nil {
  210. hm.pool.RemovePoolHook(hm.poolHooksRef)
  211. }
  212. }
  213. // Clear all active operations
  214. hm.activeMovingOps.Range(func(key, value interface{}) bool {
  215. hm.activeMovingOps.Delete(key)
  216. return true
  217. })
  218. // Reset counter
  219. hm.activeOperationCount.Store(0)
  220. return nil
  221. }
  222. // GetState returns current state using atomic counter for lock-free operation.
  223. func (hm *Manager) GetState() State {
  224. if hm.activeOperationCount.Load() > 0 {
  225. return StateMoving
  226. }
  227. return StateIdle
  228. }
  229. // processPreHooks calls all pre-hooks and returns the modified notification and whether to continue processing.
  230. func (hm *Manager) processPreHooks(ctx context.Context, notificationCtx push.NotificationHandlerContext, notificationType string, notification []interface{}) ([]interface{}, bool) {
  231. hm.hooksMu.RLock()
  232. defer hm.hooksMu.RUnlock()
  233. currentNotification := notification
  234. for _, hook := range hm.hooks {
  235. modifiedNotification, shouldContinue := hook.PreHook(ctx, notificationCtx, notificationType, currentNotification)
  236. if !shouldContinue {
  237. return modifiedNotification, false
  238. }
  239. currentNotification = modifiedNotification
  240. }
  241. return currentNotification, true
  242. }
  243. // processPostHooks calls all post-hooks with the processing result.
  244. func (hm *Manager) processPostHooks(ctx context.Context, notificationCtx push.NotificationHandlerContext, notificationType string, notification []interface{}, result error) {
  245. hm.hooksMu.RLock()
  246. defer hm.hooksMu.RUnlock()
  247. for _, hook := range hm.hooks {
  248. hook.PostHook(ctx, notificationCtx, notificationType, notification, result)
  249. }
  250. }
  251. // createPoolHook creates a pool hook with this manager already set.
  252. func (hm *Manager) createPoolHook(baseDialer func(context.Context, string, string) (net.Conn, error)) *PoolHook {
  253. if hm.poolHooksRef != nil {
  254. return hm.poolHooksRef
  255. }
  256. // Get pool size from client options for better worker defaults
  257. poolSize := 0
  258. if hm.options != nil {
  259. poolSize = hm.options.GetPoolSize()
  260. }
  261. hm.poolHooksRef = NewPoolHookWithPoolSize(baseDialer, hm.options.GetNetwork(), hm.config, hm, poolSize)
  262. hm.poolHooksRef.SetPool(hm.pool)
  263. return hm.poolHooksRef
  264. }
  265. func (hm *Manager) AddNotificationHook(notificationHook NotificationHook) {
  266. hm.hooksMu.Lock()
  267. defer hm.hooksMu.Unlock()
  268. hm.hooks = append(hm.hooks, notificationHook)
  269. }