1
0
mirror of https://github.com/redis/go-redis.git synced 2025-07-19 11:43:14 +03:00

refactor: move all push notification logic to root package and remove adapters

Consolidate all push notification handling logic in the root package to eliminate
adapters and simplify the architecture. This provides direct access to concrete
types without any intermediate layers or type conversions.

Key Changes:

1. Moved Core Types to Root Package:
   - Moved Registry, Processor, VoidProcessor to push_notifications.go
   - Moved all push notification constants to root package
   - Removed internal/pushnotif package dependencies
   - Direct implementation without internal abstractions

2. Eliminated All Adapters:
   - Removed handlerAdapter that bridged internal and public interfaces
   - Removed voidProcessorAdapter for void processor functionality
   - Removed convertInternalToPublicContext conversion functions
   - Direct usage of concrete types throughout

3. Simplified Architecture:
   - PushNotificationHandlerContext directly implemented in root package
   - PushNotificationHandler directly implemented in root package
   - Registry, Processor, VoidProcessor directly in root package
   - No intermediate layers or type conversions needed

4. Direct Type Usage:
   - GetClusterClient() returns *ClusterClient directly
   - GetSentinelClient() returns *SentinelClient directly
   - GetRegularClient() returns *Client directly
   - GetPubSub() returns *PubSub directly
   - No interface casting or type assertions required

5. Updated All Integration Points:
   - Updated redis.go to use direct types
   - Updated pubsub.go to use direct types
   - Updated sentinel.go to use direct types
   - Removed all internal/pushnotif imports
   - Simplified context creation and usage

6. Core Implementation in Root Package:
   ```go
   // Direct implementation - no adapters needed
   type Registry struct {
       handlers  map[string]PushNotificationHandler
       protected map[string]bool
   }

   type Processor struct {
       registry *Registry
   }

   type VoidProcessor struct{}
   ```

7. Handler Context with Concrete Types:
   ```go
   type PushNotificationHandlerContext interface {
       GetClusterClient() *ClusterClient    // Direct concrete type
       GetSentinelClient() *SentinelClient  // Direct concrete type
       GetRegularClient() *Client           // Direct concrete type
       GetPubSub() *PubSub                  // Direct concrete type
   }
   ```

8. Comprehensive Test Suite:
   - Added push_notifications_test.go with full test coverage
   - Tests for Registry, Processor, VoidProcessor
   - Tests for HandlerContext with concrete type access
   - Tests for all push notification constants
   - Validates all functionality works correctly

9. Benefits:
   - Eliminated complex adapter pattern
   - Removed unnecessary type conversions
   - Simplified codebase with direct type usage
   - Better performance without adapter overhead
   - Cleaner architecture with single source of truth
   - Enhanced developer experience with direct access

10. Architecture Simplification:
    Before: Client -> Adapter -> Internal -> Adapter -> Handler
    After:  Client -> Handler (direct)

    No more:
    - handlerAdapter bridging interfaces
    - voidProcessorAdapter for void functionality
    - convertInternalToPublicContext conversions
    - Complex type mapping between layers

This refactoring provides a much cleaner, simpler architecture where all
push notification logic lives in the root package with direct access to
concrete Redis client types, eliminating unnecessary complexity while
maintaining full functionality and type safety.
This commit is contained in:
Nedyalko Dyakov
2025-07-04 21:13:47 +03:00
parent d530d45b9b
commit 5972b4c23f
11 changed files with 512 additions and 345 deletions

View File

