Skip to content

Commit 217fef1

Browse files
committed
feat(autopipeline): AutoPipeliner implements UniversalClient (drop-in)
Widen the internal cmdableClient to UniversalClient and delegate the non-batched surface (AddHook, Watch, Subscribe/PSubscribe/SSubscribe, PoolStats, Do, AutoPipeline/AsyncAutoPipeline) to the underlying client, so an *AutoPipeliner can be used anywhere a redis.UniversalClient is expected: data commands are batched, everything else runs on the underlying client. Narrow Do to return *Cmd to match the interface; drop the now-redundant .(*redis.Cmd) assertions in the tests. Close() still closes only the autopipeliner, not the underlying client. This lets tests (the command-suite matrix and maintnotifications e2e) parametrize a single UniversalClient subject that includes the autopipeliner.
1 parent 7da4ff0 commit 217fef1

5 files changed

Lines changed: 105 additions & 12 deletions

autopipeline.go

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -202,10 +202,12 @@ func (cfg *AutoPipelineConfig) Validate() error {
202202
}
203203

204204
// cmdableClient is an interface for clients that support pipelining.
205-
// Both Client and ClusterClient implement this interface.
205+
// Both Client and ClusterClient implement this interface. It embeds
206+
// UniversalClient (Cmdable + Process + Do + AddHook + Watch + Subscribe... +
207+
// Close + PoolStats) so the AutoPipeliner can delegate the non-batched surface
208+
// back to the underlying client and itself satisfy UniversalClient.
206209
type cmdableClient interface {
207-
Cmdable
208-
Process(ctx context.Context, cmd Cmder) error
210+
UniversalClient
209211
// processPipelineHook is the hook-wrapped []Cmder pipeline entry — the same
210212
// method Pipeline.Exec is wired to (see Client.Pipeline). The flusher
211213
// dispatches drained batches through it directly, skipping the per-batch
@@ -586,7 +588,7 @@ func newAutoPipeliner(pipeliner cmdableClient, config *AutoPipelineConfig, block
586588
// a blocking autopipeliner the call blocks until the command has executed; on a
587589
// deferred (async) one it returns immediately and the command's result
588590
// accessors (Err/Val/Result) block until it completes.
589-
func (ap *AutoPipeliner) Do(ctx context.Context, args ...interface{}) Cmder {
591+
func (ap *AutoPipeliner) Do(ctx context.Context, args ...interface{}) *Cmd {
590592
cmd := NewCmd(ctx, args...)
591593
if len(args) == 0 {
592594
cmd.SetErr(errDoNoArgs)
@@ -624,6 +626,61 @@ func (ap *AutoPipeliner) Process(ctx context.Context, cmd Cmder) error {
624626
return ap.cmdable(ctx, cmd)
625627
}
626628

629+
// The methods below complete the UniversalClient surface by delegating to the
630+
// underlying client. They are NOT autopipelined — pub/sub, transactions (Watch),
631+
// hooks, Do and pool stats cannot be batched — so an AutoPipeliner used as a
632+
// UniversalClient batches only the typed data commands; everything here runs on
633+
// the underlying client exactly as it would there.
634+
//
635+
// Note on lifecycle: Close() (defined elsewhere) closes the AUTOPIPELINER —
636+
// drains in-flight batches and stops flushers — but does NOT close the
637+
// underlying client, whose lifecycle is owned by whoever created it.
638+
639+
// AddHook adds a hook to the underlying client. Autopipelined batches are hooked
640+
// too, since dispatch goes through the hook-wrapped pipeline entry.
641+
func (ap *AutoPipeliner) AddHook(hook Hook) { ap.pipeliner.AddHook(hook) }
642+
643+
// Watch runs a transactional function on the underlying client (not batched).
644+
func (ap *AutoPipeliner) Watch(ctx context.Context, fn func(*Tx) error, keys ...string) error {
645+
return ap.pipeliner.Watch(ctx, fn, keys...)
646+
}
647+
648+
// Subscribe opens a pub/sub on the underlying client (not batched — pub/sub
649+
// needs a dedicated connection).
650+
func (ap *AutoPipeliner) Subscribe(ctx context.Context, channels ...string) *PubSub {
651+
return ap.pipeliner.Subscribe(ctx, channels...)
652+
}
653+
654+
// PSubscribe opens a pattern pub/sub on the underlying client (not batched).
655+
func (ap *AutoPipeliner) PSubscribe(ctx context.Context, channels ...string) *PubSub {
656+
return ap.pipeliner.PSubscribe(ctx, channels...)
657+
}
658+
659+
// SSubscribe opens a sharded pub/sub on the underlying client (not batched).
660+
func (ap *AutoPipeliner) SSubscribe(ctx context.Context, channels ...string) *PubSub {
661+
return ap.pipeliner.SSubscribe(ctx, channels...)
662+
}
663+
664+
// PoolStats returns the underlying client's connection pool statistics.
665+
func (ap *AutoPipeliner) PoolStats() *PoolStats { return ap.pipeliner.PoolStats() }
666+
667+
// AutoPipeline delegates to the underlying client, which returns its cached
668+
// autopipeliner (typically this same instance). Present to satisfy the
669+
// UniversalClient surface.
670+
func (ap *AutoPipeliner) AutoPipeline(config ...*AutoPipelineConfig) (*AutoPipeliner, error) {
671+
return ap.pipeliner.AutoPipeline(config...)
672+
}
673+
674+
// AsyncAutoPipeline delegates to the underlying client. Present to satisfy the
675+
// UniversalClient surface.
676+
func (ap *AutoPipeliner) AsyncAutoPipeline(config ...*AutoPipelineConfig) (*AutoPipeliner, error) {
677+
return ap.pipeliner.AsyncAutoPipeline(config...)
678+
}
679+
680+
// validate AutoPipeliner implements UniversalClient (drop-in for the real
681+
// clients; non-data operations delegate to the underlying client).
682+
var _ UniversalClient = (*AutoPipeliner)(nil)
683+
627684
// AutoFuture is the handle returned by Submit. Call Wait (or Result on the
628685
// command after Wait) once the result is needed; it blocks only until the
629686
// command's batch has executed.

autopipeline_accessor_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,10 @@ func TestAutoPipelineSecondaryAccessors(t *testing.T) {
6666
fail(err != nil || u != 1)
6767

6868
// Generic Cmd via Do() — the whole *Cmd type was missing await().
69-
doCmd := ap.Do(ctx, "GET", sk).(*redis.Cmd)
69+
doCmd := ap.Do(ctx, "GET", sk)
7070
v, err := doCmd.Int()
7171
fail(err != nil || v != 12345)
72-
txt, err := ap.Do(ctx, "GET", sk).(*redis.Cmd).Text()
72+
txt, err := ap.Do(ctx, "GET", sk).Text()
7373
fail(err != nil || txt != "12345")
7474

7575
// SliceCmd.Val()/Result() — previously missing await().

autopipeline_blocking_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ var _ = Describe("AutoPipeline Blocking Commands", func() {
4141
// ap.Do returns a generic *redis.Cmd; use its typed accessors.
4242
start := time.Now()
4343
result := ap.Do(ctx, "BLPOP", "list", "1")
44-
val, err := result.(*redis.Cmd).StringSlice()
44+
val, err := result.StringSlice()
4545
elapsed := time.Since(start)
4646

4747
Expect(err).NotTo(HaveOccurred())
@@ -61,15 +61,15 @@ var _ = Describe("AutoPipeline Blocking Commands", func() {
6161
brpopCmd := ap.Do(ctx, "BRPOP", "list3", "1")
6262

6363
// Get results — ap.Do returns generic *redis.Cmd; use typed accessors.
64-
blpopVal, err := blpopCmd.(*redis.Cmd).StringSlice()
64+
blpopVal, err := blpopCmd.StringSlice()
6565
Expect(err).NotTo(HaveOccurred())
6666
Expect(blpopVal).To(Equal([]string{"list3", "a"}))
6767

68-
getVal, err := getCmd.(*redis.Cmd).Text()
68+
getVal, err := getCmd.Text()
6969
Expect(err).NotTo(HaveOccurred())
7070
Expect(getVal).To(Equal("value1"))
7171

72-
brpopVal, err := brpopCmd.(*redis.Cmd).StringSlice()
72+
brpopVal, err := brpopCmd.StringSlice()
7373
Expect(err).NotTo(HaveOccurred())
7474
Expect(brpopVal).To(Equal([]string{"list3", "c"}))
7575
})

autopipeline_cmdable_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,11 @@ var _ = Describe("AutoPipeline Cmdable Interface", func() {
131131
getCmd := ap.Do(ctx, "GET", "custom_key")
132132

133133
// Get results
134-
setVal, err := setCmd.(*redis.Cmd).Result()
134+
setVal, err := setCmd.Result()
135135
Expect(err).NotTo(HaveOccurred())
136136
Expect(setVal).To(Equal("OK"))
137137

138-
getVal, err := getCmd.(*redis.Cmd).Result()
138+
getVal, err := getCmd.Result()
139139
Expect(err).NotTo(HaveOccurred())
140140
Expect(getVal).To(Equal("custom_value"))
141141
})

autopipeline_universal_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,39 @@ func TestUniversalClientAutoPipeline(t *testing.T) {
5050
t.Fatal("Ring via UniversalClient.AutoPipeline should return an error")
5151
}
5252
}
53+
54+
// TestAutoPipelinerAsUniversalClient verifies an *AutoPipeliner is a drop-in
55+
// UniversalClient: data commands are batched through it, while the non-batched
56+
// surface (PoolStats, Do, ...) delegates to the underlying client.
57+
func TestAutoPipelinerAsUniversalClient(t *testing.T) {
58+
ctx := context.Background()
59+
client := redis.NewClient(&redis.Options{Addr: ":6379"})
60+
defer client.Close()
61+
if err := client.Ping(ctx).Err(); err != nil {
62+
t.Skipf("no redis: %v", err)
63+
}
64+
apc, err := client.AutoPipeline(nil)
65+
if err != nil {
66+
t.Fatal(err)
67+
}
68+
defer apc.Close()
69+
70+
// Use the autopipeliner strictly through the UniversalClient interface.
71+
var uc redis.UniversalClient = apc
72+
73+
// Data commands: batched by the autopipeliner.
74+
if v := uc.Set(ctx, "ucap:k", "v", 0).Val(); v != "OK" {
75+
t.Fatalf("Set = %q, want OK", v)
76+
}
77+
if v := uc.Get(ctx, "ucap:k").Val(); v != "v" {
78+
t.Fatalf("Get = %q, want v", v)
79+
}
80+
// Non-batched surface delegates to the underlying client.
81+
if uc.PoolStats() == nil {
82+
t.Fatal("PoolStats() returned nil via UniversalClient")
83+
}
84+
if err := uc.Do(ctx, "PING").Err(); err != nil {
85+
t.Fatalf("Do(PING): %v", err)
86+
}
87+
uc.Del(ctx, "ucap:k")
88+
}

0 commit comments

Comments
 (0)