Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .claude/skills/park/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ Park only when **the docs are as complete as the source allows** and the sole bl
external event. If the work is merely unfinished, keep working. If the source may never ship,
don't park — say so. There must be a concrete, testable **trigger condition**.

Two things make a trigger actually testable, and both have bitten:

- **Point it at the source that will ship the feature, not the one you read.** A draft PR is
often one step in a series that lands on the default branch as a different, consolidated PR.
Watch the branch the release is cut from; if the PR you wrote against targets an integration
branch rather than `master`/`main`, say so in the trigger and name the umbrella once it
exists.
- **Require "in a released version", and make it mean an ancestry check.** Merge and release can
be minutes apart, so date order proves nothing, and the newest tag is often a beta the page's
version note must not cite. The trigger is met when the merge commit is an **ancestor of a
non-prerelease tag** — record the command that shows it (`gh api
repos/<o>/<r>/compare/<merge-sha>...<tag>`, expecting `behind_by: 0`).

## Step 2 — Harvest the loose ends already on the branch

The branch's own commits hold the re-check items, from `/reflect`:
Expand Down Expand Up @@ -84,6 +97,24 @@ Record URL + snapshot in the sources table, and the re-fetch command for unpark.
against an unmerged diff — and say *why* it's low, e.g. "signatures differed between two reads
of the diff").

**Split that summary in two: the semantics, and the identifiers.** They decay at completely
different rates. What the feature *does* — the shape of the page, the ordering guarantees, the
caveats — usually survives to release intact. The **names** usually don't: types get renamed,
optional-arg methods split into no-arg plus `WithOptions` variants, fields appear, and fields
leave the type they were on. So enumerate every type, method, field and default the page
commits to as an explicit list unpark can tick off one by one, rather than describing them in
prose. A prose sentence covering four field names is one checklist item that can be half-right;
four listed names are four verdicts.

Two traps worth calling out while you write that list:

- **Distinguish "renamed" from "was never there".** Record *where* you saw each identifier
(which file, which struct), because the useful unpark finding is sometimes "this field isn't
part of this API at all" — a mis-attributed field looks exactly like a renamed one in the
snapshot, and only the location tells them apart.
- **Note where the changelog and the source disagree.** Release notes summarize and under-report;
the source is authoritative for which client types expose the API.

## Step 4 — Compose the manifest & PR body

Build the PR body: a short human summary, a prominent **do-not-merge warning** linking the
Expand Down Expand Up @@ -119,6 +150,15 @@ outward-facing — if the branch isn't pushed or the user hasn't asked, confirm
- **A stale snapshot is worse than none** — if you record a head SHA or API shape you didn't
actually verify, unpark diffs against fiction. Snapshot only what you checked; leave the
rest out and flag it in the checklist.
- **Expect the identifiers to be wrong, and don't let that shake your confidence in the page.**
Measured on DOC-6832 (go-redis automatic pipelining, parked ~3.5 weeks across two re-parks):
at release the page's structure, ordering semantics and every caveat still read correctly,
while nearly every type, method and field name had moved. That is the *normal* outcome, not a
sign the page was written too early — which is why the LOW confidence tag belongs on the
identifier list specifically, not smeared over the whole page.
- **Re-verifying during the park is what makes the final reconcile cheap.** Each re-park that
refreshes the snapshot converts churn into an already-answered checklist item; skip them and
every delta arrives at once, at the moment you least want a surprise.
- Park **cannot judge whether the source will ship.** It records a trigger; it doesn't predict
the future. A parked PR that never triggers is dead weight — `/unpark`'s scan mode is how you
find and close those.
Expand Down
210 changes: 210 additions & 0 deletions content/develop/clients/go/autopipeline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
---
bannerText: Automatic pipelining is an experimental feature and may be subject to change.
categories:
- docs
- develop
- stack
- oss
- rs
- rc
- oss
- kubernetes
- clients
description: Batch concurrent go-redis commands into pipelines automatically for high-throughput workloads.
linkTitle: Automatic pipelining
title: Automatic pipelining
weight: 42
---

[Pipelining]({{< relref "/develop/using-commands/pipelining" >}}) sends a batch
of commands to the server in a single communication, which avoids the network
and processing overhead of sending each command separately. Normally you build
a pipeline by hand (see [Pipelines and transactions]({{< relref "/develop/clients/go/transpipe" >}})),
but this means you must know in advance which commands you want to batch.

