Sending NServiceBus message inside TransactionScope
Asked Answered
O

2

1

I am trying to use NHibernate to save to a database in the same transaction as sending a message on the bus from inside an MVC application:

public void DoSomethingToEntity(Guid id)
{
    var session = _sessionFactory.OpenSession();
    CurrentSessionContext.Bind(session);

    using (var transactionScope = new TransactionScope())
    {
        var myEntity = _session.Get(id);
        myEntity.DoSomething();
        _session.Save(myEntity);
        _bus.Send(myMessage);
        transactionScope.Complete();
    }

    session.Dispose();
}

In the configuration, .MsmqTransport() is set with .IsTransactional(true).

If I do this inside a message handler (which is wrapped in its own transaction so does not need the TransactionScope) Then it all works as expected, and if I include an exception, both fail.

However, if I do it inside my own transaction in an MVC application, I get the following error after transactionScope.Complete() when leaving the using block.:

'The operation is not valid for the current state of the enlistment.'

Stack Trace: at System.Transactions.EnlistmentState.InternalIndoubt(InternalEnlistment enlistment) at System.Transactions.VolatileDemultiplexer.BroadcastInDoubt(VolatileEnlistmentSet& volatiles) at System.Transactions.TransactionStatePromotedIndoubt.EnterState(InternalTransaction tx) at System.Transactions.TransactionStatePromotedBase.InDoubtFromEnlistment(InternalTransaction tx) at System.Transactions.DurableEnlistmentDelegated.InDoubt(InternalEnlistment enlistment, Exception e) at System.Transactions.SinglePhaseEnlistment.InDoubt(Exception e) at System.Data.SqlClient.SqlDelegatedTransaction.SinglePhaseCommit(SinglePhaseEnlistment enlistment) at System.Transactions.TransactionStateDelegatedCommitting.EnterState(InternalTransaction tx) at System.Transactions.TransactionStateDelegated.BeginCommit(InternalTransaction tx, Boolean asyncCommit, AsyncCallback asyncCallback, Object asyncState) at System.Transactions.CommittableTransaction.Commit() at System.Transactions.TransactionScope.InternalDispose() at System.Transactions.TransactionScope.Dispose() at HumanResources.Application.Implementations.HolidayService.Book(BookHolidayRequest request) in C:\Users\paul.davies\Documents\GitHub\EdaCalendarExample\HumanResources.Application\Implementations\HolidayService.cs:line 76 at HumanResources.UI.Controllers.HolidayController.BookUpdate(BookHolidayViewModel viewModel) in C:\Users\paul.davies\Documents\GitHub\EdaCalendarExample\HumanResources.UI\Controllers\HolidayController.cs:line 82 at lambda_method(Closure , ControllerBase , Object[] ) at System.Web.Mvc.ActionMethodDispatcher.Execute(ControllerBase controller, Object[] parameters) at System.Web.Mvc.ReflectedActionDescriptor.Execute(ControllerContext controllerContext, IDictionary2 parameters) at System.Web.Mvc.ControllerActionInvoker.InvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary2 parameters) at System.Web.Mvc.ControllerActionInvoker.<>c_DisplayClass15.b_12() at System.Web.Mvc.ControllerActionInvoker.InvokeActionMethodFilter(IActionFilter filter, ActionExecutingContext preContext, Func`1 continuation)

Latest Edit:

This code works:

public void DoSomethingToEntity(Guid id)
{
    var session = _sessionFactory.OpenSession();
    CurrentSessionContext.Bind(session);

    using (var transactionScope = new TransactionScope())
    {
        var myEntity = _session.Get(id);
        _bus.Send(myMessage);
        transactionScope.Complete();
    }

    session.Dispose();
}

This code creates the error:

public void DoSomethingToEntity(Guid id)
{
    var session = _sessionFactory.OpenSession();
    CurrentSessionContext.Bind(session);

    using (var transactionScope = new TransactionScope())
    {
        var myEntity = _session.Get(id);
        myEntity.AnyField = "a new value";
        _bus.Send(myMessage);
        transactionScope.Complete();
    }

    session.Dispose();
}

Note that I am not saving th entity in either example. The difference is in the second example, I am modifying the entity I have got from NHibernate. This is 100% reproducable.

Oligochaete answered 19/9, 2012 at 12:42 Comment(12)
Would it be possible to send the message first from the web app and then have NSB do the DB update and any other subsequent Sends()?Its
@AdamFyles This would be possible and would work but incurs a performance overhead which I'm sure could be avoided. I know using TransactionScope is possible but I can't get past this error.Oligochaete
To whoever downvoted: it is not helpful to me or other Stack Overflow users to downvote without an explanation why.Oligochaete
Is the DTC running on your webserver?Funnel
@PaulTDavies this would allow you to immediately return and let the DB work on its own time in the background which should have better performance.Its
@AndreasÖhlund DTC all seems to be up and running, and has the same settings as in this article: deepakkapoor.net/turn-on-msdtc-windows-7 If DTC was the problem, wouldn't it also be a problem in the message handlers?Oligochaete
It seems like the EX happens when the sql driver tried to upgrade the TX to a distributed transaction (since the msmq send will force the upgrade). The reason that it works when running on a handler is that NSB has already received from msmq so the TX is already a distributed TX when the sql driver enlists. No idea why it happens thoughFunnel
Could this help? davybrion.com/blog/2010/03/…Funnel
This may not be related but you still have to call _session.Flush() before committing a TransactionScope even if the session flush mode is set to Commit - that only works for NH provided transactions.Jay
@AndreasÖhlund I feel I'm getting a bit closer. When I said it still fails with the NHibernate functionality removed, I was only talking about the save. If I remove anything to do with NH, it works. I'm getting closer to this and will update you. Please post an answer to collect SO points!Oligochaete
@Jay You got it! session.Flush() fixed it. Sorry to everyone if this is something I should not have missed. Stick an answer on to claim you points!Oligochaete
@AndreasÖhlund The above should interest you. Sorry if this is in the documentation somewhere and I missed it!Oligochaete
J
4

This may not be related but you still have to call _session.Flush() before committing a TransactionScope even if the session flush mode is set to Commit - that only works for NH provided transactions.

Jay answered 21/9, 2012 at 14:17 Comment(1)
Yes, calling session.Flush(); before transactionScope.Complete() fixed the problem. I don't fully understand why, but is to do with having both NServiceBus and NHibernate (or more specifically, ADO.NET, I think) inside the transaction.Oligochaete
I
0

As far as I can tell there is no way of being notified when a new System.Transactions.Transaction is created, and looking at the code in NHibernate it doesn't seem to have any code to deal with the situation where the TransactionScope is created AFTER creating the session.

When you create the session, it will try to enlist in the current Transaction, and if there isn't one then the session won't enlist in the transaction. I suspect that this is what's causing the transaction to fail on commit.

I would suggest creating the session INSIDE the TransactionScope - also check whether you are calling session.BeginTransaction somewhere before the TransactionScope.

Issy answered 19/9, 2012 at 16:22 Comment(7)
It is not NHibernate that is the problem, it is NServiceBus. As far as I know, there is no problem creating a TransactionScope after creating the session - it certainly seems to work the way I expected. I tried it anyway but with the same results.Oligochaete
Are you sure the queue you're sending to is transactional? Is it a local queue or remote queue?Issy
The queue is transactional, yes.Oligochaete
Does each operation complete successfully if they're in their own TransactionScopes?Issy
The Nhibernate operation does, the NService bus operation does not.Oligochaete
Have you turned on NServiceBus debug logging to see if there's another error happening? Have you tried sending a message using MSMQ directly inside the transactionscope?Issy
It turns out it may be NHibernate causing problems - see above.Oligochaete

© 2022 - 2024 — McMap. All rights reserved.