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) ?
First, i think you should make your resource manager durable:
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:
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.Commit
method, in the recovery process we can retry the file moving operation thanks to the information persisted in Prepare
phase