Skip to content

Commit 0bb1c40

Browse files
committed
refactor(autopipeline): AutoPipelineOptions + no-arg / WithOptions methods
Rename the config type AutoPipelineConfig -> AutoPipelineOptions (matching the Options naming family: Options, ClusterOptions, ...) and split each face into a clean no-arg method plus an explicit variant: AutoPipeline() // Options.AutoPipelineOptions or default AutoPipelineWithOptions(*AutoPipelineOptions) AsyncAutoPipeline() AsyncAutoPipelineWithOptions(*AutoPipelineOptions) Drops the awkward AutoPipeline(nil) for the common default case while keeping an explicit per-instance override. Both faces remain independent (a client can hold a blocking and an async autopipeliner at once). Also renames Default{,Blocking}AutoPipelineConfig -> ...Options and the *Options AutoPipelineConfig fields -> AutoPipelineOptions. Applied across *Client, *ClusterClient, *Ring, the UniversalClient interface and the AutoPipeliner drop-in; call sites, README and example updated.
1 parent 4bb2c98 commit 0bb1c40

41 files changed

Lines changed: 288 additions & 238 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -371,8 +371,8 @@ defer rdb.Close()
371371
ctx := context.Background()
372372

373373
// Blocking face: drop-in for a normal client, batched under the hood.
374-
ap, err := rdb.AutoPipeline(nil)
375-
if err != nil { // only on an invalid AutoPipelineConfig
374+
ap, err := rdb.AutoPipeline()
375+
if err != nil { // only on an invalid AutoPipelineOptions
376376
log.Fatal(err)
377377
}
378378
defer ap.Close()
@@ -395,7 +395,7 @@ For maximum throughput, submit a window on the async face and read later:
395395
396396
```go
397397
ctx := context.Background()
398-
ap, err := rdb.AsyncAutoPipeline(nil) // ordered by default
398+
ap, err := rdb.AsyncAutoPipeline() // ordered by default
399399
if err != nil {
400400
log.Fatal(err)
401401
}
@@ -412,9 +412,11 @@ for _, cmd := range cmds {
412412
}
413413
```
414414
415-
Both faces take an optional `*AutoPipelineConfig` and return `(*AutoPipeliner, error)`
416-
— the error is non-nil only for an invalid config (e.g.
417-
`ap, err := rdb.AsyncAutoPipeline(&redis.AutoPipelineConfig{MaxConcurrentBatches: 80, Unordered: true})`).
415+
Each face has a no-arg form that uses `Options.AutoPipelineOptions` (or the
416+
built-in default) and a `WithConfig` form that takes an explicit
417+
`*AutoPipelineOptions`; both return `(*AutoPipeliner, error)` — the error is
418+
non-nil only for an invalid config (e.g.
419+
`ap, err := rdb.AsyncAutoPipelineWithOptions(&redis.AutoPipelineOptions{MaxConcurrentBatches: 80, Unordered: true})`).
418420
They work on `ClusterClient` too: commands are routed to the correct shard per
419421
key, so a single batch may span many slots; ordering across nodes is per key
420422
(same-key commands stay in order, different nodes' sub-pipelines run

adaptive_delay_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ func TestAdaptiveDelayCalculation(t *testing.T) {
194194
t.Run(tt.name, func(t *testing.T) {
195195
// Create autopipeliner with test config
196196
ap := &AutoPipeliner{
197-
config: &AutoPipelineConfig{
197+
config: &AutoPipelineOptions{
198198
MaxBatchSize: tt.maxBatchSize,
199199
MaxFlushDelay: tt.maxDelay,
200200
AdaptiveDelay: tt.adaptive,
@@ -221,7 +221,7 @@ func TestAdaptiveDelayIntegerArithmetic(t *testing.T) {
221221
maxDelay := 100 * time.Microsecond
222222

223223
ap := &AutoPipeliner{
224-
config: &AutoPipelineConfig{
224+
config: &AutoPipelineOptions{
225225
MaxBatchSize: maxBatch,
226226
MaxFlushDelay: maxDelay,
227227
AdaptiveDelay: true,
@@ -263,7 +263,7 @@ func TestAdaptiveDelayIntegerArithmetic(t *testing.T) {
263263
// BenchmarkCalculateDelay benchmarks the delay calculation
264264
func BenchmarkCalculateDelay(b *testing.B) {
265265
ap := &AutoPipeliner{
266-
config: &AutoPipelineConfig{
266+
config: &AutoPipelineOptions{
267267
MaxBatchSize: 100,
268268
MaxFlushDelay: 100 * time.Microsecond,
269269
AdaptiveDelay: true,
@@ -282,7 +282,7 @@ func BenchmarkCalculateDelay(b *testing.B) {
282282
// BenchmarkCalculateDelayDisabled benchmarks with adaptive delay disabled
283283
func BenchmarkCalculateDelayDisabled(b *testing.B) {
284284
ap := &AutoPipeliner{
285-
config: &AutoPipelineConfig{
285+
config: &AutoPipelineOptions{
286286
MaxBatchSize: 100,
287287
MaxFlushDelay: 100 * time.Microsecond,
288288
AdaptiveDelay: false,

autopipeline.go

Lines changed: 41 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import (
1414
"github.com/redis/go-redis/v9/internal"
1515
)
1616

17-
// AutoPipelineConfig configures the autopipelining behavior.
18-
type AutoPipelineConfig struct {
17+
// AutoPipelineOptions configures the autopipelining behavior.
18+
type AutoPipelineOptions struct {
1919
// MaxBatchSize is the target batch size: the accumulator stops waiting for
2020
// more commands once the shard queue reaches it, so a batch flushes promptly
2121
// instead of lingering. It is a soft threshold, not a hard cap — under heavy
@@ -134,22 +134,22 @@ func numAutoPipelineShards() int {
134134
return n
135135
}
136136

137-
// DefaultAutoPipelineConfig returns the default autopipelining configuration.
137+
// DefaultAutoPipelineOptions returns the default autopipelining configuration.
138138
//
139139
// The default is ordered: MaxConcurrentBatches is 1, so batches execute
140140
// serially in submit order (a single ordered command stream) while still
141141
// reaching high throughput via deep pipelines when callers submit in windows.
142142
// To trade ordering for parallel-batch throughput, set MaxConcurrentBatches > 1
143143
// together with Unordered: true.
144-
func DefaultAutoPipelineConfig() *AutoPipelineConfig {
145-
return &AutoPipelineConfig{
144+
func DefaultAutoPipelineOptions() *AutoPipelineOptions {
145+
return &AutoPipelineOptions{
146146
MaxBatchSize: 200,
147147
MaxConcurrentBatches: 1, // ordered by default
148148
MaxFlushDelay: 0, // lowest latency; no coalescing wait (batch via in-flight backpressure)
149149
}
150150
}
151151

152-
// DefaultBlockingAutoPipelineConfig returns the default config for the
152+
// DefaultBlockingAutoPipelineOptions returns the default config for the
153153
// blocking face (Client.AutoPipeline). It uses a single ordered batch stream
154154
// (MaxConcurrentBatches: 1). Counterintuitively this maximizes throughput AND
155155
// minimizes latency for the blocking face: with one batch in flight, callers whose
@@ -162,8 +162,8 @@ func DefaultAutoPipelineConfig() *AutoPipelineConfig {
162162
// per round-trip while latency rises. For maximum throughput use the async face
163163
// (AsyncAutoPipeline) with a window of in-flight commands (inflight>1); it keeps
164164
// MaxConcurrentBatches: 1 as well.
165-
func DefaultBlockingAutoPipelineConfig() *AutoPipelineConfig {
166-
return &AutoPipelineConfig{
165+
func DefaultBlockingAutoPipelineOptions() *AutoPipelineOptions {
166+
return &AutoPipelineOptions{
167167
MaxBatchSize: 300,
168168
MaxConcurrentBatches: 1,
169169
}
@@ -172,29 +172,29 @@ func DefaultBlockingAutoPipelineConfig() *AutoPipelineConfig {
172172
// Validate reports whether the configuration is self-consistent. It returns an
173173
// error if MaxConcurrentBatches > 1 without Unordered: true — raising
174174
// concurrency gives up command ordering, so the caller must opt in explicitly.
175-
func (cfg *AutoPipelineConfig) Validate() error {
175+
func (cfg *AutoPipelineOptions) Validate() error {
176176
if cfg.MaxConcurrentBatches > 1 && !cfg.Unordered {
177-
return fmt.Errorf("redis: AutoPipelineConfig.MaxConcurrentBatches=%d requires Unordered:true "+
177+
return fmt.Errorf("redis: AutoPipelineOptions.MaxConcurrentBatches=%d requires Unordered:true "+
178178
"(parallel batches do not preserve command ordering); set Unordered:true to allow it, "+
179179
"or keep MaxConcurrentBatches=1 for an ordered stream", cfg.MaxConcurrentBatches)
180180
}
181181
// Reject obviously-wrong negatives so a typo surfaces at construction rather
182182
// than being silently coerced to a default. Zero is allowed and means "use
183183
// the default" (MaxBatchSize) or "no delay" (MaxFlushDelay).
184184
if cfg.MaxBatchSize < 0 {
185-
return fmt.Errorf("redis: AutoPipelineConfig.MaxBatchSize=%d must be >= 0", cfg.MaxBatchSize)
185+
return fmt.Errorf("redis: AutoPipelineOptions.MaxBatchSize=%d must be >= 0", cfg.MaxBatchSize)
186186
}
187187
if cfg.MaxConcurrentBatches < 0 {
188-
return fmt.Errorf("redis: AutoPipelineConfig.MaxConcurrentBatches=%d must be >= 0", cfg.MaxConcurrentBatches)
188+
return fmt.Errorf("redis: AutoPipelineOptions.MaxConcurrentBatches=%d must be >= 0", cfg.MaxConcurrentBatches)
189189
}
190190
if cfg.MaxFlushDelay < 0 {
191-
return fmt.Errorf("redis: AutoPipelineConfig.MaxFlushDelay=%s must be >= 0", cfg.MaxFlushDelay)
191+
return fmt.Errorf("redis: AutoPipelineOptions.MaxFlushDelay=%s must be >= 0", cfg.MaxFlushDelay)
192192
}
193193
if cfg.NumShards < 0 {
194-
return fmt.Errorf("redis: AutoPipelineConfig.NumShards=%d must be >= 0", cfg.NumShards)
194+
return fmt.Errorf("redis: AutoPipelineOptions.NumShards=%d must be >= 0", cfg.NumShards)
195195
}
196196
if cfg.AdaptiveDelay && cfg.MaxFlushDelay <= 0 {
197-
return fmt.Errorf("redis: AutoPipelineConfig.AdaptiveDelay requires MaxFlushDelay > 0 " +
197+
return fmt.Errorf("redis: AutoPipelineOptions.AdaptiveDelay requires MaxFlushDelay > 0 " +
198198
"(adaptive delay scales MaxFlushDelay by queue fill; with no MaxFlushDelay it would " +
199199
"silently disable batch accumulation entirely)")
200200
}
@@ -277,7 +277,7 @@ func putQueueSlice(slice []Cmder) {
277277
// the pipeline on a normal connection (see Do).
278278
// AutoPipeline / AsyncAutoPipeline return an error for an invalid config, so check it once:
279279
//
280-
// ap, err := client.AutoPipeline(nil)
280+
// ap, err := client.AutoPipeline()
281281
// if err != nil {
282282
// return err
283283
// }
@@ -307,7 +307,7 @@ type AutoPipeliner struct {
307307
cmdable // Embed cmdable to get all Redis command methods
308308

309309
pipeliner cmdableClient
310-
config *AutoPipelineConfig
310+
config *AutoPipelineOptions
311311
// blocking selects how the typed command surface (Set, Get, ...) behaves:
312312
// when true the command call itself blocks until the command has executed
313313
// (drop-in, synchronous shape); when false the call returns immediately and
@@ -423,14 +423,14 @@ func (s *apShard) stripe() *apStripe {
423423
// and cache a new one. The caller supplies its cached-slot pointer, its
424424
// closed flag (both guarded by the mutex), the explicit-config override, the
425425
// fallback config, and a build closure (the cluster one wraps
426-
// clusterAutoPipelineConfig and installs slot sharding).
426+
// clusterAutoPipelineOptions and installs slot sharding).
427427
func getOrCreateAutoPipeliner(
428428
mu *sync.Mutex,
429429
slot **AutoPipeliner,
430430
closed *bool,
431-
override *AutoPipelineConfig,
432-
fallback func() *AutoPipelineConfig,
433-
build func(*AutoPipelineConfig) (*AutoPipeliner, error),
431+
override *AutoPipelineOptions,
432+
fallback func() *AutoPipelineOptions,
433+
build func(*AutoPipelineOptions) (*AutoPipeliner, error),
434434
) (*AutoPipeliner, error) {
435435
mu.Lock()
436436
defer mu.Unlock()
@@ -457,13 +457,13 @@ func getOrCreateAutoPipeliner(
457457
// Client/ClusterClient.AutoPipeline and AsyncAutoPipeline, which also install
458458
// cluster slot-sharding. Constructing one directly would skip that wiring and
459459
// give a *ClusterClient degraded (cross-node) batching.
460-
func newAutoPipeliner(pipeliner cmdableClient, config *AutoPipelineConfig, blocking bool) (*AutoPipeliner, error) {
460+
func newAutoPipeliner(pipeliner cmdableClient, config *AutoPipelineOptions, blocking bool) (*AutoPipeliner, error) {
461461
if config == nil {
462-
config = DefaultAutoPipelineConfig()
462+
config = DefaultAutoPipelineOptions()
463463
} else {
464464
// Copy so default-filling below doesn't mutate the caller's struct — the
465-
// same *AutoPipelineConfig may be shared across clients (e.g. a reused
466-
// Options.AutoPipelineConfig), and callers may inspect it afterward.
465+
// same *AutoPipelineOptions may be shared across clients (e.g. a reused
466+
// Options.AutoPipelineOptions), and callers may inspect it afterward.
467467
cfgCopy := *config
468468
config = &cfgCopy
469469
}
@@ -495,7 +495,7 @@ func newAutoPipeliner(pipeliner cmdableClient, config *AutoPipelineConfig, block
495495
// per-key order holds).
496496
if config.NumShards > 1 && !config.Unordered && !blocking && !config.contentSharded {
497497
return nil, fmt.Errorf(
498-
"redis: AutoPipelineConfig.NumShards=%d requires Unordered:true on the deferred (async) face "+
498+
"redis: AutoPipelineOptions.NumShards=%d requires Unordered:true on the deferred (async) face "+
499499
"(commands are distributed round-robin across shards, which flush concurrently and do not preserve submit order)",
500500
config.NumShards)
501501
}
@@ -667,14 +667,24 @@ func (ap *AutoPipeliner) PoolStats() *PoolStats { return ap.pipeliner.PoolStats(
667667
// AutoPipeline delegates to the underlying client, which returns its cached
668668
// autopipeliner (typically this same instance). Present to satisfy the
669669
// UniversalClient surface.
670-
func (ap *AutoPipeliner) AutoPipeline(config *AutoPipelineConfig) (*AutoPipeliner, error) {
671-
return ap.pipeliner.AutoPipeline(config)
670+
func (ap *AutoPipeliner) AutoPipeline() (*AutoPipeliner, error) {
671+
return ap.pipeliner.AutoPipeline()
672+
}
673+
674+
// AutoPipelineWithOptions delegates to the underlying client.
675+
func (ap *AutoPipeliner) AutoPipelineWithOptions(config *AutoPipelineOptions) (*AutoPipeliner, error) {
676+
return ap.pipeliner.AutoPipelineWithOptions(config)
672677
}
673678

674679
// AsyncAutoPipeline delegates to the underlying client. Present to satisfy the
675680
// UniversalClient surface.
676-
func (ap *AutoPipeliner) AsyncAutoPipeline(config *AutoPipelineConfig) (*AutoPipeliner, error) {
677-
return ap.pipeliner.AsyncAutoPipeline(config)
681+
func (ap *AutoPipeliner) AsyncAutoPipeline() (*AutoPipeliner, error) {
682+
return ap.pipeliner.AsyncAutoPipeline()
683+
}
684+
685+
// AsyncAutoPipelineWithOptions delegates to the underlying client.
686+
func (ap *AutoPipeliner) AsyncAutoPipelineWithOptions(config *AutoPipelineOptions) (*AutoPipeliner, error) {
687+
return ap.pipeliner.AsyncAutoPipelineWithOptions(config)
678688
}
679689

680690
// validate AutoPipeliner implements UniversalClient (drop-in for the real
@@ -879,7 +889,7 @@ func (s *apShard) wake() {
879889
func (ap *AutoPipeliner) IsBlocking() bool { return ap.blocking }
880890

881891
// Config returns a copy of the effective configuration (defaults filled in).
882-
func (ap *AutoPipeliner) Config() AutoPipelineConfig { return *ap.config }
892+
func (ap *AutoPipeliner) Config() AutoPipelineOptions { return *ap.config }
883893

884894
// IsClosed reports whether the AutoPipeliner has been closed, either by an
885895
// explicit Close or by closing the owning client. A closed AutoPipeliner

autopipeline_accessor_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func TestAutoPipelineSecondaryAccessors(t *testing.T) {
2424
// The deferred face is where secondary accessors must block until the
2525
// batch executes (the blocking face waits inside the call itself, so it
2626
// could never catch a missing await barrier).
27-
ap, err := c.AsyncAutoPipeline(nil)
27+
ap, err := c.AsyncAutoPipeline()
2828
if err != nil {
2929
t.Fatal(err)
3030
}

autopipeline_backpressure_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func TestAutoPipelineWindowedNoDeadlock(t *testing.T) {
2222
if err := client.Ping(ctx).Err(); err != nil {
2323
t.Skipf("no redis: %v", err)
2424
}
25-
ap, err := client.AsyncAutoPipeline(&redis.AutoPipelineConfig{
25+
ap, err := client.AsyncAutoPipelineWithOptions(&redis.AutoPipelineOptions{
2626
MaxBatchSize: 300, MaxConcurrentBatches: 8, Unordered: true,
2727
})
2828
if err != nil {
@@ -65,7 +65,7 @@ func TestAutoPipelineSoak(t *testing.T) {
6565
if err := client.Ping(ctx).Err(); err != nil {
6666
t.Skipf("no redis: %v", err)
6767
}
68-
ap, err := client.AsyncAutoPipeline(&redis.AutoPipelineConfig{
68+
ap, err := client.AsyncAutoPipelineWithOptions(&redis.AutoPipelineOptions{
6969
MaxBatchSize: 200, MaxConcurrentBatches: 16, Unordered: true,
7070
})
7171
if err != nil {

autopipeline_blocking_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ var _ = Describe("AutoPipeline Blocking Commands", func() {
2222
Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())
2323

2424
var err error
25-
ap, err = client.AutoPipeline(nil)
25+
ap, err = client.AutoPipeline()
2626
Expect(err).NotTo(HaveOccurred())
2727
})
2828

autopipeline_buffer_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ import (
1414
func TestAPZeroCopyBuffer(t *testing.T) {
1515
ctx := context.Background()
1616
c := redis.NewClient(&redis.Options{
17-
Addr: ":6379",
18-
AutoPipelineConfig: &redis.AutoPipelineConfig{MaxBatchSize: 100, MaxConcurrentBatches: 30, Unordered: true},
17+
Addr: ":6379",
18+
AutoPipelineOptions: &redis.AutoPipelineOptions{MaxBatchSize: 100, MaxConcurrentBatches: 30, Unordered: true},
1919
})
2020
defer c.Close()
2121
c.FlushDB(ctx)
22-
ap, err := c.AutoPipeline(nil)
22+
ap, err := c.AutoPipeline()
2323
if err != nil {
2424
t.Fatal(err)
2525
}

autopipeline_close_race_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func TestAutoPipelineCloseRace(t *testing.T) {
2626
for iter := 0; iter < 50; iter++ {
2727
ctx := context.Background()
2828
c := redis.NewClient(&redis.Options{Addr: ":6379"})
29-
ap, err := c.AutoPipeline(nil)
29+
ap, err := c.AutoPipeline()
3030
if err != nil {
3131
t.Fatal(err)
3232
}

autopipeline_close_wiring_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func TestClientCloseClosesAutoPipeliner(t *testing.T) {
1818
c := redis.NewClient(&redis.Options{Addr: ":6379"})
1919
c.FlushDB(ctx)
2020

21-
ap, err := c.AutoPipeline(nil)
21+
ap, err := c.AutoPipeline()
2222
if err != nil {
2323
t.Fatal(err)
2424
}

autopipeline_cmdable_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ var _ = Describe("AutoPipeline Cmdable Interface", func() {
2222
Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())
2323

2424
var err error
25-
ap, err = client.AutoPipeline(nil)
25+
ap, err = client.AutoPipeline()
2626
Expect(err).NotTo(HaveOccurred())
2727
})
2828

0 commit comments

Comments
 (0)