|
| 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