pools: stop a stale worker_pool label counting as pool membership - #151
Conversation
The pinned-pool card reported 9 workers for gecko-3-b-osx-arm64 while the pool has 6 -- the same 6 Taskcluster returns and the same 6 the pool-filtered fleet table shows. The three extra are macmini-m2-40/41/42, dropped from that pool in ronin_puppet 290b3075 (2025-09-17) when it moved to M4s, still SimpleMDM-managed in Defective / Spares, and still labelled gecko-3-b-osx-arm64 in our DB. worker_pool is sticky on purpose: sync/puppet.py clears only puppet_role when a host leaves inventory.d, and the TC/MDM/sheets syncs only ever backfill the label. bf141e8 added the exclusion rule for exactly these hosts ("Defective/ Spares M2s tagged gecko-3-b-osx-arm64") to fleet.pool_health and workers.list_workers, but sync/load_sampler.py -- which had shipped five weeks earlier in fa53ce2 -- was never updated, so PoolLoadSample.capacity remained the one worker count in the app that reads worker_pool with no filter at all. Put the rule in one place as Worker.counts_toward_pool (MDM group + retired pool), with models.exclude_non_pool_members() as its SQL form, and route the counts through it: - load_sampler capacity/running -- the reported bug. Also fixes `running`, which could be inflated by a stale RUNNING task state on an excluded host. - fleet_summary.by_pool and fleet_showcase's per-pool rollup, which feeds the migration axes, the over-provisioned/backed-up lists and the VM pool list. Both bucket a non-member as "unknown" rather than dropping it, so by_pool still sums to the fleet size and scale.workers stays the true fleet count. - pool_health and list_workers now express the rule by calling it instead of reimplementing it; behaviour there is unchanged. consolidation_analysis is deliberately left counting every host -- it is the r8 retirement analysis, and surfacing defective/spare hosts under the pool they came from is the point. Commented as such. Also the first backend tests, and a CI job to run them. Nothing in CI executed backend/ before, which is why both misses of this rule went unnoticed; the three sampler assertions fail on the pre-fix code with capacity 9 against the expected 6. requirements-dev.txt pulls in the pinned requirements.txt verbatim and adds pytest, so tests run against prod's exact versions. Note for review: the card keeps showing 9 until the next sampler tick after deploy, and its 24h sparkline stays inflated for a day -- PoolLoadSample rows are history and are not rewritten. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
.gitignore has listed __pycache__/ and *.pyc since the start, but these were committed before it and stayed tracked, so any local python run dirties the tree and can silently ride along in a commit. Adding a pytest gate makes that a routine occurrence rather than an occasional one. Removes them from the index only; they are regenerated on import and are not used by the build (the Docker image installs from requirements.txt and compiles its own). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Docker verification against real Postgres (pre-merge)CI proved the unit tests pass. This closes the two things CI can't reach: the SQL half of the rule on Postgres rather than SQLite, and the actual Setup: Control —
|
The report
Pinning
gecko-3-b-osx-arm64to Monitored Pools shows 9 workers. The pool has 6:running/Nsync/load_sampler.py→PoolLoadSample.capacity/fleet?worker_pool=…&view=tableapi/workers.pylist_workersapi/fleet.pypool_healthLive TC agrees with the table — 6 workers (m4-120, 123, 124, 193, 195, 196), all active within 15 minutes, none quarantined.
The three phantom workers
worker_poolis sticky by design:sync/puppet.pyclears onlypuppet_rolewhen a host leavesinventory.d("worker_pool is set by several syncs"), and the TC/MDM/sheets syncs only ever backfill the label. So a host pulled from a pool keeps that pool's name forever.ronin_puppet
290b3075(2025-09-17, "m4 arm builder pool") deletedinventory.d/macmini-m2.yaml, which held exactly threegecko-3-b-osx-arm64targets: macmini-m2-40, -41, -42. They're still SimpleMDM-managed in Defective / Spares, soprune_decommissionedcorrectly keeps their rows — with the dead label. 6 + 3 = 9.This was fixed once already.
bf141e8(2026-07-15) names this very pool — "stale labels (e.g. Defective/Spares M2s tagged gecko-3-b-osx-arm64) inflated pool totals" — and addedWorker.in_excluded_mdm_groupplus the filter inpool_healthandlist_workers. It never touchedload_sampler.py, which had shipped five weeks earlier (fa53ce2) and is the only worker count in the app that readsworker_poolwith no filter at all.The fix
One rule, one place:
Worker.counts_toward_pool(excluded MDM group + retired pool), withmodels.exclude_non_pool_members()as its SQL form. Then:load_sampler—capacityandrunning. The reported bug.runningcould also be inflated by a staleRUNNINGtask state on an excluded host.fleet_summary.by_poolandfleet_showcase's per-pool rollup (feeds the migration axes, the over-provisioned / backed-up lists, the VM pool list). Both bucket a non-member as"unknown"rather than dropping it, soby_poolstill sums to the fleet size andscale.workersstays the true fleet count. This follows theDECOMMISSIONED_POOLSprecedent already infleet_summary.pool_healthandlist_workersnow call the rule instead of reimplementing it. Behaviour unchanged.Deliberately not changed:
consolidation_analysiskeeps counting every host — it's the r8 retirement analysis, and surfacing defective/spare hosts under the pool they came from is the point. Commented in place.Not affected:
reprovision._pool_planenumerates bytc_worker_pool_id(live TC registration), not the sticky label, so the destructive path could never be pointed at a phantom host.Tests — the repo's first backend tests, plus a CI job
Nothing in CI executed
backend/before, which is why both misses of this rule went unnoticed.backend/tests/test_pool_membership.py(16 tests) covers the rule's truth table, the sampler's numbers, and agreement between the property and its SQL form.Proven to fail on the pre-fix code — reverting just the
counts_toward_poolguard inpool_worker_counts:assert capacity[POOL] == 6fails with the real prod number, 9.Dependency-light on purpose (pure functions + in-memory SQLite, no Postgres, no network), so the gate is fast and can't fail for environmental reasons.
requirements-dev.txtincludes the pinnedrequirements.txtverbatim rather than re-resolvingrequirements.in, so tests run against prod's exact versions; a fresh resolve drifted 8 packages to newer releases.Verification
pytest -q→ 16 passed; 3 fail on the pre-fix code (above).ruff checkper changed file is byte-identical in finding count tomain— zero new lint findings;backend/tests/is clean.requirements-dev.txtstructurally validated for--require-hashes: 50 requirements, all==-pinned, all hashed; the only additions over the runtime lock arepytest,iniconfig,pluggy,packaging,pygments.pip install --require-hashesstep itself. No Docker daemon available this session, and a macOSpip --dry-runcan't see the linux pins (it rejectedgreenlet==3.5.4, which is already inmain'srequirements.txtand installs in theimagejob every PR). The newbackend (pytest)job is the real check.Deploy note
PoolLoadSamplerows are history and aren't rewritten, so the card keeps showing 9 until the next sampler tick after deploy, and its 24h sparkline stays inflated for about a day. Pool Health and the fleet table are correct immediately.Follow-ups for a human
backend (pytest)to the branch ruleset's required checks — it's blocking-capable but not yet required, so today it can't stop a merge. Same gap#118left for the terraform job.__pycache__/*.pycfiles (already in.gitignore; they dirty the tree on any local python run, which a pytest gate makes routine).Worker_Config/ pool label in SimpleMDM. Clearing it there would fix the data as well as the display.🤖 Generated with Claude Code