compiler-explorer / compiler-explorer/ce-ci

The "large" runner label is not a constraint: large jobs run on small runners

Open
#24 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
HCL
Stars
2
Forks
2
PR merge metrics
No merged PRs in 30d

Description

> Written by Claude (an LLM), at Matt's request, after investigating a run of nightly OOM kills. Numbers below are measured from the GitHub API and kernel OOM reports; Matt has seen the analysis.

## The problem

`"large"` does not mean "a large machine". It means **no size constraint at all**, so large jobs routinely run on small runners.

GitHub matches `runs-on` as *"the runner must carry at least these labels"*. Fewer labels therefore makes a job schedulable on **more** machines, not fewer. Our pools register as:

| pool | registered labels |
|---|---|
| linux-x64 ("large") | `{self-hosted, ce, linux, x64}` |
| linux-x64-medium | `{self-hosted, ce, linux, x64, medium}` |
| linux-x64-small | `{self-hosted, ce, linux, x64, small}` |

A small runner's set is a **superset** of the large request, so it satisfies it. The same holds for scale-up routing: as #20 puts it, `exactMatch` means "job labels must be a subset of the runner's labels", so a bare `[self-hosted, ce, linux, x64]` job matches the small pool's matcher too.

The intent in `compiler-workflows/make_builds.py` was "untagged builds land on large, so we don't have to tag every build". The implementation says "untagged builds land anywhere".

## Evidence

`clang_llvmflang` job records, sampled 2026-04-30 to 2026-08-14 (107 records):

| runner pool | runs | successes |
|---|---|---|
| `ce-x64-small` | 31 | **0** |
| `ce-x64` / `ce-x64-medium` | 76 | 15 |

Thirty-one runs on a 16 GiB box, not one success, for four months. Each died as an OOM kill at ~03:00 and nothing surfaced it. `circt_trunk`'s only recent failure was the same thing: it is not memory-hungry, it simply landed on a 16 GiB machine.

compiler-explorer/clang-builder#119 has since made the job counts derive from the machine's RAM, so a misrouted build now runs slowly instead of dying. **This issue is therefore no longer about availability, it is about jobs silently getting a machine a quarter the intended size.**

## Who asks for the bare form today

- `compiler-workflows`: all ~57 generated `build-daily-*.yml` (size defaults to `large`)
- `infra/.github/workflows/bespoke-build.yaml:68` and `infra/.github/workflows/adhoc-command.yml:32` — both `inputs.size == 'large' && fromJSON('["self-hosted","ce","linux","x64"]')`

Everything else is explicit (`small`, `medium`, `arm64`, `lin-builder`, `win-builder`, `admin`) and is unaffected.

## The fix, and why it can be rolled out without breaking anything

Give the large pool a label of its own, **while keeping the bare matcher**, so existing callers keep working during the transition. `modules/multi-runner/main.tf` at our pinned v7.9.0 unions every matcher into the registered labels:

```hcl
runner_extra_labels = { for k, v in var.multi_runner_config :
k => sort(setunion(flatten(v.matcherConfig.labelMatchers), compact(v.runner_config.runner_extra_labels))) }
```

So with two matchers the pool registers `{self-hosted, ce, linux, x64, large}` and still matches bare-form jobs for both scheduling and scale-up.

### Rollout

