Problem
UploadPending currently coordinates quota in PostgreSQL, while object PUT/delete happens in R2 outside the database transaction. The request paths have no immutable per-key claim or lease spanning those two systems.
Two concrete interleavings were found while reviewing #570:
- An upload completion can enter its serializable transaction before expiry while a stale-session sweep observes the row after expiry, deletes the R2 object, and then loses its
deleteMany race. The completion can commit Upload metadata for an object that no longer exists.
- A PUT can validate an active pending row, remain in flight while expiry/completion/cleanup removes the row/object, and then finish late, recreating an R2 object with no owning
UploadPending or Upload row. Concurrent/repeated PUTs can also overwrite an object after completion measured its metadata.
PostgreSQL serializable isolation alone cannot roll back or order an R2 side effect.
Current safety decision
#570 should not add opportunistic R2 cleanup to ordinary create/complete requests. Until a proven protocol exists, preserve data integrity and accept that expired objects may require later garbage collection; do not risk deleting an object referenced by committed metadata.
Proposed direction
Use a per-key state/claim protocol, for example reserved -> uploading -> uploaded -> completing -> cleaning, with an immutable attempt ID and bounded lease expiry, or an equivalent protocol with the same guarantees. The exact schema is open to design, but it must coordinate:
- authenticated raw PUT;
- upload completion and idempotent retries;
- expiry/quota cleanup;
- explicit upload deletion;
- worker/process failure and lease recovery.
Cleanup should run as its own bounded lifecycle job, not piggyback on unrelated auth cleanup or static importer workflows.
Acceptance criteria
Related
Problem
UploadPendingcurrently coordinates quota in PostgreSQL, while object PUT/delete happens in R2 outside the database transaction. The request paths have no immutable per-key claim or lease spanning those two systems.Two concrete interleavings were found while reviewing #570:
deleteManyrace. The completion can commitUploadmetadata for an object that no longer exists.UploadPendingorUploadrow. Concurrent/repeated PUTs can also overwrite an object after completion measured its metadata.PostgreSQL serializable isolation alone cannot roll back or order an R2 side effect.
Current safety decision
#570 should not add opportunistic R2 cleanup to ordinary create/complete requests. Until a proven protocol exists, preserve data integrity and accept that expired objects may require later garbage collection; do not risk deleting an object referenced by committed metadata.
Proposed direction
Use a per-key state/claim protocol, for example
reserved -> uploading -> uploaded -> completing -> cleaning, with an immutable attempt ID and bounded lease expiry, or an equivalent protocol with the same guarantees. The exact schema is open to design, but it must coordinate:Cleanup should run as its own bounded lifecycle job, not piggyback on unrelated auth cleanup or static importer workflows.
Acceptance criteria
Related