My thoughts
I have identical idea.
Here is a beta approach (C#):
class Program
{
Random rnd = new Random();
void DoJob1()
{
if (rnd.NextDouble() <= 0.5)
throw new ArgumentException("First case");
Console.WriteLine("First job done.");
}
void DoJob1Rollback()
{
Console.WriteLine("First job rollback.");
}
void DoJob2()
{
if (rnd.NextDouble() <= 0.5)
throw new ArgumentException("Second case");
Console.WriteLine("Second job done.");
}
void DoJob2Rollback()
{
Console.WriteLine("Second job rollback.");
}
void Run()
{
bool success = false;
while (true)
{
try
{
TransactHelper.DoTransaction(
DoJob1,
DoJob2
);
success = true;
}
catch (Exception ex)
{
Console.WriteLine("--------------------------------------");
Console.WriteLine("Exception: " + ex.Message);
// Console.WriteLine("Stack trace:");
// Console.WriteLine(ex.StackTrace);
// Console.WriteLine();
}
if (!success)
{
// ask the user for another chance
Console.Write("Retry? ");
if (Console.ReadLine().ToLower() != "yes")
break;
}
else
break;
}
Console.WriteLine("Batch job {0}", success ? "succeeded." : "did not succeed.");
Console.WriteLine("Press Enter to exit.");
Console.ReadLine();
}
static void Main(string[] args)
{
(new Program()).Run();
}
}
This looks as nice, as running TransactHelper.DoTransaction
class TransactHelper
{
public static void DoTransaction(params ThreadStart[] actions)
{
int i = 0;
int n = actions.Length;
// exception to pass on
Exception ret_ex = null;
// do the list of jobs
for (; i < n; ++i)
{
try
{
ThreadStart ts = actions[i];
ts();
}
catch (Exception ex) // register exception
{
ret_ex = ex;
break;
}
}
if (ret_ex != null) // exception registered, rollback what's done
{
int k = i; // method which failed
for (; i >= 0; --i)
{
MethodInfo mi = actions[i].Method;
string RollbackName = mi.Name + "Rollback";
// set binding flags - the same as the method being called
BindingFlags flags = (mi.IsStatic) ? BindingFlags.Static : BindingFlags.Instance;
if (mi.IsPrivate)
flags |= BindingFlags.NonPublic;
if (mi.IsPublic)
flags |= BindingFlags.Public;
// call rollback
MethodInfo miRollback = mi.DeclaringType.GetMethod(RollbackName, flags);
miRollback.Invoke(actions[i].Target, new object[] { });
}
throw new TransactionException("Transaction failed on method " + actions[k].Method.Name, ret_ex);
}
}
}
[global::System.Serializable]
public class TransactionException : Exception
{
public TransactionException() { }
public TransactionException(string message) : base(message) { }
public TransactionException(string message, Exception inner) : base(message, inner) { }
protected TransactionException(
System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context)
: base(info, context) { }
}
However, there are pitfalls.
- Additional pain by maintaining two methods instead of one
- What if an exception occurs in a xRollback method ? (however, it is the same situation as it occurs in the catch{} block)
- (Current design runs only methods without arguments — this is easily extended)
I've initially thought on this problem as I worked in a team which produces plenty of code like:
_field = new Field();
_field.A = 1;
_filed.B = "xxx";
instead of
Field tmp = new Field();
tmp.A = 1;
tmp.B = "xxx";
_field = tmp;
The beauty of the transaction are:
- data integrity (!)
- you can suggest the user to repeat the action
P.S.
Maybe I'm trying to re-invent the wheel?
Is there a library for a wiser approach?
What are the pitfalls of this design I didn't foresee?