views:

107

answers:

4

Question by abstract example

Suppose you have 2 methods: DoJob1(), DoJob2(). Each of them has transaction-like behavior, that is, either does its job or reports an error.

How should I write a method which executes DoJob1() then DoJob2(), but is transaction-like itself, that is, guarantees the roll-back of the action performed by DoJob1() in case an error occurs while processing DoJob2()?

Of course, you are free to choose the way of error handling (bool return value, real exceptions, global error variable — you name it).

Background

The idea is to write (some) methods transaction-like. And when an exception occurs, suggest the user to repeat the 'transaction'.

I have a thought on the possible approach to the problem, which I am going to post in a while; (in order not to limit your imagination)

+1  A: 

In general I do:

transactionalJob1()
    transaction_begin()
    doJob1()
    transaction_end()
    exception:
        log
        transaction_rollback()

transactionalJob2()
    transaction_begin()
    doJob2()
    transaction_end()
    exception:
        log
        transaction_rollback()

transactionalJob1And2()
    transaction_begin()
    doJob1()
    doJob2()
    transaction_end()
    exception:
        transaction_rollback()

If your language of choice supports template methods you may wrap it all up.

Manrico Corazzi
I try apply the concept of transaction to the common programming, where there is no 'transaction_begin()'; but there are just method calls, exceptions, attributes...Idea! Maybe your transaction_begin() will write stuff (literally, 'state' to a global list), and then either transaction_end() or transaction_rollback() will execute rollbacks() ?
modosansreves
This assumes that doJob1 and doJob2 do not do their own transaction handling, but instead rely on the caller to set up transaction context. This is of course the preferable design, but probably not the case for the OP.
Thilo
A: 

My thoughts

I have identical idea.

