Repository navigation
fix(files): lock the workspace row before inserting a workspace file - #8972
Conversation
Upload, upload-session registration, owned-file creation, and fork file copy inserted the workspace_files row first and locked the workspace row only in the later storage increment. The insert's search-index trigger upserts the per-workspace workspace_file_search_dispatch_queue row, so these transactions held the queue row while waiting on the workspace row. updateWorkspaceFileContent takes the same two locks in the opposite order (workspace row, then the content UPDATE whose trigger reaches the queue row), and the two deadlocked. Take lockWorkspaceStorageForMutationInTx before the insert in each path so every storage-charging file write locks folder -> workspace row -> file rows/queue -> payer, and document that order on the lock helper.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
There was a problem hiding this comment.
All reported issues were addressed across 4 files
Heads up: you’ve reached your flex budget. Increase your flex budget or wait for usage to reset.
Turn on auto-fix | Re-trigger cubic
|
…cope the folder-lock wording
|
@greptile |
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
No issues found across 4 files
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Heads up: you’ve reached your flex budget. Increase your flex budget or wait for usage to reset.
Turn on auto-fix | Re-trigger cubic
Summary
deadlock detected(40P01) between a file content write and a concurrent new-file insert in the same workspace. It recurs in bursts when a workspace gets concurrent uploads and overwrites, e.g. an agent writing many files.updateWorkspaceFileContentlocks the workspace row (lockWorkspaceStorageForMutationInTx,FOR NO KEY UPDATE), then UPDATEs the file. The AFTER triggerworkspace_files_search_index_pendingthen upserts the one-row-per-workspaceworkspace_file_search_dispatch_queuerow.uploadWorkspaceFile,registerUploadedWorkspaceFileand forkexecuteForkFileBlobCopiesINSERT the file first, so the trigger takes the queue row. They lock the workspace row only later, inincrementStorageUsageForBillingContextInTx.select … from workspace … for no key update.lockWorkspaceStorageForMutationInTxright before the file insert in those three paths and increateOwnedWorkspaceFile.createOwnedWorkspaceFileuses non-workspace contexts, so the trigger never reaches the queue row today. It gets the same order for consistency.lockWorkspaceStorageForMutationInTx: folder advisory lock → workspace row → file rows → file writes (trigger queue row) → payer row.ON CONFLICT DO NOTHINGwould lose wakeups.Why it's safe
acquireFolderMutationLock/withFolderTreeLockcall is the first lock in its transaction. Mapped-import andlockForkRevisiontake the folder lock before the workspace row.purgeCreatedWorkspaceFile, cleanup-soft-deletes, payer transfer, and fork KB rollback.workspace_filesinserts don't charge storage in the same transaction, or don't reach the queue row:mothershipcontext.executioncontext and have no tx.knowledge-basecontext, which the trigger skips.SKIP LOCKED.lock_timeoutthe folder lock already sets. The lock is taken after folder-path resolution, so the workspace row is held only across the insert and the increment that already followed it.Type of Change
Testing
apps/sim/lib/uploads/contexts/workspace/__integration__/storage-lock-order.integration.tsruns against real Postgres, once each foruploadWorkspaceFileandregisterUploadedWorkspaceFile:updateWorkspaceFileContentdoes.content_updated_aton an existing file in the same workspace. Its trigger upserts the queue row the seed upload created.deadlock_timeout(1s, well under the 5slock_timeout), so a settled result isrejected.check:auditsor integration suites locally; CI runs them.Checklist
test-auditauthoring gate)