Search code examples
design-patternsatomic

How to implement "atomicity" for a series of operations? (not necessarily multithreading related)


The Problem

My programm has an operation which consists of a series of single operations that all change global state. Each of the elementary operations can fail and leave the global state in an undefined state.

The Question

Is there a generic pattern that helps me to make the composed operation "atomic" in the sense that if one of the suboperations fails, the global state is left unchanged?

I use C++, so if answers contain code, please prefer that language if you have a choice. But I do not mind examples in other languages.

Comments

  • This resembles the "atomicity" of a database, where you add all or nothing when doing a commit. How is that implemented?

  • In my case my global state is the state of the file-system. I need to add or remove multiple files at once and want to make sure that they are all added or nothing is added.

My Thoughts

The best I can come up with is a class that gets a list of operations and their inverse operations. If any of the operations fails, it executes the reverse operations of the already executed operations. But what to do if one of the reverse operations fails?

How would the interface of that class look like? Would I have an extra class that resembles an reversible operation? Do you know a place where I can read more about this problem?


Solution

  • The basic solution is commit-or-rollback.

    The generic C++ implementation is to build a list of objects supporting 3 methods:

    • run() which will perform all the steps which could fail, without producing visible side-effects. It must throw without changing anything if it fails
    • commit() which may not throw, and must only make the steps performed during run() visible
    • rollback() which may not throw, and must only undo the steps performed during run()

    and then iterate over that list calling run() on everything. If any throw, catch it and rollback() everything that ran successfully. If none throw, commit() them all.


    Approximate workflow for your situation is - for each file-creating-object:

    1. call run() to create (and populate?) the file in the correct directory, but with a temporary name - ideally hidden

      • if this fails, call rollback() on each successfully run() file-creating-object. This must delete the temporary file and may not fail.

        Then, give up, retry from scratch, prompt the user or something.

    2. repeat #1 until you've created all files
    3. they all succeeded, so call commit() on each object. This must rename the temporary file to its final name and may not fail

    This requires that the commit step cannot fail. In this case, that assumes renaming a file can't fail - you have to ensure this is really true (ie, there are no name collisions) before you get to the commit stage.


    Note this does have intermediate non-atomic changes - the only way to avoid that in general is to have two copies of the global state, and an atomic swap to commit the modified one. You can't do this for a filesystem in general, even if you're writing the driver.