Skip to content

Defer tentacle upgrade when a deployment script is in flight on the machine#443

Merged
ppXD merged 2 commits into
mainfrom
fix/p3-upgrade-defers-to-active-deploy
Jun 14, 2026
Merged

Defer tentacle upgrade when a deployment script is in flight on the machine#443
ppXD merged 2 commits into
mainfrom
fix/p3-upgrade-defers-to-active-deploy

Conversation

@ppXD

@ppXD ppXD commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • A tentacle upgrade restarts the agent process. If a deployment currently has a script in flight on that agent (recorded before the StartScript RPC, per the in-flight checkpoint), the restart kills the running script. The upgrade now consults the deployment checkpoint's in-flight state and defers (returns Failed with a retry-later detail) when the machine is busy, instead of dispatching.
  • Asymmetric by design: only the upgrade — rare, operator-triggered, already holding the per-machine upgrade lock — checks for an active deployment. The deployment path is unchanged: no new lock, no self-contention on parallel same-machine StartWithPrevious steps, no new dependency on the deploy hot path. (The earlier machine-exclusive-mutex approach was abandoned precisely because a symmetric lock over-serialized coexisting deploys.)
  • New IInFlightScriptStore.IsMachineBusyAsync(machineId) scans only checkpoints that still carry in-flight entries — a checkpoint exists only for an active/paused deployment and is deleted on success — so the scan set stays small. Backed by a pure InFlightScriptMap.ContainsMachine helper.

Placement & residual window

The check sits inside the existing per-machine upgrade Redis lock, immediately before dispatch and before the H4 metadata write — so a deferred upgrade leaves no metadata stragglers, and the cheap up-to-date / downgrade short-circuits still run first (a no-op upgrade never scans). The TOCTOU window between "machine is free" and "agent restarts" is minimal and accepted: a deploy that starts a script in that sub-second window is itself resilient — a killed in-flight script is classified transient and pauses/re-attaches on resume (#434), rather than failing the deployment.

Failed (not a new enum value) matches the established H4 contention convention for "couldn't dispatch right now, retry later" — non-breaking.

Test plan

  • Unit — InFlightScriptMap.ContainsMachine: matches any slot for the machine regardless of step/action; other-machine-only → false; empty/malformed/legacy JSON → false (never falsely blocks).
  • Unit — MachineUpgradeService: defers without dispatching when the store reports busy (asserts the strategy is never invoked + the operator-facing detail); proceeds to dispatch when free.
  • Integration (real Postgres) — InFlightScriptStore.IsMachineBusyAsync: in-flight script → busy (unrelated machine not); cleared slot → free; scan spans tasks (two tasks' in-flight machines both reported); untouched machine → free.
  • Full unit suite green (6035) + InFlightScriptStore integration suite green (13).

…achine

A tentacle upgrade restarts the agent. If a deployment currently has a
script in flight on that agent (recorded before the StartScript RPC), the
restart kills the running script. The upgrade now consults the deployment
checkpoint's in-flight state and defers (Failed, retry-later) when the
machine is busy, instead of dispatching.

Asymmetric by design: only the upgrade — rare, operator-triggered, already
holding the per-machine upgrade lock — checks for an active deployment. The
deployment path is unchanged: no new lock, no self-contention on parallel
same-machine steps, no extra dependency. The check sits inside the existing
upgrade lock immediately before dispatch, so the TOCTOU window is minimal,
and a deploy that begins in that window is itself resilient — a killed
in-flight script pauses and re-attaches on resume.

IInFlightScriptStore.IsMachineBusyAsync scans only checkpoints that still
carry in-flight entries (a checkpoint exists only for an active/paused
deployment, deleted on success), so the scan set stays small.
@ppXD ppXD added this to the 1.9.1 milestone Jun 14, 2026
…DI wiring

Adversarial review of the change surfaced three fix-optional items, all
addressed here:

- IsMachineBusyAsync excluded only "[]" but not "{}", the idle value
  EnsureExistsAsync seeds and the column default. Idle checkpoints leaked
  past the DB filter (still correct via the parse-fallback, but wasteful
  and contrary to the doc-comment). Exclude both idle shapes.
- Nothing pinned that the optional IInFlightScriptStore ctor param is
  actually injected in production — the all-mock unit suite would not catch
  the IScopedDependency scan dropping the store, silently disabling the
  guard. Add an integration test asserting the real SquidModule resolves
  IInFlightScriptStore to the real store. (A full UpgradeAsync drive-through
  is infeasible in integration: the guard runs inside the Redis lock and
  RedisSafeRunner eagerly connects, which the integration DI config lacks.)
- The deferral shares MachineUpgradeStatus.Failed with genuine failures and
  is distinguished only by Detail text, which a unit test pins. Note that
  the text is a load-bearing contract at the guard so it is reworded in
  lockstep.

Plus an integration test that a seeded-but-never-dispatched checkpoint
("{}") reports no machine busy.
@ppXD
ppXD merged commit 8c611ac into main Jun 14, 2026
15 checks passed
@ppXD
ppXD deleted the fix/p3-upgrade-defers-to-active-deploy branch June 14, 2026 23:38
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