*Automatic pipelining* removes that requirement. When many goroutines issue
commands concurrently, `go-redis` coalesces them into deep pipelines for you,
without any pipeline code in your application. This is useful in high-throughput or
high-concurrency scenarios. At low concurrency, a plain client is
simpler and just as fast, and a hand-written pipeline is generally faster than
an auto-generated one.

Automatic pipelining requires
`github.com/redis/go-redis/v9` v9.22.0 or later.

## Blocking and asynchronous pipelining

Automatic pipelining has two methods that share the same underlying engine:

- **Blocking** (`AutoPipeline()`) is a drop-in replacement for a normal
client. Each command call blocks until it executes and returns its own
value and error, exactly like a plain client, so existing code keeps
working unchanged. Under concurrency, the engine batches commands from all
goroutines into back-to-back pipelines behind the scenes. Per-goroutine
ordering is preserved.
- **Asynchronous** (`AsyncAutoPipeline()`) offers the highest throughput.
Command calls return immediately; reading a result with
`Val()`, `Result()`, or `Err()` blocks until the batch executes. Submit a
sequence of commands and then read the results afterwards to keep each
pipeline as deep as possible.

Both methods are available on `Client`, `ClusterClient`, and `Ring`.

## Blocking usage

Call `AutoPipeline()` to get an `AutoPipeliner`, then call command methods on it
just as you would on a normal client. Each call blocks until it executes, but
concurrent callers' commands are batched together automatically:

```go
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
defer rdb.Close()
ctx := context.Background()

// Blocking: a drop-in for a normal client, batched under the hood.
ap, err := rdb.AutoPipeline()
if err != nil { // only returned for invalid AutoPipelineOptions
log.Fatal(err)
}
defer ap.Close()

var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
key := fmt.Sprintf("key:%d", i)
if err := ap.Set(ctx, key, i, 0).Err(); err != nil { // blocks until executed
log.Printf("set %s: %v", key, err)
}
}(i)
}
wg.Wait()
```

## Asynchronous usage

For maximum throughput, use asynchronous execution. Command calls return
immediately, so you can submit a sequence of commands and read their results
afterwards:

```go
ctx := context.Background()

ap, err := rdb.AsyncAutoPipeline() // ordered by default
if err != nil {
log.Fatal(err)
}
defer ap.Close()

cmds := make([]*redis.StatusCmd, 0, 200)
for i := 0; i < 200; i++ {
// Returns immediately without executing.
cmds = append(cmds, ap.Set(ctx, fmt.Sprintf("key:%d", i), i, 0))
}
for _, cmd := range cmds {
if err := cmd.Err(); err != nil { // blocks until the batch executes
log.Printf("set: %v", err)
}
}
```

## Configuration

`AutoPipeline()` and `AsyncAutoPipeline()` take no arguments. They use the
`AutoPipelineOptions` set on the client's options, if any, and otherwise use
reasonable default values. To pass options for a single
autopipeliner, use `AutoPipelineWithOptions()` or
`AsyncAutoPipelineWithOptions()` instead:

```go
// On the client, used by both methods.
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
AutoPipelineOptions: &redis.AutoPipelineOptions{MaxFlushDelay: 100 * time.Microsecond},
})

// Or for a single autopipeliner.
ap, err := rdb.AsyncAutoPipelineWithOptions(&redis.AutoPipelineOptions{
MaxConcurrentBatches: 80,
Unordered: true,
})
```

All four methods return `(*AutoPipeliner, error)`. The error is non-nil only
when the options are invalid (for example, setting `MaxConcurrentBatches`
greater than one without also setting `Unordered`). Invalid options never cause a
panic.

The configuration options are:

