Skip to content

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

Open
lochlanbennettodlum-taktile wants to merge 1 commit into
github-aws-runners:mainfrom
lochlanbennettodlum-taktile:fix/scale-down-idle-confirmation
Open

feat(scale-down): opt-in idle confirmation window before terminating not-busy runners#5397
lochlanbennettodlum-taktile wants to merge 1 commit into
github-aws-runners:mainfrom
lochlanbennettodlum-taktile:fix/scale-down-idle-confirmation

Conversation

@lochlanbennettodlum-taktile

Copy link
Copy Markdown

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:

  • markIdle / unmarkIdle are optional on ScaleDownComputeProvider. Providers that can't persist per-runner state just don't get the window. EC2 uses an instance tag, same as ghr:orphan, so no IAM changes. @edersonbrilhante happy to make this a separate capability interface if you'd rather.
  • 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, provider without idle support, 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.

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

3 participants