Skip to content

Commit df4bb09

Browse files
worstellampagent
andcommitted
docs: add request path, cache tiering, and git restore docs
Amp-Thread-ID: https://ampcode.com/threads/T-019f8629-56a0-727e-826f-c14bdade11f1 Co-authored-by: Amp <amp@ampcode.com>
1 parent 33cbc47 commit df4bb09

4 files changed

Lines changed: 205 additions & 2 deletions

File tree

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ Redirect Git traffic through cachew:
1818
insteadOf = https://github.com/
1919
```
2020

21-
Restore a repository from a snapshot (with automatic delta bundle to reach HEAD):
21+
Restore a repository from a snapshot (with automatic delta bundle to reach HEAD); see
22+
[docs/git-restore.md](docs/git-restore.md) for how the restore flow and parallel snapshot downloads work:
2223

2324
```sh
2425
cachew git restore https://github.com/org/repo ./repo
@@ -109,7 +110,9 @@ Multiple backends can be configured simultaneously — they are automatically co
109110
are ordered from lowest/nearest to highest/authoritative. Reads check each tier in order and backfill lower tiers on a
110111
hit. Writes go to all tiers in parallel. Replica invalidations evict only non-authoritative tiers; the final cache block
111112
is authoritative. Tiered caches use the metadata backend to track authoritative ETags and invalidate stale lower-tier
112-
copies before falling through to the authoritative tier.
113+
copies before falling through to the authoritative tier. See [docs/tiering.md](docs/tiering.md) for a full explanation
114+
of the tiering semantics, and [docs/architecture.md](docs/architecture.md) for how requests flow through strategies to
115+
the cache.
113116

114117
### Memory
115118

docs/architecture.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Architecture: the request path
2+
3+
How an HTTP request flows through Cachew, from client to upstream.
4+
5+
```
6+
┌────────┐ ┌──────┐ ┌──────────────┐ ┌──────────────────┐ ┌───────┐
7+
│ Client │──▶│ OPA │──▶│ Interceptors │──▶│ Strategy (route) │──▶│ Cache │
8+
└────────┘ └──────┘ └──────────────┘ └────────┬─────────┘ └───────┘
9+
│ miss
10+
11+
┌──────────┐
12+
│ Upstream │
13+
└──────────┘
14+
```
15+
16+
- **OPA** authorizes every request (`internal/opa`).
17+
- **Strategies** are protocol-aware handlers (Git, GitHub releases, Go
18+
modules, Hermit, Artifactory, host, proxy). Each is registered against the
19+
mux at config load (`internal/config/config.go`); strategies implementing
20+
`strategy.Interceptor` wrap the mux instead, so they can inspect the raw
21+
request line.
22+
- Each strategy receives a **namespaced view** of the cache
23+
(`cache.Namespace`), so strategies never collide on keys.
24+
- Most strategies use the shared handler (`internal/strategy/handler`), which
25+
implements the cache-or-fetch loop: look up the key, serve from cache on a
26+
hit, and on a miss stream the upstream response to the client and the cache
27+
simultaneously. Strategies are not limited to the Cache — e.g. the Git
28+
strategy also maintains bare clones and serves packs directly.
29+
- The **api-v1 strategy** (`internal/strategy/apiv1.go`) exposes the composed
30+
cache itself over HTTP (`/api/v1/object/{namespace}/{key}`, …). This is the
31+
API that `client/` and the `Remote` cache implementation speak, and it is
32+
what makes instance-to-instance tiering possible.
33+
34+
Strategies see a single `Cache`; whether it is one backend or several tiers
35+
composed together is invisible to them. That composition is described in
36+
[tiering.md](tiering.md).
37+
38+
## Ranged and parallel downloads
39+
40+
`client.ParallelGet` (`client/parallel_get.go`) downloads a large object as
41+
many concurrent ETag-pinned byte-range requests instead of one stream. Its
42+
main consumer is `cachew git restore`'s snapshot download — see
43+
[git-restore.md](git-restore.md) for the full semantics. The same machinery is
44+
reused inside the S3 backend (`internal/cache/s3_parallel_get.go`): a single
45+
S3 stream is limited to a fraction of the available bandwidth, so whole-object
46+
and large ranged reads fan out into parallel sub-range requests against the
47+
pinned object revision.
48+
49+
Ranged reads interact with cache tiering — a partial body must never be
50+
backfilled into a lower tier as if it were the whole object; see
51+
[tiering.md](tiering.md).
52+
53+
## The Cache contract
54+
55+
Strategies and cache backends all program against the same interface
56+
(`internal/cache/api.go`), whose key guarantees are:
57+
58+
- Expired objects are never returned.
59+
- Objects are invisible until completely written and closed.
60+
- `Delete` is atomic; missing objects invalidate successfully.
61+
- Conditional options (`If-None-Match`, `If-Match`, `If-Range`, `Range`) are
62+
evaluated against the stored ETag with RFC 9110 semantics.