@ -0,0 +1,187 @@
package pushprocessor
import (
"context"
"fmt"
"github.com/redis/go-redis/v9/internal/proto"
)
// Processor handles push notifications with a registry of handlers.
type Processor struct {
registry *Registry
}
// NewProcessor creates a new push notification processor.
func NewProcessor() *Processor {
return &Processor{
registry: NewRegistry(),
}
}
// GetHandler returns the handler for a specific push notification name.
// Returns nil if no handler is registered for the given name.
func (p *Processor) GetHandler(pushNotificationName string) Handler {
return p.registry.GetHandler(pushNotificationName)
}
// RegisterHandler registers a handler for a specific push notification name.
// Returns an error if a handler is already registered for this push notification name.
// If protected is true, the handler cannot be unregistered.
func (p *Processor) RegisterHandler(pushNotificationName string, handler Handler, protected bool) error {
return p.registry.RegisterHandler(pushNotificationName, handler, protected)
}
// UnregisterHandler removes a handler for a specific push notification name.
// Returns an error if the handler is protected or doesn't exist.
func (p *Processor) UnregisterHandler(pushNotificationName string) error {
return p.registry.UnregisterHandler(pushNotificationName)
}
// ProcessPendingNotifications checks for and processes any pending push notifications.
// The handlerCtx provides context about the client, connection pool, and connection.
func (p *Processor) ProcessPendingNotifications(ctx context.Context, handlerCtx HandlerContext, rd *proto.Reader) error {
// Check for nil reader
if rd == nil {
return nil
}
// Check if there are any buffered bytes that might contain push notifications
if rd.Buffered() == 0 {
return nil
}
// Process all available push notifications
for {
// Peek at the next reply type to see if it's a push notification
replyType, err := rd.PeekReplyType()
if err != nil {
// No more data available or error reading
break
}
// Push notifications use RespPush type in RESP3
if replyType != proto.RespPush {
break
}
notificationName, err := rd.PeekPushNotificationName()
if err != nil {
// Error reading - continue to next iteration
break
}
// Skip notifications that should be handled by other systems
if shouldSkipNotification(notificationName) {
break
}
// Try to read the push notification
reply, err := rd.ReadReply()
if err != nil {
return fmt.Errorf("failed to read push notification: %w", err)
}
// Convert to slice of interfaces
notification, ok := reply.([]interface{})
if !ok {
continue
}
// Handle the notification directly
if len(notification) > 0 {
// Extract the notification type (first element)
if notificationType, ok := notification[0].(string); ok {
// Skip notifications that should be handled by other systems
if shouldSkipNotification(notificationType) {
continue
}
// Get the handler for this notification type
if handler := p.registry.GetHandler(notificationType); handler != nil {
// Handle the notification with context
handler.HandlePushNotification(ctx, handlerCtx, notification)
}
}
}
}
return nil
}
// shouldSkipNotification checks if a notification type should be ignored by the push notification
// processor and handled by other specialized systems instead (pub/sub, streams, keyspace, etc.).
func shouldSkipNotification(notificationType string) bool {
switch notificationType {
// Pub/Sub notifications - handled by pub/sub system
case "message", // Regular pub/sub message
"pmessage", // Pattern pub/sub message
"subscribe", // Subscription confirmation
"unsubscribe", // Unsubscription confirmation
"psubscribe", // Pattern subscription confirmation
"punsubscribe", // Pattern unsubscription confirmation
"smessage", // Sharded pub/sub message (Redis 7.0+)
"ssubscribe", // Sharded subscription confirmation
"sunsubscribe", // Sharded unsubscription confirmation
// Stream notifications - handled by stream consumers
"xread-from", // Stream reading notifications
"xreadgroup-from", // Stream consumer group notifications
// Client tracking notifications - handled by client tracking system
"invalidate", // Client-side caching invalidation
// Keyspace notifications - handled by keyspace notification subscribers
// Note: Keyspace notifications typically have prefixes like "__keyspace@0__:" or "__keyevent@0__:"
// but we'll handle the base notification types here
"expired", // Key expiration events
"evicted", // Key eviction events
"set", // Key set events
"del", // Key deletion events
"rename", // Key rename events
"move", // Key move events
"copy", // Key copy events
"restore", // Key restore events
"sort", // Sort operation events
"flushdb", // Database flush events
"flushall": // All databases flush events
return true
default:
return false
}
}
// VoidProcessor discards all push notifications without processing them.
type VoidProcessor struct{}
// NewVoidProcessor creates a new void push notification processor.
func NewVoidProcessor() *VoidProcessor {
return &VoidProcessor{}
}
// GetHandler returns nil for void processor since it doesn't maintain handlers.
func (v *VoidProcessor) GetHandler(pushNotificationName string) Handler {
return nil
}
// RegisterHandler returns an error for void processor since it doesn't maintain handlers.
// This helps developers identify when they're trying to register handlers on disabled push notifications.
func (v *VoidProcessor) RegisterHandler(pushNotificationName string, handler Handler, protected bool) error {
return fmt.Errorf("cannot register push notification handler '%s': push notifications are disabled (using void processor)", pushNotificationName)
}
// UnregisterHandler returns an error for void processor since it doesn't maintain handlers.
// This helps developers identify when they're trying to unregister handlers on disabled push notifications.
func (v *VoidProcessor) UnregisterHandler(pushNotificationName string) error {
return fmt.Errorf("cannot unregister push notification handler '%s': push notifications are disabled (using void processor)", pushNotificationName)
}
// ProcessPendingNotifications for VoidProcessor does nothing since push notifications
// are only available in RESP3 and this processor is used for RESP2 connections.
// This avoids unnecessary buffer scanning overhead.
func (v *VoidProcessor) ProcessPendingNotifications(ctx context.Context, handlerCtx HandlerContext, rd *proto.Reader) error {
// VoidProcessor is used for RESP2 connections where push notifications are not available.
// Since push notifications only exist in RESP3, we can safely skip all processing
// to avoid unnecessary buffer scanning overhead.
return nil
}