package home import ( "context" "errors" "fmt" "io" "net" "strings" "testing" "testing/synctest" "time" "github.com/redis/go-redis/v9" ) func TestSubscriptionTelemetryPreservesHeartbeatDeadline(t *testing.T) { for _, test := range []struct { name string timeout time.Duration interval time.Duration stale bool heartbeats bool traffic string }{ {name: "default timeout", timeout: homeSubscriptionReceiveTimeout, interval: 30 * time.Second}, {name: "timeout longer than sampling interval", timeout: 75 * time.Second, interval: 30 * time.Second}, {name: "stale telemetry", timeout: homeSubscriptionReceiveTimeout, interval: time.Second, stale: true}, {name: "normal heartbeats renew deadline", timeout: homeSubscriptionReceiveTimeout, interval: time.Second, heartbeats: true}, {name: "config traffic cannot renew deadline", timeout: 75 * time.Second, interval: 30 * time.Second, traffic: redisChannelConfig}, {name: "cluster traffic cannot renew deadline", timeout: 75 * time.Second, interval: 30 * time.Second, traffic: redisChannelCluster}, {name: "unrelated pong cannot renew deadline", timeout: 75 * time.Second, interval: 30 * time.Second, traffic: "unrelated-pong"}, } { t.Run(test.name, func(t *testing.T) { synctest.Test(t, func(t *testing.T) { local, peer := net.Pipe() defer func() { if errClose := peer.Close(); errClose != nil { t.Error(errClose) } }() go serveRedisCommandTestConn(peer, &redisCommandLog{}, func(args []string) string { if strings.EqualFold(args[0], "SUBSCRIBE") { return "*3\r\n$9\r\nsubscribe\r\n$6\r\nconfig\r\n:1\r\n" } return balanceTestRedisReply(args) }) client := redis.NewClient(&redis.Options{ Addr: "127.0.0.1:8327", Protocol: 2, DisableIdentity: true, MaxRetries: -1, Dialer: func(context.Context, string, string) (net.Conn, error) { return local, nil }, }) defer func() { if errClose := client.Close(); errClose != nil { t.Error(errClose) } }() ctx := context.Background() pubsub := client.Subscribe(ctx, redisChannelConfig) defer func() { if errClose := pubsub.Close(); errClose != nil { t.Error(errClose) } }() if errACK := receiveSubscriptionACKs(ctx, pubsub, test.timeout, []string{redisChannelConfig}); errACK != nil { t.Fatal(errACK) } sampler := &clusterRTTSampler{history: &clusterLatencyHistory{}, node: clusterNode{IP: "home", Port: 8327}} started := time.Now() wantElapsed, wantHeartbeats := test.timeout, 0 if test.heartbeats { wantElapsed += 4 * time.Second wantHeartbeats = 2 } stop := make(chan struct{}) defer close(stop) go func() { timer := time.NewTimer(2 * time.Second) defer timer.Stop() for { select { case <-stop: return case <-timer.C: } elapsed := time.Since(started) if elapsed >= wantElapsed { return } token := "" if test.traffic == "" && (!test.heartbeats || (elapsed != 2*time.Second && elapsed != 4*time.Second)) { token = sampler.begin(time.Now()) if test.stale { sampler.expire(time.Now(), true) } } frame := fmt.Sprintf("*2\r\n$4\r\npong\r\n$%d\r\n%s\r\n", len(token), token) if test.traffic == "unrelated-pong" { frame = "*2\r\n$4\r\npong\r\n$5\r\nother\r\n" } else if test.traffic != "" { frame = fmt.Sprintf("*3\r\n$7\r\nmessage\r\n$%d\r\n%s\r\n$2\r\n{}\r\n", len(test.traffic), test.traffic) } if _, errWrite := io.WriteString(peer, frame); errWrite != nil { // The receiver closes the pipe when its heartbeat deadline expires. if !errors.Is(errWrite, io.ErrClosedPipe) && !errors.Is(errWrite, net.ErrClosed) { t.Errorf("write telemetry PONG: %v", errWrite) } return } timer.Reset(test.interval) } }() heartbeats := 0 deadline := started.Add(test.timeout) for { event, errReceive := receiveSubscriptionEvent(ctx, pubsub, sampler, deadline) if errReceive != nil { if !isTimeoutError(errReceive) { t.Fatalf("receive error = %v, want heartbeat timeout", errReceive) } break } if test.traffic != "" { continue } pong, ok := event.(*redis.Pong) if !ok || pong.Payload != "" { t.Fatalf("telemetry escaped to the heartbeat consumer: %#v", event) } heartbeats++ deadline = time.Now().Add(test.timeout) } if elapsed := time.Since(started); elapsed != wantElapsed || heartbeats != wantHeartbeats { t.Fatalf("elapsed=%v heartbeats=%d, want %v and %d", elapsed, heartbeats, wantElapsed, wantHeartbeats) } if samples := sampler.history.report(time.Now()); test.traffic == "" && (len(samples) != 1 || samples[0].Valid == test.stale) { t.Fatalf("telemetry was not sampled independently of liveness: %+v", samples) } }) }) } }