docs/git-restore.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Git restore: snapshots, bundles, and parallel downloads
2+
3+
`cachew git restore <repo-url> <dir>` recreates a working tree from the
4+
server's cached artifacts instead of running `git clone`. The flow
5+
(`cmd/cachew/git.go`):
6+
7+
1. **Snapshot download** — fetch `/git/{repo}/snapshot.tar.zst`, a periodic
8+
`.tar.zst` archive of the working tree, and pipe it straight into
9+
extraction so decompression overlaps the transfer. Extraction goes into a
10+
staging directory renamed into place on success, so a failed download never
11+
leaves a half-written checkout.
12+
2. **Delta bundle** — the snapshot response advertises a bundle URL
13+
(`X-Cachew-Bundle-Url`) covering commits from the snapshot's commit to the
14+
mirror's current HEAD. The client fetches and applies it to catch up
15+
without talking to the upstream. A bundle failure is a warning, not an
16+
error — the restore continues with the snapshot state.
17+
3. **Freshen** (only with `--ref`/`--commit`) — ask the server to ensure the
18+
required refs/commits exist on its mirror, then `git pull --ff-only` from
19+
origin so the working tree catches up. Skipped entirely when the
20+
snapshot+bundle already contain everything requested.
21+
22+
## Parallel snapshot download
23+
24+
The snapshot is downloaded as many concurrent byte-range requests
25+
(`client.ParallelGet`, driven by `--download-concurrency` and
26+
`--download-chunk-size-mb`):
27+
28+
- **Discovery**: the first chunk is requested with a `Range` header; the
29+
response reveals the object's total size and ETag, plus the snapshot
30+
metadata headers (commit, bundle URL). The git strategy advertises these on
31+
both full (200) and ranged (206) responses, so the client learns everything
32+
it needs from this one response (`internal/strategy/git/snapshot.go`).
33+
- **Pinning**: every subsequent chunk carries `If-Match` with the discovery
34+
ETag, so an object rewritten mid-download (e.g. by the periodic snapshotter)
35+
is rejected rather than spliced together from two revisions.
36+
- **Streaming**: chunks complete out of order and are reassembled into a
37+
sequential stream with bounded buffering, feeding `tar` extraction as bytes
38+
arrive.
39+
40+
## Fallbacks and failure modes
41+
42+
Single-stream fallback happens at discovery time only: a server that ignores
43+
the range, an object with no ETag to pin to, an object that fits in the first
44+
chunk, or `--download-concurrency=1` all degrade to one full read.
45+
46+
After discovery, a mid-download rewrite is **fatal**: pinned chunks fail their
47+
`If-Match` precondition and the download errors rather than delivering a
48+
corrupt archive. The client does not retry single-stream and does not fall
49+
back to `git clone` — retrying the restore (or cloning) is the caller's
50+
decision. Bundle failures, by contrast, are non-fatal as described above.

