Search code examples
c#transactionsfilesystemsatomic

Atomicity: Implementing a Resource Manager


I am trying to make sense of the following documentation:

I find the documentation hard to read and understand, in particular it gives a false sense of support for transactions at application level. Let's consider the following SO reference, where poster requested support for File.Move within a transaction:

As of today, the accepted answer is (truncated):

TxFileManager fileMgr = new TxFileManager();
using (TransactionScope scope1 = new TransactionScope())
{
    fileMgr.Copy(srcFileName, destFileName);

    scope1.Complete();
}

The first thing that stands out is that the accepted answer slightly modified the original poster request, in that answer changed line:

fileMgr.Move(srcFileName, destFileName);

into:

fileMgr.Copy(srcFileName, destFileName);

The important point being that the underlying system (the actual filesystem) may not provide an atomic File.Move operation (eg. obviously when crossing file system boundary).

How should I read the above documentation to understand what are the actual requirements when implementing a Resource Manager ? In particular, is it possible to implement a Resource Manager when the underlying system does not offer a true atomic operation (we can take the example of the File.Move operation accross file system boundaries as an example) ?


Solution

  • First, i think you should make your resource manager durable:

    https://learn.microsoft.com/en-us/dotnet/api/system.transactions.transaction.enlistdurable?redirectedfrom=MSDN&view=net-5.0#overloads

    Then you can save your file moving operation to a durable storage (file, database table,..) in the Prepare method (phase 1 of 2-phase commit protocol) with some format like (Operation:FileMove, SrcFile, DesFile), this also helps with recovery later if the process crashes:

    https://learn.microsoft.com/en-us/dotnet/api/system.transactions.preparingenlistment?view=net-5.0 .

    public void Prepare(PreparingEnlistment preparingEnlistment)
        {
            Console.WriteLine("Prepare notification received");
    
            //Validate if the File.Move is valid if it's executed later, then save your file moving operation to a temporary durable storage
    
            //If work finished correctly, reply prepared
            preparingEnlistment.Prepared();
    
            // otherwise, do a ForceRollback
            preparingEnlistment.ForceRollback();
        }
    

    In phase 2 (Commit), you can load the operation from the durable storage to actually move the file (use retry to ensure true atomic operation)

     public void Commit(Enlistment enlistment)
        {
            Console.WriteLine("Commit notification received");
    
            //Read to file moving operation from phase 1 to move the file and retry if the failure is transient
    
            //Declare done on the enlistment when the files have been moved 
            enlistment.Done();
        }
    

    The point of saving the file moving operation in phase 1 is to ensure that:

    • If any failure occurs in the Commit phase (due to File.Move operation accross file system boundaries) => enlistment.Done() is not called so the Commit method can be executed later, we still have the information in phase 1 to retry.
    • Because our resource manager is durable, if the process crashes while in Commit method, in the recovery process we can retry the file moving operation thanks to the information persisted in Prepare phase