Use shared per-bundle locks for asset transfer and render#2464
Use shared per-bundle locks for asset transfer and render#2464AbanoubGhadban merged 10 commits intomasterfrom
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughSwitches upload handling from a single global lock to per-bundle locks (derived from getRequestBundleFilePath), implements per-bundle copy flows with aggregate settling and standardized lock errors, updates render-path aggregation to await all bundle operations, and expands tests to validate cross-endpoint coordination. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant UploadHandler as Upload (/worker.ts)
participant RenderHandler as Render (/handleRenderRequest.ts)
participant LockMgr as LockManager (locks.ts)
participant FS as FileSystem
Note over Client,UploadHandler: Upload flow
Client->>UploadHandler: POST /upload-assets (bundleTimestamp)
UploadHandler->>LockMgr: getRequestBundleFilePath(ts) -> acquire lock(bundlePath)
alt lock acquired
UploadHandler->>FS: copyUploadedAssets -> write files to bundlePath
FS-->>UploadHandler: copy complete
UploadHandler->>LockMgr: release lock(bundlePath)
UploadHandler-->>Client: 200 OK
else lock failed
LockMgr-->>UploadHandler: lock error
UploadHandler-->>Client: error
end
Note over Client,RenderHandler: Render-request flow (may target same bundle)
Client->>RenderHandler: POST /bundles/... (bundleTimestamp)
RenderHandler->>LockMgr: getRequestBundleFilePath(ts) -> acquire lock(bundlePath)
alt lock acquired
RenderHandler->>FS: copyUploadedAssets / write render-request files
FS-->>RenderHandler: copy complete
RenderHandler->>LockMgr: release lock(bundlePath)
RenderHandler-->>Client: 200 OK
else lock failed
LockMgr-->>RenderHandler: lock error
RenderHandler-->>Client: error
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR replaces the global Key changes:
The implementation correctly handles lock failures by throwing and stopping execution, while allowing successful locks to be released in their respective finally blocks. Confidence Score: 4/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant Upload as /upload-assets
participant Render as /render
participant Lock as Lock System
participant FS as File System
Note over Upload,Render: Concurrent requests to same bundle
Upload->>Lock: lock(bundle-123.js)
activate Lock
Lock-->>Upload: acquired
Render->>Lock: lock(bundle-123.js)
Note right of Lock: Waits - same bundle
Upload->>FS: copy assets to bundle-123/
Upload->>Lock: unlock(bundle-123.js)
deactivate Lock
activate Lock
Lock-->>Render: acquired
Render->>FS: write bundle-123/bundle-123.js
Render->>Lock: unlock(bundle-123.js)
deactivate Lock
Note over Upload,Render: Different bundles run in parallel
Upload->>Lock: lock(bundle-456.js)
activate Lock
Lock-->>Upload: acquired
Render->>Lock: lock(bundle-789.js)
activate Lock
Lock-->>Render: acquired (different bundle)
par Parallel execution
Upload->>FS: copy assets to bundle-456/
Upload->>Lock: unlock(bundle-456.js)
deactivate Lock
and
Render->>FS: write bundle-789/bundle-789.js
Render->>Lock: unlock(bundle-789.js)
deactivate Lock
end
Last reviewed commit: 53f6ce9 |
Additional Comments (1)
|
Review: Per-bundle lock for /upload-assetsThe core idea is sound: using That said, there are a few issues to address before merging. 1. Partial-failure semantics with
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
packages/react-on-rails-pro-node-renderer/src/worker.ts (1)
350-357:formatExceptionMessageis semantically misused here — consider a plain error string instead
formatExceptionMessageis designed for JS SSR rendering errors and embeds the label"JS code for rendering request was:"around its first argument. PassingtaskDescription(an asset-upload description) as that argument produces confusing log output:JS code for rendering request was: Uploading files [...] to bundle directories: [...]A simple template string would be clearer and avoids the misleading label:
♻️ Suggested alternative
- const msg = formatExceptionMessage( - taskDescription, - errorMessage, - `Failed to acquire lock ${lockfileName}. Worker: ${workerIdLabel()}.`, - ); - throw new Error(msg); + throw new Error( + `Failed to acquire lock ${lockfileName} for worker ${workerIdLabel()}: ${(errorMessage as Error)?.message ?? errorMessage}`, + );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/react-on-rails-pro-node-renderer/src/worker.ts` around lines 350 - 357, The code misuses formatExceptionMessage (meant for JS SSR errors) when wasLockAcquired is false, producing misleading "JS code for rendering request was:" labels; replace the formatExceptionMessage call with a plain, descriptive error string that includes taskDescription, errorMessage, the lockfileName, and workerIdLabel() (i.e., construct a template message like "Failed to acquire lock <lockfileName> for <taskDescription>: <errorMessage>. Worker: <workerIdLabel()>."), then throw new Error(msg) — update the block that references wasLockAcquired, taskDescription, errorMessage, lockfileName, and workerIdLabel to use this plain message instead of formatExceptionMessage.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/react-on-rails-pro-node-renderer/src/worker.ts`:
- Around line 343-393: The current use of Promise.all over copyPromises (created
from targetBundles) causes fast-fail behavior so a single bundle failure (from
lock() or copyUploadedAssets()) returns a 400 even though other bundle copies
completed; change the aggregation to Promise.allSettled(copyPromises), inspect
the settled results, and build the HTTP response via setResponse: return a 200
when all settled results are fulfilled (as today), otherwise return a response
that includes per-bundle statuses (which bundles succeeded vs failed) and the
error details (use errorResponseResult or a new error payload) so callers can
see partial success; keep existing unlock(lockfileName) and finally handling in
the per-bundle async function intact and reference functions/variables like
targetBundles, copyPromises, copyUploadedAssets, lock, unlock, setResponse, and
errorResponseResult when making the change.
---
Nitpick comments:
In `@packages/react-on-rails-pro-node-renderer/src/worker.ts`:
- Around line 350-357: The code misuses formatExceptionMessage (meant for JS SSR
errors) when wasLockAcquired is false, producing misleading "JS code for
rendering request was:" labels; replace the formatExceptionMessage call with a
plain, descriptive error string that includes taskDescription, errorMessage, the
lockfileName, and workerIdLabel() (i.e., construct a template message like
"Failed to acquire lock <lockfileName> for <taskDescription>: <errorMessage>.
Worker: <workerIdLabel()>."), then throw new Error(msg) — update the block that
references wasLockAcquired, taskDescription, errorMessage, lockfileName, and
workerIdLabel to use this plain message instead of formatExceptionMessage.
Review: Use shared per-bundle locks for asset transfer and renderThe core idea is sound and a clear improvement: replacing the global Issues found1. Partial-success inconsistency with multi-bundle requests (moderate) When In practice this is rare (lock failures require ~13.5 s of contention), and a retry with 2. Double-embedded When a lock fails, Additionally, 3. Inconsistent lock-failure pattern vs
Positive notes
|
|
Responding to both Claude reviews: First review (on commit before test update):
These were all addressed before the review was posted (the first review was based on the first commit only). Both reviews — Promise.all partial-success: formatExceptionMessage misuse + double taskDescription: Inconsistency with handleRenderRequest.ts: |
Review: Per-bundle lock sharing between
|
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
packages/react-on-rails-pro-node-renderer/src/worker.ts (1)
379-385: Non-first failures fromallSettledare silently dropped.Only
firstFailure.reasonis re-thrown. If multiple bundles fail (e.g., two lock timeouts), only one error is surfaced in the HTTP response and in the structured log at lines 397-401. The other failures vanish.Consider logging all rejected reasons before throwing:
Proposed improvement
const results = await Promise.allSettled(copyPromises); - const firstFailure = results.find( - (r): r is PromiseRejectedResult => r.status === 'rejected', - ); - if (firstFailure) { - throw firstFailure.reason; + const failures = results.filter( + (r): r is PromiseRejectedResult => r.status === 'rejected', + ); + if (failures.length > 0) { + if (failures.length > 1) { + log.warn({ + msg: `${failures.length} bundle copy operations failed`, + errors: failures.map((f) => String(f.reason)), + task: taskDescription, + }); + } + throw failures[0].reason; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/react-on-rails-pro-node-renderer/src/worker.ts` around lines 379 - 385, The current Promise.allSettled handling only re-throws the first failure (firstFailure.reason) so other rejected promises from copyPromises are lost; update the logic around results (from Promise.allSettled) and firstFailure (typed as PromiseRejectedResult) to collect all rejected results, log or aggregate their .reason values (e.g., join messages or wrap in an AggregateError) and then throw a combined error (or the AggregateError) so every failure is surfaced rather than only the first.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/react-on-rails-pro-node-renderer/src/worker.ts`:
- Around line 354-361: The lock-acquisition error path currently duplicates the
task description: formatExceptionMessage(taskDescription, ...) in the lock
failure throws an error that is later caught and has "Task: ${taskDescription}"
appended again; fix by removing the redundant append in the catch path (the
string that adds "Task: ${taskDescription}") so the caught error message isn't
doubled, or alternatively change the formatExceptionMessage invocation in the
lock-failure branch to pass a shorter label (e.g., an empty string or brief ID)
instead of taskDescription; update the code references around
formatExceptionMessage, wasLockAcquired, workerIdLabel, and the catch block that
appends Task: ${taskDescription} accordingly.
- Around line 338-403: The lock-acquisition error handling misuses
formatExceptionMessage (it expects a rendering request) and duplicates
taskDescription in the final error response; replace the formatExceptionMessage
call inside the copyPromises map (where lock, lockfileName, wasLockAcquired,
errorMessage are handled) with a dedicated lock error string that includes
errorMessage and lockfileName (e.g., `Failed to acquire lock ${lockfileName}:
${errorMessage}`) and throw that Error, and when catching errors in the outer
catch block avoid appending taskDescription twice by building the response
message once (use the thrown error as err and supply taskDescription only via
the log's task field or include it once in the message passed to
errorResponseResult); update references around lock(), formatExceptionMessage,
taskDescription, setResponse, and errorResponseResult accordingly.
---
Nitpick comments:
In `@packages/react-on-rails-pro-node-renderer/src/worker.ts`:
- Around line 379-385: The current Promise.allSettled handling only re-throws
the first failure (firstFailure.reason) so other rejected promises from
copyPromises are lost; update the logic around results (from Promise.allSettled)
and firstFailure (typed as PromiseRejectedResult) to collect all rejected
results, log or aggregate their .reason values (e.g., join messages or wrap in
an AggregateError) and then throw a combined error (or the AggregateError) so
every failure is surfaced rather than only the first.
Review SummaryThe core idea — replacing the global A few issues worth addressing before merge: Functional issues
Test coverage gap
Minor / style
|
There was a problem hiding this comment.
🧹 Nitpick comments (2)
packages/react-on-rails-pro-node-renderer/src/worker/handleRenderRequest.ts (2)
163-176:Promise.allSettled+ re-throw preserves external semantics — LGTM.After the
firstFailureguard throws on line 171, every element insettledis guaranteed to bePromiseFulfilledResult. The cast on line 174 is therefore safe; however, a type-guard filter avoids theascast and keeps TypeScript happy without assertion:♻️ Optional: replace the cast with a type-guard filter
- const results = settled.map((r) => (r as PromiseFulfilledResult<ResponseResult | undefined>).value); + const results = settled + .filter((r): r is PromiseFulfilledResult<ResponseResult | undefined> => r.status === 'fulfilled') + .map((r) => r.value);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/react-on-rails-pro-node-renderer/src/worker/handleRenderRequest.ts` around lines 163 - 176, Replace the unsafe type assertion when mapping settled results by filtering settled for fulfilled entries instead of using "as": after computing "settled" and checking "firstFailure", use a type-guard to select only PromiseFulfilledResult items (from the "settled" array) and then map their .value to produce "results" (of type ResponseResult | undefined), referencing the existing variables "settled", "firstFailure", "handlingPromises" and the ResponseResult type so the code no longer needs the "as PromiseFulfilledResult<...>" cast.
198-198: Remove stale commented-out code.Line 198 is dead code left over from a prior refactor and adds noise without value.
♻️ Proposed removal
- // const bundleFilePathPerTimestamp = getRequestBundleFilePath(bundleTimestamp);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/react-on-rails-pro-node-renderer/src/worker/handleRenderRequest.ts` at line 198, Remove the stale commented-out line that declares bundleFilePathPerTimestamp (the commented "const bundleFilePathPerTimestamp = getRequestBundleFilePath(bundleTimestamp);") from inside the handleRenderRequest function; it's dead code left from a refactor and should simply be deleted to reduce noise and keep the function body clean.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@packages/react-on-rails-pro-node-renderer/src/worker/handleRenderRequest.ts`:
- Around line 163-176: Replace the unsafe type assertion when mapping settled
results by filtering settled for fulfilled entries instead of using "as": after
computing "settled" and checking "firstFailure", use a type-guard to select only
PromiseFulfilledResult items (from the "settled" array) and then map their
.value to produce "results" (of type ResponseResult | undefined), referencing
the existing variables "settled", "firstFailure", "handlingPromises" and the
ResponseResult type so the code no longer needs the "as
PromiseFulfilledResult<...>" cast.
- Line 198: Remove the stale commented-out line that declares
bundleFilePathPerTimestamp (the commented "const bundleFilePathPerTimestamp =
getRequestBundleFilePath(bundleTimestamp);") from inside the handleRenderRequest
function; it's dead code left from a refactor and should simply be deleted to
reduce noise and keep the function body clean.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/react-on-rails-pro-node-renderer/src/worker/handleRenderRequest.ts (1)
176-176: Optional: preferfilterover cast for stronger type narrowingThe
as PromiseFulfilledResult<...>cast is safe here (all rejections were already thrown), but a filter-based approach avoids the unsafe cast and is more idiomatic TypeScript.♻️ Type-safe alternative
- const results = settled.map((r) => (r as PromiseFulfilledResult<ResponseResult | undefined>).value); + const results = settled + .filter((r): r is PromiseFulfilledResult<ResponseResult | undefined> => r.status === 'fulfilled') + .map((r) => r.value);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/react-on-rails-pro-node-renderer/src/worker/handleRenderRequest.ts` at line 176, The current line uses an unsafe cast to extract values from settled: "const results = settled.map((r) => (r as PromiseFulfilledResult<ResponseResult | undefined>).value);". Replace this with a type-safe filter+map by first filtering settled for entries with r.status === 'fulfilled' and then mapping to r.value so TypeScript narrows to PromiseFulfilledResult<ResponseResult | undefined> without the cast; update any uses of results accordingly (refer to the settled variable and the results binding).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@packages/react-on-rails-pro-node-renderer/src/worker/handleRenderRequest.ts`:
- Line 176: The current line uses an unsafe cast to extract values from settled:
"const results = settled.map((r) => (r as PromiseFulfilledResult<ResponseResult
| undefined>).value);". Replace this with a type-safe filter+map by first
filtering settled for entries with r.status === 'fulfilled' and then mapping to
r.value so TypeScript narrows to PromiseFulfilledResult<ResponseResult |
undefined> without the cast; update any uses of results accordingly (refer to
the settled variable and the results binding).
Review: Per-bundle locking for asset transfer and renderThe core design is correct and well-motivated. Replacing the global A few issues worth addressing: Partial bundle failures silently droppedIn the Aggregating all failure reasons (e.g. collecting all rejected reasons into one error message before throwing) would give operators enough information to diagnose multi-bundle upload failures without guessing.
|
## Summary - Replaces the global `transferring-assets` lock in the `/upload-assets` endpoint with per-bundle locks using the same lock key (`getRequestBundleFilePath`) as `handleRenderRequest` - This ensures that asset copies from `/upload-assets` and bundle writes from render requests to the same bundle directory are mutually exclusive, preventing race conditions that could corrupt assets - Different bundles can now be processed concurrently (improved throughput vs the old global lock) - Switches `Promise.all` to `Promise.allSettled` in both `/upload-assets` and `handleNewBundlesProvided` to prevent a cleanup race: `Promise.all` short-circuits on first rejection, leaving in-flight copies running while the `onResponse` hook deletes `req.uploadDir` Closes #2463 ## Test plan - [x] Existing `uploadRaceCondition.test.ts` tests pass (concurrent `/upload-assets` isolation) - [x] Existing `handleRenderRequest.test.ts` tests pass (per-bundle lock behavior) - [x] Cross-endpoint race condition test verifying `/upload-assets` and render requests coordinate via the shared lock - [x] CI passes 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Switched to per-bundle locking and per-bundle asset copy flows, parallelized bundle processing, ensured all bundle operations settle before responding, and improved logging and standardized error handling. * **Tests** * Expanded tests and helpers to validate per-bundle isolation, cross-endpoint coordination, and concurrent uploads/renders targeting the same bundle directory without contamination. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
## Summary - Replaces the global `transferring-assets` lock in the `/upload-assets` endpoint with per-bundle locks using the same lock key (`getRequestBundleFilePath`) as `handleRenderRequest` - This ensures that asset copies from `/upload-assets` and bundle writes from render requests to the same bundle directory are mutually exclusive, preventing race conditions that could corrupt assets - Different bundles can now be processed concurrently (improved throughput vs the old global lock) - Switches `Promise.all` to `Promise.allSettled` in both `/upload-assets` and `handleNewBundlesProvided` to prevent a cleanup race: `Promise.all` short-circuits on first rejection, leaving in-flight copies running while the `onResponse` hook deletes `req.uploadDir` Closes #2463 ## Test plan - [x] Existing `uploadRaceCondition.test.ts` tests pass (concurrent `/upload-assets` isolation) - [x] Existing `handleRenderRequest.test.ts` tests pass (per-bundle lock behavior) - [x] Cross-endpoint race condition test verifying `/upload-assets` and render requests coordinate via the shared lock - [x] CI passes 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Switched to per-bundle locking and per-bundle asset copy flows, parallelized bundle processing, ensured all bundle operations settle before responding, and improved logging and standardized error handling. * **Tests** * Expanded tests and helpers to validate per-bundle isolation, cross-endpoint coordination, and concurrent uploads/renders targeting the same bundle directory without contamination. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
Summary
transferring-assetslock in the/upload-assetsendpoint with per-bundle locks using the same lock key (getRequestBundleFilePath) ashandleRenderRequest/upload-assetsand bundle writes from render requests to the same bundle directory are mutually exclusive, preventing race conditions that could corrupt assetsPromise.alltoPromise.allSettledin both/upload-assetsandhandleNewBundlesProvidedto prevent a cleanup race:Promise.allshort-circuits on first rejection, leaving in-flight copies running while theonResponsehook deletesreq.uploadDirCloses #2463
Test plan
uploadRaceCondition.test.tstests pass (concurrent/upload-assetsisolation)handleRenderRequest.test.tstests pass (per-bundle lock behavior)/upload-assetsand render requests coordinate via the shared lock🤖 Generated with Claude Code
Summary by CodeRabbit
Refactor
Tests