Skip to content

GH-4840, GH-4841, GH-4843: sharpen the side-effect AOT roots, guard the HTTP rooting order, run a Balanced native lane - #4847

Merged
jeremydmiller merged 1 commit into
mainfrom
gh-4840-4841-4843-aot-followups
Oct 6, 2026
Merged

jeremydmiller merged 1 commit into
mainfrom
gh-4840-4841-4843-aot-followups

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #4840, closes #4841, closes #4843. All three are follow-ups to #4825 / #4828, and all three are about the same thing: a Native AOT rooting block that can be silently short. One PR because the measurement each issue asked for needed the same native lanes, and the one real bug that measurement found cuts across them.

#4840: the side-effect root filter

SideEffectAotRoots now roots the interfaces that declare Execute/ExecuteAsync (DeclaredOnly), which is literally the test SideEffectPolicy.findMethod applies on its walk. The castability-to-ISideEffect proxy is gone. Two shapes where the two answers differ are now tests in aot_roots_for_side_effects, and both were red against the old filter before the change:

  • an intermediate member-less marker between the return type and the declaring interface is no longer rooted;
  • an interface that declares Execute without itself inheriting ISideEffect is rooted. findMethod walks every interface of the return type, so that is where it finds the method, and the old filter left exactly that metadata unrooted.

IWolverineReturnType and INotToBeRouted are now asserted out by name rather than falling out of the filter by accident.

The IL2037 question, measured. The Fisher lane now returns a member-less marker side-effect interface (IAotFisherMarkerOp : IFisherOp) from a handler, which is the shape CoreTests' fixture always had and nothing had ever published. The emitted [DynamicDependency(All, typeof(IAotFisherMarkerOp))] publishes with zero ILC warnings under TreatWarningsAsErrors. ILC counts an interface's own base interfaces as members, so All over an ISideEffect descendant can never resolve nothing. No "drop member-less roots" filter was added, because there is nothing for it to drop; the lane keeps the fact measured instead of assumed.

What the measurement actually found instead is a real bug on main: the native Fisher lane crashed in PrepopulateRoutingCache with EmptyMessageRouter<IAotFisherMarkerOp> missing native code. HandlerChain.PublishedTypes() listed every variable a handler creates, side effects included, so AllMessageTypes() handed every side-effect return type to the routing cache warm-up, which closed a router over it reflectively. A side effect is consumed by its policy inside the chain and is never routed; nothing can root a router for it. PublishedTypes() now skips INotToBeRouted types. Covered by a_side_effect_return_type_is_not_a_published_type (red without the fix) and by the Fisher lane itself, which is green natively with the marker. The library-declared IFisherOp happened not to trip the same close, which is why no lane saw this before an application-declared interface went through one.

#4841: the EndpointMetadataProviderTypes ordering assumption

Decided for the recorded form, deliberately. It cannot drift from the three tryApplyAsEndpointMetadataProvider call sites, and the silent-drift failure mode of the declarative form is the one nothing can check. The ordering assumption it makes is now loud: under codegen write, HttpGraph.BuildFiles refuses, by chain name, when any chain's endpoint was never built, so "nothing was recorded" can no longer read as "nothing needed rooting". Only under WithinCodegenCommand, because AssertPreBuiltTypesExist legitimately walks BuildFiles before BuildEndpoint during Static attach and emits nothing there. Both directions are tests: the guard fires (red against main's HttpGraph), and the Static-attach path still enumerates. The full codegen drift gate (all eleven projects) regenerates unchanged apart from the Fisher lane's own new handler, so no real application trips it.

#4843: a clustered durable app in a native image

The Marten lane now runs DurabilityMode.Balanced, the first native image in CI to do so. It waits until the node has assumed leadership and has been assigned the wolverinedb:// durability agent before serving anything, so election, EvaluateAssignments, the AssignAgent round trip and the agent teardown on StopAsync are all load-bearing. Green natively, no other change to the lane's pre-gen.

The honest limit, measured both ways. With the fourteen #4842 router factories stripped back out, this single Balanced node still boots and passes, on net9.0 and on net10.0. The lane covers the clustered path; it does not reproduce failure 4. That matches the thread: nobody in this repository has observed that close fire in a native image, and the direct construction in #4842 stands on reasoning plus the coverage test, not on a reproduction. The csproj header and the lane say so, so that nobody over-reads a green run. Option 3 from the issue (the static coverage test) remains the thing that actually guards that mechanism.

Gates

  • wolverine.slnx Release -f net9.0: clean
  • CoreTests: 3297 green, 2 skipped, 0 failed (full suite)
  • Wolverine.Http.Tests: 1179 green, 10 skipped
  • Native, local (osx-arm64): Fisher lane green with the marker, 0 ILC warnings; Marten lane green in Balanced mode on net9.0 and net10.0
  • Codegen drift gate: all eleven projects regenerate without drift, apart from the Fisher lane's intended new handler file and registry

🤖 Generated with Claude Code

https://claude.ai/code/session_017DbTkhs3JxSMCWasTzPdRq

…he HTTP rooting order, run a Balanced native lane

GH-4840: SideEffectAotRoots roots the interfaces that DECLARE Execute/ExecuteAsync,
which is the test findMethod itself applies, instead of the castability proxy. Two
shapes where the answers differ are now tests. The IL2037 question is measured: the
Fisher lane returns a member-less marker side-effect interface and publishes with zero
ILC warnings, so no member-less filter is needed.

That measurement found a real bug: HandlerChain.PublishedTypes() listed side-effect
return types, so PrepopulateRoutingCache closed EmptyMessageRouter<T> over an
application-declared side-effect interface at startup and the native image died.
A side effect is never routed; PublishedTypes() now skips INotToBeRouted types.

GH-4841: the recorded EndpointMetadataProviderTypes form is kept, and its ordering
assumption is loud: under codegen write, HttpGraph.BuildFiles refuses by chain name
while any endpoint is unbuilt. Static attach is unaffected.

GH-4843: the Marten native lane runs DurabilityMode.Balanced and waits for leadership
and the durability agent assignment. Measured limit: with the fourteen #4842 router
factories stripped, the single node still boots on net9.0 and net10.0, so the lane
covers the clustered path without reproducing that failure.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017DbTkhs3JxSMCWasTzPdRq
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment