Skip to content

fix: wait for the stopped state in mode:stop before returning#69

Merged
kurok merged 1 commit into
mainfrom
fix/stop-wait-for-stopped
Jul 10, 2026
Merged

fix: wait for the stopped state in mode:stop before returning#69
kurok merged 1 commit into
mainfrom
fix/stop-wait-for-stopped

Conversation

@kurok

@kurok kurok commented Jul 10, 2026

Copy link
Copy Markdown

Problem

mode: stop with reuse: stop returns as soon as StopInstances is accepted — the instance is still in the stopping state for the next ~30–60s. Every warm-pool lookup (findStoppedPoolInstance and workflow-side pre-attach filters) matches instance-state-name=stopped only, so a workflow run queued tightly behind the stopping one sees an empty pool and cold-launches a duplicate instance. The duplicate then sits stopped all day (the leak reaper's default reaper-stopped-max-age correctly skips same-day instances) and every subsequent run warns Found 2 stopped warm-pool instances (…); the reaper is not draining them.

Evidence (terraform-provider-namecheap CI, 2026-07-10, UTC)

time event
08:45:48.70 run A stop-runner: {"step":"stop_instance","instance_id":"i-060edd…"}is stopped (warm pool) printed 0.2 ms later
08:45:51 run A stop-runner job completes (instance actually in stopping)
08:45:55 run B's workflow-queue releases (run A finished)
08:46:04 run B: {"step":"warm_start","outcome":"pool_empty"} → cold-launches i-087854…
09:26 → 10:08 every run warns Found 2 stopped warm-pool instances

Same two-instance pattern the previous day (i-049890…/i-03e692…).

Fix

Block stopInstanceById on waitUntilInstanceStopped (maxWaitTime: 300, mirroring waitForInstanceRunning's conventions) so "mode: stop finished" implies "instance is reusable" — that is the serialization boundary queued follow-up runs rely on. A stop that can't complete within the window now fails the step loudly (previously a silent race).

Consumer-side counterpart (widening the pre-attach filter to stopped,stopping + aws ec2 wait instance-stopped) ships in the terraform-provider-namecheap workflow; either fix alone closes the race, together they're belt-and-braces.

Testing

  • stopInstanceById test asserts the waiter is invoked after StopInstances with the right instance id; new negative test asserts a waiter timeout propagates.
  • Full suite: 19 suites / 245 tests green; npm run lint clean; dist/ rebuilt via npm ci && npm run package.

Cost note

stop-runner jobs now run ~30–60s longer (hosted-runner minutes) — that's the price of the boundary being truthful; the duplicate t3.medium + EBS parked daily cost more.

StopInstances only moves the instance to 'stopping'; mode:stop with
reuse:stop returned immediately, so a workflow run queued right behind
the stopping one saw an empty warm pool (findStoppedPoolInstance and
consumers filter on instance-state-name=stopped) and cold-launched a
duplicate instance that the reaper does not drain until its
stopped-max-age passes.

Observed in terraform-provider-namecheap CI on 2026-07-10: run A's
stop-runner exited at 08:45:51 (0.2ms after StopInstances), run B's
queue released at 08:45:55, and at 08:46:04 run B logged
warm_start outcome:pool_empty and launched a second instance; every
subsequent run that day warned 'Found 2 stopped warm-pool instances'.

Block in stopInstanceById on waitUntilInstanceStopped (maxWaitTime
300s, mirroring waitForInstanceRunning) so 'mode:stop finished'
implies 'instance is reusable'. A stop that cannot complete within
the window now fails the step loudly instead of leaving a silent
stopping-state race.

Signed-off-by: yuriyryabikov <22548029+kurok@users.noreply.github.com>
@kurok kurok merged commit 54ee3e4 into main Jul 10, 2026
5 checks passed
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