Skip to content

6.x: PrepopulateRoutingCache crashes startup for AddStickyHandler-bound ASB subscriptions (CreateSender NotSupportedException) #4510

Description

@BaharAtNode

Summary

In Wolverine 6.x, WolverineRuntime.PrepopulateRoutingCache (new in 6.0, runs during StartAsync) crashes host startup with a NotSupportedException from AzureServiceBusSubscription.CreateSender for any message type whose only handler is bound to an Azure Service Bus subscription via AddStickyHandler. This is a regression from 5.x, where routes were built lazily on first send rather than eagerly at startup, so the same configuration worked fine there.

Root cause (traced)

PrepopulateRoutingCache iterates every discovered message type and calls RoutingFor(messageType), which eventually reaches LocalRouting.FindRoutes:

// WolverineRuntime.Routing.cs
if (options.HandlerGraph.CanHandle(messageType))
{
    var endpoints = options.LocalRouting.DiscoverSenders(messageType, runtime).ToList();
    ...
    return endpoints.Select(e => MessageRoute.For(messageType, e, runtime));
}

LocalTransport.DiscoverSenders includes every endpoint the message type's handler chain is sticky-bound to:

// LocalTransport.cs
foreach (var endpoint in chain.ByEndpoint.SelectMany(x => x.Endpoints))
    yield return endpoint;

chain.ByEndpoint is populated by AddStickyHandler. So any endpoint sticky-bound to a handler — including an Azure Service Bus subscription, which is receive-only and has never supported sending — gets treated as a "local sending candidate." MessageRoute.For then calls Endpoint.StartSending → CreateSender, and AzureServiceBusSubscription.CreateSender unconditionally throws:

protected override ISender CreateSender(IWolverineRuntime runtime)
{
    throw new NotSupportedException();
}

This conflates "sticky-bound for inbound delivery" with "valid outbound local-routing target." A subscription being the exclusive delivery channel for a handler says nothing about whether anything should ever be able to send to it locally.

Minimal repro

using var host = await Host.CreateDefaultBuilder()
    .UseWolverine(opts =>
    {
        opts.UseAzureServiceBus(connectionString);

        opts.ListenToAzureServiceBusSubscription("my-subscription")
            .FromTopic("my-topic")
            .RequireSessions()
            .ProcessInline()
            .AddStickyHandler(typeof(MyHandler));
    })
    .StartAsync();

MyMessage (the type MyHandler handles) is never explicitly published or sent anywhere else in the application. StartAsync() still throws:

System.NotSupportedException: Specified method is not supported.
   at Wolverine.AzureServiceBus.Internal.AzureServiceBusSubscription.CreateSender(IWolverineRuntime runtime)
   at Wolverine.Configuration.Endpoint.StartSending(IWolverineRuntime runtime, Uri replyUri)
   at Wolverine.Configuration.EndpointCollection.buildSendingAgent(Uri uri, Action`1 configureNewEndpoint)
   at Wolverine.Configuration.EndpointCollection.GetOrBuildSendingAgent(Uri address, Action`1 configureNewEndpoint)
   at Wolverine.Runtime.Routing.MessageRoute..ctor(Type messageType, Endpoint endpoint, IWolverineRuntime runtime)
   at Wolverine.Runtime.Routing.MessageRoute.For(Type messageType, Endpoint endpoint, IWolverineRuntime runtime)
   at Wolverine.Runtime.LocalRouting.<>c__DisplayClass0_0.<FindRoutes>b__2(Endpoint e)
   at Wolverine.Runtime.WolverineRuntime.findRoutes(Type messageType)
   at Wolverine.Runtime.WolverineRuntime.RoutingFor(Type messageType)
   at Wolverine.Runtime.WolverineRuntime.PrepopulateRoutingCache(IEnumerable`1 messageTypes)
   at Wolverine.Runtime.WolverineRuntime.StartAsync(CancellationToken cancellationToken)

Impact

RequireSessions()+ProcessInline()+AddStickyHandler() on an ASB subscription is a documented, common pattern for fanning one topic out to several session-scoped, exclusively-bound handlers (avoiding Wolverine's default global-by-type dispatch running every sticky handler for every message). Any application using this pattern on 6.x will crash at startup, in every environment, the moment PrepopulateRoutingCache runs — this isn't confined to a narrow edge case.

Suggested fix direction

LocalTransport.DiscoverSenders's "sticky assignments" branch should not yield an endpoint that structurally cannot act as a sender (e.g. filter on whatever capability/interface distinguishes a listen-only endpoint), or PrepopulateRoutingCache/MessageRoute.For should tolerate a NotSupportedException/similar signal from CreateSender for a sticky-bound endpoint rather than letting it fault host startup — a sticky binding's purpose is inbound delivery routing, not proof that a local send target exists.

Environment

  • WolverineFx 6.39.1
  • Wolverine.AzureServiceBus 6.39.1
  • Confirmed absent in 5.39.0 — PrepopulateRoutingCache does not exist in that version's WolverineRuntime.HostService.cs at all; routes were built lazily there, so the same configuration never hit this path.

Activity

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