Skip to content

The cleanup job deleted the chunks. Why were active files still using them?

The Object Exists. Is It Safe? · Part 6 of 6 · 6 min read

Read the worked explanation ↓

Interview question. A deduplicating object store separates logical file manifests from shared content-addressed chunks. Its garbage collector marks unreachable chunks and deletes them to reclaim space. One morning an active customer's file cannot be restored because the collector removed chunks that a newer manifest still references. Explain the race and redesign deletion so live data cannot be reclaimed.

I wouldn't begin by adding another seven-day delay. The fundamental problem is deciding when a chunk is safe to delete while other workers are allowed to publish new references to it.

Imagine chunks addressed by their content hash. File A and File B can both point to chunk X. A reference count records committed file references, or a mark-and-sweep collector traverses reachable manifests and marks their chunks.

The collector marks X absent, a concurrent publisher points to X, and unsafe sweep removes live data before a fenced second check.
The collector marks X absent, a concurrent publisher points to X, and unsafe sweep removes live data before a fenced second check.
Think first

A deduplicated backup service has 15 PB stored in content-addressed chunks. Daily GC scans manifests while uploads and restores run continuously. Specify a correct mark, write barrier, candidate deletion, and physical removal protocol under crashes.

Take a few minutes to form an approach, then compare it with the worked explanation.

Reveal the worked explanation

Write down the failing timeline

At 10:00, the collector takes a snapshot of the file catalog and begins its mark phase. At that instant no committed manifest references X, so X is marked unreachable. At 10:01, an upload worker notices that X already exists in the deduplicated chunk store and publishes a new file manifest referring to X. At 10:02, the collector uses its earlier mark result and physically deletes X. The new manifest remains committed, but restoring its contents fails.

This isn't a checksum problem. X was valid when uploaded, and its hash may still be correct in the manifest. It's a concurrency problem between adding a reference and reclaiming the referenced object. Strong consistency of individual chunk GETs also doesn't help because the collector made a decision using an earlier consistent view.

If a worker decrements X's count to zero, queues deletion, and another worker increments it after the deletion is already committed, the count can say one while the bytes are gone. A correct counter protocol needs atomic reference publication, explicit pending states or some other agreed barrier.

Define the invariant before picking a garbage collector

I want a simple safety property: a chunk referenced by any committed, retained manifest must not be physically deleted. A newly uploading session may reserve chunks before publication. A snapshot that is no longer visible to new readers may still be pinned by an existing restore operation.

One approach uses generations or epochs. A collector starts at generation G and marks reachable chunks from an authoritative metadata snapshot. All new manifest commits after G either write into a protected newer generation or record new references in a way the collector cannot miss. Sweep only chunks proven unreachable relative to both the snapshot and the allowed concurrent writes. This resembles the write barrier in a concurrent garbage collector for process memory, adapted to a distributed metadata store.

A second approach uses staged deletion. Instead of removing bytes immediately when the mark pass finds a candidate, transition the chunk from live to candidate, record the candidate's generation, and wait a safe interval. Before physical deletion, re-evaluate reachability through an authoritative metadata source under a suitable fence. If a publisher can still attach a reference after the last check but before removal, this is not sufficient. The publication path must reject attaching to a chunk whose deletion generation is active, or atomically revive and protect it before the sweep can proceed.

Make publication and reclamation agree on ownership

For a deduplicating system, a practical protocol is to allocate a chunk identity that includes an immutable content hash and a separate generation or storage-instance ID. When a writer wants to publish a manifest, it must first acquire or validate a live reference to each chunk within the coordinator's transaction or an equivalent fenced protocol. The writer can re-upload or explicitly restore it to a valid state through the same authority that owns deletion.

The collector, meanwhile, may transition candidate chunks into a terminal deleting state only after confirming that no committed or protected pending references remain. Storage deletion can happen asynchronously after that logical transition. This design has a conservative failure mode: it may leak some unreferenced chunks if a worker crashes, but it should not delete live chunks. A periodic reconciliation pass can recover space later.

What about a new writer uploading the same hash after the old instance was deleted? If physical names can be reused, an old delayed delete operation might arrive after the new upload and remove the replacement. Include a generation-qualified physical key or conditional deletion tied to a specific version. Fencing needs to reach the storage system where deletion actually occurs, not stop at a database row that a stale worker can ignore.

A long-running reader is another kind of reference

Suppose a restore begins reading a 500 GB file manifest. The user deletes the file while the restore runs. If the service promises that an authorized restore can finish once started, that reader must pin the relevant manifest and chunks for the required duration. A lease may work if workers renew it reliably, and the storage service ensures deletion cannot race a still-valid lease. Alternatively an immutable snapshot can remain retained for a known recovery window.

I would test the race with a deterministic scheduler that pauses GC after marking X unreachable, commits a new manifest pointing to X, then resumes deletion. Repeat with worker crashes before and after the final reachability check, delayed physical DELETE requests, duplicate retries, and simultaneous legal holds. Verify that every committed retained manifest has readable valid chunks, even if reclaimable storage leaks temporarily.

At Principal level, the interesting tradeoff is not 'how often do you run GC?' It's whether the publication protocol and deletion protocol share a provable ordering rule. A system can recover leaked disk space later. It cannot reconstruct a chunk that every physical copy has already been told to delete.

Go deeper

Distributed tracing GC needs a snapshot boundary and a write barrier for references added after that boundary. Reference counting needs atomicity, resurrection rules and protection for readers of old snapshots. A time grace period alone is not a proof unless bounded publication delay, bounded reader lifetime, and fencing are enforced. Delayed object-store DELETE requests introduce an ABA problem if physical keys are reused after deletion. Generation-qualified keys, object versions and conditional deletes can prevent a stale cleanup task from deleting new content with the same logical hash. A safe design favors false positives that retain unreachable chunks over false negatives that destroy reachable data.

Check your reasoning

  • Describe the collector-snapshot and concurrent-manifest publication race.
  • State the retained-manifest-to-chunk physical existence invariant.
  • Require a write barrier, shared fence, or transactionally protected references.
  • Protect pinned reads, pending uploads, versioned keys and delayed deletes.
  • Prefer leaked chunks over live-data loss and test schedule interleavings.

Common mistake: A second reference-count check is always enough even if a new publisher can increment immediately afterward.

Which protocol prevents garbage collection from deleting a chunk referenced by a concurrent publisher?

Read the answer explanation

Coordinate publication and deletion with a shared fenced authority and revalidation — Without a synchronization boundary between new references and deletes, another race remains.

Apply it elsewhere

A vector-index engine removes obsolete segment objects after compaction while existing queries hold earlier manifests. Design snapshot pinning and physical deletion so no live query loses a segment.

Compare your approach

Show a concrete mark/sweep race, state the committed-reference safety invariant, establish a publication barrier or fence, protect readers and pending writes, prevent stale delete ABA, and test adversarial interleavings.

One more constraint

Is checking 'reference count equals zero' immediately before DELETE sufficient when a publisher can concurrently increment the count?

Quick revision

GC can delete live content when it marks a chunk unreachable, then a concurrent publisher creates a reference before physical deletion. The solution is not just a longer timer. Reference publication and deletion must share an ordering or fencing protocol. Use authoritative snapshots plus a write barrier, or a transactionally protected reference state, stage deletion, and prevent writers from attaching to delete-pending chunks. Protect pinned readers, version physical keys to stop delayed-delete races, and prefer temporary leaks to irreversible loss.

References