- [x] **1. ce-ci** — #25 (merged and applied 2026-08-14) — `templates/runner-configs/linux-x64.yaml`, add a second matcher, keeping the existing one:
```yaml
labelMatchers:
- [self-hosted, ce, linux, x64, large]
- [self-hosted, ce, linux, x64]
```
`terraform fmt` + `validate`, then apply. **Non-breaking**: nothing requests `large` yet, and bare-form callers still match.
- [x] **2. Verify** a large runner comes up carrying the `large` label before touching any caller (e.g. run a `bespoke-build` and check the runner name/labels, or inspect the runner list).
- [x] **3. compiler-workflows** — compiler-explorer/compiler-workflows#78 (merged 2026-08-14) — `SIZE_TO_LABELS["large"]` becomes `[ 'self-hosted', 'ce', 'linux', 'x64', 'large' ]`, then `make build-yamls` to regenerate. The "untagged defaults to large" behaviour is preserved: `make_builds.py` still defaults `size="large"`, that default now just means something.
- [x] **4. Confirm** the daily builds are landing on `ce-x64_*` runners only (the job's `runner_name` is in the API), then
- [x] **5. infra** — compiler-explorer/infra#2301 (merged 2026-08-14) — `bespoke-build.yaml` and `adhoc-command.yml`. The `size == 'large'` special case is *deleted* rather than extended: once all three sizes have a label, the conditional collapses to the `format()` branch already used for small and medium.
- [ ] **6. ce-ci** — once nothing requests the bare form, drop the second matcher so `large` is the only way in. (No PR yet, deliberately: it is only safe after 3 and 5 have landed and been observed.)

Steps 1, 3 and 5 are each independently deployable and reversible. **Order matters**: doing 3 or 5 before 1 means large jobs request a label no runner has, and they queue forever.

## Relationship to #20

This is a step *towards* #20, not a divergence. Under `bidirectionalLabelMatch` the match becomes exact in both directions, so a bare `[self-hosted, ce, linux, x64]` job would match only the large pool — which incidentally fixes this problem too, but on the v8 timeline (#20 estimates late 2026) and only after the builder short forms are sorted.

Doing the above first means that when #20 lands, `compiler-workflows` and `infra` are already sending the exact label set the large pool registers, so that migration has one less thing to fix rather than one more.

Worth noting on #20 as well: it is currently written up purely as "these builder workflows will break". It is also a **fix** for misrouting that is happening today, which is a point in its favour that has not been costed.

---

## PRs

| step | PR | state |
|---|---|---|
| 1 | compiler-explorer/ce-ci#25 | ✅ merged and applied |
| 3 | compiler-explorer/compiler-workflows#78 | ✅ merged |
| 5 | compiler-explorer/infra#2301 | ✅ merged |
| 6 | not raised yet | after 3 and 5 are observed working |

Steps 3 and 5 are drafts on purpose: merging either before step 1 is applied makes those jobs request a label no runner carries, and they queue indefinitely rather than failing.

One thing the tooling caught that is worth recording: both `compiler-workflows` and `infra` keep a `.github/actionlint.yaml` allowlist of valid self-hosted labels, and the pre-commit hook rejected `large` until it was added. So a fourth place needed to know about the label, which nothing in the plan above had predicted.

---

### Step 2 result (2026-08-14)

Dispatched `adhoc-command.yml` from infra#2301's branch with `size=large`, so the job requested the new label without anything being merged:

```
labels = ["self-hosted","ce","linux","x64","large"]
runner = ce-x64_i-0a0b133b5809271fc <- large pool
status = success
```

The pool registers `large`, scale-up matches on it, and a freshly launched large runner took the job. Steps 3 and 5 are unblocked.

**Unrelated drift surfaced by the same apply, for the record:** the plan carried 25 changes, only two of which were this work (the `RUNNER_LABELS` env on `linux-x64` scale-up, and the `runner_matcher_config` SSM parameter). The other 23 were lambda `source_code_hash` updates: the deployed lambdas matched no v7.x release and none of v6.3.0-v6.10.1 either, i.e. they were years older than the `v7.9.0` pinned in both `main.tf` files. Cause is that `lambdas-download/` is a separate terraform root whose zips are gitignored, so bumping the version does not refresh them without a second, manual `terraform apply`. That has now been corrected as part of this apply, and the successful dispatch above confirms the upgraded webhook/dispatcher path works. A guard against it recurring is #26.

### Step 4 result (2026-08-14)

Dispatched `circt_trunk` and `clang_llvmflang` manually rather than waiting for the 01:00 schedule. Both were picked up within ~2 minutes by freshly launched **c6a.8xlarge** (32 vCPU / 64 GiB) instances from the large pool, and neither touched a small runner:

```
circt ce-x64_i-052df67d8b843de65 success, 45.3 min
llvmflang ce-x64_i-0ffad1bfa9d0516cc success, 114.9 min
```

`clang_llvmflang` had failed 60 consecutive nightly runs; this is its first success since 2026-06-16. It needed both halves: a guaranteed 64 GiB machine from this work, and RAM-derived compile-job counts from compiler-explorer/clang-builder#119 (8 jobs instead of the hardcoded 12). Wall clock went from 79 min at 12 jobs to 114.9 at 8, as expected.

Only step 6 remains, deliberately left for a few days of observation.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with modules/multi-runner/main.tf and templates/runner-configs/linux-x64.yaml, then inspect the remaining bare-form callers in compiler-workflows and infra. Observe several days of large-runner scheduling and confirm no jobs request the bare matcher; done means the second matcher can be removed without queuing or misrouting jobs.

Written by the indexing model from the issue text.

Assessment

Tech stack
terraform
Domain
devops, infrastructure
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.