JasperFx / wolverine

Supercharged .NET server side development!
https://wolverinefx.net
MIT License
1.25k stars 137 forks source link

.IntegrateWithWolverine() in Marten config causes loss of response for invoked command #1114

Closed AlexZeitler closed 1 week ago

AlexZeitler commented 2 weeks ago

Describe the bug

Scenario

Projects in a .NET 8 solution:

The client is expected to receive an PurchaseOrder instance back from the Orders/PlaceOrderHandler.cs after invoking the PlaceOrder command.

This works fine until this line is added to the Orders/Program.cs Marten Configuration in line 38:

 .IntegrateWithWolverine();

As soon as this line is being added, invoking the PlaceOrder command fails with this error message in the RetailClient:

fail: Wolverine.Runtime.RemoteInvocation.FailureAcknowledgementHandler[0]
Received failure acknowledgement on reply for message 08dcfa93-7a7d-c24c-d843-ae60e95c0000 from service Orders with message 'No response was created for expected response 'Orders.PurchaseOrder''
fail: Microsoft.Extensions.Hosting.Internal.Host[9]
BackgroundService failed
Wolverine.Runtime.RemoteInvocation.WolverineRequestReplyException: Request failed: No response was created for expected response 'Orders.PurchaseOrder'
at Wolverine.Runtime.Routing.MessageRoute.InvokeAsync[T](Object message, MessageBus bus, CancellationToken cancellation, Nullable`1 timeout, String tenantId) in /home/runner/work/wolverine/wolverine/src/Wolverine/Runtime/Routing/MessageRoute.cs:line 151

To Reproduce Follow the description in this repro repository: https://github.com/AlexZeitler/repro-marten-wolverine-integration-no-response

Expected behavior Response is sent to client even with .IntegrateWithWolverine() being called in Marten config.

Desktop (please complete the following information):

Additional context Tested with Wolverine 3.1.0 and 2.17.0

AlexZeitler commented 2 weeks ago

While debugging, I noticed this behavior:

When setting a breakpoint in https://github.com/JasperFx/wolverine/blob/main/src/Wolverine/Runtime/MessageContext.cs#L400 and waiting there, the client already crashed with the exception posted initially without even hitting Line 402.

This doesn't happen when .IntegrateWithWolverine() is not used in Orders.

nkosi23 commented 1 week ago

Can you try switching the order in which you initialize Marten and Wolverine in Orders\Program.cs ? I mean, call UseWolverine() before calling AddMarten(). Because IntegrateWithWolverine() seems to be looking for a Wolverine Service registration internally.

AlexZeitler commented 1 week ago

I tried moving this block below the hostBuilder.UseWolverine() call:

hostBuilder.ConfigureServices(
  services =>
  {
    services.AddMarten(
        options =>
        {
          options.Connection(
            new NpgsqlConnectionStringBuilder()
            {
              Host = "localhost",
              Password = "123456",
              Database = "orders",
              Port = 5401,
              Username = "orders"
            }.ToString()
          );
          options.Events.StreamIdentity = StreamIdentity.AsString;
          options.Projections.Add<OrderProjection>(ProjectionLifecycle.Inline);
          options.DisableNpgsqlLogging = true;
        }
      )
      .IntegrateWithWolverine()
      .UseLightweightSessions();
  }

And I tried using to call AddMarten inside UseWolverine() like this:

options.Services.AddMarten(
          mo =>
          {
            mo.Connection(
              new NpgsqlConnectionStringBuilder()
              {
                Host = "localhost",
                Password = "123456",
                Database = "orders",
                Port = 5401,
                Username = "orders"
              }.ToString()
            );
            mo.Events.StreamIdentity = StreamIdentity.AsString;
            mo.Projections.Add<OrderProjection>(ProjectionLifecycle.Inline);
            mo.DisableNpgsqlLogging = true;
          }
        )
        .IntegrateWithWolverine()
        .UseLightweightSessions();

Neither fixed it.

jeremydmiller commented 1 week ago

@AlexZeitler It's a use case that just isn't supported yet. See the notes below:

public class OrderPlacedHandler
{
    public async Task<object[]> Handle(
        PlaceOrder message,
        IDocumentSession session,
        ILogger logger
    )
    {
        var (orderId, customerId, amount) = message;
        logger.LogInformation("Received Order: {OrderId}", message.OrderId);
        var orderPlaced = new OrderPlaced(
            orderId,
            customerId,
            amount
        );
        session.Events.Append(orderId, orderPlaced);

        // HERE's THE PROBLEM, this tries to send out cascaded messages right here
        // because of the integration with Wolverine, and that latches the MessageContext so no other 
        // messages go out
        await session.SaveChangesAsync();

        var order = await session.LoadAsync<PurchaseOrder>(orderId);

        return [order, orderPlaced];
    }
}

This isn't terribly easy to address right this second. Is there any way you could do this all asynchronously and have the PurchaseOrder messaged back w/o doing request/response?

AlexZeitler commented 1 week ago

Ok, I'll work around this by consuming the OrderPlaced message in the client and pull the order via HTTP