Skip to content

Starved cross-silo source discovery produces FALSE CompileErrors that the bake gate reads as image regressions #1218

Closed

Description

@rbuergi

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 with TimedOut/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

  1. 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, not CompileError. (A genuinely empty set with a healthy query is Content-broken NodeTypes no longer stall rollouts; a baking silo no longer captures traffic #1204's NoSources — already non-gating.)
  2. 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.
  3. 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

  1. added a commit that references this issue on Aug 11, 2026
  2. rbuergi commented on Aug 12, 2026

    @rbuergi
    ContributorAuthor

    Status check — proposals 1 and 2 are done on main; only proposal 3 remains.

    Shipped in PR #1226 (+827 lines, incl. SourceDiscoveryUnavailableTest.cs and SourceSnapshotEstablishmentTest.cs):
    src/MeshWeaver.Graph/Configuration/SourceSnapshot.cs (IsEstablished, Established/Empty/Unavailable) plus MeshNodeCompilationService.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 a CompileError, which was this issue's core ask.

    Proposal 3 is not done. The 60 s SubscribeRequest timeouts (~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 at src/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.

  3. rbuergi commented on Aug 12, 2026

    @rbuergi
    ContributorAuthor

    🚨 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). DirectSourceProbe is the other leg of RaceSourceSnapshot; the cached leg it races already reads as System, so the asymmetry was invisible.

    MeshService.Query stamps request.UserId at call time, inside a Defer that DelaySubscription has already moved to a ThreadPool tick. So the stamp is null and GetEffectiveUserId resolves that to Anonymous.

    Why that is worse than it sounds: an empty Anonymous answer was harmless — the probe's own .Where filtered it out. But a partial Anonymous answer is both Established and non-empty, so it wins the race, and Roslyn is handed a short source set. That is exactly this issue's phantom CS0246 signature, arriving by a different mechanism than starved cross-silo discovery — and it specifically fits the cross-partition shared= detail reported here, since those are the sources most likely to be partially readable as Anonymous.

    So the accounting for this issue is now:

    PR #1279 fixes the five compile-path read sites for #1253's symptom, but the Anonymous-wins-the-race behaviour in DirectSourceProbe deserves 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.)

  4. rbuergi commented on Aug 12, 2026

    @rbuergi
    ContributorAuthor

    Verified: the second route is fully covered by #1279 (merged, de54cd002). No further change needed for it.

    Re-read on merged main, MeshNodeCompilationService.DirectSourceProbe now reads:

    return Observable
        // Observable.Using INSIDE DelaySubscription, never around it: the resource factory
        // then runs on the delayed subscribe — the very tick that calls mesh.Query …
        .Using(() => accessService?.ImpersonateAsSystem() ?? Disposable.Empty, _ => probe)
        .Catch(…)
        .DelaySubscription(subscriptionDelay ?? SourceStallProbeDelay);

    The ordering is the part that matters, and it is right: DelaySubscription schedules the
    subscribe onto a ThreadPool tick, the Using resource factory runs inside that subscribe, and
    the per-leg Observable.Defer(() => mesh.Query…) calls run inside the same subscribe — which is
    exactly when MeshService.Query stamps request.UserId from AccessService. The stamp is now
    System instead of null→Anonymous, so the probe can no longer produce an ESTABLISHED-but-PARTIAL
    set 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:

    • the cached leg (GetSourceCollection) already impersonates System;
    • the sourcesOverride route bypasses the race entirely (SourceSnapshot.Established(override)),
      but its only producer is NodeTypeBatchBake.BakeOne, which is System-scoped end-to-end and
      already refuses an unestablished empty set;
    • NodeTypeCompilationHelpers.RunCompile passes sourcesOverride: null and wraps the compile in
      Observable.Using(ImpersonateAsSystem, …).

    One gap worth recording, not code: no test pins the probe's identity. #1279's
    CompileIncludeSystemScopeTest asserts 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 internal
    visibility 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions