diff --git a/plan/pds-writes.md b/plan/pds-writes.md index 419db052..031a9edb 100644 --- a/plan/pds-writes.md +++ b/plan/pds-writes.md @@ -25,9 +25,6 @@ there. That is correct and it means a long-lived reader pays a full export every time the server restarts under enough churn. -- [ ] **An object store behind the blob trait.** The filesystem implementation - is the first one, not the only one intended, and the trait exists so the - second does not rewrite the callers. - [ ] **A write rebuilds the whole repository.** A tree is a pure function of its leaves, so two exports of one state are byte-identical and nothing can drift — which is what makes it affordable. The cost is the rebuild, @@ -112,6 +109,47 @@ there. references found inside them. A revision this deployment cannot place — same as `export_repo` — is answered with the whole listing rather than refused. +- [x] **An object store behind the blob trait, proved by a second + implementation.** `ObjectBlobStore` (`crates/didbot-pds/src/object_blobs.rs`) + answers `BlobStore` the way `FileBlobStore` does, sharing the same + `BlobIndex` and the same write-ahead log's `BlobUploaded`/`BlobCollected` + entries unchanged — the module doc `FileBlobStore` already carried said + an object store wants "this index, unchanged, with `PutObject` where the + rename is," and that is exactly what changed. `Provisioner::with_blob_store` + already took any `Arc`, so plugging it in is a + construction-site change; `tests/object_blobs.rs` runs a slice of + `tests/blobs.rs`'s own scenarios against it to show the callers did not + move. + + **`fetch` answers with a signed redirect, not a stream.** `BlobStore::fetch`'s + own doc already anticipated this — "An object-store implementation would + lean on the checksum the store already keeps instead" of a second + read-and-hash pass — and `didbot-serve`'s `get_blob` handler already + passes `Fetch::Redirect` through. `ObjectBlobStore::fetch` checks the + index first, so a record referencing a blob that was never uploaded, or + was already collected, answers `NotFound` without ever asking the + backend for a URL. + + **Which way this fails.** `ObjectBackend::put` runs before the log entry + is appended, the same order `FileBlobStore` uses: a backend that is + unreachable mid-write fails the upload before anything is durable, and + the caller may retry. A crash between a successful `put` and the log + append is the one gap this leaves — the object is durably stored and + durably unreachable through this server, because nothing indexes it — + and that is leaking, not corrupting, the same direction this epic's + other blob item leans. Closing it needs a way to list a backend's keys + to reconcile against, which `ObjectBackend` deliberately does not have + yet; see the module's doc comment for why. + + **No real backend ships here.** `MemoryObjectBackend` and + `FailingObjectBackend` prove the trait and exercise its failure paths. + A real one (S3, most plausibly, hand-rolled against SigV4 the way + `didbot-dns`'s `Route53Dns` avoids `aws-sdk-s3` and stays off a build + that never uses it) needs a bucket to test the signing against, which is + why it is not this change — see the module doc for the shape it would + take: `presigned_get` is a local computation and never touches the + network, so it can share the SigV4 primitives `route53.rs` already + hand-rolls once such a bucket exists. - [x] **A batch is one commit, and its `swapCommit` is a real compare-and-swap.** `applyWrites` used to write each of its records through the single-write path, so a batch of five moved the repository