docs/tiering.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# How cache tiering works
2+
3+
How multiple cache backends are composed into a single tiered cache with
4+
fallback, backfill, and invalidation semantics. For how a request reaches the
5+
cache in the first place, see [architecture.md](architecture.md). Source of
6+
truth: `internal/cache/tiered.go` and `internal/cache/api.go`.
7+
8+
Cache backends are object stores with per-object TTLs and metadata. Three are
9+
configurable: `memory`, `disk`, and `s3`. Configuring more than one `cache`
10+
block composes them into a `Tiered` cache automatically
11+
(`cache.MaybeNewTiered`); with a single block the cache is wrapped so that
12+
`Invalidate` is a no-op, since the only tier is authoritative.
13+
14+
**Order matters.** Cache blocks are ordered nearest-first. The **final tier is
15+
authoritative** — typically shared storage like S3 — and everything before it
16+
is treated as a local copy that can be re-fetched.
17+
18+
```
19+
cache memory { } # tier 0: nearest, fastest
20+
cache disk { } # tier 1
21+
cache s3 { ... } # tier 2: authoritative
22+
```
23+
24+
```
25+
read: probe in order, first hit wins
26+
────────────────────────────────────▶
27+
┌────────┐ ┌──────┐ ┌────────────────┐
28+
│ memory │ │ disk │ │ s3 (authorit.) │
29+
└────────┘ └──────┘ └────────────────┘
30+
◀────────────────────────────────────
31+
backfill tier 0 on a deeper hit
32+
33+
write: all tiers in parallel
34+
```
35+
36+
## Reads
37+
38+
`Open`/`Stat` probe tiers in order and return the first definitive answer.
39+
When a deeper tier hits, the returned reader transparently **backfills tier
40+
0** as the caller reads (`backfillReadCloser`), so the next read is served
41+
locally. Backfill is asynchronous and safe: only a stream consumed to EOF
42+
commits the tier-0 entry; a partial read or mid-stream error discards it.
43+
44+
Conditional requests complicate "definitive". A tier holding a *different
45+
version* than the request's validators name (failed `If-Match`, `If-Range`
46+
miss) is not a definitive miss — deeper tiers are consulted for the named
47+
version. Only when no tier holds it does the first tier's outcome stand. A
48+
tier that errored while being probed takes precedence, so outages are not
49+
misreported as missing versions.
50+
51+
Ranged reads return partial bodies, which must never be backfilled as whole
52+
objects. Instead a bounded background **healer** re-fetches the full object
53+
from the serving tier and refreshes tier 0 out of band, so a divergent tier 0
54+
still converges even when clients only ever issue ranged requests — as the
55+
parallel snapshot downloader does (see [architecture.md](architecture.md)).
56+
57+
## Writes
58+
59+
`Create` writes to **all tiers in parallel** through a single writer; the
60+
first error aborts every in-flight write. Entries only become readable once
61+
completely written and closed — a cancelled context discards the object in
62+
every tier.
63+
64+
## ETags and stale-tier invalidation
65+
66+
The tiered cache records the authoritative ETag for each key in the metadata
67+
store (the `metadata` block; see [metadatadb-s3.md](metadatadb-s3.md)). On
68+
reads, a tier whose ETag no longer matches the recorded authoritative ETag is
69+
**invalidated and skipped**, falling through to the next tier. This is how a
70+
replica whose local tiers have diverged converges back onto the shared tier.
71+
72+
## Delete vs Invalidate
73+
74+
- `Delete` removes the object from **every** tier.
75+
- `Invalidate` evicts stale copies from the **non-authoritative** tiers only;
76+
the final tier is left intact by construction. This is what replicas use to
77+
drop local copies without destroying shared state.
78+
79+
## Instance-to-instance tiering
80+
81+
Cachew is designed to run as a local instance (workstation/CI) backed by a
82+
shared remote instance. The pieces:
83+
84+
- The remote instance serves its cache via the **api-v1 strategy** (see
85+
[architecture.md](architecture.md)).
86+
- `client/` is a standalone Go client for that API, and
87+
`internal/cache/remote.go` adapts it to the `Cache` interface — a remote
88+
Cachew instance as a cache tier.

0 commit comments

Comments
 (0)