Relationships
#3531 Datastore rework Phase 4: a provider capability to remove abandoned Litestream lineage paths on buckets (replicaPaths), so chunk GC isn't deferred forever
Opened by stack72 · 10/15/2026· Shipped 10/15/2026
Background (read this first)
swamp is reworking its datastore layer (tracking: swamp-club#2865). In v3 on a bucket (Phase 4E), each checkout's commits.db is replicated by Litestream to <prefix>/_litestream/<fence>/, one path per lineage (swamp-club#3482, D2). A takeover from another checkout starts a new lineage on a new path, with a full snapshot. Lineages are published as control-plane records under _control/lineage/<fence>/.
What's shipped:
- swamp-club#3507: the provider's
replicaTarget(), which tells core where the replica lives; - swamp-club#3491: chunk GC on a replicated copy is deferred while any lineage other than the current one is registered, because a restore's truncation can leave chunks only an older lineage's head names;
- D2: collects old lineages (paths, then records) only on a file replica. On S3 and GCS it keeps both, and the design record says "the paths stay until 4E collects them through
storage/1".
The gap
storage/1 can't do that. It's chunk-keyed (<prefix>/_blobs/<namespace>/sha256/<hh>/<sha256>) and can't list or delete Litestream's objects under <prefix>/_litestream/<fence>/. So on buckets today:
- old lineage paths are never removed. Every takeover between checkouts leaves a full snapshot plus LTX files behind;
- their records stay too, so #3491's rule keeps chunk collection deferred forever on S3 and GCS. Chunks are never collected, and retention (swamp-club#3473) frees no chunk space.
This has to be in the extension contract before the extensions' replica release (swamp-club#3508), or Phase 4E needs a second extension release.
Design
A small optional capability next to
replicaTarget():replicaPaths(): ReplicaPathStore, scoped to<prefix>/_litestream/:list(signal): the fence directories present (<fence>/), so GC can find what nothing records any more;removePath(fence, signal): deletes every object under<prefix>/_litestream/<fence>/, paging through listings, and succeeds when nothing is there. It must never touch anything outside that path. Validatefenceas the 12-digit padded form core writes (replicaPathFor), and refuse anything else.
Core: D2's lineage collection (
collectV3Lineages,v3_copy.ts) uses it on S3 and GCS, with the same rules as the file replica:- only after the new lineage's first confirmed sync, once the lease check passed;
- never the current lineage, or one started after it;
- paths first, then records;
- best effort, never failing a command.
Once old lineages are collected, #3491's deferral lifts and chunk GC runs.
Without the capability, behaviour is as today: paths and records are kept, and chunk GC stays deferred. Say so in the method's doc and in
datastore-litestream.md, and fix the record's "throughstorage/1" wording.Conformance: extend
assertReplicaTargetConformance, or addassertReplicaPathConformance, inpackages/testing:removePathdeletes exactly one fence's objects and leaves a sibling fence and_blobs/alone;listsees what was written;- a malformed fence is refused.
Run it on the in-memory remote, then publish
@swamp-club/swamp-testing.
Tests
- Unit tests for the core wiring with a fake store: which fences are removed, ordering, and best-effort failure.
- Integration, dormant path:
- a file replica keeps working as today;
- with the in-memory remote's capability, a takeover's old lineage path and records are removed after the new lineage confirms, and chunk collection then runs.
- The conformance suite.
Blocked by: nothing. swamp-club#3508 (the extensions' side) should implement replicaPaths() in the same release as replicaTarget(), so 4E needs only one extension release.
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.