ci: wait out the stopping-state warm-pool race; move nightly drain to 23:07 UTC - #290
Merged
Merged
Conversation
… 23:07 UTC The previous run's stop-runner returns while its instance is still in the 'stopping' state (StopInstances is fire-and-forget in the action), but every warm-pool lookup filters on instance-state-name=stopped. A run queued right behind it therefore saw an empty pool and cold-launched a duplicate instance that nothing drained until the nightly pass. Observed on 2026-07-10: run A stopped i-060edd at 08:45:51, run B's lookup at 08:46:04 hit warm_start outcome:pool_empty and launched i-087854; every later run that day warned 'Found 2 stopped warm-pool instances'. Same pattern the day before. Pre-attach step now also matches 'stopping' and runs aws ec2 wait instance-stopped before the action's own lookup, so a tightly-queued run reuses the instance instead of duplicating it. If the wait times out we fall back to a cold launch with a warning. ec2-github-runner#69 fixes the same race action-side (mode:stop waits for the stopped state); this consumer-side belt also covers older pins. The nightly full drain moves from 02:37 UTC to 23:07 UTC so the pool is destroyed right after the working day instead of mid-night: GitHub's scheduler delays these crons by hours (the 2026-07-10 drain fired at 06:27), and an end-of-day slot keeps even a badly delayed drain landing before the next workday. :07 offset per the same top-of-hour-congestion advice the leak reaper already follows. The Determine-pass comparison and SECURITY_COMPLIANCE.md reference are updated to match byte-for-byte. Signed-off-by: yuriyryabikov <22548029+kurok@users.noreply.github.com>
🔐 Security scan summaryCommit
📦 Dependencies
Vulnerabilities (Trivy, top 50)
📎 Full machine-readable reports are attached to this run as artifacts: |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #290 +/- ##
=======================================
Coverage 89.22% 89.22%
=======================================
Files 15 15
Lines 1643 1643
=======================================
Hits 1466 1466
Misses 111 111
Partials 66 66 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This was referenced Jul 10, 2026
dm1tr1yvovk
approved these changes
Jul 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Two changes to the warm-pool runner lifecycle, plus a serialization audit:
1. Close the stopping-state race (workflow side)
The previous run's stop-runner returns while its instance is still
stopping(the action'sStopInstancescall is fire-and-forget), but every warm-pool lookup filtersinstance-state-name=stopped. A run queued tightly behind it saw an "empty" pool and cold-launched a duplicate instance that nothing drained until the nightly pass.Log-proven on 2026-07-10 (UTC): run A's stop-runner logged
is stopped (warm pool)0.2 ms afterStopInstancesand exited 08:45:51 → run B's queue released 08:45:55 → 08:46:04{"step":"warm_start","outcome":"pool_empty"}→ second instance; every later run warnedFound 2 stopped warm-pool instances (…); the reaper is not draining them. Same two-instance pattern on 2026-07-09.The "Pre-attach sandbox EIP" step now also matches
stoppingand runsaws ec2 wait instance-stoppedbefore the action's own lookup (preferring an already-stoppedinstance when both exist; falling back to a cold launch with a warning if the wait times out). namecheap/ec2-github-runner#69 fixes the same race action-side (mode: stopwaits forstopped); this consumer-side belt also covers older action pins. After #69 merges, bumping the action pin here makes the pair complete.2. Nightly drain at 23:07 UTC instead of 02:37 UTC
End-of-day destruction should happen at end of day. GitHub delays these crons by hours (the 2026-07-10 "02:37" drain actually fired at 06:27), so the earlier slot also buys headroom: even a badly delayed drain lands before the next workday.
:07offset keeps the top-of-hour-congestion convention the leak reaper already follows (the file documents it). TheDetermine passcron-string comparison and the SECURITY_COMPLIANCE.md reference are updated byte-for-byte, as the workflow comment requires.3. Single-runner / queueing audit (no change needed)
Verified from the same logs: only EC2-bound runs enter the FIFO queue (
start-runner's gate excludes forks/Dependabot/branch pushes; mock acceptance stays parallel on hosted runners), the queue held run B until run A fully completed (waiting for run #29080496277: current status: in_progress), and no two runs ever held the runner concurrently. The only defect was the boundary itself — "run finished" preceded "instance reusable" — which items 1 and #69 close from both sides.Validation
actionlintclean on both workflows; YAML parses.Pre-attached … to stopped warm-pool instanceon the second run with noFound 2warning.Determine pass → kind=nightly-drainfromEVENT_SCHEDULE=7 23 * * *and terminate the day's instance;workflow_dispatchwithmode: nightly-drain,dry_run: truepreviews it safely.