Skip to content

feat: event-driven runner count cache to reduce DescribeInstances - #5281

Open
yalafe wants to merge 8 commits into
github-aws-runners:mainfrom
yalafe:feat/runner-count-cache-upstream
Open

feat: event-driven runner count cache to reduce DescribeInstances#5281
yalafe wants to merge 8 commits into
github-aws-runners:mainfrom
yalafe:feat/runner-count-cache-upstream

Conversation

@yalafe

@yalafe yalafe commented Aug 19, 2026

Copy link
Copy Markdown

Description

Reduces the DescribeInstances load in the scale-up path (#4710) with an opt-in, event-driven runner count cache. Builds on the approach in #4983 by s1v4-d@, reworked onto current main (plugin layout) with a few additions. Opt-in via runner_count_cache = { enable = true }; default is unchanged behaviour.

Approach :
Why: scale-up currently counts active + pending runners via DescribeInstances on every decision, which gets slow and rate-limited at scale. We built a fix, then found it lines up with #4710 (npalm@) and #4983 (s1v4-d@) - so bringing it
here rather than maintaining it separately.

What: replace that per-decision call with a DynamoDB counter kept up to date by an EventBridge EC2 state-change Lambda, a short in-memory TTL cache in scale-up, and a DescribeInstances fallback when the counter is cold/stale - combining both mitigations from #4710.

What this adds on top of #4983:

  • Idempotent counter - per-instance marker (INSTANCE#<id>, COUNTED/TERMINATED) written with the counter update in one TransactWriteItems; +1 once, -1 only when counted, can't go negative. Lets it count on the first active event (pending or running) - matching the original "active and pending" count and avoiding boot-window over-provisioning.
  • DLQ + retry + alarm on the EventBridge target - no silent event loss.
  • Provider-agnostic read path - a decorator around the getCurrentRunners contract (no EC2 specifics). The counter Lambda is the only EC2-aware piece.

Consistency trade-off (upfront): the counter is eventually consistent (updated a few seconds after each state change via the event path), so it can briefly under-report just-launched runners - the "can scale beyond max" drawback noted in #4710. Bounded by in-process accounting within a scale-up invocation and the staleness fallback to DescribeInstances. The marker leaves room for a synchronous +1 at launch as a follow-up if needed.

Test Plan

  • Unit tests, all passing:
  • runner-count-cache Lambda - 11 tests: +1/-1 transitions, duplicate running/terminated are no-ops (idempotency), out-of-order terminated doesn't underflow, env-filter/tag guards, and non-cancellation errors rethrow for retry.
    runner count cache classes (in-memory + DynamoDB) - 21 tests.
  • getCurrentRunners decorator - 5 tests: in-memory hit, fresh DDB counter, stale/miss → provider fallback, reset-after-createRunners.
  • Existing control-plane scale-up suite still green (113 tests) - decorator is a transparent pass-through when the feature is disabled.
  • ESLint + Prettier pass on the new lambda code.
  • terraform validate clean; terraform plan shows only the expected new resources when runner_count_cache.enable = true, and no changes when disabled (opt-in).
  • Deployed to a test account and triggered live workflow runs: the counter tracked the fleet from 0 up to ~100 runners and back to 0, with DescribeInstances kept off the scale-up hot path and the DLQ staying empty. Note: single-repo test setup, so this is functional evidence rather than a 20K/day-scale benchmark.

Related Issues

Addresses #4710. Builds on the approach in #4983 . Read path is written to align with the storage/plugin-layout refactor in #5277.

yalafe added 8 commits August 18, 2026 09:55
…o upstream main

Base for reworking the count-cache feature on current upstream (plugin layout).
Not yet rebased/genericized. cache.ts here is github-aws-runners#4983's version and overwrites
main's existing cache.ts - to reconcile next.
…r-count caches into runner-count-cache.ts

- Restore upstream githubCache in cache.ts (was clobbered by github-aws-runners#4983)
- Move ec2RunnerCountCache + dynamoDbRunnerCountCache to runner-count-cache.ts
- Drop duplicated githubCache/GhRunners/Octokit import
- 21/21 count-cache tests pass
…nsactWriteItems)

- Guard each +1/-1 with a per-instance marker (COUNTED/TERMINATED) in an atomic
  TransactWriteItems, so at-least-once/out-of-order EventBridge delivery cannot
  double-count. Replaces the 'count only running' workaround; counts on first
  active event without missing pending->terminated.
- Marker guard prevents negative drift by construction (a -1 only applies when the
  matching +1 was recorded) -> separate write-floor unnecessary; read clamp remains.
- Add lambda.test.ts (11 tests) covering dup/out-of-order/underflow/filter/rethrow;
  github-aws-runners#4983 shipped no test for this lambda.
…e target

- retry_policy (1h max age, 10 attempts) + dead_letter_config on the target
- SQS DLQ (14d retention, SSE) + scoped queue policy for events.amazonaws.com
- CloudWatch alarm on DLQ depth > 0 so silent delivery loss is observable
Closes the retry-exhaustion silent-loss gap github-aws-runners#4983 had no handling for.
- withRunnerCountCache wraps any ScaleUpComputeProvider (contract-level, provider-
  agnostic): tiered getCurrentRunners in-memory -> DDB counter (if fresh) ->
  delegate to the provider's own count on miss/stale; reset in-memory after create.
- Applied at provider composition in scale-up.ts; scale-up stays provider-agnostic.
- Transparent pass-through when RUNNER_COUNT_CACHE_TABLE_NAME is unset (opt-in).
- 5 decorator tests + 113 existing scale-up tests pass; tsc clean.
- modules/runners/scale-up.tf: RUNNER_COUNT_CACHE_TABLE_NAME + STALE_THRESHOLD_MS env
  vars on the scale-up lambda, and a count-gated dynamodb:GetItem policy on the table
- modules/runners/variables.tf + variables.runner-count-cache.tf: runner_count_cache vars
- main.tf: instantiate module.runner_count_cache (enable-gated) and pass
  {table_name, stale_threshold_ms} into the runners module
Feature stays fully opt-in (runner_count_cache.enable=false by default; decorator is
pass-through when the table env var is empty).
- yarn install registers the workspace package (functions/*) in the lockfile so
  'yarn dist' builds the ncc bundle + zip
- fix lambda.tf zip path to the function root (matches the dist script output,
  consistent with the other functions)
Build verified: runner-count-cache.zip produced; 11 lambda tests pass.
@yalafe
yalafe requested review from a team as code owners August 19, 2026 16:26
@yalafe yalafe changed the title Feat/runner count cache upstream: event-driven runner count cache to reduce DescribeInstances feat: event-driven runner count cache to reduce DescribeInstances Aug 19, 2026
@yalafe yalafe closed this Aug 19, 2026
@yalafe yalafe reopened this Aug 19, 2026
@edersonbrilhante

Copy link
Copy Markdown
Contributor

I liked the idea, but this should be added in compute provider ec2. We want to keep the controle plane neutral provider, so we can add new compute provider.

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.

2 participants