Skip to content

Lifecycle & maintenance

Everything here runs in cavs-worker, off the request path. Nothing on this page blocks a push or a pull.

Stage Trigger What it does Status
Usage reconciliation upload.session.finalized, repository.usage.changed Sums real stored bytes per namespace, writes the daily usage snapshot, evaluates quota thresholds live
Cleanup every 5 minutes Expires stale upload sessions and invitations, sweeps orphaned index sessions live
Notifications invitation, quota, billing and storage-health events In-app notifications and email live
Webhooks events with a public mapping Signed outbound deliveries with retries live
Storage health every ~10 minutes Probes BYOS connections, flips health status, notifies on transitions live
Snapshots snapshot.requested Captures a point-in-time storage record live
Health & recommendations scheduled Recomputes repository health checks and recommendations live
Garbage collection gc.requested Reclaims chunks nothing references coordinator only — see below
Repack repack.requested Compacts dead bytes out of packfiles coordinator only
Index compaction index.compaction.requested Compacts the Git-index snapshots coordinator only

Every stage is idempotent — events are deduplicated by id before handling, so at-least-once delivery never double-counts.

An upload session lives for 6 hours. The cleanup loop expires sessions past that and sweeps the objects that were authorized but never finalized.

Consequences:

  • A job that dies mid-upload leaves nothing permanent. Re-run it.
  • Re-opening with the same Idempotency-Key returns the same session, so a retry resumes rather than restarting.
  • Objects only become visible in the repository at finalize. A push killed before finalize publishes nothing — there is no half-published state.

Invitations expire on the same loop.

Finalizing a push publishes repository.usage.changed. The worker then:

  1. Sums the real stored bytes under the repository’s namespace, directly from the object store.
  2. Writes the daily usage snapshot that the dashboard and GET …/usage read.
  3. Compares against the plan and emits storage.quota.warning or storage.quota.exceeded when a threshold is crossed.

If the object store is briefly unreachable it falls back to a conservative estimate and retries. So a figure can be approximate for a short window — never silently wrong for long.

Latency is seconds. Object count and generation update immediately at finalize; byte totals follow.

Event Effect
storage.quota.warning Notification + email + webhook. Nothing is blocked.
storage.quota.exceeded Notification + email + webhook, and the organization is marked PAST_DUE.

Upload authorization checks storage quota before presigning, using logical sizes as an upper bound — so a batch that would clearly overflow is refused up front with 402 quota_exceeded rather than half-written.

BYOS repositories skip the storage-quota check entirely (your bucket, your capacity) but object-size limits still apply.

You delete Immediately Eventually
An object reference (a commit, a branch) index updated chunks it uniquely held become unreferenced
A repository metadata gone, repository disappears from the API bytes queued for GC
An organization all metadata gone all managed bytes queued for GC
A BYOS storage connection connection record and encrypted credentials gone nothing — your bucket is untouched

CAVS does not automatically expire your data. Objects live until the repository or organization is deleted. Custom retention policies are a Business-plan feature (features.custom_retention).

If you want time-based cleanup today, drive it yourself: list objects sorted by age, decide what is dead, delete the referencing refs.

Terminal window
# Objects, oldest last — inspect the tail
curl -sS "$API/repositories/$REPO/objects?sort=recent&limit=200" -H "$H" \
| jq -r '.objects[] | "\(.created_at)\t\(.size)\t\(.oid)"' | tail -50

The Git index is snapshotted per generation and swapped atomically, so a read never sees a half-written tree. Old generations are swept on finalize and on index-session expiry.

Re-index from scratch after a history rewrite:

Terminal window
cav repo index --full

Integrity is structural rather than scheduled:

  • Chunks are addressed by BLAKE3-256; objects by SHA-256 with a Merkle root.
  • Every download verifies its oid client-side and refuses to write a mismatch.
  • A corrupt local chunk cache is detected, quarantined and repaired in place.
  • Reconstruction is byte-identical or it fails — never partial.
Terminal window
# Maintenance entries appear in the repository activity feed
curl -sS "$API/repositories/$REPO/activity?limit=50" -H "$H" \
| jq '.events[] | select(.type | test("gc|repack|snapshot|index"))'

maintenance.gc.completed is a public webhook event if you want to react to it.

Self-hosted operators get per-stage metrics — consumer lag, handler duration, failures, reconciliation outcomes. See Observability.