mirror of
https://github.com/redis/go-redis.git
synced 2025-12-25 00:01:00 +03:00
feat: RESP3 notifications support & Hitless notifications handling [CAE-1088] & [CAE-1072] (#3418)
- Adds support for handling push notifications with RESP3. - Using this support adds handlers for hitless upgrades. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Hristo Temelski <hristo.temelski@redis.com>
This commit is contained in:
80
pubsub.go
80
pubsub.go
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/redis/go-redis/v9/internal"
|
||||
"github.com/redis/go-redis/v9/internal/pool"
|
||||
"github.com/redis/go-redis/v9/internal/proto"
|
||||
"github.com/redis/go-redis/v9/push"
|
||||
)
|
||||
|
||||
// PubSub implements Pub/Sub commands as described in
|
||||
@@ -21,7 +22,7 @@ import (
|
||||
type PubSub struct {
|
||||
opt *Options
|
||||
|
||||
newConn func(ctx context.Context, channels []string) (*pool.Conn, error)
|
||||
newConn func(ctx context.Context, addr string, channels []string) (*pool.Conn, error)
|
||||
closeConn func(*pool.Conn) error
|
||||
|
||||
mu sync.Mutex
|
||||
@@ -38,6 +39,12 @@ type PubSub struct {
|
||||
chOnce sync.Once
|
||||
msgCh *channel
|
||||
allCh *channel
|
||||
|
||||
// Push notification processor for handling generic push notifications
|
||||
pushProcessor push.NotificationProcessor
|
||||
|
||||
// Cleanup callback for hitless upgrade tracking
|
||||
onClose func()
|
||||
}
|
||||
|
||||
func (c *PubSub) init() {
|
||||
@@ -69,10 +76,18 @@ func (c *PubSub) conn(ctx context.Context, newChannels []string) (*pool.Conn, er
|
||||
return c.cn, nil
|
||||
}
|
||||
|
||||
if c.opt.Addr == "" {
|
||||
// TODO(hitless):
|
||||
// this is probably cluster client
|
||||
// c.newConn will ignore the addr argument
|
||||
// will be changed when we have hitless upgrades for cluster clients
|
||||
c.opt.Addr = internal.RedisNull
|
||||
}
|
||||
|
||||
channels := mapKeys(c.channels)
|
||||
channels = append(channels, newChannels...)
|
||||
|
||||
cn, err := c.newConn(ctx, channels)
|
||||
cn, err := c.newConn(ctx, c.opt.Addr, channels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -153,12 +168,31 @@ func (c *PubSub) releaseConn(ctx context.Context, cn *pool.Conn, err error, allo
|
||||
if c.cn != cn {
|
||||
return
|
||||
}
|
||||
|
||||
if !cn.IsUsable() || cn.ShouldHandoff() {
|
||||
c.reconnect(ctx, fmt.Errorf("pubsub: connection is not usable"))
|
||||
}
|
||||
|
||||
if isBadConn(err, allowTimeout, c.opt.Addr) {
|
||||
c.reconnect(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *PubSub) reconnect(ctx context.Context, reason error) {
|
||||
if c.cn != nil && c.cn.ShouldHandoff() {
|
||||
newEndpoint := c.cn.GetHandoffEndpoint()
|
||||
// If new endpoint is NULL, use the original address
|
||||
if newEndpoint == internal.RedisNull {
|
||||
newEndpoint = c.opt.Addr
|
||||
}
|
||||
|
||||
if newEndpoint != "" {
|
||||
// Update the address in the options
|
||||
oldAddr := c.cn.RemoteAddr().String()
|
||||
c.opt.Addr = newEndpoint
|
||||
internal.Logger.Printf(ctx, "pubsub: reconnecting to new endpoint %s (was %s)", newEndpoint, oldAddr)
|
||||
}
|
||||
}
|
||||
_ = c.closeTheCn(reason)
|
||||
_, _ = c.conn(ctx, nil)
|
||||
}
|
||||
@@ -167,9 +201,6 @@ func (c *PubSub) closeTheCn(reason error) error {
|
||||
if c.cn == nil {
|
||||
return nil
|
||||
}
|
||||
if !c.closed {
|
||||
internal.Logger.Printf(c.getContext(), "redis: discarding bad PubSub connection: %s", reason)
|
||||
}
|
||||
err := c.closeConn(c.cn)
|
||||
c.cn = nil
|
||||
return err
|
||||
@@ -185,6 +216,11 @@ func (c *PubSub) Close() error {
|
||||
c.closed = true
|
||||
close(c.exit)
|
||||
|
||||
// Call cleanup callback if set
|
||||
if c.onClose != nil {
|
||||
c.onClose()
|
||||
}
|
||||
|
||||
return c.closeTheCn(pool.ErrClosed)
|
||||
}
|
||||
|
||||
@@ -436,9 +472,14 @@ func (c *PubSub) ReceiveTimeout(ctx context.Context, timeout time.Duration) (int
|
||||
}
|
||||
|
||||
err = cn.WithReader(ctx, timeout, func(rd *proto.Reader) error {
|
||||
// To be sure there are no buffered push notifications, we process them before reading the reply
|
||||
if err := c.processPendingPushNotificationWithReader(ctx, cn, rd); err != nil {
|
||||
// Log the error but don't fail the command execution
|
||||
// Push notification processing errors shouldn't break normal Redis operations
|
||||
internal.Logger.Printf(ctx, "push: conn[%d] error processing pending notifications before reading reply: %v", cn.GetID(), err)
|
||||
}
|
||||
return c.cmd.readReply(rd)
|
||||
})
|
||||
|
||||
c.releaseConnWithLock(ctx, cn, err, timeout > 0)
|
||||
|
||||
if err != nil {
|
||||
@@ -451,6 +492,12 @@ func (c *PubSub) ReceiveTimeout(ctx context.Context, timeout time.Duration) (int
|
||||
// Receive returns a message as a Subscription, Message, Pong or error.
|
||||
// See PubSub example for details. This is low-level API and in most cases
|
||||
// Channel should be used instead.
|
||||
// Receive returns a message as a Subscription, Message, Pong, or an error.
|
||||
// See PubSub example for details. This is a low-level API and in most cases
|
||||
// Channel should be used instead.
|
||||
// This method blocks until a message is received or an error occurs.
|
||||
// It may return early with an error if the context is canceled, the connection fails,
|
||||
// or other internal errors occur.
|
||||
func (c *PubSub) Receive(ctx context.Context) (interface{}, error) {
|
||||
return c.ReceiveTimeout(ctx, 0)
|
||||
}
|
||||
@@ -532,6 +579,27 @@ func (c *PubSub) ChannelWithSubscriptions(opts ...ChannelOption) <-chan interfac
|
||||
return c.allCh.allCh
|
||||
}
|
||||
|
||||
func (c *PubSub) processPendingPushNotificationWithReader(ctx context.Context, cn *pool.Conn, rd *proto.Reader) error {
|
||||
// Only process push notifications for RESP3 connections with a processor
|
||||
if c.opt.Protocol != 3 || c.pushProcessor == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create handler context with client, connection pool, and connection information
|
||||
handlerCtx := c.pushNotificationHandlerContext(cn)
|
||||
return c.pushProcessor.ProcessPendingNotifications(ctx, handlerCtx, rd)
|
||||
}
|
||||
|
||||
func (c *PubSub) pushNotificationHandlerContext(cn *pool.Conn) push.NotificationHandlerContext {
|
||||
// PubSub doesn't have a client or connection pool, so we pass nil for those
|
||||
// PubSub connections are blocking
|
||||
return push.NotificationHandlerContext{
|
||||
PubSub: c,
|
||||
Conn: cn,
|
||||
IsBlocking: true,
|
||||
}
|
||||
}
|
||||
|
||||
type ChannelOption func(c *channel)
|
||||
|
||||
// WithChannelSize specifies the Go chan size that is used to buffer incoming messages.
|
||||
|
||||
Reference in New Issue
Block a user