Here is a beta approach (C#):

class Program
{
    Random rnd = new Random();

    void DoJob1()
    {
        if (rnd.NextDouble() <= 0.5)
            throw new ArgumentException("First case");

        Console.WriteLine("First job done.");
    }

    void DoJob1Rollback()
    {
        Console.WriteLine("First job rollback.");
    }

    void DoJob2()
    {
        if (rnd.NextDouble() <= 0.5)
            throw new ArgumentException("Second case");

        Console.WriteLine("Second job done.");
    }

    void DoJob2Rollback()
    {
        Console.WriteLine("Second job rollback.");
    }

    void Run()
    {
        bool success = false;

        while (true)
        {
            try
            {
                TransactHelper.DoTransaction(
                    DoJob1,
                    DoJob2
                );
                success = true;
            }
            catch (Exception ex)
            {
                Console.WriteLine("--------------------------------------");
                Console.WriteLine("Exception: " + ex.Message);
                // Console.WriteLine("Stack trace:");
                // Console.WriteLine(ex.StackTrace);
                // Console.WriteLine();
            }

            if (!success)
            {
                // ask the user for another chance
                Console.Write("Retry? ");
                if (Console.ReadLine().ToLower() != "yes")
                    break;
            }
            else
                break;
        }

        Console.WriteLine("Batch job {0}", success ? "succeeded." : "did not succeed.");

        Console.WriteLine("Press Enter to exit.");
        Console.ReadLine();
    }

    static void Main(string[] args)
    {
        (new Program()).Run();
    }
}

This looks as nice, as running TransactHelper.DoTransaction

class TransactHelper
{
    public static void DoTransaction(params ThreadStart[] actions)
    {
        int i = 0;
        int n = actions.Length;

        // exception to pass on
        Exception ret_ex = null;

        // do the list of jobs
        for (; i < n; ++i)
        {
            try
            {
                ThreadStart ts = actions[i];
                ts();
            }
            catch (Exception ex)    // register exception
            {
                ret_ex = ex;
                break;
            }
        }

        if (ret_ex != null)         // exception registered, rollback what's done
        {
            int k = i;              // method which failed
            for (; i >= 0; --i)
            {
                MethodInfo mi = actions[i].Method;
                string RollbackName = mi.Name + "Rollback";

                // set binding flags - the same as the method being called
                BindingFlags flags = (mi.IsStatic) ? BindingFlags.Static : BindingFlags.Instance;
                if (mi.IsPrivate)
                    flags |= BindingFlags.NonPublic;
                if (mi.IsPublic)
                    flags |= BindingFlags.Public;

                // call rollback
                MethodInfo miRollback = mi.DeclaringType.GetMethod(RollbackName, flags);
                miRollback.Invoke(actions[i].Target, new object[] { });
            }

            throw new TransactionException("Transaction failed on method " + actions[k].Method.Name, ret_ex);
        }
    }
}

[global::System.Serializable]
public class TransactionException : Exception
{
    public TransactionException() { }
    public TransactionException(string message) : base(message) { }
    public TransactionException(string message, Exception inner) : base(message, inner) { }
    protected TransactionException(
      System.Runtime.Serialization.SerializationInfo info,
      System.Runtime.Serialization.StreamingContext context)
        : base(info, context) { }
}

However, there are pitfalls.

  • Additional pain by maintaining two methods instead of one
  • What if an exception occurs in a xRollback method ? (however, it is the same situation as it occurs in the catch{} block)
  • (Current design runs only methods without arguments — this is easily extended)

I've initially thought on this problem as I worked in a team which produces plenty of code like:

_field = new Field();
_field.A = 1;
_filed.B = "xxx";

instead of

Field tmp = new Field();
tmp.A = 1;
tmp.B = "xxx";

_field = tmp;

The beauty of the transaction are:

  • data integrity (!)
  • you can suggest the user to repeat the action

P.S.

Maybe I'm trying to re-invent the wheel? Is there a library for a wiser approach? What are the pitfalls of this design I didn't foresee?

modosansreves
A: 

The second approach I undertook is just to log roll-back actions in a separate class.

class Transaction
{
    IList<Action> _rollBacks = new List<Action>();

    public void AddRollBack(Action action)
    {
        _rollBacks.Add(action);
    }

    public void Clear()
    {
        _rollBacks.Clear();
    }
    public void RollBack()
    {
        for (int i = _rollBacks.Count - 1; i >= 0; --i)
        {
            _rollBacks[i]();
            _rollBacks.RemoveAt(i);
        }
    }
}

The actual code is written in pairs, e.g.:

File.Move(file, file + "__");
string s = (string) file.Clone();
tr.AddRollBack(() => File.Move(s + "__", s));

When I need to do something transactionally, I create a transaction object, and cover sensitive code into try ... catch ... finally

Transaction tr = new Transaction();

UpdateLogic ul = new UpdateLogic();
ul.Transaction = tr;

// ........ more initialization (of classes)

try
{
    // .... more initialization, which is a part of transaction
    ul.DoPreparation();
    ul.DoCopying();

    tr.Clear();                         // at this point most of update is ok

    ul.DoCleanup();

    ShowMessage("Update completed", "Update completed successfully.", false);
}
catch (Exception ex)
{
    // handel error
}
finally
{
    // show message in UI
    try
    {
        tr.RollBack();
    }
    catch (Exception ex)
    {
        ShowMessage("Error while performing rollback of actions", ex.Message, true);
    }

    // ... update is done        
}

It is very convenient, because this guarantees integrity of files. With wise coding, the integrity is maintained even if the process is killed in between of doing things or rolling back (yet leaving some trash in filesystem).

modosansreves
A: 

That depends on what kind of work the method is supposed to do. In general, first the transaction's side-effects are stored into a temporary place, and then when the transaction commits, the side-effects are stored to a permanent place in one atomic operation. The details of how to do that are very different depending on whether you are modifying something in the file system (read from some books that how database transaction logs work), some in-memory data structure, something over the network or something else.

For example, I've once written a transactional in-memory key-value database (part of http://dimdwarf.sourceforge.net/). It keeps a temporary list of all modifications done during the transaction. Then when the transaction commits, the modified keys are locked in the database, the modifications are stored in the database (this operation must not fail), after which the keys are unlocked. Only one transaction may commit at a time, and no other transaction can see the changes until the transaction has fully committed.

Esko Luontola