You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(webhook): configurable queue selection for matching runners (#5190)
## Description
A `workflow_job` whose labels match several runner configurations is
always dispatched to the **first** matching queue (after the
`exactMatch` sort). When multiple pools intentionally share a generic
label — e.g. an "any architecture" or "this size or larger" label
spanning several runner configs — every cold scale-up funnels to a
single queue, overloading one pool while equally-valid pools sit idle.
There is currently no way to spread that load.
This adds a configurable **queue selection strategy**, applied to the
*equally-best* matches (those sharing the top `exactMatch` priority
tier):
- **`first`** (default): unchanged — deterministic first match.
- **`random`**: pick one uniformly, spreading jobs across the matching
queues so a single pool's queue does not become a bottleneck.
- **`all`**: dispatch to *every* matching queue — scaling up one runner
per matching pool and letting the first available runner take the job
(speed over cost). GitHub assigns the queued job to exactly one runner;
the losers are reaped by scale-down.
`exactMatch` priority is preserved: `random`/`all` only ever operate
within the highest-priority matching tier, never a lower-priority match.
The strategy applies to standard jobs; dynamic (`ghr-`) label jobs
continue to use the first compliant queue.
### Caveats for `all` (deliberate opt-in)
- Multiplies **instance launches** per job (losers idle until
scale-down's `minimum_running_time_in_minutes`).
- Multiplies **runner registrations** per job, increasing GitHub API
usage — relevant where API rate limits are already a concern.
- Only truly races when `enable_job_queued_check = false` (otherwise
later scale-ups see the job already taken and skip).
## Changes
**Lambda** — new `QUEUE_SELECTION_STRATEGY` env var (validated; defaults
to `first`), read by both the direct webhook and the EventBridge
dispatcher; `selectQueues()` implements `first`/`random`/`all` within
the top-priority matching tier.
**Terraform** — a `queue_selection_strategy` variable (validated
`first`/`random`/`all`, default `first`) on the root and `multi-runner`
modules, threaded through the `webhook` module `config` into the
`direct`/`eventbridge` lambda env var, plus regenerated terraform-docs.
> **RFC note:** Per CONTRIBUTING (discuss major changes first), open
questions for maintainers — happy to adjust:
> - global setting (as implemented) vs. per-runner-config option?
> - naming (`queue_selection_strategy`; values `first`/`random`/`all`)?
> - should `all` (and `random`) extend to the dynamic-label path?
## Test Plan
- Added unit tests in `dispatch.test.ts`: default picks first; `random`
spreads across equally-matching queues (`Math.random` mocked); `random`
preserves `exactMatch` priority; `all` dispatches to every top-tier
match but not lower-priority ones; invalid strategy rejected at config
load.
- `yarn test` (webhook): **40/40 pass**. `yarn build` (ncc typecheck)
passes. ESLint: 0 errors. Prettier: clean.
- `terraform fmt -check` and `terraform validate` pass on the root and
`multi-runner` modules; terraform-docs regenerated.
## Related Issues
Motivation is load distribution across pools that share generic labels
(avoiding single-queue hotspots), and a speed-over-cost option for
large-scale environments. No existing upstream issue — happy to open one
to track the discussion if preferred.
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Copy file name to clipboardExpand all lines: README.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -175,6 +175,7 @@ Join our discord community via [this invite link](https://discord.gg/bxgXW8jJGh)
175
175
| <aname="input_pool_runner_owner"></a> [pool\_runner\_owner](#input\_pool\_runner\_owner)| The pool will deploy runners to the GitHub org ID, set this value to the org to which you want the runners deployed. Repo level is not supported. |`string`|`null`| no |
176
176
| <aname="input_prefix"></a> [prefix](#input\_prefix)| The prefix used for naming resources |`string`|`"github-actions"`| no |
177
177
| <aname="input_queue_encryption"></a> [queue\_encryption](#input\_queue\_encryption)| Configure how data on queues managed by the modules is encrypted at REST. Options are encrypted via SSE, non encrypted and via KMS. By default encrypted via SSE is enabled. See for more details the Terraform `aws_sqs_queue` resource https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sqs_queue.| <pre>object({<br/> kms_data_key_reuse_period_seconds = number<br/> kms_master_key_id = string<br/> sqs_managed_sse_enabled = bool<br/> })</pre> | <pre>{<br/> "kms_data_key_reuse_period_seconds": null,<br/> "kms_master_key_id": null,<br/> "sqs_managed_sse_enabled": true<br/>}</pre> | no |
178
+
| <aname="input_queue_selection_strategy"></a> [queue\_selection\_strategy](#input\_queue\_selection\_strategy)| Strategy used to pick a queue when multiple runner configurations match a job equally well. `first` keeps the historical deterministic behaviour (the first matching queue by priority). `random` spreads jobs across the matching queues to avoid concentrating load on a single one. `all` scales up one runner per matching queue and lets the first to become available take the job (favouring speed over cost; this multiplies instance launches and runner registrations per job). |`string`|`"first"`| no |
178
179
| <aname="input_redrive_build_queue"></a> [redrive\_build\_queue](#input\_redrive\_build\_queue)| Set options to attach (optional) a dead letter queue to the build queue, the queue between the webhook and the scale up lambda. You have the following options. 1. Disable by setting `enabled` to false. 2. Enable by setting `enabled` to `true`, `maxReceiveCount` to a number of max retries. | <pre>object({<br/> enabled = bool<br/> maxReceiveCount = number<br/> })</pre> | <pre>{<br/> "enabled": false,<br/> "maxReceiveCount": null<br/>}</pre> | no |
179
180
| <aname="input_repository_white_list"></a> [repository\_white\_list](#input\_repository\_white\_list)| List of github repository full names (owner/repo\_name) that will be allowed to use the github app. Leave empty for no filtering. |`list(string)`|`[]`| no |
180
181
| <aname="input_role_path"></a> [role\_path](#input\_role\_path)| The path that will be added to role path for created roles, if not set the environment name will be used. |`string`|`null`| no |
Copy file name to clipboardExpand all lines: modules/multi-runner/README.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -158,6 +158,7 @@ module "multi-runner" {
158
158
| <aname="input_pool_lambda_timeout"></a> [pool\_lambda\_timeout](#input\_pool\_lambda\_timeout)| Time out for the pool lambda in seconds. |`number`|`60`| no |
159
159
| <aname="input_prefix"></a> [prefix](#input\_prefix)| The prefix used for naming resources |`string`|`"github-actions"`| no |
160
160
| <aname="input_queue_encryption"></a> [queue\_encryption](#input\_queue\_encryption)| Configure how data on queues managed by the modules in ecrypted at REST. Options are encrypted via SSE, non encrypted and via KMSS. By default encryptes via SSE is enabled. See for more details the Terraform `aws_sqs_queue` resource https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sqs_queue.| <pre>object({<br/> kms_data_key_reuse_period_seconds = number<br/> kms_master_key_id = string<br/> sqs_managed_sse_enabled = bool<br/> })</pre> | <pre>{<br/> "kms_data_key_reuse_period_seconds": null,<br/> "kms_master_key_id": null,<br/> "sqs_managed_sse_enabled": true<br/>}</pre> | no |
161
+
| <aname="input_queue_selection_strategy"></a> [queue\_selection\_strategy](#input\_queue\_selection\_strategy)| Strategy used to pick a queue when multiple runner configurations match a job equally well. `first` keeps the historical deterministic behaviour (the first matching queue by priority). `random` spreads jobs across the matching queues to avoid concentrating load on a single one. `all` scales up one runner per matching queue and lets the first to become available take the job (favouring speed over cost; this multiplies instance launches and runner registrations per job). |`string`|`"first"`| no |
161
162
| <aname="input_repository_white_list"></a> [repository\_white\_list](#input\_repository\_white\_list)| List of github repository full names (owner/repo\_name) that will be allowed to use the github app. Leave empty for no filtering. |`list(string)`|`[]`| no |
162
163
| <aname="input_role_path"></a> [role\_path](#input\_role\_path)| The path that will be added to the role; if not set, the environment name will be used. |`string`|`null`| no |
163
164
| <aname="input_role_permissions_boundary"></a> [role\_permissions\_boundary](#input\_role\_permissions\_boundary)| Permissions boundary that will be added to the created role for the lambda. |`string`|`null`| no |
description="Strategy used to pick a queue when multiple runner configurations match a job equally well. `first` keeps the historical deterministic behaviour (the first matching queue by priority). `random` spreads jobs across the matching queues to avoid concentrating load on a single one. `all` scales up one runner per matching queue and lets the first to become available take the job (favouring speed over cost; this multiplies instance launches and runner registrations per job)."
0 commit comments