1
votes

I'm using NServiceBus to handle some calculation messages. I have a new requirement to handle calculation errors by writing them the same database. I'm using NHibernate as my DAL which auto enlists to the NServiceBus transaction and provides rollback in case of exceptions, which is working really well. However if I write this particular error to the database, it is also rolled back which is a problem.

I knew this would be a problem, but I thought I could just wrap the call in a new transaction with the TransactionScopeOption = Suppress. However the error data is still rolled back. I believe that's because it was using the existing session with has already enlisted in the NServiceBus transaction.

Next I tried opening a new session from the existing SessionFactory within the suppression transaction scope. However the first call to the database to retrieve or save data using this new session blocks and then times out.

InnerException: System.Data.SqlClient.SqlException Message=Timeout expired. The timeout period elapsed prior to completion of the >operation or the server is not responding.

Finally I tried creating a new SessionFactory using it to open a new session within the suppression transaction scope. However again it blocks and times out.

I feel like I'm missing something obvious here, and would greatly appreciate any suggestions on this probably common task.

2
Would it be possible to just let NSB push the errors to the error queue and then have another process those messages? - Adam Fyles

2 Answers

1
votes

As Adam suggests in the comments, in most cases it is preferred to let the entire message fail processing, giving the built-in Retry mechanism a chance to get it right, and eventually going to the error queue. Then another process can monitor the error queue and do any required notification, including logging to a database.

However, there are some use cases where the entire message is not a failure, i.e. on the whole, it "succeeds" (whatever the business-dependent definition of that is) but there is some small part that is in error. For example, a financial calculation in which the processing "succeeds" but some human element of the data is "in error". In this case I would suggest catching that exception and sending a new message which, when processed by another endpoint, will log the information to your database.

I could see another case where you want the entire message to fail, but you want the fact that it was attempted noted somehow. This may be closest to what you are describing. In this case, create a new TransactionScope with TransactionScopeOption = Suppress, and then (again) send a new message inside that scope. That message will be sent whether or not your full message transaction rolls back.

You are correct that your transaction is rolling back because the NHibernate session is opened while the transaction is in force. Trying to open a new session inside the suppressed transaction can cause a problem with locking. That's why, most of the time, sending a new message asynchronously is part of the solution in these cases, but how you do it is dependent upon your specific business requirements.

0
votes

I know I'm late to the party, but as an alternative suggestion, you coudl simply raise another separate log message, which NSB handles independently, for example:

public void Handle(DebitAccountMessage message)
{
    var account = this.dbcontext.GetById(message.Id);
    if (account.Balance <= 0)
    {
        // log request - new handler
        this.Bus.Send(new DebitAccountLogMessage
            {
                originalMessage = message,
                account = account,
                timeStamp = DateTime.UtcNow
            });

        // throw error - NSB will handle
        throw new DebitException("Not enough funds");
    }
}

public void Handle(DebitAccountLogMessage message)
{
    var messageString = message.originalMessage.Dump();
    var accountString = message.account.Dump(DumpOptions.SuppressSecurityTokens);
    this.Logger.Log(message.UniqueId, string.Format("{0}, {1}", messageString, accountString);
}