feat: event-driven runner count cache to reduce DescribeInstances - #5281
feat: event-driven runner count cache to reduce DescribeInstances#5281yalafe wants to merge 9 commits into
Conversation
…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.
|
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. |
…ider Addresses review feedback on github-aws-runners#5281: keep the control plane provider-neutral and let each compute provider own how it counts its runners. - Move the tiered read (in-memory -> DynamoDB counter -> DescribeInstances fallback) into the EC2 provider's getCurrentRunners; reset the in-memory cache after createRunners. - Move runner-count-cache.{ts,test.ts} into aws/ec2/src/control-plane and add @aws-sdk/client-dynamodb to the compute-providers package. - Remove the control-plane withRunnerCountCache decorator so the control plane calls the provider directly and is unaware of the cache. - Feature stays opt-in (RUNNER_COUNT_CACHE_TABLE_NAME); DescribeInstances is the fallback when disabled or when the counter is cold/stale. The EventBridge counter Lambda and its Terraform are already EC2-specific and unchanged; they can move under the provider plugin as a follow-up if preferred.
…ider Addresses review feedback on github-aws-runners#5281: keep the control plane provider-neutral and let each compute provider own how it counts its runners. - Move the tiered read (in-memory -> DynamoDB counter -> DescribeInstances fallback) into the EC2 provider's getCurrentRunners; reset the in-memory cache after createRunners. - Move runner-count-cache.{ts,test.ts} into aws/ec2/src/control-plane and add @aws-sdk/client-dynamodb to the compute-providers package. - Remove the control-plane withRunnerCountCache decorator so the control plane calls the provider directly and is unaware of the cache. - Feature stays opt-in (RUNNER_COUNT_CACHE_TABLE_NAME); DescribeInstances is the fallback when disabled or when the counter is cold/stale.
f5e9fdf to
b8694b6
Compare
Done, thanks. Moved the count-cache read into the EC2 provider's getCurrentRunners (in-memory → DynamoDB counter → DescribeInstances fallback) and removed the control-plane decorator, so the control plane is provider-neutral. Opt-in unchanged; tests updated. |
|
@yalafe we are working to support multi computer provider(ec2, microvm, etc), multi storage provider(ssm, dynamodb, etc) and orchestration provider(GH webhook, GH scaleset) . Your code is a great idea, we need to adapt to be compatible with multi storage provider |
Description
Reduces the
DescribeInstancesload 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 currentmain(plugin layout) with a few additions. Opt-in viarunner_count_cache = { enable = true }; default is unchanged behaviour.Approach :
Why: scale-up currently counts active + pending runners via
DescribeInstanceson 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 ithere 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
DescribeInstancesfallback when the counter is cold/stale - combining both mitigations from #4710.What this adds on top of #4983:
INSTANCE#<id>, COUNTED/TERMINATED) written with the counter update in oneTransactWriteItems;+1once,-1only when counted, can't go negative. Lets it count on the first active event (pendingorrunning) - matching the original "active and pending" count and avoiding boot-window over-provisioning.getCurrentRunnerscontract (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+1at launch as a follow-up if needed.Test Plan
running/terminatedare no-ops (idempotency), out-of-orderterminateddoesn't underflow, env-filter/tag guards, and non-cancellation errors rethrow for retry.runner count cache classes (in-memory + DynamoDB) - 21 tests.
terraform validateclean;terraform planshows only the expected new resources whenrunner_count_cache.enable = true, and no changes when disabled (opt-in).DescribeInstanceskept 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.