1
0
mirror of https://github.com/redis/go-redis.git synced 2026-10-22 19:49:00 +03:00

fix(conn): preserve hooks in WithTimeout clones (#4095)

* fix(conn): preserve hooks in WithTimeout clones

* test(conn): address timeout clone review nits
This commit is contained in:
Luca Candela
2026-10-19 08:46:52 -07:00
committed by GitHub
parent 372b420c56
commit 0c02a49410
2 changed files with 107 additions and 0 deletions

View File

@@ -553,6 +553,11 @@ func (c *baseClient) clone() *baseClient {
// exited, which is harmless.
cscRefreshQueue: c.cscRefreshQueue,
}
// A baseClient built without NewClient (e.g. internal CSC tests) has no
// hook state; keep the zero mixin instead of nil-dereferencing in clone.
if c.hooksMixin.state != nil {
clone.hooksMixin = c.hooksMixin.clone()
}
// The miss coalescer travels with the cache (an atomic.Pointer cannot appear
// in the literal above); a clone shares the OWNER's coalescer, whose
// lifecycle stays owner-only — a clone's Close does not stop it.

102
timeout_hooks_test.go Normal file
View File

@@ -0,0 +1,102 @@
package redis_test
import (
"context"
"errors"
"net"
"sync/atomic"
"testing"
"time"
"github.com/redis/go-redis/v9"
)
type countingHook struct {
process atomic.Int32
pipeline atomic.Int32
}
func TestWithTimeoutPreservesHooks(t *testing.T) {
reachedDial := errors.New("dial reached")
parent := redis.NewClient(&redis.Options{
Addr: "unused:1",
MaxRetries: -1,
Dialer: func(context.Context, string, string) (net.Conn, error) {
return nil, reachedDial
},
})
t.Cleanup(func() {
if err := parent.Close(); err != nil {
t.Error(err)
}
})
early := new(countingHook)
parent.AddHook(early)
clone := parent.WithTimeout(100 * time.Millisecond)
late := new(countingHook)
parent.AddHook(late)
cloneOnly := new(countingHook)
clone.AddHook(cloneOnly)
for _, tc := range []struct {
name string
client *redis.Client
}{
{"parent", parent}, {"clone", clone},
} {
t.Run(tc.name, func(t *testing.T) {
if err := tc.client.Ping(t.Context()).Err(); !errors.Is(err, reachedDial) {
t.Errorf("PING error = %v, want dial sentinel", err)
}
_, err := tc.client.Pipelined(t.Context(), func(pipe redis.Pipeliner) error {
pipe.Ping(t.Context())
return nil
})
if !errors.Is(err, reachedDial) {
t.Errorf("pipeline error = %v, want dial sentinel", err)
}
_, err = tc.client.TxPipelined(t.Context(), func(pipe redis.Pipeliner) error {
pipe.Ping(t.Context())
return nil
})
if !errors.Is(err, reachedDial) {
t.Errorf("transaction pipeline error = %v, want dial sentinel", err)
}
})
}
for _, tc := range []struct {
name string
hook *countingHook
process, pipeline int32
}{
{"inherited", early, 2, 4}, {"parent-only", late, 1, 2}, {"clone-only", cloneOnly, 1, 2},
} {
if got := tc.hook.process.Load(); got != tc.process {
t.Errorf("%s ProcessHook calls = %d, want %d", tc.name, got, tc.process)
}
if got := tc.hook.pipeline.Load(); got != tc.pipeline {
t.Errorf("%s ProcessPipelineHook calls = %d, want %d", tc.name, got, tc.pipeline)
}
}
if parent.Options().ReadTimeout == 100*time.Millisecond || parent.Options().WriteTimeout == 100*time.Millisecond {
t.Error("clone changed parent timeouts")
}
if clone.Options().ReadTimeout != 100*time.Millisecond || clone.Options().WriteTimeout != 100*time.Millisecond {
t.Error("clone did not apply its timeouts")
}
}
func (h *countingHook) DialHook(next redis.DialHook) redis.DialHook {
return next
}
func (h *countingHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook {
return func(ctx context.Context, cmd redis.Cmder) error {
h.process.Add(1)
return next(ctx, cmd)
}
}
func (h *countingHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook {
return func(ctx context.Context, cmds []redis.Cmder) error {
h.pipeline.Add(1)
return next(ctx, cmds)
}
}