feat(store): the put seam — two internal paths, the three numbered rules, Batch, kv-only rejection
- `put(len: Option<u64>, stream: impl AsyncRead + Unpin + Send) -> (Key, Pin)`: known-length one-pass flow; unknown-length buffer-to-threshold flow with the mid-stream fresh-stage flush (ADR-003 option (d), three numbered rules: flush-and-continue, derivation over the eventual stored artifact as a whole via buffer form and StagingCursor read-back form, zero residue on stream error) - staged-put seam amended (operator sign-off): `put_staged()` keyless handout, `StagingCursor::commit(self, key)` publishes under the store-core-resolved key — the canonical key completes at stream end and the wave-1 handout shape could not express the commit-rename flow - pin-before-publish rides the arbitration lock on both paths and both tiers; dedup re-puts refcount the existing entry - Batch seam: `Store::batch()` + `put_in_batch()`, members publish pin- before-publish, group pins drop together on `Batch::commit` (new public verb) or drop - kv-only rejection (ADR-012 §6.2): known-length over-threshold rejects immediately (stream never read), unknown-length at mid-stream overflow; zero residue; error carrier `Io(String)` (named design point) - declared-len mismatch rejects `Verification` zero-residue (named design point); stream shape generic-no-dyn (named design point) - incremental BlobHasher (pinned byte-equal to Algorithm::derive); AnyStaging cursor dispatch; git-interop put-path leg against git hash-object; eleven put-path behavior tests Verification: cargo test (default/--features mem/--all-features) green; clippy --all-targets -D warnings green in all three feature shapes; cargo fmt --check green; cargo doc --no-deps at the 24-warning baseline.
This commit is contained in:
1 parent
889be4cada
commit
539dabcbf7
8 files changed
+1723
-105
No files matched your search
+36
-23
@@ -108,6 +108,17 @@ pub(crate) trait ReadCursor: Send {
|
||||
/// on discard with zero residue (the stage-hygiene invariant,
|
||||
/// store-api.md §Write path; exact-count tested per engine).
|
||||
///
|
||||
/// **The key resolves at commit, not at handout (named design point,
|
||||
/// the wave-2 amendment of the wave-1 handout shape).** The canonical
|
||||
/// key is the derivation over preamble + content, and it completes at
|
||||
/// stream end — after the staging cursor already exists (put paths
|
||||
/// stream content once into the cursor while hashing; ADR-003's put
|
||||
/// paths). So `commit` carries the store-core-resolved key and the
|
||||
/// stage's identity before that point is only the engine-internal
|
||||
/// stage file/tx (unique per `put_staged` handout). Publish is the
|
||||
/// CAS first-writer-wins entry under that committed key exactly as
|
||||
/// [`Backend::put`].
|
||||
///
|
||||
/// Every engine implements the staged form: kv-tier bodies are the
|
||||
/// degenerate in-memory stage committed through the engine's whole-value
|
||||
/// row write (the store core selects the form by tier policy, ADR-010
|
||||
@@ -121,15 +132,18 @@ pub(crate) trait StagingCursor: ReadCursor {
|
||||
/// "restart the stream" requirement anywhere).
|
||||
async fn append(&mut self, chunk: &[u8]) -> Result<(), Error>;
|
||||
|
||||
/// Publish the entry: the stage's bytes become the CAS entry in the
|
||||
/// Publish the entry: the stage's bytes become the CAS entry under
|
||||
/// `key` — the store-core-resolved canonical key (the derivation
|
||||
/// completes at stream end; see the type's design point) — in the
|
||||
/// engine's put shape (commit-rename on `local`, the in-tx `lowrite`
|
||||
/// batch on pg-lo, the row write on kv bodies). This is the staged
|
||||
/// form's terminal step — the store core never re-uses the cursor
|
||||
/// after the call. On failure the stage is **already discarded** —
|
||||
/// zero residue by the stage-hygiene invariant; there is no
|
||||
/// post-failure discard to drive (the consuming call took the
|
||||
/// cursor).
|
||||
async fn commit(self) -> Result<(), Error>;
|
||||
/// batch on pg-lo, the row write on kv bodies). CAS first-writer-wins:
|
||||
/// a losing race against an existing entry is dedup (`Ok`), never a
|
||||
/// failure. This is the staged form's terminal step — the store core
|
||||
/// never re-uses the cursor after the call. On failure the stage is
|
||||
/// **already discarded** — zero residue by the stage-hygiene
|
||||
/// invariant; there is no post-failure discard to drive (the
|
||||
/// consuming call took the cursor).
|
||||
async fn commit(self, key: &[u8]) -> Result<(), Error>;
|
||||
|
||||
/// Discard the stage: every non-commit path converges here, zero
|
||||
/// residue. Driven explicitly by the store core (stream error,
|
||||
@@ -189,11 +203,12 @@ pub(crate) trait Backend: Send + Sync {
|
||||
async fn put(&self, key: &[u8], value: &[u8]) -> Result<(), Error>;
|
||||
|
||||
/// The staged put's cursor handout (ADR-010 §1). The store core
|
||||
/// selects the form before content flows: the large tier's
|
||||
/// stage-then-commit-rename contract, kv engines' degenerate
|
||||
/// in-memory stage. See [`StagingCursor`] for the contract every
|
||||
/// body converges on.
|
||||
async fn put_staged(&self, key: &[u8]) -> Result<Self::Staging, Error>;
|
||||
/// selects the form before content flows (the cursor's identity
|
||||
/// before commit is the engine-internal stage only — the canonical
|
||||
/// key resolves at [`StagingCursor::commit`], the type's design
|
||||
/// point). See [`StagingCursor`] for the contract every body
|
||||
/// converges on.
|
||||
async fn put_staged(&self) -> Result<Self::Staging, Error>;
|
||||
|
||||
/// Unconditional delete — arbitration has already happened above the
|
||||
/// trait (the facade decides and owns the refusal: `GcRefuse` never
|
||||
@@ -442,7 +457,6 @@ mod tests {
|
||||
}
|
||||
|
||||
struct TestStaging {
|
||||
key: Vec<u8>,
|
||||
staged: Vec<u8>,
|
||||
rows: Arc<Mutex<BTreeMap<Vec<u8>, Vec<u8>>>>,
|
||||
fail_commit: bool,
|
||||
@@ -464,13 +478,13 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn commit(mut self) -> Result<(), Error> {
|
||||
async fn commit(mut self, key: &[u8]) -> Result<(), Error> {
|
||||
if self.fail_commit {
|
||||
self.staged.clear();
|
||||
return Err(Error::Io("engine commit failure (test double)".to_owned()));
|
||||
}
|
||||
let value = std::mem::take(&mut self.staged);
|
||||
self.rows.lock().entry(self.key.clone()).or_insert(value);
|
||||
self.rows.lock().entry(key.to_vec()).or_insert(value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -508,9 +522,8 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn put_staged(&self, key: &[u8]) -> Result<Self::Staging, Error> {
|
||||
async fn put_staged(&self) -> Result<Self::Staging, Error> {
|
||||
Ok(TestStaging {
|
||||
key: key.to_vec(),
|
||||
staged: Vec::new(),
|
||||
rows: self.rows.clone(),
|
||||
fail_commit: self.fail_commit,
|
||||
@@ -603,7 +616,7 @@ mod tests {
|
||||
let backend = TestBackend::default();
|
||||
let key = b"\x01staged-key".to_vec();
|
||||
|
||||
let mut staging = backend.put_staged(&key).await.unwrap();
|
||||
let mut staging = backend.put_staged().await.unwrap();
|
||||
staging.append(&CONTENT[..8]).await.unwrap();
|
||||
staging.append(&CONTENT[8..16]).await.unwrap();
|
||||
staging.append(&CONTENT[16..]).await.unwrap();
|
||||
@@ -620,7 +633,7 @@ mod tests {
|
||||
assert_eq!(bytes, CONTENT, "staged artifact readable as a whole");
|
||||
assert_eq!(digest, slice_digest(CONTENT));
|
||||
|
||||
staging.commit().await.expect("commit publishes");
|
||||
staging.commit(&key).await.expect("commit publishes");
|
||||
assert!(backend.has(&key).await.unwrap(), "entry published");
|
||||
|
||||
let cursor = backend
|
||||
@@ -650,7 +663,7 @@ mod tests {
|
||||
let backend = TestBackend::default();
|
||||
let key = b"\x01discard-key".to_vec();
|
||||
|
||||
let mut staging = backend.put_staged(&key).await.unwrap();
|
||||
let mut staging = backend.put_staged().await.unwrap();
|
||||
staging.append(b"residue").await.unwrap();
|
||||
staging.discard().await;
|
||||
|
||||
@@ -676,9 +689,9 @@ mod tests {
|
||||
};
|
||||
let key = b"\x01failure-key".to_vec();
|
||||
|
||||
let mut staging = backend.put_staged(&key).await.unwrap();
|
||||
let mut staging = backend.put_staged().await.unwrap();
|
||||
staging.append(CONTENT).await.unwrap();
|
||||
let err = staging.commit().await.unwrap_err();
|
||||
let err = staging.commit(&key).await.unwrap_err();
|
||||
assert!(matches!(err, Error::Io(_)), "engine media failure");
|
||||
|
||||
assert!(
|
||||
|
||||
Reference in new issue
Block a user