Skip to content

ci(dependent-suites): request after core's own suites; name the queue when it waits - #6331

Merged
rbuergi merged 4 commits into
mainfrom
ci/dependent-suites-after-core-and-census
Oct 8, 2026
Merged

rbuergi merged 4 commits into
mainfrom
ci/dependent-suites-after-core-and-census

Conversation

@rbuergi

@rbuergi rbuergi commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Why

The required Dependent suites (MeshWeaver.Plugins) gate (#6313 + MeshWeaver.Plugins#3168) blocked core merges on runner capacity. #6320 went red on "No verdict in time" after its 42-minute wait (triage: rbuergi/Feedback/dependent-suites-gate-times-out-on-queued-candidate-20261008T1740Z).

Measured 2026-10-08 (REST):

  • At 17:30Z there were 10 live candidate runs and 48 candidate legs queued for the shared aks-silos-dind pool, beside about 30 queued Plugins-CI dind jobs. 31 dind jobs were running.
  • In fix(gui): a click's one-off read finds its id seeded at render; guard holds src/ to it (#6058) #6320's candidate (Plugins run 37809289143), the candidate legs finished by 17:05Z. Its single control leg was queued from 17:08Z, and the waiter gave up at 17:23:48Z.
  • The candidate legs' builds took 7.0–8.5 min each, rebuilding the same core closure eight times.
  • MW_BUILD_QUEUE=off, so the CI queue's gate tier is bypassed and the order is GitHub's plain FIFO.

What changes (core half)

  1. Requested after core's own suites.
    • Dependent suites (request) now needs build and test. It dispatches only when both are green, or when the tree is a reused green.
    • Otherwise the request is not sent and goes red by name: "Dependent suites NOT requested — waiting for core's own suites". Consolidate test results repeats that sentence. It is never a skip.
    • I ran the step locally under six input combinations: both green, reused, test failure, build failure, cancelled, and skipped. All six behaved as intended.
  2. Named when it waits (await-dependent-verdict.py).
    • Every 5 min, and again at the deadline, the waiter prints a queue census. It reads only Actions metadata of Plugins' core-candidate.yml runs, using permission-actions: read on the same App token. That covers this candidate's job states, its queue position, the run keys ahead of it, and Plugins' total of queued runs.
    • On a timeout the step title says why, for example:

      no runner within 42 min (queued behind 1 candidate run(s): 37807892279-1) — waiting for a runner: Control / control (leg 1/1)

    • The census never decides anything. The self-test includes a negative control: silence with the census on is still no verdict. Mutating the predicate makes the self-test fail.
    • Checked live against Plugins with the four real keys of the moment.
  3. Remedy corrected. "Re-run this job once that run has finished" only reads a verdict if the candidate was still measuring. A candidate whose core run finished stands down, and is now reaped by Plugins' next admission. The step therefore says: re-run the waiter at once, or re-run Dependent suites (request) to get a fresh key.
  4. Superseded heads already cancel their candidate. This is the pr-<N> concurrency group with cancel-in-progress. MeshWeaver.Plugins#3187 closes the remaining gap with a reaper.

The 42-minute wait and every timeout-minutes are unchanged.

The other half

MeshWeaver.Plugins#3187: build once per arm. The union of the legs is built once and shipped through dedupe-tree.py, and the legs run --no-build. It also adds the reaper. The two halves are independent and either can land first. There is no Pairs-with: on purpose, because a counterpart would double the candidate runs this PR is trying to reduce.

Checks run locally

  • await-dependent-verdict.py --self-test: 26/26.
  • check-plugins-break-declaration.py --self-test, merge-queue-steward.py --self-test, check-workflow-shell.py, check-workflow-timeouts.py --root . (113 jobs, cap 45) and check-guard-step-masking.py --root ..
  • actionlint on dotnet-test.yml.
  • MeshWeaver.Documentation.Test: Release -warnaserror 0 warnings / 0 errors, and the ledger guards (46 tests, filtered) are green.

Doc: Doc/Architecture/CrossRepoPairGate § "Runner capacity — requested after core's own suites, built once per arm, named when it waits".

Mirror-sync: none — no i18n catalog change.

🤖 Generated with Claude Code

… when it waits

The required `Dependent suites (MeshWeaver.Plugins)` gate blocked core merges on runner capacity
(core #6320 red on "No verdict in time"; triage
rbuergi/Feedback/dependent-suites-gate-times-out-on-queued-candidate-20261008T1740Z). Measured
2026-10-08 17:30Z: 10 live candidate runs, 48 candidate legs queued for the shared dind pool.

* `Dependent suites (request)` now needs `build` and `test` and sends the dispatch only once core's
  own suites are green on the candidate (or the tree is a reused green). Otherwise it is NOT sent
  and goes RED by name — "waiting for core's own suites" — and Consolidate repeats it. Never a skip.
* await-dependent-verdict.py prints a QUEUE CENSUS every 5 min and at the deadline (Actions
  metadata of Plugins' core-candidate.yml runs, `actions: read` on the same App token): this
  candidate's jobs, its position among live candidate runs, the keys ahead, Plugins' queued total.
  A timeout now says "no runner within 42 min (queued behind N candidate run(s): …) — waiting for a
  runner: <job>", "still running at the deadline", or "no run carries this key". The census never
  decides anything (self-test + negative control: silence is still no verdict).
* The no-verdict step's remedy is corrected: a candidate whose core run has finished stands down
  (and is now reaped by Plugins' next admission), so re-run the waiter at once, or re-run the
  request for a fresh key.

The 42-minute wait and every cap are unchanged. Doc: Architecture/CrossRepoPairGate § "Runner
capacity". The Plugins half (build once per arm; reap) is MeshWeaver.Plugins#3187.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 8, 2026 17:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The dependent gate can validate a different merge tree than core tested, and queue metadata handling can expose or misreport private workflow state.

4 open findings
What changed in this PR

This PR reduces dependent-suite runner contention by delaying Plugins validation until core CI passes and improves queue-timeout diagnostics.

Changes:

  • Gates dependent-suite dispatch on successful core build/tests.
  • Adds a Plugins queue census and clearer timeout reasons.
  • Updates architecture documentation and dependency guards.
File Description
.github/​workflows/​dotnet-test.yml Delays dispatch and reports timeout context.
.github/​scripts/​await-dependent-verdict.py Adds queue census and diagnostic output.
src/​MeshWeaver.Documentation/​Data/​Architecture/​CrossRepoPairGate.md Documents the revised workflow.
test/​MeshWeaver.Documentation.Test/​PlatformNeverDependsOnPluginsGuard.cs Updates the cross-repository API-read ledger.

🧠 Review effort: Balanced


💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +3903 to +3906
if [ "$REUSED" = "true" ]; then
echo "this tree was already tested green (precheck reuse) — core's own suites are green; requesting"
elif [ "$BUILD" = "success" ] && [ "$TEST" = "success" ]; then
echo "core's own build ($BUILD) and tests ($TEST) are green on this candidate — requesting the dependent's suites"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This finding is real, but it is not introduced by this diff. The request job has always dispatched github.sha and its first parent: the line it names checks out the run's fixed github.sha, unchanged here. Before this PR, needs did not include build at all.

The exact tested merge cannot be dispatched as written. fresh-merge re-derives that merge commit locally on the runner and never pushes it. Plugins' scope job then runs git fetch origin <candidate> against GitHub, so a local-only merge sha would fail there.

Failing whenever build.outputs.main-sha != github.sha^1 would turn the gate red every time main moves between the PR event and the build, which happens several times an hour. That is the same false red this PR is removing.

What this PR does change: Plugins is now requested only after core's own suites ran green on that run. It does not claim the two trees are byte-identical. Closing the gap needs a reachable ref for the tested merge, for example pushing it to refs/core-candidate-merge/<key>. That is a separate change with its own write-permission decision, and I will file it into triage rather than widen this PR.

Comment on lines +138 to +139
out["queued"] = sorted(j.get("name", "") for j in jobs if j.get("status") in ("queued", "waiting", "pending"))
out["running"] = sorted(j.get("name", "") for j in jobs if j.get("status") == "in_progress")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9db6ea2. Job names now pass through an allow-list (_PUBLIC_JOB) of core-candidate.yml's own fixed shapes:

  • Candidate|Control / … (leg N/M)
  • (build once for N legs)
  • the four named jobs

Any other name is printed as <job>. A self-test case puts a portal-hosts (Secret.Customer.Test) job in the census and asserts that Secret never reaches the sentence while <job> and the allowed name do. I also checked it live against three current keys: all real names still pass.

Comment on lines +176 to +180
if c["queued"] and not c["running"]:
return (f"no runner within {minutes} min ({behind}) — waiting for a runner: {_names(c['queued'])} — "
f"{c.get('pluginsQueuedRuns')} Plugins run(s) queued in total — {c['run']}")
return (f"the candidate was still running at the deadline ({len(c['running'])} job(s) running: {_names(c['running'])}; "
f"{len(c['queued'])} waiting for a runner; {behind}) — {c['run']}")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9db6ea2. A run that is still live (queued/requested/waiting/pending) with no job materialised now gets its own sentence: "no runner within 42 min (queued behind N …) — the candidate run itself never started: it is 'queued' with no job materialised yet (admission / concurrency)". A self-test case pins it, and it is no longer read as "still running".

| request | `dotnet-test.yml` → `Dependent suites (request)` | **only once core's own build and tests are green on the candidate** (or the tree is a reused green) — otherwise it is NOT sent and goes red as "waiting for core's own suites"; then it proves the receiver exists on Plugins' default branch and sends `repository_dispatch core-candidate-suites` with the candidate (`github.sha`), its **first parent** as the base and a key unique to the run attempt — once for Plugins' default branch, once more at a declared counterpart's head |
| scope | Plugins `core-candidate.yml` → `scripts/core-candidate-scope.py` | the per-realm, affected-only selection above, printed as an audit table (selected realms with edges, then not-selected realms); `refused` when it cannot compute one; legs cut by the portal-host lane's measured weights |
| candidate arm | Plugins `core-candidate-arm.yml` | each leg built **from source** against the candidate (`-p:MeshWeaverRoot`, no image — the candidate is unpublished) and run; exit codes and dead hosts recorded exactly as the platform canary records them |
| candidate arm | Plugins `core-candidate-arm.yml` | the union of the legs' suites built **once, from source** against the candidate (`-p:MeshWeaverRoot`, no image — the candidate is unpublished), shipped to the legs through `scripts/dedupe-tree.py`, and run there with `--no-build`; a one-leg arm builds in its leg; exit codes and dead hosts recorded exactly as the platform canary records them |

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9db6ea2. The three places that describe Plugins-side behaviour now say it lands with MeshWeaver.Plugins#3187:

  • the candidate-arm row: "before it, each leg builds its own suites from source"
  • point 2 (build once)
  • point 4 (the reaper)

The page stays true whichever half lands first.

…us; name a run that never started; doc sequences with Plugins#3187

Review findings on #6331.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

    22 files  ±0      22 suites  ±0   50m 1s ⏱️ +17s
11 146 tests ±0  10 953 ✅ ±0  193 💤 ±0  0 ❌ ±0 
11 159 runs  ±0  10 966 ✅ ±0  193 💤 ±0  0 ❌ ±0 

Results for commit fde1915. ± Comparison against base commit 784c9d5.

This pull request removes 40 and adds 16 tests. Note that renamed tests count towards both.

   --- End of inner exception stack trace ---
   --- End of inner exception stack trace ---, expected: True)
   --- End of inner exception stack trace ---, isDenial: True)
 ---> (Inner Exception #1) MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.<---
 ---> (Inner Exception #1) MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432<---
 ---> (Inner Exception #1) System.ArgumentException: Value does not fall within the expected range.<---
 ---> (Inner Exception #1) System.InvalidOperationException: boom<---
 ---> (Inner Exception #1) System.InvalidOperationException: source B is misconfigured<---
 ---> (Inner Exception #1) System.Net.Sockets.SocketException (0xFFFDFFFF): Name or service not known<---
…
Memex.Portal.Shared.Test.InstanceIdRulesMatchTheRegistryTest ‑ TheSetupHostAgreesWithTheRegistry(candidate: "64314a1d-dbbf-4d88-9e60-7d1bdd6bb1ce")
Memex.Portal.Shared.Test.SessionDenialIsAnAnswerTest ‑ OnlyAVerdictReadsAsADenial(shape: "the same verdict nested, as a late denial dispatch"···, failure: System.InvalidOperationException: write failed
 ---> System.UnauthorizedAccessException: Access denied
   --- End of inner exception stack trace ---, isDenial: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "a stall as an aggregate's FIRST member", fault: System.AggregateException: One or more errors occurred. (Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.)
 ---> MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.
   --- End of inner exception stack trace ---, expected: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "a stall as an aggregate's SECOND member", fault: System.AggregateException: One or more errors occurred. (The operation has timed out.) (Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.)
 ---> System.TimeoutException: The operation has timed out.
   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.<---
, expected: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "a stall nested in an aggregate inside a wrapper", fault: System.InvalidOperationException: outer
 ---> System.AggregateException: One or more errors occurred. (Value does not fall within the expected range.) (Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.)
 ---> System.ArgumentException: Value does not fall within the expected range.
   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.<---

   --- End of inner exception stack trace ---, expected: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "a stall wrapped as InnerException", fault: System.InvalidOperationException: outer
 ---> MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.
   --- End of inner exception stack trace ---, expected: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "an aggregate of unrelated faults", fault: System.AggregateException: One or more errors occurred. (The operation has timed out.) (Value does not fall within the expected range.)
 ---> System.TimeoutException: The operation has timed out.
   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) System.ArgumentException: Value does not fall within the expected range.<---
, expected: False)
MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest ‑ Classifies(shape: "a MIXED aggregate — one transient branch, one genu"···, exception: System.AggregateException: One or more errors occurred. (Failed to connect to 10.42.18.4:5432) (source B is misconfigured)
 ---> MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432
   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) System.InvalidOperationException: source B is misconfigured<---
, expected: False)
MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest ‑ Classifies(shape: "a nested aggregate with one genuine leaf", exception: System.AggregateException: One or more errors occurred. (One or more errors occurred. (Failed to connect to 10.42.18.4:5432) (boom)) (Failed to connect to 10.42.18.4:5432)
 ---> System.AggregateException: One or more errors occurred. (Failed to connect to 10.42.18.4:5432) (boom)
 ---> MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432
   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) System.InvalidOperationException: boom<---

   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432<---
, expected: False)
MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest ‑ Classifies(shape: "a nested aggregate, all leaves transient", exception: System.AggregateException: One or more errors occurred. (One or more errors occurred. (Failed to connect to 10.42.18.4:5432)) (Failed to connect to 10.42.18.4:5432)
 ---> System.AggregateException: One or more errors occurred. (Failed to connect to 10.42.18.4:5432)
 ---> MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432
   --- End of inner exception stack trace ---
   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432<---
, expected: True)
…

♻️ This comment has been updated with latest results.

rbuergi and others added 2 commits October 8, 2026 20:28
…entical verdict, -35% dind runner-minutes

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@rbuergi
rbuergi merged commit 3655d3a into main Oct 8, 2026
@rbuergi
rbuergi deleted the ci/dependent-suites-after-core-and-census branch October 10, 2026 13:09
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