Repository navigation
Starved cross-silo source discovery produces FALSE CompileErrors that the bake gate reads as image regressions #1218
Description
Evidence (memex-cloud, 2026-08-11, ci.2947, activation-path bake during a rollout)
Sampled 56 outcomes of an in-flight gated bake:
outcome mix: TimedOut=16 CompileError=14 Compiled=26
gap s: p50=0.25 p90=60.00 max=60.2 total=908s
gaps >60s: 15, costing 900s (i.e. 99% of the elapsed bake is 60s timeouts)
Two distinct defects show up here.
1. The 60-second timeouts dominate the bake, and they are a ROLLOUT artifact
Every slow gap is exactly ~60.0 s — the SubscribeRequest timeout, not the 5-minute per-type budget. During a rollout the OLD pod still holds the existing activations for these types, so the new pod's per-type activation round-trips to a busy peer and times out. [PreferLocalPlacement] (#1204) only biases where a NOT-yet-activated grain is placed; it cannot move an activation that already exists on the other silo.
At 16 timeouts per 56 types, a full 237-type sweep costs ~68 × 60 s ≈ 68 minutes of pure timeout. This is the same cost centre as the 2026-08-10/11 incident, and it is inherent to driving compiles through hub activations while another silo is serving.
2. Starved source discovery is reported as a COMPILE ERROR, and the gate treats it as a regression
DynamicTypePreWarmer: BusinessRules/Scope → CompileError
CS0246: The type or namespace name 'ScopeLibrary' could not be found
CS0103: The name 'ScopeTestsArea' does not exist in the current context
BusinessRules/Scope compiled successfully in the same day's baseline run (18 m 55 s sweep, compiled=228). Nothing about the type changed. Its source queries returned empty/partial under the same cross-silo starvation as #1 (core #694), so Roslyn compiled it against nothing and produced a genuine-looking CS0246.
That is indistinguishable, at the gate, from a real image regression — so BakePhase.Regressed sticks, readiness is refused, and the rollout stalls on types that are perfectly healthy. 14 of 56 sampled types were in this state.
Why this matters beyond one rollout
- A compile whose SOURCE SET could not be established is not a verdict about the code. It must not be classified as
CompileError(which gates) — it belongs withTimedOut/Unevaluated(which correctly do not). Compare Content-broken NodeTypes no longer stall rollouts; a baking silo no longer captures traffic #1204, where the same principle was applied to deleted-sources content verdicts. - Until then, every rollout of a large mesh can stall itself on false regressions, and the operator's only lever is disarming the gate — the exact "we can't tell a real regression from an infrastructure hiccup" situation the gate exists to avoid.
Proposals
- Never compile against an unestablished source set. If a type declares sources and discovery yields empty/partial while the query timed out or errored, emit
TimedOut/Unevaluated, notCompileError. (A genuinely empty set with a healthy query is Content-broken NodeTypes no longer stall rollouts; a baking silo no longer captures traffic #1204'sNoSources— already non-gating.) - Make the compiler's source snapshot report why it is empty (query error/timeout vs genuinely no matches) so downstream classification is possible at all — today both look identical.
- Sequence the bake behind the rollout, or bake before joining the serving pool, so a bake never competes with the outgoing silo (relates to The NodeType bake can compile a half-applied plugin update and report it as an image regression #1214's sequencing proposal and to Batch bake's source discovery is truncated by the default query limit (50) — 169/237 types got zero sources in production #1216: batch bake avoids this entirely by performing no activations).
🤖 Generated with Claude Code
Activity
Merge pull request #1226 from Systemorph/fix/1218-unestablished-sourc…
rbuergi commentedon Aug 12, 2026
Status check — proposals 1 and 2 are done on main; only proposal 3 remains.
Shipped in PR #1226 (+827 lines, incl.
SourceDiscoveryUnavailableTest.csandSourceSnapshotEstablishmentTest.cs):src/MeshWeaver.Graph/Configuration/SourceSnapshot.cs(IsEstablished,Established/Empty/Unavailable) plusMeshNodeCompilationService.cs:580-582, 596-680, 777-861(SnapshotSourcesCore,RaceSourceSnapshot,DirectSourceProbe) →SourceDiscoveryUnavailableException→CompilationStatus.Unavailable→PreWarmStatus.TimedOut→ non-gating. So an unestablished source set can no longer be reported as aCompileError, which was this issue's core ask.Proposal 3 is not done. The 60 s
SubscribeRequesttimeouts (~68 min on a 237-type sweep) are inherent to the activation-path bake, and batch bake — which performs no activations — is still hard-coded off atsrc/MeshWeaver.Hosting/DynamicTypePreWarmerHostedService.cs:190(var batchBake = false;). That flip is open PR #1238.Narrowing this issue to proposal 3; it closes when #1238 lands. #1213 closed as covered by the same fix.
rbuergi commentedon Aug 12, 2026
🚨 Do not close this on the starvation fix alone — there is a second, independent route to the same phantom
CS0246.Found while fixing #1253 (PR #1279).
DirectSourceProbeis the other leg ofRaceSourceSnapshot; the cached leg it races already reads as System, so the asymmetry was invisible.MeshService.Querystampsrequest.UserIdat call time, inside aDeferthatDelaySubscriptionhas already moved to a ThreadPool tick. So the stamp is null andGetEffectiveUserIdresolves that toAnonymous.Why that is worse than it sounds: an empty Anonymous answer was harmless — the probe's own
.Wherefiltered it out. But a partial Anonymous answer is bothEstablishedand non-empty, so it wins the race, and Roslyn is handed a short source set. That is exactly this issue's phantomCS0246signature, arriving by a different mechanism than starved cross-silo discovery — and it specifically fits the cross-partitionshared=detail reported here, since those are the sources most likely to be partially readable as Anonymous.So the accounting for this issue is now:
SourceSnapshot+SourceDiscoveryUnavailableException)PR #1279 fixes the five compile-path read sites for #1253's symptom, but the
Anonymous-wins-the-race behaviour inDirectSourceProbedeserves its own verification here before this issue is closed.(Reposting: an earlier version of this comment lost its final sentence to a shell-quoting slip.)
rbuergi commentedon Aug 12, 2026
Verified: the second route is fully covered by #1279 (merged,
de54cd002). No further change needed for it.Re-read on merged
main,MeshNodeCompilationService.DirectSourceProbenow reads:The ordering is the part that matters, and it is right:
DelaySubscriptionschedules thesubscribe onto a ThreadPool tick, the
Usingresource factory runs inside that subscribe, andthe per-leg
Observable.Defer(() => mesh.Query…)calls run inside the same subscribe — which isexactly when
MeshService.Querystampsrequest.UserIdfromAccessService. The stamp is nowSystem instead of null→
Anonymous, so the probe can no longer produce an ESTABLISHED-but-PARTIALset that wins
RaceSourceSnapshot, and both legs of the race read under the same identity.Swept for a third route to the same shape and found none:
GetSourceCollection) already impersonates System;sourcesOverrideroute bypasses the race entirely (SourceSnapshot.Established(override)),but its only producer is
NodeTypeBatchBake.BakeOne, which is System-scoped end-to-end andalready refuses an unestablished empty set;
NodeTypeCompilationHelpers.RunCompilepassessourcesOverride: nulland wraps the compile inObservable.Using(ImpersonateAsSystem, …).One gap worth recording, not code: no test pins the probe's identity. #1279's
CompileIncludeSystemScopeTestasserts the@@-include read, not that the probe reads as System —so a regression here would be silent again. Cheap to add later (the probe would need
internalvisibility plus a two-namespace RLS fixture where Anonymous can read one source location and not
the other); flagging rather than doing it, since the fix itself is in.