0

I have a system where I try to implement a middleware for our APIs to have idempotency handled. for this all of the APIs send a message to system where another service takes these messages and either forwards them to real handler with minor data juggling, or responds by itself.

frontend system (client) shall send TRequestWithIdempotency message and expect a TResponseWithIdempotency message back. internal TRequestInternal and TResponseInternal shall be used if state machine is new and API operation has not been processed/executed yet (operation result not known).

I tried to use StateMachines since I saw an example at https://github.com/MassTransit/MassTransit/blob/develop/tests/MassTransit.QuartzIntegration.Tests/RequestRequest_Specs.cs which is very similar to what I am trying. also I changed the test code and system works there good but at below the system fails at end of operation flow. I can see logs about internal API operations successful and also I can see response in logs but at one point before replying to frontend (giving back the TResponseWithIdempotency reply), the mentioned error occurs.

my internal consumer and RequestStateMachine is registered to system as ConfigureInMemoryBus function in the test file from github.

let me know if something is missing. My state machine does not have a Completed state as in error and the error comes from RequestState which I do not change or touch.

I use MassTransit 7.3.1 (not 8 unfortunately), Localstack

    // TRequest and TResponse are same with TRequestWithIdempotency and TResponseWithIdempotency types in statemachine
    var requestClient = bus.CreateRequestClient<TRequest>(RequestTimeout.After(s: 120));
    var requestHandle = requestClient.Create(request);
    var response = requestHandle.GetResponse<TResponse>(false);

my operation state

    public class IdempotentPurchaseOperationState<TResponse> : SagaStateMachineInstance
    {
        public Guid Id => CorrelationId;
        public Guid CorrelationId { get; set; }
        public int CurrentState { get; set; }

        public Guid? MyRequestId { get; set; }
        public TResponse? ResponseMessage { get; set; }
    }

my state machine definition

    // there are some Get... functions which convert the data and returns proper data instances

        public State InternalRequestCompleted { get; private set; }
        public State InternalRequestFaulted { get; private set; }

        public Request<TState, TRequestInternal, TResponseInternal, IdempotentRequestFault> RequestFromHandler { get; private set; }
        public Event<TRequestWithIdempotency> IdempotentRequestReceived { get; private set; }

        public IdempotentPurchaseOperationProcessManager()
        {
            InstanceState(x => x.CurrentState, InternalRequestCompleted, InternalRequestFaulted);
            Event(() => IdempotentRequestReceived, x =>
            {
                x.CorrelateById(x => x.CorrelationId ?? GetIdempotencyKey(x.Message));
                x.SetSagaFactory(e => new TState
                {
                    CorrelationId = GetIdempotencyKey(e.Message),
                });
            });
            Request(() => RequestFromHandler, x => x.MyRequestId, x => x.Timeout = TimeSpan.FromSeconds(30));

            Initially(
                When(IdempotentRequestReceived)
               .Request(RequestFromHandler, ctx =>
               {
                   return ctx.Init<TRequestInternal>(GetInternalRequestFromProcessManagerRequest(ctx.Data));
               })
               .RequestStarted()
               .TransitionTo(RequestFromHandler.Pending)
            );

            During(RequestFromHandler.Pending,
                When(RequestFromHandler.Completed)
                   .RequestCompleted(ctx =>
                   {
                       return ctx.Init<TResponseWithIdempotency>(GetProcessManagerResponseFromInternalResponse(ctx.Data));
                   })
                   .TransitionTo(InternalRequestCompleted),

                When(RequestFromHandler.Completed2)
                   .RequestCompleted(context =>
                   {
                       return context.Init<IdempotentRequestFault>
                       (new IdempotentRequestFaultImpl
                       {
                           ExceptionMessages = context.Data.ExceptionMessages
                       });
                   })
                   .TransitionTo(InternalRequestFaulted),
                When(RequestFromHandler.Faulted)
                   .RequestCompleted(context =>
                   {
                       return context.Init<Fault<TRequestInternal>>
                       (new
                       {
                           Message = context.Instance.InitialMessage,
                           context.Data.FaultId,
                           context.Data.FaultedMessageId,
                           context.Data.Timestamp,
                           context.Data.Exceptions,
                           context.Data.Host,
                           context.Data.FaultMessageTypes
                       });
                   })
                   .TransitionTo(InternalRequestFaulted)
            );

            During(InternalRequestCompleted,
                When(IdempotentRequestReceived)
                .RespondAsync(ctx => {
                    return ctx.Init<TResponseWithIdempotency>(ctx.Instance.ResponseMessage);
                })
            );

            During(InternalRequestFaulted,
                When(IdempotentRequestReceived)
                .Finalize());
        }

I get below exception. on next API call with same idempotency key I get correct and expected response.

sev="ERROR" msg="Message handling threw error." corrid="d9d7b1c8-d59f-4f02-bac1-03c35a91fc8b" reqid="01000000-ac14-0242-5b09-08da1e0e6950" op="Consumer:RequestCompleted" messageType="RequestCompleted" duration=339 Exception=Automatonymous.NotAcceptedStateMachineException: Automatonymous.Requests.RequestState(00060000-ac14-0242-af78-08da1d5a3ecf) Saga exception on receipt of Automatonymous.Contracts.RequestCompleted: Not accepted in state Final

 ---> Automatonymous.UnhandledEventException: The Completed event is not handled during the Final state for the RequestStateMachine state machine

   at Automatonymous.AutomatonymousStateMachine`1.DefaultUnhandledEventCallback(UnhandledEventContext`1 context)

   at Automatonymous.AutomatonymousStateMachine`1.UnhandledEvent(EventContext`1 context, State state)

   at Automatonymous.States.StateMachineState`1.Automatonymous.State<TInstance>.Raise[T](EventContext`2 context)

   at Automatonymous.AutomatonymousStateMachine`1.Automatonymous.StateMachine<TInstance>.RaiseEvent[T](EventContext`2 context)

   at Automatonymous.Pipeline.StateMachineSagaMessageFilter`2.Send(SagaConsumeContext`2 context, IPipe`1 next)

   --- End of inner exception stack trace ---

   at Automatonymous.Pipeline.StateMachineSagaMessageFilter`2.Send(SagaConsumeContext`2 context, IPipe`1 next)

   at Automatonymous.Pipeline.StateMachineSagaMessageFilter`2.Send(SagaConsumeContext`2 context, IPipe`1 next)

ugurg
  • 1
  • 1

1 Answers1

0

You are likely calling RequestCompleted() more than once for the same request, resulting in multiple completed events being delivered to the RequestStateMachine for the same request. In that case, the request state machine has already finalized for that request.

You might benefit more from using a Durable Future, which I cover in Season 3 on YouTube. It's more suitable to your scenario, with less overhead and was intended to replace the RequestStateMachine.

Chris Patterson
  • 28,659
  • 3
  • 47
  • 59
  • thanks for the answer @Chris. I started with Future but by class definition Future expects Request/Response types which in my case had to be multiple, per API. I could not register Future state to system because system complained that a registration was already done by other types. such as Future was done but I could not use Future then – ugurg Apr 14 '22 at 12:59