1
0
mirror of https://github.com/redis/go-redis.git synced 2026-10-24 07:59:20 +03:00
Files
go-redis/pubsub_sticky_error_test.go
Vedant Madane 9bfbc24c9e fix(pubsub): avoid panics on Channel conflict and Ring empty subscribe (#3968)
* fix(pubsub): avoid panics on Channel conflict and Ring empty subscribe

* fix(pubsub): avoid data race on Channel conflict warn

Conflict-path logging used getContext(), which reads c.cmd while
the other Channel Receive loop may write it — fails CI under -race.

Closes nothing; Refs #3761

* fix(pubsub): prioritize closed state over stickyErr on conn retrieval

* fix(pubsub): override stickyErr on Close for channel exit

Match SingleConnPool.Close by setting stickyErr to ErrClosed so
failed Ring PubSub Channel receivers always observe the closed
exit signal. Add .gitattributes to keep Go sources on LF.

* fix(pubsub): address review feedback on sticky error channel handling and docs

- Remove dead stickyErr write in Close()
- Short-circuit Channel() and ChannelWithSubscriptions() when stickyErr is set to return an already-closed channel and log a warning
- Add warning log in failedPubSub for visibility
- Update doc comments for Subscribe/PSubscribe/SSubscribe and Channel methods
- Remove repo-wide .gitattributes

Signed-off-by: Vedant Madane <6527493+VedantMadane@users.noreply.github.com>

---------

Signed-off-by: Vedant Madane <6527493+VedantMadane@users.noreply.github.com>
2026-10-19 15:07:24 +03:00

68 lines
2.0 KiB
Go

package redis_test
import (
"context"
"testing"
"github.com/redis/go-redis/v9"
)
func TestRingSubscribeEmptyChannelsNoPanic(t *testing.T) {
ring := redis.NewRing(&redis.RingOptions{
Addrs: map[string]string{"shard1": "localhost:6379"},
})
defer ring.Close()
pubsub := ring.Subscribe(context.Background())
if pubsub == nil {
t.Fatal("expected non-nil PubSub")
}
// Receive should surface sticky error, not panic.
_, err := pubsub.Receive(context.Background())
if err == nil {
t.Fatal("expected sticky error from empty Subscribe")
}
if err := pubsub.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
// After Close, sticky construction error is overridden by ErrClosed so
// Channel/initMsgChan can terminate (pool.ErrClosed is the exit signal).
_, err = pubsub.Receive(context.Background())
if err != redis.ErrClosed {
t.Fatalf("after Close want redis.ErrClosed, got %v", err)
}
}
func TestPubSubChannelMutualExclusionNoPanic(t *testing.T) {
// Construct via a client that may not be reachable — Subscribe without
// channels just builds a PubSub handle.
t.Run("ChannelAfterChannelWithSubscriptions", func(t *testing.T) {
client := redis.NewClient(&redis.Options{Addr: "127.0.0.1:1"})
defer client.Close()
pubsub := client.Subscribe(context.Background())
defer pubsub.Close()
_ = pubsub.ChannelWithSubscriptions()
ch := pubsub.Channel()
// Conflicting call returns an already-closed channel (no panic).
msg, ok := <-ch
if ok || msg != nil {
t.Fatalf("expected closed channel from conflicting Channel() call, got msg=%v ok=%v", msg, ok)
}
})
t.Run("ChannelWithSubscriptionsAfterChannel", func(t *testing.T) {
client := redis.NewClient(&redis.Options{Addr: "127.0.0.1:1"})
defer client.Close()
pubsub := client.Subscribe(context.Background())
defer pubsub.Close()
_ = pubsub.Channel()
ch := pubsub.ChannelWithSubscriptions()
msg, ok := <-ch
if ok || msg != nil {
t.Fatalf("expected closed channel from conflicting ChannelWithSubscriptions() call, got msg=%v ok=%v", msg, ok)
}
})
}