Repository navigation
fix(v2-orchestrator): initialize workspaces through a durable callback - #638
mattdevdba wants to merge 3 commits into
Conversation
eipasteur
left a comment
There was a problem hiding this comment.
[P2] Concurrent dispatches can start two init jobs — lambda/agentcore/commands/init-ws-start.js:63–90
Two dispatches arriving while the first heartbeat is pending both see an empty activeJobs entry and launch initWs against the same workspace. I reproduced this with identical and different callback IDs: both requests are accepted and two jobs start. The second job also overwrites the tracked entry, which the first job’s cleanup can remove while the second is still running.
I think this is worth addressing before merge, since it allows concurrent clones and branch operations in the same workspace. One approach could be to reserve the entry before awaiting the heartbeat, let identical callbacks share the acceptance result, and make cleanup conditional on the job still owning that entry.
A test with overlapping initial heartbeats would help cover this case; the current tests wait for the first acceptance before sending the second request.
|
Thanks @eipasteur, good catch. Addressed in d148021, along the lines you suggested:
New tests cover overlapping initial heartbeats with identical and different callback IDs, a failed initial heartbeat shared with the duplicate, and cleanup not removing another job's entry. Three of them fail against the previous implementation. The agentcore suite, |
jeromevdl
left a comment
There was a problem hiding this comment.
[P2] A dispatch retry after the init job finished is treated as a new job: lambda/agentcore/commands/init-ws-start.js (finally → release)
d148021 covers duplicates that overlap the initial heartbeat or the running job, but the activeJobs entry is released when the job completes. init-ws-dispatch uses the default step retry, which retries every error. If the dispatch call throws after the container already accepted it (lost response, SDK timeout) and the job finishes before the retry arrives, the retry finds no entry and starts over. It then sends an initial heartbeat to a callback that is already completed:
- If the service rejects that heartbeat, the dispatch returns
init_ws_callback_heartbeat_failedandinitWorkspacefails the run withinit_ws_failed, although init succeeded. - If the service accepts it, a second
initWsruns in the workspace while stage 1 starts.
This is unlikely (it needs a lost response plus a fast init), and run-stage-start has the same gap. Still, both outcomes are wrong. Suggestion: keep completed callback ids for a bounded time and answer a same-callback retry with alreadyCompleted instead of starting a new job. Regression test: complete a job, dispatch the same callbackId again, and assert that initWs ran once and the response is a success.
Nit: the delivery-failure and heartbeat-failure logs don't include callbackId. It can be recovered from the durable execution history, but having it in the log makes the "complete it manually" comment practical.
Complexity and test coverageAutomated quality report using code complexity and test coverage to highlight potential hot spots in this PR.
🟢 Improving · 🟠 Needs attention · 🔴 High risk · 🟣 Highest risk — = unavailable · n/a = not applicable · * = incomplete tests |
init-ws ran as one synchronous step, so cloning a large repository was bounded by the orchestrator Lambda's 900-second timeout and the durable retry re-ran init-ws against a half-written checkout. New runs now dispatch an init-ws-start background job and suspend on a durable callback with a heartbeat, as runStage does. The callback has a 2-hour timeout and a 15-minute heartbeat timeout, and failures still surface as init_ws_failed. The mode is memoized with the run claim, so runs that checkpointed before this change replay the original init-ws step. A side-effect-free probe detects environment revisions built before init-ws-start and keeps the synchronous step for them. The intent API exposes orchestratorStartedAt so the intent page no longer flags a claimed run that is still initializing as stalled after 2 minutes. Fixes aws-samples#637.
Two init-ws-start dispatches arriving while the first heartbeat was pending both saw no active job and started a clone into the same workspace. The second also overwrote the tracked entry, which the first job's cleanup could then remove while the second was still running. The job is now reserved before the heartbeat is awaited. A duplicate of the same callback waits for and shares the first dispatch's acceptance, a different callback is refused, and cleanup only releases an entry the job still owns.
The init-ws-dispatch step retries every error. If the first response was lost and the job finished before the retry arrived, the reservation was already released, so the retry started a second clone and heartbeated a callback that was already completed. init-ws-start now remembers a finished job's callback id for an hour and answers a same-callback dispatch with alreadyCompleted. A different callback still starts a new job. The init-ws-start log records now carry the callbackId. Also give the final-completion and frozen-checkpoint replay tests added on main the init-ws-start container behaviour they now need.
d148021 to
534b1fe
Compare
|
Thanks @jeromevdl, agreed. Addressed in 534b1fe: a finished job's callback id is kept for an hour, and a same-callback dispatch retry is answered I also rebased onto |
Issue #, if available: Fixes #637
Description of changes:
Problem
The orchestrator runs
init-wsas one synchronous durable step, so the clone, intent-branch publish, Intent anchor and state seed all have to finish inside one orchestrator Lambda invocation. A large repository goes past the 900-second function timeout, and the durable retry runsinit-wsagain against a half-written checkout. Separately, the intent page shows a claimed run that is stillCREATEDafter 2 minutes as "Run stalled — never started", offering a restart that races the clone. Details are in #637.Solution
init-ws-start(AgentCore): runs the existinginit-wslogic as a background job. It establishes the callback heartbeat before cloning, keeps heartbeating while the clone runs, and completes the callback withinit-ws's own{ ok, reason, detail }result, including wheninit-wsthrows. A duplicate dispatch of the same attempt is acknowledged without a second clone, and a different attempt is refused while a clone is still writing the workspace. Registered inagent-command-registry.jsandhttp-server.js.initWorkspacereplaces the inlineinit-wsstep. New runs runinit-ws-probe,createCallback('init-ws-cb'), theninit-ws-dispatch, mirroringrunStage. The callback has a 2-hour timeout and a 15-minute heartbeat timeout, and every failure returns the same{ ok: false, reason }shape, so the caller'sinit_ws_failedpath is unchanged.initWsModefield in the memoizedmint-run-idresult. A run that checkpointed before this change has no such field and replays exactly the originalinit-wsstep. No existing operation is renamed or reordered.init-ws-startwithout acallbackId, so it starts no job. A current image refuses it withmissing_callback_idon HTTP 200. An older image answersunknown command(HTTP 400 through AgentCore), and the run keeps the synchronous step before any callback exists. A probe transport failure is retried like any durable step, not taken as an older image.mapIntentreturnsorchestratorStartedAt.IntentViewkeeps the 2-minute stall threshold for runs no orchestrator has claimed. For claimed runs it waits until the init callback window has passed (2 h 10 min).docs/concepts/execution.mddescribes the callback, its limits and the fallbacks.Testing
init-ws-start.test.jscovers the probe refusal, the accept while the clone runs, heartbeat set-up and continuation, failure delivery (including a throwinginit-ws), duplicate and conflicting dispatches, the busy tracker, and routing throughdispatchInvocation(including the 400 an image without the handler returns).initWsModeexisted (orchestrator.test.js,orchestrator-replay.test.js,section-integration.test.js,skeleton-switch.test.js).IntentView.test.tsxcovers a claimed run that is still initializing, an unclaimed hand-off after 2 minutes, and a claimed run after the callback window.npx vitest run --project=agentcore --project=v2-orchestrator --project=intents --project=sharedon Node 24.22.1: 4,727 tests pass. Two timing tests in files this PR doesn't touch (resolve-conflict.test.js,bedrock-iam-shell.test.js) timed out under the full parallel run and pass when run alone.npm --prefix frontend test: 976 pass.EnvironmentsTab.test.tsxfailed once under load and passes alone (28/28); this PR doesn't touch it.npm --prefix frontend run buildpasses.npm run lint,format:check,dep:checkandsecretlintpass.Impact
orchestratorStartedAt.By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.