Skip to content

Record the in-flight ScriptTicket before the StartScript RPC#431

Merged
ppXD merged 1 commit into
mainfrom
fix/start-script-record-before-rpc
Jun 11, 2026
Merged

Record the in-flight ScriptTicket before the StartScript RPC#431
ppXD merged 1 commit into
mainfrom
fix/start-script-record-before-rpc

Conversation

@ppXD

@ppXD ppXD commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • DispatchOrReattachAsync recorded the in-flight ScriptTicket after StartScriptAsync returned (HalibutMachineExecutionStrategy.cs). A server crash — or a lost RPC response — in the window between the agent launching the script and the server committing the ticket left no durable pointer, so the resumed run dispatched a fresh ticket and ran the (non-idempotent) deployment script a second time.
  • Fix: record the ticket before firing StartScript (a one-statement reorder). A resume then always knows a ticket to re-probe — TryReattachAsync re-attaches if the agent still holds the script, or falls through to a clean fresh dispatch (via the agent's Complete+UnknownResult unknown-ticket sentinel) if it never started.

This is the StartScript half of the SOTA-audit P1 (server-side idempotency, aligning with Octopus ScriptServiceV2). The RPC-retry + observe-loop pointer-preservation half (GetStatus/CompleteScript) follows in a separate PR.

Why it's non-breaking + generic

  • Pure reorder of two adjacent calls that already exist; reuses the same IInFlightScriptStore and checkpoint column. No new state, no signature change.
  • It only ever avoids a duplicate — the agent is idempotent for a known ticket (LocalScriptService/ScriptPodService dedupe by ticket), so a retried/known StartScript returns status, not a second launch.
  • Composes with the existing reattach (Serialize InFlightScriptStore reads on the per-task lock stripe #430 stripe-locking), checkpoint-preserving timeout (Pause and preserve checkpoint on deployment timeout, add manual resume #428), and circuit breaker — it makes the reattach path the recovery mechanism, not a victim. If RecordDispatchedAsync itself fails, the dispatch now aborts cleanly before any agent-side launch (strictly safer than the old order).

Test plan

  • Integration regression HalibutResumeReattachTests.FreshDispatch_RecordsInFlightTicket_BeforeStartScriptRpc — a hook captures, at the instant StartScript fires, what the store has durably committed (read through a fresh DI scope, as a resumed server would). Verified red→green: fails with "record-before-RPC violated" before the reorder, passes after.
  • Checkpoints integration sweep: 12/12. Halibut/reattach unit: 158/158. dotnet build 0 errors.

DispatchOrReattachAsync recorded the in-flight ticket only AFTER
StartScriptAsync returned. If the server crashed (or the RPC response was
lost) in the window between the agent launching the script and that
record committing, the resumed run found no pointer and dispatched a
fresh ticket — running the (non-idempotent) deployment script a second
time.

Record the ticket BEFORE firing StartScript so a resume always knows a
ticket to re-probe: TryReattachAsync re-attaches if the agent still has
the script, and falls through to a clean fresh dispatch (via the
Complete+UnknownResult sentinel) if it never started. The agent is
idempotent for a known ticket, so this only ever avoids a duplicate, and
it composes with the existing reattach/checkpoint/circuit-breaker design
rather than changing it.

Adds an integration regression test that captures, at the instant
StartScript fires, what the store has durably recorded: red before the
reorder, green after.
@ppXD ppXD added this to the 1.8.19 milestone Jun 11, 2026
@ppXD
ppXD merged commit deccd86 into main Jun 11, 2026
14 of 17 checks passed
@ppXD
ppXD deleted the fix/start-script-record-before-rpc branch June 11, 2026 03:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant