diff --git a/src/Testing/CoreTests/Runtime/framework_router_factories_cover_every_agent_message.cs b/src/Testing/CoreTests/Runtime/framework_router_factories_cover_every_agent_message.cs new file mode 100644 index 0000000000..4e5ecd51ee --- /dev/null +++ b/src/Testing/CoreTests/Runtime/framework_router_factories_cover_every_agent_message.cs @@ -0,0 +1,83 @@ +using System.Reflection; +using JasperFx.Core.Reflection; +using Shouldly; +using Wolverine.Runtime; +using Wolverine.Runtime.Agents; +using Xunit; + +namespace CoreTests.Runtime; + +/// +/// GH-4825. PrepopulateRoutingCache walks every message type the handler graph discovered, and +/// RoutingFor's cache-miss path closes MessageRouter<T> / EmptyMessageRouter<T> +/// over each one reflectively. An application with a durable message store has the framework's own agent +/// messages in that set — and they are internal, so +/// HandlerRegistryCodeFile.onlyPublic() drops them and the emitted [DynamicDependency] +/// rooting block cannot name them: generated code cannot put a non-public type inside a +/// typeof(). In a native image ILC has therefore trimmed exactly those instantiations. +/// +/// Direct construction in _frameworkRouterFactories is the only fix available, which is +/// what GH-4287 already did for itself. +/// +/// +/// Asserted as COVERAGE of the real set rather than as a list of the eight types that were missing. +/// A list would be satisfied the day it was written and say nothing when the next agent message is +/// added — and the cost of missing one is a native image that fails at startup, which no analyzer +/// reports and only a Balanced-mode publish reproduces. +/// +public class framework_router_factories_cover_every_agent_message +{ + [Fact] + public void every_internal_agent_message_has_a_directly_constructed_router_factory() + { + var factories = frameworkRouterFactoryKeys(); + + // Not vacuous: if the reflection above ever stops finding the field, this fails rather than + // passing over an empty set. + factories.ShouldNotBeEmpty(); + + var uncovered = agentMessageTypes() + .Where(x => !factories.Contains(x)) + .OrderBy(x => x.FullName, StringComparer.Ordinal) + .ToArray(); + + uncovered.ShouldBeEmpty( + $"These agent message types are internal, so no emitted rooting block can name them, and nothing constructs their routers directly -- a native image with a durable message store will fail in PrepopulateRoutingCache: {string.Join(", ", uncovered.Select(x => x.Name))}"); + } + + [Fact] + public void the_agent_message_scan_actually_finds_something() + { + // The guard for the test above. agentMessageTypes() returning nothing would make it green while + // covering no types at all, which is precisely the failure mode this whole class of AOT bug has. + agentMessageTypes().ShouldNotBeEmpty(); + + // And they really are non-public, which is the reason they cannot be rooted from generated code. + foreach (var type in agentMessageTypes()) + { + type.IsPublic.ShouldBeFalse($"{type.FullName} is public, so the rooting block could name it"); + } + } + + private static Type[] agentMessageTypes() + { + return typeof(IAgentCommand).Assembly + .GetTypes() + .Where(x => x is { IsAbstract: false, IsInterface: false, IsGenericTypeDefinition: false }) + .Where(x => x.CanBeCastTo() || x.CanBeCastTo()) + .Where(x => !x.IsPublic) + .ToArray(); + } + + private static HashSet frameworkRouterFactoryKeys() + { + var field = typeof(WolverineRuntime).GetField("_frameworkRouterFactories", + BindingFlags.NonPublic | BindingFlags.Static); + + field.ShouldNotBeNull("WolverineRuntime._frameworkRouterFactories was renamed; this test needs updating"); + + var dictionary = (System.Collections.IDictionary)field!.GetValue(null)!; + + return dictionary.Keys.Cast().ToHashSet(); + } +} diff --git a/src/Testing/Wolverine.AotSmoke.Marten/Wolverine.AotSmoke.Marten.csproj b/src/Testing/Wolverine.AotSmoke.Marten/Wolverine.AotSmoke.Marten.csproj index c1f65f8587..e1fcd0847a 100644 --- a/src/Testing/Wolverine.AotSmoke.Marten/Wolverine.AotSmoke.Marten.csproj +++ b/src/Testing/Wolverine.AotSmoke.Marten/Wolverine.AotSmoke.Marten.csproj @@ -21,6 +21,9 @@ not the Configuration.Applier that the GH-4778 rooting block covers. 3. IntegrateWithWolverine registers Envelope with Schema.For(), and in a native image Marten's DocumentMapping cannot find Envelope.Id. + 4. With a durable message store, PrepopulateRoutingCache closes MessageRouter over the + framework's internal agent messages (StopAgents and its siblings), which nothing rooted. + NOT reproduced by this lane, which runs Solo, where agent messaging is never used (GH-4843). The endpoint shapes are the ones the reporting application uses. Boots, serves both endpoints and reads the started stream back. Needs the postgresql service from docker-compose.yml. @@ -29,7 +32,9 @@ dotnet run -f net9.0 codegen write - This project carries NO hand-written roots, so the lane fails until the library itself is fixed. + This project carries NO hand-written roots, so a regression in the library's own rooting fails the + lane outright. (It said "the lane fails until the library itself is fixed" while that was true; the + library was fixed in #4828 and the lane passes.) --> true false diff --git a/src/Wolverine/Runtime/WolverineRuntime.Routing.cs b/src/Wolverine/Runtime/WolverineRuntime.Routing.cs index 3940a42d9b..8cce8558f9 100644 --- a/src/Wolverine/Runtime/WolverineRuntime.Routing.cs +++ b/src/Wolverine/Runtime/WolverineRuntime.Routing.cs @@ -227,7 +227,36 @@ private static readonly Dictionary(), [typeof(Acknowledgement)] = routerFactory(), [typeof(FailureAcknowledgement)] = routerFactory(), - [typeof(IAgentCommand)] = routerFactory() + [typeof(IAgentCommand)] = routerFactory(), + + // GH-4825. The concrete agent messages the framework itself handles. They are internal, so the + // `codegen write` rooting block cannot name them -- HandlerRegistryCodeFile.onlyPublic() drops + // every non-public type, because generated code cannot put one inside a typeof(). An application + // with a durable message store walks all of them through PrepopulateRoutingCache at startup, so + // in a native image the reflective close in RoutingFor fires against instantiations nothing + // could root. Direct construction is the only fix available, exactly as GH-4287 did for + // IAgentCommand above. + // + // Held to COVERAGE of the real set by framework_router_factories_cover_every_agent_message, + // not to this list: the eight types the reported failure named were the ones that application's + // lane happened to reach, and that test immediately found six more with the identical defect -- + // the Balanced-mode leader's own assignment messages, built in + // NodeAgentController.EvaluateAssignments and AssignmentGrid. Add a new agent message and that + // test fails until it is covered here. + [typeof(AgentPresenceReport)] = routerFactory(), + [typeof(AgentsStarted)] = routerFactory(), + [typeof(AgentsStopped)] = routerFactory(), + [typeof(AssignAgent)] = routerFactory(), + [typeof(AssignAgents)] = routerFactory(), + [typeof(QueryAgentPresence)] = routerFactory(), + [typeof(ReassignAgent)] = routerFactory(), + [typeof(ReassignAgents)] = routerFactory(), + [typeof(StartAgent)] = routerFactory(), + [typeof(StartAgents)] = routerFactory(), + [typeof(StopAgent)] = routerFactory(), + [typeof(StopAgents)] = routerFactory(), + [typeof(StopRemoteAgent)] = routerFactory(), + [typeof(StopRemoteAgents)] = routerFactory() }; private static Func, IMessageRouter> routerFactory()