Skip to content

Shared Payload Services

Ordinary collection assumes that one repository owns its payload store. If several repositories share the same blobs, one repository’s roots cannot say when a blob is safe to delete. Another repository may still have a committed record for it.

repository A ── records ─┐
repository B ── records ─┼─ BlobId ── shared payload service
repository C ── records ─┘

Casita’s logical collection removes unreachable records from one repository and leaves physical deletion to the shared service.

Payload ownership API What it removes
One repository owns the store preview_collection, collect, try_collect Unreachable records, then unreferenced blobs and chunks. Requires BlobGc.
Several repositories share the store preview_logical_collection, collect_logical, try_collect_logical This repository’s unreachable records only. Requires MetadataStore.

The casita gc command uses physical collection for a local repository. Do not add BlobGc to a shared backend just to use it: one repository cannot see all physical owners. CombinedBlobStore likewise has no BlobGc implementation.

let preview = repository.preview_logical_collection().await?;
println!("unreachable records: {}", preview.logical_objects);
let outcome = repository.collect_logical().await?;
if let Some(revision) = outcome.revision {
println!("pruned at repository revision {revision}");
}

collect_logical() waits for active mutation and retention holds; try_collect_logical() returns Busy instead. A stale commit fails with a typed revision error, so retry from a new snapshot. Preview and outcome report record counts, not bytes reclaimed: one repository cannot know the physical savings.

An unrooted but committed record still owns its payload. Roots decide which records survive the next logical collection; the shared service tracks payload ownership for every committed record, rooted or not.

The service needs an aggregate owner set, reference count, or equivalent ledger for each physical blob. Its state and ownership changes must share a durable atomic boundary:

Event Required service action
A new record commits Add ownership only if the record was inserted; an idempotent retry must not count twice.
Logical collection prunes a record Remove ownership in the same transaction as the record.
A root changes Leave physical ownership alone; roots affect a later prune.
An upload finishes before its record commits Hold a staging lease so deletion cannot race publication.
Ownership reaches zero Delete only after all write leases end; delayed deletion may leak space but must not remove another owner’s data.

The state backend still needs revision checks. The payload service also needs writer leases and deletion fencing so a stale deletion request cannot remove a newly published blob. Logical collection alone does not provide these service rules or resolve concurrent changes to names.

Logical collection controls lifetime, not access. A hosted service must authorize record and payload reads. Exposing a global has(BlobId) or raw blob read would let a tenant test whether another tenant stores known content. Dedupe hits, physical locations, and cross-tenant storage totals can leak the same equality signal.

If equality across tenants must remain private, use separate storage or encryption domains. Removing one tenant’s record does not immediately erase a shared physical blob that another tenant still owns.

BlobId still identifies plaintext bytes. Object keys, links, and roots remain logical state in each repository. Authentication, encryption, aggregate ownership, and deletion policy belong to the service around Casita.

See Garbage Collection for ordinary collection and Division of Responsibility for application policy.