Created
February 22, 2018 14:30
-
-
Save gubenkoved/a7eb3ad42a1e339930d4906ce48a6304 to your computer and use it in GitHub Desktop.
CodeExecutionContainer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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