It is a common matter to have an operation in the server that requires few steps. Sometimes it is necessary that all the steps will complete successfully, and if one fails, a rollback should be made. SQL has the transaction term to support this kind of situation, and I’m about to show how it can be done in code.
My role of thumb for this kind of infrastructure is that the code should be easy to use, flexible enough for many different cases, and relays on a default behavior that can be overridden when needed.
Now lets think about a process of this kind:
The Single Step
- A process is created from small steps, each one is responsible for a single task.
- Each step should get the necessary input data and manipulate it.
- Each step should know how to execute its task. (Daa..)
- Each task should have the knowledge how to rollback this task.
- Each step should be able to make several retry attempts for the execution and rollback.
- Each step should report if the action was successful or failed.
- Execute – run the task
- Retry – do “Execute” again
- Rollback – do nothing
- Retry Rollback - do Rollback again
public enum eStatus
{
Pending,
Success,
Error
}
public abstract class BaseStep
{
public virtual int ExecuteAttempts { get { return 1; } }
public virtual int RollbackAttempts { get { return 1; } }
public abstract eStatus Execute();
public virtual eStatus Retry()
{
return Execute();
}
public virtual eStatus Rollback()
{
return eStatus.Success;
}
public virtual eStatus RetryRollback()
{
return Rollback();
}
}
The Process Manager
The manager is responsible for creating the steps in the right order and execute each one. It is also responsible for coordinating the retry attempts and rollbacks. It implements an interface with one method:
public interface IProcessManager
{
eStatus Process();
}
and a base class that encapsulates the default behavior:
public abstract class BaseProcessManager : IProcessManager
{
private eStatus _currentStatus;
private int _currentStep;
private Func<eStatus> _action;
protected List<BaseStep> _steps;
protected BaseProcessManager()
{
_currentStatus = eStatus.Pending;
_steps = new List<BaseStep>();
Setup();
}
protected abstract void Setup();
public virtual eStatus Process()
{
do
{
_action = _steps[_currentStep].Execute;
Run();
_action = _steps[_currentStep].Retry;
Retry(_steps[_currentStep].ExecuteAttempts);
_currentStep++;
} while (_currentStatus != eStatus.Error && _currentStep < _steps.Count);
if (_currentStatus == eStatus.Success)
return eStatus.Success;
do
{
_currentStep--;
_action = _steps[_currentStep].Rollback;
Run();
_action = _steps[_currentStep].RetryRollback;
Retry(_steps[_currentStep].RollbackAttempts);
} while (_currentStep > 0);
return eStatus.Error;
}
private void Run()
{
_currentStatus = eStatus.Pending;
_currentStatus = _action.Invoke();
}
private void Retry(int numberOfAttempts)
{
int attempt = 1;
while (_currentStatus == eStatus.Error && attempt < numberOfAttempts)
{
_currentStatus = _action.Invoke();
attempt++;
}
}
}
Now what is left for a developer to do is:
- Implement The BaseProcessManager, and override the Setup() method, where he should create the steps.
- Create steps that implement the BaseStep abstract class, with the step logic.
Thats it!I added a small app demonstrating this ides – grab it here.Special thanks to Lior Hakim and Omry Hay!