| Field | Description |
| :---- | :---------- |
| `MaxBatchSize` | Target number of commands the engine coalesces into a single pipeline before flushing. This is a soft threshold rather than a hard cap, so a busy queue can flush a larger batch. Defaults to 200. |
| `MaxBatchBytes` | Soft limit on the total size of arguments (in bytes) for a batch, so that large values flush as several bounded writes instead of one very large one. Defaults to 0, meaning no byte limit. |
| `MaxFlushDelay` | Maximum time the engine waits to accumulate more commands before flushing a batch. Larger values build deeper pipelines at the cost of latency. Defaults to 0, which adds no accumulation wait. |
| `AdaptiveDelay` | Scales `MaxFlushDelay` down as the queue fills, so a busy queue flushes sooner. Requires `MaxFlushDelay` to be greater than 0. Defaults to `false`. |
| `MaxConcurrentBatches` | Number of batches that may execute at once. Defaults to 1, which gives a single ordered stream. Values greater than 1 require `Unordered` set to `true` because concurrent batches do not preserve a single ordered stream. |
| `Unordered` | Allows commands to execute without preserving a single ordered stream, which enables higher concurrency. |
| `NumShards` | Number of independent command queues, or shards, that the engine flushes separately. Defaults to 0, meaning a single shard, which funnels every caller into one queue so batches stay deep. Cluster clients default to several slot-routed shards instead. With `AsyncAutoPipeline()`, values greater than 1 require `Unordered` to be set to `true`. |

`MaxBatchSize` is the one default that differs between the two methods. If you
set no options at all, `AutoPipeline()` uses a built-in preset that targets 300
commands instead of 200. As soon as you supply `AutoPipelineOptions`, either on
the client or to `AutoPipelineWithOptions()`, that preset no longer applies, and
a `MaxBatchSize` you leave unset means 200.

Connection and buffer tuning is not part of `AutoPipelineOptions`. Batches use
the client's pipeline connections, which you size with the
`PipelineReadBufferSize`, `PipelineWriteBufferSize`, and `PipelinePoolSize`
fields of the client's options.

Each client holds at most two autopipeliners: one for the blocking method and
one for the asynchronous method. Each of them is a
[*singleton*](https://en.wikipedia.org/wiki/Singleton_pattern) that the client
creates on first use and then shares with every later caller.

Options therefore only take effect on the call that creates the singleton. If a
blocking autopipeliner already exists, a later `AutoPipelineWithOptions()` call
returns that same instance and ignores the options you passed, because
`AutoPipeline()` and `AutoPipelineWithOptions()` share one singleton between
them. `Close()` stops the singleton for every caller and the next call creates a
fresh one, so closing is also how you apply different options. Closing the
client is permanent: both methods then return `ErrClosed`.

## Cluster usage

`AutoPipeline()` and `AsyncAutoPipeline()` also work on `ClusterClient`.
Commands are routed to the correct shard by key, so the client installs
slot-based shard routing to keep each shard's batch on a single master node
(rather than splitting every batch across all nodes at flush time). This is why
cluster clients default to several shards instead of one. A single batch may
span many slots. Ordering is per key: same-key commands stay in order, while
sub-pipelines on different nodes run concurrently.

Commands that must reach every node or shard, such as
[`FLUSHALL`]({{< relref "/commands/flushall" >}}), cannot be added to a pipeline, so
the cluster client rejects them with an error rather than let them spoil a
batch shared with other callers. Run them on the plain client instead.

## Caveats and limitations

- A command's context is not honored once it is queued, because batches
execute on the autopipeliner's own context. Use a plain client if you need
per-command deadlines.
- Blocking commands such as [`BLPOP`]({{< relref "/commands/blpop" >}}) and
[`WAIT`]({{< relref "/commands/wait" >}}) are never batched and run directly
on your context.
- The generic `Do`, `DoRaw`, and `DoRawWriteTo` methods run outside the
pipeline, on a normal connection, because an arbitrary command name can
carry connection state or block the connection. Prefer the typed methods
(`ap.Set()`, `ap.Get()`, and so on), which are always batched.
- On a dropped connection, a batch is retried as a whole, up to the client's
`MaxRetries`, so non-idempotent commands may execute twice. Set
`MaxRetries: -1`, or use a plain client, for commands that must never be
retransmitted.

## More information

See the [`go-redis`](https://github.com/redis/go-redis) repository for the
`example/autopipeline` usage tour and further API details.
4 changes: 4 additions & 0 deletions content/develop/clients/go/transpipe.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ There are two types of batch that you can use:
See the [Transactions]({{< relref "develop/using-commands/transactions" >}})
page for more information.

If you want the client to batch concurrent commands into pipelines for you
without writing any pipeline code, see
[Automatic pipelining]({{< relref "/develop/clients/go/autopipeline" >}}).

## Execute a pipeline

To execute commands in a pipeline, you first create a pipeline object
Expand Down
Loading