Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Where should I perform action when implementing IEnlistmentNotification?

I am trying to create a custom "resource manager" by implementing IEnlistmentNotification interface. This interface has following methods:

  • Prepare()
  • Commit()
  • Rollback()
  • InDoubt()

While it's clear that rollback code should go in Rollback() method I am not sure in which method should I implement the code that performs the actual operation? Should it go in Prepare() or Commit() or maybe some other custom method in the class that will be called from outer code from inside of TransactionScope block?

like image 336
matori82 Avatar asked Jan 13 '14 15:01

matori82


2 Answers

The actual work should be performed in another method. Prepare and Commit are there to implement a 2-phase commit mechanism.

The pattern is as follows:

using(var transaction = new TransactionScope())
{
    var rc1 = new ResourceManager();
    rc1.DoWork();
    var rc2 = new ResourceManager();
    rc2.DoWork();
    transaction.Complete();
}

In this example the DoWork should execute the action. When exiting the transaction scope, the Prepare method of both resource managers will be called. If they both called enlistment.Prepared(); then the Commit methods of both managers will be called. That commit should never fail!

For example, when working with files, DoWork should rename the file to indicate you are processing it, and then read and process the file. If either action fails, it should throw an exception causing the Rollback to be called. Rollback should rename the file back to its original name. Prepare could rename the file to indicate it should be deleted and check whether it is allowed to delete the file. If either action fails, it should throw an exception. Commit would then actually delete the file. This will not fail because we already checked the security, but even if it does, it should not throw an exception.

You could actually delete the file in the Prepare method and call enlistment.Done();. This would indicate that the call to Commit is not needed anymore. But the problem with that is that after you deleted the file, the other resource manager could throw an exception in its Prepare. Because you indicated you were Done, your rollback method will not be called. And even if is was called, there would be no way for you to recover your action...

I hope this explains things a bit...

like image 95
Marc Selis Avatar answered Oct 19 '22 22:10

Marc Selis


Here is some sample code that has some implementation and unit tests. Creating a base class will let me focus on the actions I need to do instead of dealing with transactions everywhere.

public abstract class TransactionCreator : IEnlistmentNotification
{
    protected TransactionCreator()
    {
        System.Transactions.Transaction.Current.EnlistVolatile(this, EnlistmentOptions.None);
    }

    public void Commit(Enlistment enlistment)
    {
        Complete();
        enlistment.Done();
    }

    public void InDoubt(Enlistment enlistment)
    {
        enlistment.Done();
    }

    //Don't throw an exception here. Instead call ForceRollback()
    public void Prepare(PreparingEnlistment preparingEnlistment)
    {
        try
        {
            Execute();
            preparingEnlistment.Prepared();
        }
        catch (Exception e)
        {
            preparingEnlistment.ForceRollback(e);
        }
    }

    public void Rollback(Enlistment enlistment)
    {
        Revert();
        enlistment.Done();
    }

    public abstract void Execute();
    public abstract void Complete();
    public abstract void Revert();
}

To test the IEnlistmentNotification implementation we'll:

-Test the expected flow

-Mock the Execute method to fail in an object and see the rollback called in the other.

(I'm using NSubstitute for mocks, but can ignore that)

[TestFixture]
public class TransactionCreatorTest
{
    [Test]
    public void Test_file_gets_created_on_transaction_complete()
    {
        TransactionCreator creator;

        using (var scope = new TransactionScope())
        {
            creator = Substitute.For<TransactionCreator>();

            scope.Complete();
        }
        creator.Received().Execute();
        creator.DidNotReceive().Revert();
    }

    [Test]
    public void Test_file_gets_does_not_get_created_on_rollback()
    {
        TransactionCreator creator = null;
        try
        {
            using (var scope = new TransactionScope())
            {
                creator = Substitute.For<TransactionCreator>();
                var failed = Substitute.For<TransactionCreator>();
                failed.When(x => x.Execute()).Do(x => { throw new Exception(); });
                scope.Complete();
            }
        }
        catch (TransactionAbortedException ex)
        {
            Console.Out.WriteLine(ex);
        }


        creator.Received().Execute();
        creator.Received().Revert();
    }
}
like image 2
Oscar Fraxedas Avatar answered Oct 19 '22 20:10

Oscar Fraxedas