Skip to content

Commit 47dd490

Browse files
committed
feat: enhance push notification handlers with context information
1 parent c44c8b5 commit 47dd490

File tree

11 files changed

+242
-128
lines changed

11 files changed

+242
-128
lines changed

internal/pool/conn.go

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"sync/atomic"
88
"time"
99

10-
"github.com/redis/go-redis/v9/internal"
1110
"github.com/redis/go-redis/v9/internal/proto"
1211
"github.com/redis/go-redis/v9/internal/pushnotif"
1312
)
@@ -78,16 +77,8 @@ func (cn *Conn) RemoteAddr() net.Addr {
7877
func (cn *Conn) WithReader(
7978
ctx context.Context, timeout time.Duration, fn func(rd *proto.Reader) error,
8079
) error {
81-
// Process any pending push notifications before executing the read function
82-
// This ensures push notifications are handled as soon as they arrive
83-
if cn.PushNotificationProcessor != nil {
84-
// Type assert to the processor interface
85-
if err := cn.PushNotificationProcessor.ProcessPendingNotifications(ctx, cn.rd); err != nil {
86-
// Log the error but don't fail the read operation
87-
// Push notification processing errors shouldn't break normal Redis operations
88-
internal.Logger.Printf(ctx, "push: error processing pending notifications in WithReader: %v", err)
89-
}
90-
}
80+
// Push notification processing is now handled by the client before calling WithReader
81+
// This ensures proper context (client, connection pool, connection) is available to handlers
9182

9283
if timeout >= 0 {
9384
if err := cn.netConn.SetReadDeadline(cn.deadline(ctx, timeout)); err != nil {

internal/pool/pool.go

Lines changed: 20 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"time"
1010

1111
"github.com/redis/go-redis/v9/internal"
12+
"github.com/redis/go-redis/v9/internal/proto"
1213
"github.com/redis/go-redis/v9/internal/pushnotif"
1314
)
1415

@@ -237,11 +238,6 @@ func (p *ConnPool) dialConn(ctx context.Context, pooled bool) (*Conn, error) {
237238
cn := NewConn(netConn)
238239
cn.pooled = pooled
239240

240-
// Set push notification processor if available
241-
if p.cfg.PushNotificationProcessor != nil {
242-
cn.PushNotificationProcessor = p.cfg.PushNotificationProcessor
243-
}
244-
245241
return cn, nil
246242
}
247243

@@ -392,23 +388,18 @@ func (p *ConnPool) popIdle() (*Conn, error) {
392388
func (p *ConnPool) Put(ctx context.Context, cn *Conn) {
393389
if cn.rd.Buffered() > 0 {
394390
// Check if this might be push notification data
395-
if cn.PushNotificationProcessor != nil && p.cfg.Protocol == 3 {
396-
// Only process for RESP3 clients (push notifications only available in RESP3)
397-
err := cn.PushNotificationProcessor.ProcessPendingNotifications(ctx, cn.rd)
398-
if err != nil {
399-
internal.Logger.Printf(ctx, "push: error processing pending notifications: %v", err)
400-
}
401-
// Check again if there's still unread data after processing push notifications
402-
if cn.rd.Buffered() > 0 {
403-
internal.Logger.Printf(ctx, "Conn has unread data after processing push notifications")
404-
p.Remove(ctx, cn, BadConnError{})
391+
if p.cfg.Protocol == 3 {
392+
if replyType, err := cn.rd.PeekReplyType(); err == nil && replyType == proto.RespPush {
393+
// For push notifications, we allow some buffered data
394+
// The client will process these notifications before using the connection
395+
internal.Logger.Printf(ctx, "push: connection has buffered data, likely push notifications - will be processed by client")
405396
return
406397
}
407-
} else {
408-
internal.Logger.Printf(ctx, "Conn has unread data")
409-
p.Remove(ctx, cn, BadConnError{})
410-
return
411398
}
399+
// For non-RESP3 or data that is not a push notification, buffered data is unexpected
400+
internal.Logger.Printf(ctx, "Conn has unread data")
401+
p.Remove(ctx, cn, BadConnError{})
402+
return
412403
}
413404

414405
if !cn.pooled {
@@ -554,19 +545,17 @@ func (p *ConnPool) isHealthyConn(cn *Conn) bool {
554545

555546
// Check connection health, but be aware of push notifications
556547
if err := connCheck(cn.netConn); err != nil {
557-
// If there's unexpected data and we have push notification support,
558-
// it might be push notifications (only for RESP3)
559-
if err == errUnexpectedRead && cn.PushNotificationProcessor != nil && p.cfg.Protocol == 3 {
560-
// Try to process any pending push notifications (only for RESP3)
561-
ctx := context.Background()
562-
if procErr := cn.PushNotificationProcessor.ProcessPendingNotifications(ctx, cn.rd); procErr != nil {
563-
internal.Logger.Printf(ctx, "push: error processing pending notifications during health check: %v", procErr)
564-
return false
565-
}
566-
// Check again after processing push notifications
567-
if connCheck(cn.netConn) != nil {
568-
return false
548+
// If there's unexpected data, it might be push notifications (RESP3)
549+
// However, push notification processing is now handled by the client
550+
// before WithReader to ensure proper context is available to handlers
551+
if err == errUnexpectedRead && p.cfg.Protocol == 3 {
552+
if replyType, err := cn.rd.PeekReplyType(); err == nil && replyType == proto.RespPush {
553+
// For RESP3 connections with push notifications, we allow some buffered data
554+
// The client will process these notifications before using the connection
555+
internal.Logger.Printf(context.Background(), "push: connection has buffered data, likely push notifications - will be processed by client")
556+
return true // Connection is healthy, client will handle notifications
569557
}
558+
return false // Unexpected data, not push notifications, connection is unhealthy
570559
} else {
571560
return false
572561
}

internal/pushnotif/processor.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ func (p *Processor) UnregisterHandler(pushNotificationName string) error {
3939
}
4040

4141
// ProcessPendingNotifications checks for and processes any pending push notifications.
42-
func (p *Processor) ProcessPendingNotifications(ctx context.Context, rd *proto.Reader) error {
42+
// The handlerCtx provides context about the client, connection pool, and connection.
43+
func (p *Processor) ProcessPendingNotifications(ctx context.Context, handlerCtx *HandlerContext, rd *proto.Reader) error {
4344
// Check for nil reader
4445
if rd == nil {
4546
return nil
@@ -98,8 +99,8 @@ func (p *Processor) ProcessPendingNotifications(ctx context.Context, rd *proto.R
9899

99100
// Get the handler for this notification type
100101
if handler := p.registry.GetHandler(notificationType); handler != nil {
101-
// Handle the notification
102-
handler.HandlePushNotification(ctx, notification)
102+
// Handle the notification with context
103+
handler.HandlePushNotification(ctx, handlerCtx, notification)
103104
}
104105
}
105106
}
@@ -176,10 +177,10 @@ func (v *VoidProcessor) UnregisterHandler(pushNotificationName string) error {
176177
}
177178

178179
// ProcessPendingNotifications for VoidProcessor does nothing since push notifications
179-
// are only available in RESP3 and this processor is used when they're disabled.
180+
// are only available in RESP3 and this processor is used for RESP2 connections.
180181
// This avoids unnecessary buffer scanning overhead.
181-
func (v *VoidProcessor) ProcessPendingNotifications(ctx context.Context, rd *proto.Reader) error {
182-
// VoidProcessor is used when push notifications are disabled (typically RESP2 or disabled RESP3).
182+
func (v *VoidProcessor) ProcessPendingNotifications(ctx context.Context, handlerCtx *HandlerContext, rd *proto.Reader) error {
183+
// VoidProcessor is used for RESP2 connections where push notifications are not available.
183184
// Since push notifications only exist in RESP3, we can safely skip all processing
184185
// to avoid unnecessary buffer scanning overhead.
185186
return nil

internal/pushnotif/pushnotif_test.go

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ func NewTestHandler(name string, returnValue bool) *TestHandler {
2525
}
2626
}
2727

28-
func (h *TestHandler) HandlePushNotification(ctx context.Context, notification []interface{}) bool {
28+
func (h *TestHandler) HandlePushNotification(ctx context.Context, handlerCtx *HandlerContext, notification []interface{}) bool {
2929
h.handled = append(h.handled, notification)
30+
// Store the handler context for testing if needed
31+
_ = handlerCtx
3032
return h.returnValue
3133
}
3234

@@ -131,6 +133,13 @@ func testProcessPendingNotifications(processor *Processor, ctx context.Context,
131133
return nil
132134
}
133135

136+
// Create a test handler context
137+
handlerCtx := &HandlerContext{
138+
Client: nil,
139+
ConnPool: nil,
140+
Conn: nil,
141+
}
142+
134143
for {
135144
// Check if there are push notifications available
136145
replyType, err := reader.PeekReplyType()
@@ -175,8 +184,8 @@ func testProcessPendingNotifications(processor *Processor, ctx context.Context,
175184
if notificationType, ok := notification[0].(string); ok {
176185
// Get the handler for this notification type
177186
if handler := processor.registry.GetHandler(notificationType); handler != nil {
178-
// Handle the notification
179-
handler.HandlePushNotification(ctx, notification)
187+
// Handle the notification with context
188+
handler.HandlePushNotification(ctx, handlerCtx, notification)
180189
}
181190
}
182191
}
@@ -420,14 +429,19 @@ func TestProcessor(t *testing.T) {
420429
ctx := context.Background()
421430

422431
// Test with nil reader
423-
err := processor.ProcessPendingNotifications(ctx, nil)
432+
handlerCtx := &HandlerContext{
433+
Client: nil,
434+
ConnPool: nil,
435+
Conn: nil,
436+
}
437+
err := processor.ProcessPendingNotifications(ctx, handlerCtx, nil)
424438
if err != nil {
425439
t.Errorf("ProcessPendingNotifications with nil reader should not error, got: %v", err)
426440
}
427441

428442
// Test with empty reader (no buffered data)
429443
reader := proto.NewReader(strings.NewReader(""))
430-
err = processor.ProcessPendingNotifications(ctx, reader)
444+
err = processor.ProcessPendingNotifications(ctx, handlerCtx, reader)
431445
if err != nil {
432446
t.Errorf("ProcessPendingNotifications with empty reader should not error, got: %v", err)
433447
}
@@ -533,21 +547,21 @@ func TestProcessor(t *testing.T) {
533547

534548
// Test the actual ProcessPendingNotifications method with real proto.Reader
535549
// Test with nil reader
536-
err = processor.ProcessPendingNotifications(ctx, nil)
550+
err = processor.ProcessPendingNotifications(ctx, handlerCtx, nil)
537551
if err != nil {
538552
t.Errorf("ProcessPendingNotifications with nil reader should not error, got: %v", err)
539553
}
540554

541555
// Test with empty reader (no buffered data)
542556
protoReader := proto.NewReader(strings.NewReader(""))
543-
err = processor.ProcessPendingNotifications(ctx, protoReader)
557+
err = processor.ProcessPendingNotifications(ctx, handlerCtx, protoReader)
544558
if err != nil {
545559
t.Errorf("ProcessPendingNotifications with empty reader should not error, got: %v", err)
546560
}
547561

548562
// Test with reader that has some data but not push notifications
549563
protoReader = proto.NewReader(strings.NewReader("+OK\r\n"))
550-
err = processor.ProcessPendingNotifications(ctx, protoReader)
564+
err = processor.ProcessPendingNotifications(ctx, handlerCtx, protoReader)
551565
if err != nil {
552566
t.Errorf("ProcessPendingNotifications with non-push data should not error, got: %v", err)
553567
}
@@ -637,22 +651,27 @@ func TestVoidProcessor(t *testing.T) {
637651
t.Run("ProcessPendingNotifications", func(t *testing.T) {
638652
processor := NewVoidProcessor()
639653
ctx := context.Background()
654+
handlerCtx := &HandlerContext{
655+
Client: nil,
656+
ConnPool: nil,
657+
Conn: nil,
658+
}
640659

641660
// VoidProcessor should always succeed and do nothing
642-
err := processor.ProcessPendingNotifications(ctx, nil)
661+
err := processor.ProcessPendingNotifications(ctx, handlerCtx, nil)
643662
if err != nil {
644663
t.Errorf("VoidProcessor ProcessPendingNotifications should never error, got: %v", err)
645664
}
646665

647666
// Test with various readers
648667
reader := proto.NewReader(strings.NewReader(""))
649-
err = processor.ProcessPendingNotifications(ctx, reader)
668+
err = processor.ProcessPendingNotifications(ctx, handlerCtx, reader)
650669
if err != nil {
651670
t.Errorf("VoidProcessor ProcessPendingNotifications should never error, got: %v", err)
652671
}
653672

654673
reader = proto.NewReader(strings.NewReader("some data"))
655-
err = processor.ProcessPendingNotifications(ctx, reader)
674+
err = processor.ProcessPendingNotifications(ctx, handlerCtx, reader)
656675
if err != nil {
657676
t.Errorf("VoidProcessor ProcessPendingNotifications should never error, got: %v", err)
658677
}

internal/pushnotif/types.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,32 @@ import (
66
"github.com/redis/go-redis/v9/internal/proto"
77
)
88

9+
// HandlerContext provides context information about where a push notification was received.
10+
// This allows handlers to make informed decisions based on the source of the notification.
11+
type HandlerContext struct {
12+
// Client is the Redis client instance that received the notification
13+
Client interface{}
14+
15+
// ConnPool is the connection pool from which the connection was obtained
16+
ConnPool interface{}
17+
18+
// Conn is the specific connection on which the notification was received
19+
Conn interface{}
20+
}
21+
922
// Handler defines the interface for push notification handlers.
1023
type Handler interface {
11-
// HandlePushNotification processes a push notification.
24+
// HandlePushNotification processes a push notification with context information.
25+
// The handlerCtx provides information about the client, connection pool, and connection
26+
// on which the notification was received, allowing handlers to make informed decisions.
1227
// Returns true if the notification was handled, false otherwise.
13-
HandlePushNotification(ctx context.Context, notification []interface{}) bool
28+
HandlePushNotification(ctx context.Context, handlerCtx *HandlerContext, notification []interface{}) bool
1429
}
1530

1631
// ProcessorInterface defines the interface for push notification processors.
1732
type ProcessorInterface interface {
1833
GetHandler(pushNotificationName string) Handler
19-
ProcessPendingNotifications(ctx context.Context, rd *proto.Reader) error
34+
ProcessPendingNotifications(ctx context.Context, handlerCtx *HandlerContext, rd *proto.Reader) error
2035
RegisterHandler(pushNotificationName string, handler Handler, protected bool) error
2136
}
2237

options.go

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -217,19 +217,11 @@ type Options struct {
217217
// When unstable mode is enabled, the client will use RESP3 protocol and only be able to use RawResult
218218
UnstableResp3 bool
219219

220-
// PushNotifications enables general push notification processing.
221-
// When enabled, the client will process RESP3 push notifications and
222-
// route them to registered handlers.
223-
//
224-
// For RESP3 connections (Protocol: 3), push notifications are always enabled
225-
// and cannot be disabled. To avoid push notifications, use Protocol: 2 (RESP2).
226-
// For RESP2 connections, push notifications are not available.
227-
//
228-
// default: always enabled for RESP3, disabled for RESP2
229-
PushNotifications bool
220+
// Push notifications are always enabled for RESP3 connections (Protocol: 3)
221+
// and are not available for RESP2 connections. No configuration option is needed.
230222

231223
// PushNotificationProcessor is the processor for handling push notifications.
232-
// If nil, a default processor will be created when PushNotifications is enabled.
224+
// If nil, a default processor will be created for RESP3 connections.
233225
PushNotificationProcessor PushNotificationProcessorInterface
234226
}
235227

osscluster.go

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1623,7 +1623,7 @@ func (c *ClusterClient) processTxPipelineNode(
16231623
}
16241624

16251625
func (c *ClusterClient) processTxPipelineNodeConn(
1626-
ctx context.Context, _ *clusterNode, cn *pool.Conn, cmds []Cmder, failedCmds *cmdsMap,
1626+
ctx context.Context, node *clusterNode, cn *pool.Conn, cmds []Cmder, failedCmds *cmdsMap,
16271627
) error {
16281628
if err := cn.WithWriter(c.context(ctx), c.opt.WriteTimeout, func(wr *proto.Writer) error {
16291629
return writeCmds(wr, cmds)
@@ -1641,7 +1641,7 @@ func (c *ClusterClient) processTxPipelineNodeConn(
16411641
trimmedCmds := cmds[1 : len(cmds)-1]
16421642

16431643
if err := c.txPipelineReadQueued(
1644-
ctx, rd, statusCmd, trimmedCmds, failedCmds,
1644+
ctx, node, cn, rd, statusCmd, trimmedCmds, failedCmds,
16451645
); err != nil {
16461646
setCmdsErr(cmds, err)
16471647

@@ -1653,30 +1653,50 @@ func (c *ClusterClient) processTxPipelineNodeConn(
16531653
return err
16541654
}
16551655

1656-
return pipelineReadCmds(rd, trimmedCmds)
1656+
return node.Client.pipelineReadCmds(ctx, cn, rd, trimmedCmds)
16571657
})
16581658
}
16591659

16601660
func (c *ClusterClient) txPipelineReadQueued(
16611661
ctx context.Context,
1662+
node *clusterNode,
1663+
cn *pool.Conn,
16621664
rd *proto.Reader,
16631665
statusCmd *StatusCmd,
16641666
cmds []Cmder,
16651667
failedCmds *cmdsMap,
16661668
) error {
16671669
// Parse queued replies.
1670+
// To be sure there are no buffered push notifications, we process them before reading the reply
1671+
if err := node.Client.processPendingPushNotificationWithReader(ctx, cn, rd); err != nil {
1672+
// Log the error but don't fail the command execution
1673+
// Push notification processing errors shouldn't break normal Redis operations
1674+
internal.Logger.Printf(ctx, "push: error processing pending notifications before reading reply: %v", err)
1675+
}
16681676
if err := statusCmd.readReply(rd); err != nil {
16691677
return err
16701678
}
16711679

16721680
for _, cmd := range cmds {
1681+
// To be sure there are no buffered push notifications, we process them before reading the reply
1682+
if err := node.Client.processPendingPushNotificationWithReader(ctx, cn, rd); err != nil {
1683+
// Log the error but don't fail the command execution
1684+
// Push notification processing errors shouldn't break normal Redis operations
1685+
internal.Logger.Printf(ctx, "push: error processing pending notifications before reading reply: %v", err)
1686+
}
16731687
err := statusCmd.readReply(rd)
16741688
if err == nil || c.checkMovedErr(ctx, cmd, err, failedCmds) || isRedisError(err) {
16751689
continue
16761690
}
16771691
return err
16781692
}
16791693

1694+
// To be sure there are no buffered push notifications, we process them before reading the reply
1695+
if err := node.Client.processPendingPushNotificationWithReader(ctx, cn, rd); err != nil {
1696+
// Log the error but don't fail the command execution
1697+
// Push notification processing errors shouldn't break normal Redis operations
1698+
internal.Logger.Printf(ctx, "push: error processing pending notifications before reading reply: %v", err)
1699+
}
16801700
// Parse number of replies.
16811701
line, err := rd.ReadLine()
16821702
if err != nil {

0 commit comments

Comments
 (0)