Skip to content

Avatar: orphaned object versions from crash mid-rotation (no reaper) #192

Description

@martsokha

Context

Avatars are content-addressed: the NATS object is keyed by {owner_id}_{content_hash} and the serve route (GET /avatars/{kind}/{id}/{version}/) fetches that object directly — no DB lookup, immutable cache. On re-upload the service does: put(new)update(avatar_url)delete(old_version) (old hash parsed from the current avatar_url).

The residual

The ordering guarantees the DB URL always points at an existing object (no user-visible gap). But if the process crashes after the DB update and before delete(old), the previous version's object is left in NATS, unreferenced forever. Over a long-lived deployment with frequent avatar changes, these orphans accumulate unbounded.

  • Severity: low — each orphan is a tiny (~≤ a few KB, ≤512px WebP) unreferenced object; only leaks on a crash during a re-upload; not user-visible.
  • Not a concern pre-launch / at low avatar churn.

Why the obvious fix isn't in yet

The natural fix is a sweep-on-upload (after storing the new version, delete every {owner_id}_* object except the current). But NATS object-store list() has no server-side prefix filter — it lists the entire bucket. Sweeping on every upload would mean a full-bucket scan (every account's every avatar) per upload, which is too expensive. A cheap targeted "find by prefix" isn't available in the NATS API.

Options when this matters

  1. Background reaper — periodic task lists the bucket once and deletes any {id}_* object not referenced by the DB. Amortizes the full-bucket scan away from the hot path. (Preferred if churn grows.)
  2. Sweep-on-upload — only viable if a prefix-filtered list is added to the NATS object-store wrapper (or accept the full scan for small buckets).
  3. Overwrite single object per owner — eliminates orphans but re-introduces a DB lookup on serve (loses DB-free serving) to validate the URL version. Rejected as it sacrifices the main design win.

Current state

Shipped with put → update → delete-old (single targeted delete, no listing). This handles the normal case with zero orphans; only a crash mid-rotation leaks one. Documented here as a known residual.

Metadata

Metadata

Assignees

No one assigned

    Labels

    choremaintenance, dependency updates, code cleanup

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions