Skip to content

Instantly share code, notes, and snippets.

@gubenkoved
Created February 22, 2018 14:30
Show Gist options
  • Save gubenkoved/a7eb3ad42a1e339930d4906ce48a6304 to your computer and use it in GitHub Desktop.
Save gubenkoved/a7eb3ad42a1e339930d4906ce48a6304 to your computer and use it in GitHub Desktop.
CodeExecutionContainer
internal enum CodeExecutionContainerState
{
Created,
Running,
Succeeded,
Failed,
CancellationRequested,
Cancelled,
AbortRequested,
Aborted
}
/// <summary>
/// Provides ability to execute code in dedicated Thread and ability to Cancel its execution using CancellationToken ("softly")
/// and if code was unable to shutdown itself in timely fashion kill it using Thread.Abort().
/// </summary>
internal class CodeExecutionContainer
{
#region Fields
private ILogger _log = Logger.Get(typeof(CodeExecutionContainer));
private Action _work;
private CancellationTokenSource _cancellationTokenSource;
private Action<CodeExecutionContainer> _successHandler;
private Action<CodeExecutionContainer> _errorHandler;
private Thread _workerThread;
private volatile CodeExecutionContainerState _state;
private Stopwatch _stopwatch;
private object _syncRoot = new object();
#endregion
#region Properties
/// <summary>
/// Current container state.
/// </summary>
public CodeExecutionContainerState State
{
get
{
return _state;
}
}
/// <summary>
/// Exception, the result of task execution. Can be null if work succeeded.
/// </summary>
public Exception Exception { get; private set; }
/// <summary>
/// Returns elapsed time (>0 only after work gets called).
/// </summary>
public TimeSpan Elapsed => _stopwatch.Elapsed;
/// <summary>
/// Time interval for cancellable work to shutdown itself prior to hard killing using Thread.Abort().
/// </summary>
public TimeSpan TimeIntervalForSoftCancellation { get; set; }
/// <summary>
/// Gets or sets flags indicates that auto-cancellation is needed by this container.
/// Default value is true.
/// </summary>
public bool UseAutoCancellation { get; set; } = true;
/// <summary>
/// Time interval after which task cancellation will be automatically requested or worker will be just killed if underlying work do not accepts CancellationToken.
/// </summary>
public TimeSpan TimeBeforeAutoCancellation { get; set; }
internal CancellationTokenSource CancellationTokenSource => _cancellationTokenSource;
/// <summary>
/// Gets or sets logging service for this instance. Can be used to redirect logs.
/// </summary>
public ILogger LogService
{
get
{
return _log;
}
set
{
_log = value;
}
}
#endregion
#region Constructor
/// <summary>
/// Light constructor. The underlying work will be considered as non cancellable and will be killed in a hard way
/// in case of timeout expiration or manual cancellation request.
/// </summary>
public CodeExecutionContainer(Action work)
: this(work, null, null, null)
{
}
/// <summary>
/// Light constructor for cancellable work. The underlying task work will be notified using CancellationToken
/// and will be killed in a hard way only if code will be unable to shutdown in timely fashion (TimeIntervalForSoftCancellation).
/// The cancellation token source will be auto-generated.
/// </summary>
public CodeExecutionContainer(Action<CancellationToken> cancellableWork,
Action<CodeExecutionContainer> successHandler = null, Action<CodeExecutionContainer> errorHandler = null)
{
CancellationTokenSource tokenSource = new CancellationTokenSource();
Action workWrapper = () => cancellableWork.Invoke(tokenSource.Token);
Init(workWrapper, tokenSource, successHandler, errorHandler);
}
/// <param name="work">Mandatory delegate to execute some job.</param>
/// <param name="cancellationTokenSource">Optional, if specified the container will try to use it first to cancel execution (if requested).</param>
/// <param name="successHandler">Optional, delegate to be executed after code successfully completed.</param>
/// <param name="errorHandler">Optional, delegate to be executed after code failed with Exception.</param>
public CodeExecutionContainer(Action work, CancellationTokenSource cancellationTokenSource, Action<CodeExecutionContainer> successHandler, Action<CodeExecutionContainer> errorHandler)
{
Init(work, cancellationTokenSource, successHandler, errorHandler);
}
private void Init(Action work, CancellationTokenSource cancellationTokenSource, Action<CodeExecutionContainer> successHandler, Action<CodeExecutionContainer> errorHandler)
{
if (work == null)
{
throw new ArgumentException("Work delegate is null.");
}
_state = CodeExecutionContainerState.Created;
_work = work;
_cancellationTokenSource = cancellationTokenSource;
_successHandler = successHandler;
_errorHandler = errorHandler;
_stopwatch = new Stopwatch();
TimeIntervalForSoftCancellation = Configuration.TimeForSoftCancellation;
TimeBeforeAutoCancellation = TimeSpan.FromMinutes(60.0);
}
#endregion
#region Public manage methods
/// <summary>
/// Starts code execution in dedicated Thread.
/// </summary>
public void Start(string workId = null)
{
workId = workId ?? $"{Guid.NewGuid()} (auto)";
lock (_syncRoot)
{
if (_state != CodeExecutionContainerState.Created)
throw new InvalidOperationException("Start operation only possible once when state equals to Created.");
// update status to Running instantly
_state = CodeExecutionContainerState.Running;
_workerThread = new Thread(() => WorkerThreadWork(workId))
{
IsBackground = false,
};
_workerThread.Name = "CEXC:" + _workerThread.ManagedThreadId.ToString();
_workerThread.Start();
if (UseAutoCancellation)
{
var autoCancelationWatchThread = new Thread(() =>
{
try
{
DateTime startDate = DateTime.UtcNow;
do
{
Thread.Sleep(1000);
} while (DateTime.UtcNow - startDate < TimeBeforeAutoCancellation
&& _state == CodeExecutionContainerState.Running);
// if we got here then Timeout is expired and it's time to initiate Cancellation artificially
// or State is no more Running
// if still running -> request Cancellation
if (_state == CodeExecutionContainerState.Running)
{
_log.Warn("Artificial Cancellation requested.");
Cancel();
}
}
catch (Exception e)
{
_log.Error($"Auto Cancelation Thread error: {e.Message}", e);
}
})
{
IsBackground = true,
Priority = ThreadPriority.BelowNormal,
Name = "Code execution auto cancellation thread",
};
autoCancelationWatchThread.Start();
}
}
}
/// <summary>
/// Sends cancellation request (if CancelationTokenSource was provided) and if code was unable to shutdown gracefully
/// in a timely fashion (TimeIntervalForSoftCancellation) then call Thread.Abort().
/// </summary>
public void Cancel()
{
if (_state != CodeExecutionContainerState.Running)
{
_log.Warn("Cancel operation makes no sense if current state <> Running");
return;
}
// if task supports cancellation
if (_cancellationTokenSource != null)
{
_log.Warn("Soft cancellation.");
_cancellationTokenSource.Cancel();
_state = CodeExecutionContainerState.CancellationRequested;
// start waiting for Thread cancelation asynchronously
ThreadPool.QueueUserWorkItem(s => WaitForCancel());
}
else // task does not support cancellation
{
// Cancellation Token Source was not provided, so just kill the thread in a "hard" way (it's fast, so do it synchronously)
HardThreadAbort();
}
}
#endregion
#region Helpers
private void WorkerThreadWork(string workId)
{
try
{
_log.Info($"Starting to perform work {workId}");
_stopwatch.Start();
_work.Invoke();
_state = _state == CodeExecutionContainerState.CancellationRequested
? CodeExecutionContainerState.Cancelled
: CodeExecutionContainerState.Succeeded;
_log.Info($"Work {workId} succeeded.");
}
catch (Exception e)
{
_log.Error($"Work {workId} failed: {e.Message}", e);
if (_state == CodeExecutionContainerState.AbortRequested)
{
_state = CodeExecutionContainerState.Aborted;
}
else if (_state == CodeExecutionContainerState.CancellationRequested)
{
_state = CodeExecutionContainerState.Cancelled;
}
else
{
_state = CodeExecutionContainerState.Failed;
}
Exception = e;
}
finally
{
_stopwatch.Stop();
if (_state == CodeExecutionContainerState.Succeeded)
{
if (_successHandler != null)
TryToCallSafe("Calling SuccessHandler", () => _successHandler.Invoke(this));
}
else
{
if (_errorHandler != null)
TryToCallSafe("Calling ErrorHandler", () => _errorHandler.Invoke(this));
else
_log.Warn($"Error happened, but no ErrorHandler is passed in. Consider adding it.");
}
}
}
private void WaitForCancel()
{
DateTime startToWait = DateTime.UtcNow;
while (DateTime.UtcNow - startToWait < TimeIntervalForSoftCancellation)
{
if (_state == CodeExecutionContainerState.Succeeded || _state == CodeExecutionContainerState.Failed)
{
// prevent hard abort if task failed or succeeded
return;
}
Thread.Sleep(100);
}
// time for soft cancellation expired, do it in a hard way
HardThreadAbort();
}
private void HardThreadAbort()
{
_log.Warn("Hard cancellation.");
if (_state == CodeExecutionContainerState.CancellationRequested || _state == CodeExecutionContainerState.Running)
{
_state = CodeExecutionContainerState.AbortRequested;
_workerThread.Abort();
}
_workerThread.Join();
}
/// <summary>
/// Method for safe execution where we can allow unhandled Exception - explicitly created Threads
/// </summary>
private void TryToCallSafe(string label, Action a)
{
try
{
a.Invoke();
}
catch (Exception e)
{
_log.Error($"Error while {label}: {e.Message}", e);
}
}
#endregion
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment