Skip to content

feat(scale-down): opt-in idle confirmation window before terminating not-busy runners - #5397

Merged
edersonbrilhante merged 2 commits into
github-aws-runners:mainfrom
lochlanbennettodlum-taktile:fix/scale-down-idle-confirmation
Sep 9, 2026
Merged

feat(scale-down): opt-in idle confirmation window before terminating not-busy runners#5397
edersonbrilhante merged 2 commits into
github-aws-runners:mainfrom
lochlanbennettodlum-taktile:fix/scale-down-idle-confirmation

Conversation

@lochlanbennettodlum-taktile

@lochlanbennettodlum-taktile lochlanbennettodlum-taktile commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Description

Closes #5085. Supersedes #5228.

This is @jensenbox's idle confirmation window from #5228, rebased onto the new provider layout and with the lint failures fixed. All credit for the approach and the production data is theirs, I just carried it forward since it stopped applying after the plugin refactor.

Quick recap of why: the GitHub busy flag isn't trustworthy enough to terminate on a single reading. People on #5085 have CloudTrail evidence of it reading false seconds after a job started, and in some cases minutes into a running job. The deregister-then-recheck idea in #5086 can't help because a GET after DELETE returns 404, which we already treat as "not busy".

So instead, opt in with scale_down_idle_confirmation_seconds. On a not-busy reading the runner gets tagged and termination is deferred. It's only terminated when a later evaluation still says not-busy and the window has elapsed. A busy reading in between clears the tag and starts over. Default is 0, which is the current behaviour.

A couple of things I changed while porting:

  • ScaleDownComputeProvider gains markIdle / unmarkIdle. They're required, per @edersonbrilhante's note below about DynamoDB becoming the state store. EC2 uses an instance tag for now, same as ghr:orphan, so no IAM changes. The template provider stubs both.
  • Runners kept warm by idle_config also get their tag cleared. They never go through the removal path, so an old tag on one would otherwise let it be terminated on the first not-busy reading once the idle count drops. Added a test for this.
  • The variable is threaded through the root module, modules/runners and modules/multi-runner (including the new translation layers). I left the experimental webhook orchestration module for a follow-up.

Test Plan

  • yarn format-check, yarn lint, yarn build, yarn test all green
  • New tests for: window start, deferral, termination after the window, busy reset, kept-idle reset, disabled window, EC2 tag payloads
  • terraform validate, fmt -check and terraform test pass for modules/runners and modules/multi-runner
  • From the original PR: 14 hours on the author's fleet with a 300s window, zero mid-job terminations vs roughly one burst per hour before

Related Issues

#5085, #5228, #5086, #5201

🤖 Generated with Claude Code

…y runners

GitHub's busy flag can be stale: it reads false for runners that are actively
executing a job, both shortly after job assignment (observed 25-60s lag) and
deep into a running job (observed 12+ minutes). See github-aws-runners#5085. A single busy=false
reading is therefore not sufficient evidence that a runner is idle, and
scale-down can terminate a runner mid-job.

SCALE_DOWN_IDLE_CONFIRMATION_SECONDS (default 0, previous behaviour) requires
busy=false readings spanning at least that window before terminating. Any
busy=true reading in between clears the marker and restarts the window.

Built on the compute-provider plugin framework introduced in github-aws-runners#5234:

- core: RunnerInfo gains `idleDetectedAt`; ScaleDownComputeProvider gains
  `markIdle` / `unmarkIdle`. Both are OPTIONAL, so this is not a breaking
  change for provider plugins -- a provider with nowhere to persist per-runner
  state stays type-valid, and scale-down skips the window for it rather than
  failing. Only providers implementing them opt into the behaviour.
- aws/ec2: implements both via instance tags (`ghr:idle_detected_at`), the same
  mechanism `ghr:orphan` already uses, so no new state store is needed.
- templates/provider: the scaffold documents both as optional.
- The orchestration in scale-runners/scale-down.ts is provider-agnostic and
  calls through the interface rather than tagging EC2 directly.

Ported from github-aws-runners#5228 onto current main (github-aws-runners#5308, github-aws-runners#5312, github-aws-runners#5342 and the multi-runner
effective-configuration layers). Additions over the original:

- Runners kept idle by `idle_config` also have their marker cleared. They are
  never evaluated for removal, so a marker left on them would go stale and
  permit immediate termination once the idle count drops.
- `modules/runners` validates the variable is >= 0; multi-runner threads it
  through the translation and resolved-config layers; user docs added to
  docs/configuration.md.
- Lint fixes in the test file and capability-shape assertions updated.

Co-authored-by: Lochlan Bennett-Odlum <lochlan.bennett-odlum@taktile.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@edersonbrilhante

Copy link
Copy Markdown
Contributor

@lochlanbennettodlum-taktile we plan to add dynamodb as state db. We can make the new functions required.

@spcaipers-arm

Copy link
Copy Markdown
Contributor

@edersonbrilhante can we merge all PRs related to this issue soon? We have problems on a daily basis, and I don't want to fork it just to patch it.

Per maintainer feedback on github-aws-runners#5397: a DynamoDB state store is planned, so every
compute provider will be able to persist per-runner state. Make `markIdle` and
`unmarkIdle` required members of ScaleDownComputeProvider instead of optional,
drop the "provider without idle support" fallback and its test, and update the
contract-test mocks and the provider template accordingly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@lochlanbennettodlum-taktile

Copy link
Copy Markdown
Contributor Author

Done in 0758f90: markIdle / unmarkIdle are now required on ScaleDownComputeProvider, the optional-provider fallback and its test are gone, and the contract mocks and template are updated. Makes sense with DynamoDB coming as the state store. PR description updated to match.

@edersonbrilhante edersonbrilhante left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work

@edersonbrilhante
edersonbrilhante merged commit 595f3e5 into github-aws-runners:main Sep 9, 2026
56 of 57 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.

Scale-down can terminate a runner that picks up a job between busy check and termination

4 participants