Harden blob GC and supervise the content-index worker
Deduplication GC (garbage_collect, Phase 2): - Add an orphan grace period before a ref_count=0 blob's backing file is physically deleted, mirroring git's gc.pruneExpire. New storage.blobs.orphaned_at records when a blob last reached ref_count 0; the delete trigger and every decrement / 0-ref insert path stamp it, every re-reference clears it. - Cross-check that no manifest lists the chunk and no file points at the blob before deleting it (mirrors Phase 1's file check), so a stale ref_count can only delay collection, never delete live content. - Unlink the backing files with bounded parallel fan-out. Together these close a TOCTOU where a concurrent upload of identical content could re-reference a chunk in the window between the GC row delete committing and the backing file being unlinked. Individual file deletes still reclaim eagerly; only bulk empty-trash and the periodic sweep observe the grace window. Trash: match ErrorKind::NotFound instead of substring-matching the error message when treating an already-deleted item as success. Content-index worker: supervise the drain loop and restart it with backoff after a panic, instead of letting a panic silently freeze the search index while the dirty queue grows unbounded. Adds migration 20260802000000_blob_gc_grace.sql and an integration test covering the grace window and reference cross-checks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0172rsVwzTwD216R9HXT2aU4
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
-- Garbage-collection safety for orphaned blobs.
|
||||
--
|
||||
-- The dedup GC deletes a blob row (committed) and then unlinks the backing
|
||||
-- file. A concurrent uploader of identical content can re-reference a chunk in
|
||||
-- that window. Two mechanisms make the sweep safe:
|
||||
-- (a) garbage_collect() never collects a blob still referenced by a manifest
|
||||
-- (chunk) or a file (legacy whole-file blob) — cross-checks backed by
|
||||
-- idx_chunk_manifests_chunk_hashes_gin and idx_files_blob_hash. A stale
|
||||
-- ref_count = 0 on live content can then only delay collection, never
|
||||
-- delete it.
|
||||
-- (b) garbage_collect() never collects a blob that became unreferenced only
|
||||
-- moments ago — the grace period below, mirroring git's gc.pruneExpire,
|
||||
-- so a writer about to pin a just-orphaned chunk cannot race the sweep.
|
||||
--
|
||||
-- `orphaned_at` records when ref_count last reached 0. NULL means the row is
|
||||
-- referenced (ref_count > 0) or predates this column.
|
||||
|
||||
ALTER TABLE storage.blobs ADD COLUMN IF NOT EXISTS orphaned_at TIMESTAMPTZ;
|
||||
|
||||
-- Existing orphans start their grace window now, so applying this migration
|
||||
-- never triggers an immediate sweep of content a writer might still be racing.
|
||||
UPDATE storage.blobs
|
||||
SET orphaned_at = now()
|
||||
WHERE ref_count <= 0 AND orphaned_at IS NULL;
|
||||
|
||||
-- GC scan index: orphan rows ordered by when they became collectible. Replaces
|
||||
-- the old ref_count-only partial index (the GC now also filters on orphaned_at).
|
||||
DROP INDEX IF EXISTS storage.idx_blobs_orphaned;
|
||||
CREATE INDEX IF NOT EXISTS idx_blobs_gc_eligible
|
||||
ON storage.blobs (orphaned_at) WHERE ref_count = 0;
|
||||
|
||||
-- Stamp orphaned_at when a file delete drops a blob's ref_count to 0, so the
|
||||
-- grace window starts at the moment of orphaning. No-op for multi-chunk files
|
||||
-- whose file_hash is not itself a storage.blobs row.
|
||||
CREATE OR REPLACE FUNCTION storage.decrement_blob_ref()
|
||||
RETURNS trigger AS $$
|
||||
BEGIN
|
||||
UPDATE storage.blobs
|
||||
SET ref_count = GREATEST(ref_count - 1, 0),
|
||||
orphaned_at = CASE WHEN GREATEST(ref_count - 1, 0) = 0 THEN now() ELSE orphaned_at END
|
||||
WHERE hash = OLD.blob_hash;
|
||||
RETURN OLD;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON COLUMN storage.blobs.orphaned_at IS
|
||||
'When ref_count last reached 0; GC waits a grace period past this before deleting (NULL = referenced or pre-migration)';
|
||||
Reference in New Issue
Block a user