From 01191103452100b294d90e8924f35ba7ed079b7c Mon Sep 17 00:00:00 2001 From: Edouard Vanbelle Date: Fri, 28 Aug 2026 08:12:25 +0200 Subject: [PATCH] test(api): diagnose all leftovers, and name the pinning source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The run gave the decisive fact: `derived=1`. A content_derived_blobs row still points at the leftover blob, so GC is CORRECT to keep it — the leak is the row, not the bytes. purge_derived_blobs only runs when the SOURCE is reaped, so the question is why that never happened. So the dump now prints the source hash and what still holds it: src_files, src_manifest, src_blob. If the source has a live file the answer is "not deleted"; if it has none but a positive refcount, a release was missed upstream; if it has no row at all, the source was reaped WITHOUT purging, which would be a real ordering bug in reap_blob. Also fixes the dump reporting only one of three blobs. `docker compose exec -T` reads stdin, so it consumed the rest of the here-string feeding the loop — the other two were never queried and vanished silently. The same silent-truncation shape the diagnosis exists to expose, in the diagnosis. `< /dev/null` closes it. --- tests/api/storage_cleanup_check.sh | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/api/storage_cleanup_check.sh b/tests/api/storage_cleanup_check.sh index 2bee36e2..66117a5d 100755 --- a/tests/api/storage_cleanup_check.sh +++ b/tests/api/storage_cleanup_check.sh @@ -447,8 +447,21 @@ if [[ -n "$BLOB_FILES" ]]; then || ' blob_refs=' || COALESCE((SELECT ref_count::text FROM storage.blobs WHERE hash='$h'), '-') || ' files=' || (SELECT count(*) FROM storage.files WHERE blob_hash='$h') || ' derived=' || (SELECT count(*) FROM storage.content_derived_blobs WHERE blob_hash='$h') - || ' attached=' || (SELECT count(*) FROM storage.file_attached_blobs WHERE blob_hash='$h');" \ + || ' attached=' || (SELECT count(*) FROM storage.file_attached_blobs WHERE blob_hash='$h') + -- When a derived row is what pins the blob, the question is + -- why its SOURCE was never reaped — purge_derived_blobs only + -- runs from the source's reap. Print the source and whether + -- anything still holds it. + || COALESCE((SELECT ' src=' || d.source_hash + || ' src_files=' || (SELECT count(*) FROM storage.files WHERE blob_hash = d.source_hash) + || ' src_manifest=' || COALESCE((SELECT ref_count::text FROM storage.chunk_manifests WHERE file_hash = d.source_hash), '-') + || ' src_blob=' || COALESCE((SELECT ref_count::text FROM storage.blobs WHERE hash = d.source_hash), '-') + FROM storage.content_derived_blobs d WHERE d.blob_hash='$h' LIMIT 1), '');" \ + < /dev/null \ 2> >(grep -v 'Executing external compose provider' >&2) || true + # `< /dev/null`: `docker compose exec -T` reads stdin, and without this it + # consumes the rest of the here-string — so only the FIRST leftover was + # ever diagnosed and the others vanished silently. done <<< "$BLOB_FILES" log "Leftover blob files ($BLOB_COUNT):"