Skip to main content

max / makenotwork

mnw: fix synckit blob orphan on 402 confirm paths (deepaudit F2) blob_confirm_upload cleared the pending_uploads row before the quota gate, so every 402 refusal (QuotaExceeded, NoSubscription, billing_inactive) left the staged S3 object with no sync_blobs row and no pending row -- invisible to cleanup_orphaned_uploads, permanently unreferenced and uncharged. The new 10 GB multipart ceiling multiplied each leak 20x. Move remove_pending_upload into the Stored|AlreadyStored arm only; every refusal path now leaves the pending row so the orphan reaper reclaims the object (verified it handles the Synckit bucket, cleanup.rs:592). 22 blob + orphan integration tests green. Not addressed (needs your call): F1 (multipart declared size unbound between start and parts) -- the report's fix persists the declared size on the pending_uploads row, which needs a prod migration; flagging rather than authoring a money-path migration autonomously. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-24 19:56 UTC
Commit: fc59ca8d49f3c93ad3b2c6ff5ac27638a411ecaf
Parent: f886db8
1 file changed, +6 insertions, -3 deletions
@@ -493,9 +493,6 @@ pub(super) async fn blob_confirm_upload(
493 493 )));
494 494 }
495 495
496 - // Clear the pending upload record now that the upload is confirmed.
497 - db::pending_uploads::remove_pending_upload(&db, sync_user.user_id, &s3_key, "synckit").await?;
498 -
499 496 // Record the blob and enforce the cap atomically. Internal apps gate on the
500 497 // user's paid subscription + per-user cap; developer apps on the app/per-key
501 498 // counters. Both are single-transaction (lock, check, insert, count).
@@ -536,6 +533,12 @@ pub(super) async fn blob_confirm_upload(
536 533
537 534 match outcome {
538 535 db::synckit::BlobConfirm::Stored | db::synckit::BlobConfirm::AlreadyStored => {
536 + // Only now that the blob is durably recorded do we drop the pending
537 + // row. Every refusal path below leaves it in place so the orphan reaper
538 + // (cleanup_orphaned_uploads) reclaims the unreferenced object; clearing
539 + // it before the quota gate stranded the object permanently and uncharged.
540 + db::pending_uploads::remove_pending_upload(&db, sync_user.user_id, &s3_key, "synckit")
541 + .await?;
539 542 Ok(StatusCode::NO_CONTENT.into_response())
540 543 }
541 544 db::synckit::BlobConfirm::NoSubscription => Ok((