10

Suppose I have two functions DoTaskA and DoTaskB—both capable of throwing TaskException—with their corresponding "rollback" functions UndoTaskA and UndoTaskB. What is the best pattern to use so that either both succeed or both fail?

The best I have now is

bool is_task_a_done = false,
     is_task_b_done = false;

try {
    DoTaskA();
    is_task_a_done = true;

    DoTaskB();
    is_task_b_done = true;
} catch (TaskException &e) {
    // Before rethrowing, undo any partial work.
    if (is_task_b_done) {
        UndoTaskB();
    }
    if (is_task_a_done) {
        UndoTaskA();
    }
    throw;
}

I know that is_task_b_done is unnecessary, but maybe good to show code symmetry in case we add a third or a fourth task later on.

Don't like this code because of the auxiliary boolean variables. Perhaps there is something in the new C++11 that I'm not aware of, which can code this up more nicely?

kirakun
  • 2,770
  • 1
  • 25
  • 41
  • 1
    If you are inside catch block, `is_task_b_done` is always `false` – UltraInstinct Jun 11 '12 at 16:13
  • I know, I've added the comment that it's only there for code symmetry in case we add a third or fourth task. – kirakun Jun 11 '12 at 16:14
  • Is your app multi-threaded? Did you take a look at [RAII](http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization)? – dirkgently Jun 11 '12 at 16:18
  • For simplicity let's assume it is not multithreaded. Just look for a design pattern here. How would I try this with RAII? – kirakun Jun 11 '12 at 16:20

5 Answers5

13

A little RAII commit/rollback scope guard might look like this:

#include <utility>
#include <functional>

class CommitOrRollback
{
    bool committed;
    std::function<void()> rollback;

public:
    CommitOrRollback(std::function<void()> &&fail_handler)
        : committed(false),
          rollback(std::move(fail_handler))
    {
    }

    void commit() noexcept { committed = true; }

    ~CommitOrRollback()
    {
        if (!committed)
            rollback();
    }
};

So, we're assuming we'll always create the guard object after the transaction succeeds, and call commit only after all the transactions have succeeded.

void complicated_task_a();
void complicated_task_b();

void rollback_a();
void rollback_b();

int main()
{
    try {
        complicated_task_a();
        // if this ^ throws, assume there is nothing to roll back
        // ie, complicated_task_a is internally exception safe
        CommitOrRollback taskA(rollback_a);

        complicated_task_b();
        // if this ^ throws however, taskA will be destroyed and the
        // destructor will invoke rollback_a
        CommitOrRollback taskB(rollback_b);


        // now we're done with everything that could throw, commit all
        taskA.commit();
        taskB.commit();

        // when taskA and taskB go out of scope now, they won't roll back
        return 0;
    } catch(...) {
        return 1;
    }
}

PS. As Anon Mail says, it's better to push all those taskX objects into a container if you have many of them, giving the container the same semantics (call commit on the container to have it commit each owned guard object).


PPS. In principle, you can use std::uncaught_exception in the RAII dtor instead of explicitly committing. I prefer to explicitly commit here because I think it's clearer, and also works correctly if you exit scope early with a return FAILURE_CODE instead of an exception.

Useless
  • 64,155
  • 6
  • 88
  • 132
  • Neat answer. I like the comment that `std::uncaught_exception` could be used to avoid the `commit` which is somewhat detached from the main body flow, making it easy to forget. We should never use mixed error-handling paradigms anyway. – kirakun Jun 11 '12 at 17:08
7

It is hard to achieve Transaction consistency in C++. There is a nice method described using the ScopeGuard pattern in the Dr Dobb's journal. The beauty of the approach is that this takes cleanup in both the normal situations and in the exception scenarios. It utilizes the fact that object destructors are ensured to call on any scope exits and exception case is just another scope exit.

PermanentGuest
  • 5,213
  • 2
  • 27
  • 36
  • 2
    Perhaps you could reproduce (or sketch) the code inline, just in case Dr. Dobbs happens to be down when someone is reading this answer? – Useless Jun 12 '12 at 13:22
1

Have you thought about CommandPattern? Command Pattern description

You encapsulate all data that is needed to do what DoTaskA() does in an object of a command class, with the bonus, that you can reverse all of this, if needed (thus no need to have a special undo if failed to execute). Command pattern is especially good for handling "all or nothing" situations.

If you have multiple commands which build on each other, as your example can be read, then you should investigate chain of responsibility

perhaps a reactor pattern may come in handy (reactor description here) this will invert the flow of control, but it feels natural and has the benefit of turning your system into a strong multithreaded, multicomponent design. but it may be overkill here, hard to tell from the example.

Mare Infinitus
  • 8,024
  • 8
  • 64
  • 113
1

The best way to achieve this is with scope guards, basically a small RAII idiom that will invoke a rollback handler if an exception is thrown.

I've ask about a simple implementation of ScopeGuard a bit ago and the question evolved into a nice implementation i'm using in my production projects. It works with c++11 and lambdas as the rollback handlers.

my source has actually two versions: one that will invoke the rollback handler if the constructor handler throws, and another that will not throw if that happens.

check the source and usage examples in here.

Community
  • 1
  • 1
lurscher
  • 25,930
  • 29
  • 122
  • 185
0

For scalability, you want to save the fact that you need to do an undo for a task in a container. Then, in the catch block, you just invoke all of the undos that are recorded in the container.

The container can, for instance, contain function objects to undo the task that have completed successfully.

Anon Mail
  • 4,660
  • 1
  • 18
  • 21