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?
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With