Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ Transaction-like Pattern for All or Nothing Work

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?

like image 959
kirakun Avatar asked Jun 11 '12 16:06

kirakun


3 Answers

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.

like image 188
Mare Infinitus Avatar answered Oct 24 '22 05:10

Mare Infinitus


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.

like image 13
Useless Avatar answered Oct 24 '22 04:10

Useless


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.

like image 7
PermanentGuest Avatar answered Oct 24 '22 06:10

PermanentGuest