Skip to content

PubSub Channel health-check PING can block after Sentinel failover #3811

Description

@abdellani
  1. The client connects to the master through Sentinel, and creates a channel to monitor the changes happening under the keyspace "parent*"
  2. The master goes down
  3. wait for the failover
  4. the channel remains open but stops receiving events after the failover.

Expected Behavior

When the sentinel fails over to the replica, the client should continue receiving the events coming from the replica.

Current Behavior

The channel doesn't receive any updates after the failover

Possible Solution

The ping command keeps sticking for a long period of time after the master's shutdown

go-redis/pubsub.go

Lines 696 to 701 in 3dfa4b6

case <-timer.C:
if pingErr := c.pubSub.Ping(ctx); pingErr != nil {
c.pubSub.mu.Lock()
c.pubSub.reconnect(ctx, pingErr)
c.pubSub.mu.Unlock()
}

Steps to Reproduce

  1. Run docker compose up provided below
  2. Run the code from a container inside the network redis
  3. Shutdown redis-master
  4. Update the key "parent", the client will not detect the events.

Context (Environment)

The inspection of this issue traefik/traefik#12965 led me to this issue.

Detailed Description

I started with 3 instances

  1. master instance
  2. replica instance
  3. Sentinel
 services:
  redis-master:
    image: redis:8.6
    command: redis-server --appendonly yes --notify-keyspace-events KEA
    networks:
      redis:
        aliases:
          - redis-master

  redis-replica:
    image: redis:8.6
    command: redis-server --replicaof redis-master 6379 --appendonly yes --notify-keyspace-events KEA
    depends_on:
      - redis-master
    networks:
      redis:
        aliases:
          - redis-replica

  sentinel:
    image: redis:8.6
    entrypoint: []
    command: >
      sh -c '
        cat > /tmp/sentinel.conf <<EOF
        port 26379
        sentinel monitor mymaster redis-master 6379 1
        sentinel down-after-milliseconds mymaster 5000
        sentinel failover-timeout mymaster 10000
        sentinel resolve-hostnames yes
      EOF
        redis-sentinel /tmp/sentinel.conf'
    depends_on:
      - redis-master
      - redis-replica
    networks:
      redis:
        aliases:
          - sentinel

networks:
  redis:

Example code

func main() {
	ctx := context.Background()

	rdb := redis.NewFailoverClient(&redis.FailoverOptions{
		MasterName:    "mymaster",
		SentinelAddrs: []string{"sentinel:26379"},
	})
	defer rdb.Close()

	for {
		fmt.Println("subscribing...")

		// pubsub := rdb.PSubscribe(ctx, "__keyspace@0__:parent*")
		pubsub := rdb.Subscribe(ctx, "__keyspace@0__:parent")

		_, err := pubsub.Receive(ctx)
		if err != nil {
			fmt.Println("####receive subscription error:", err)
			_ = pubsub.Close()
			time.Sleep(time.Second)
			continue
		}

		ch := pubsub.Channel()

		for msg := range ch {
			fmt.Printf("####channel=%s payload=%s\n", msg.Channel, msg.Payload)
		}

		fmt.Println("pubsub channel closed")
		_ = pubsub.Close()
		time.Sleep(time.Second)
	}
}

Possible Implementation

I tried to introduce a timeout, it worked, but I'm not sure that it's the best solution

func (c *channel) initHealthCheck() {
	c.ping = make(chan struct{}, 1)
	go func() {
		timer := time.NewTimer(time.Minute)
		timer.Stop()

		for {
			timer.Reset(c.checkInterval)
			select {
			case <-c.ping:
				if !timer.Stop() {
					<-timer.C
				}
			case <-timer.C:
				ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
				if pingErr := c.pubSub.Ping(ctx); pingErr != nil {
					reconnectCtx, _ := context.WithTimeout(context.Background(), 10*time.Second)
					c.pubSub.mu.Lock()
					c.pubSub.reconnect(reconnectCtx, pingErr)
					c.pubSub.mu.Unlock()
				}
			case <-c.pubSub.exit:
				return
			}
		}
	}()
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions