internal AsyncOperationContext(TaskOperation op, TWork work, TExecutor executor, Task predecessor, OperationExecutionOptions options, CancellationToken cancellationToken) : base(op, work, predecessor, options, cancellationToken) { this.Executor = executor; this.ExecutorSource = new TaskCompletionSource <TExecutor>(this.ResultSource); }
/// <summary> /// Runs the specified test method. /// </summary> internal void RunTest(Delegate testMethod, string testName) { testName = string.IsNullOrEmpty(testName) ? string.Empty : $" '{testName}'"; this.Logger.WriteLine($"<TestLog> Running test{testName}."); this.Assert(testMethod != null, "Unable to execute a null test method."); this.Assert(Task.CurrentId != null, "The test must execute inside a controlled task."); ulong operationId = this.GetNextOperationId(); var op = new TaskOperation(operationId, this.Scheduler); this.Scheduler.RegisterOperation(op); op.OnEnabled(); Task task = new Task(async() => { try { // Update the current asynchronous control flow with the current runtime instance, // allowing future retrieval in the same asynchronous call stack. AssignAsyncControlFlowRuntime(this); OperationScheduler.StartOperation(op); if (testMethod is Action <IActorRuntime> actionWithRuntime) { actionWithRuntime(this); } else if (testMethod is Action action) { action(); } else if (testMethod is Func <IActorRuntime, CoyoteTasks.Task> functionWithRuntime) { await functionWithRuntime(this); } else if (testMethod is Func <CoyoteTasks.Task> function) { await function(); } else { throw new InvalidOperationException($"Unsupported test delegate of type '{testMethod.GetType()}'."); } IO.Debug.WriteLine("<ScheduleDebug> Completed operation {0} on task '{1}'.", op.Name, Task.CurrentId); op.OnCompleted(); // Task has completed, schedule the next enabled operation. this.Scheduler.ScheduleNextEnabledOperation(); } catch (Exception ex) { this.ProcessUnhandledExceptionInOperation(op, ex); } }); this.Scheduler.ScheduleOperation(op, task.Id); task.Start(); this.Scheduler.WaitOperationStart(op); }
/// <summary> /// Schedules the specified task operation for execution. /// </summary> private void ScheduleTaskOperation(TaskOperation op, Task task) { IO.Debug.WriteLine("<CreateLog> Operation '{0}' was created to execute task '{1}'.", op.Name, task.Id); task.Start(); this.Scheduler.WaitOperationStart(op); this.Scheduler.ScheduleNextOperation(); }
/// <summary> /// Creates a new task operation. /// </summary> internal TaskOperation CreateTaskOperation() { ulong operationId = this.GetNextOperationId(); var op = new TaskOperation(operationId, this.Scheduler); this.Scheduler.RegisterOperation(op); return(op); }
public CoyoteTasks.Task ScheduleAction(Action action, Task predecessor, CancellationToken cancellationToken) { // TODO: support cancellations during testing. this.Assert(action != null, "The task cannot execute a null action."); ulong operationId = this.Runtime.GetNextOperationId(); var op = new TaskOperation(operationId, this.Scheduler); this.Scheduler.RegisterOperation(op); op.OnEnabled(); var task = new Task(() => { try { // Update the current asynchronous control flow with the current runtime instance, // allowing future retrieval in the same asynchronous call stack. CoyoteRuntime.AssignAsyncControlFlowRuntime(this.Runtime); OperationScheduler.StartOperation(op); if (predecessor != null) { op.OnWaitTask(predecessor); } action(); } catch (Exception ex) { // Report the unhandled exception unless it is our ExecutionCanceledException which is our // way of terminating async task operations at the end of the test iteration. if (!(ex is ExecutionCanceledException)) { this.ReportUnhandledExceptionInOperation(op, ex); } // and rethrow it throw; } finally { IO.Debug.WriteLine("<ScheduleDebug> Completed operation '{0}' on task '{1}'.", op.Name, Task.CurrentId); op.OnCompleted(); } }); // Schedule a task continuation that will schedule the next enabled operation upon completion. task.ContinueWith(t => this.Scheduler.ScheduleNextEnabledOperation(), TaskScheduler.Current); IO.Debug.WriteLine("<CreateLog> Operation '{0}' was created to execute task '{1}'.", op.Name, task.Id); this.Scheduler.ScheduleOperation(op, task.Id); task.Start(); this.Scheduler.WaitOperationStart(op); this.Scheduler.ScheduleNextEnabledOperation(); return(new CoyoteTasks.Task(this, task)); }
public CoyoteTasks.Task <TResult> ScheduleFunction <TResult>(Func <CoyoteTasks.Task <TResult> > function, Task predecessor, CancellationToken cancellationToken) { // TODO: support cancellations during testing. this.Assert(function != null, "The task cannot execute a null function."); ulong operationId = this.Runtime.GetNextOperationId(); var op = new TaskOperation(operationId, this.Scheduler); this.Scheduler.RegisterOperation(op); op.OnEnabled(); var task = new Task <Task <TResult> >(() => { try { // Update the current asynchronous control flow with the current runtime instance, // allowing future retrieval in the same asynchronous call stack. CoyoteRuntime.AssignAsyncControlFlowRuntime(this.Runtime); OperationScheduler.StartOperation(op); if (predecessor != null) { op.OnWaitTask(predecessor); } CoyoteTasks.Task <TResult> resultTask = function(); this.OnWaitTask(operationId, resultTask.UncontrolledTask); return(resultTask.UncontrolledTask); } catch (Exception ex) { // Report the unhandled exception and rethrow it. this.ReportUnhandledExceptionInOperation(op, ex); throw; } finally { IO.Debug.WriteLine("<ScheduleDebug> Completed operation '{0}' on task '{1}'.", op.Name, Task.CurrentId); op.OnCompleted(); } }); Task <TResult> innerTask = task.Unwrap(); // Schedule a task continuation that will schedule the next enabled operation upon completion. innerTask.ContinueWith(t => this.Scheduler.ScheduleNextEnabledOperation(), TaskScheduler.Current); IO.Debug.WriteLine("<CreateLog> Operation '{0}' was created to execute task '{1}'.", op.Name, task.Id); this.Scheduler.ScheduleOperation(op, task.Id); task.Start(); this.Scheduler.WaitOperationStart(op); this.Scheduler.ScheduleNextEnabledOperation(); return(new CoyoteTasks.Task <TResult>(this, innerTask)); }
internal OperationContext(TaskOperation op, TWork work, Task predecessor, OperationExecutionOptions options, CancellationToken cancellationToken) { this.Operation = op; this.Work = work ?? throw new ArgumentNullException(nameof(work)); this.Predecessor = predecessor; this.ResultSource = new TaskCompletionSource <TResult>(); this.Options = options; this.CancellationToken = cancellationToken; }
internal Task ScheduleAction(Action action, Task predecessor, bool isYield, bool failException, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); TaskOperation op = this.Runtime.CreateTaskOperation(); OperationExecutionOptions options = OperationContext.CreateOperationExecutionOptions(failException, isYield); var context = new OperationContext <Action, object>(op, action, predecessor, options, cancellationToken); var task = new Task(this.ExecuteOperation, context, cancellationToken); this.ScheduleTaskOperation(op, task); return(context.ResultSource.Task); }
internal Task <TResult> ScheduleFunction <TResult>(Func <TResult> function, Task predecessor, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); TaskOperation op = this.Runtime.CreateTaskOperation(); var context = new OperationContext <Func <TResult>, TResult>(op, function, predecessor, OperationExecutionOptions.None, cancellationToken); var task = new Task <TResult>(this.ExecuteOperation <Func <TResult>, TResult, TResult>, context, cancellationToken); this.ScheduleTaskOperation(op, task); return(context.ResultSource.Task); }
internal Task <Task> ScheduleFunction(Func <Task> function, Task predecessor, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); Task <Task> task = null; TaskOperation op = this.Runtime.CreateTaskOperation(); var context = new AsyncOperationContext <Func <Task>, Task, Task>(op, function, task, predecessor, OperationExecutionOptions.None, cancellationToken); task = new Task <Task>(this.ExecuteOperation <Func <Task>, Task, Task>, context, cancellationToken); this.ScheduleTaskOperation(op, task); return(context.ExecutorSource.Task); }
public CoyoteTasks.Task <TResult> ScheduleDelegate <TResult>(Delegate work, Task predecessor, CancellationToken cancellationToken) { // TODO: support cancellations during testing. this.Assert(work != null, "The task cannot execute a null delegate."); ulong operationId = this.Runtime.GetNextOperationId(); var op = new TaskOperation(operationId, this.Scheduler); this.Scheduler.RegisterOperation(op); op.OnEnabled(); var task = new Task <TResult>(() => { try { // Update the current asynchronous control flow with the current runtime instance, // allowing future retrieval in the same asynchronous call stack. CoyoteRuntime.AssignAsyncControlFlowRuntime(this.Runtime); OperationScheduler.StartOperation(op); if (predecessor != null) { op.OnWaitTask(predecessor); } if (work is Func <Task> funcWithTaskResult) { Task resultTask = funcWithTaskResult(); this.OnWaitTask(operationId, resultTask); if (resultTask is TResult typedResultTask) { return(typedResultTask); } } else if (work is Func <Task <TResult> > funcWithGenericTaskResult) { Task <TResult> resultTask = funcWithGenericTaskResult(); this.OnWaitTask(operationId, resultTask); return(resultTask.Result); } else if (work is Func <TResult> funcWithGenericResult) { return(funcWithGenericResult()); } return(default);
/// <summary> /// Execute the (asynchronous) operation with the specified context. /// </summary> private TResult ExecuteOperation <TWork, TExecutor, TResult>(object state) { // Extract the expected operation context from the task state. var context = state as AsyncOperationContext <TWork, TExecutor, TResult> ?? state as OperationContext <TWork, TResult>; TaskOperation op = context.Operation; CancellationToken ct = context.CancellationToken; TResult result = default; Exception exception = null; // The operation execution logic uses two task completion sources: (1) an executor TCS and (2) a result TCS. // We do this to model the execution of tasks in the .NET runtime. For example, the `Task.Factory.StartNew` // method has different semantics from `Task.Run`, e.g. the returned task from `Task.Factory.StartNew(Func<T>)` // completes at the start of an asynchronous operation, so someone cannot await on it for the completion of // the operation. Instead, someone needs to first use `task.Unwrap()`, and then await on the unwrapped task. // To model this, the executor TCS completes at the start of the operation, and contains in its `task.AsyncState` // a reference to the result TCS. This approach allows us to implement `task.Unwrap` in a way that gives access // to the result TCS, which someone can then await for the asynchronous completion of the operation. try { // Update the current asynchronous control flow with the current runtime instance, // allowing future retrieval in the same asynchronous call stack. CoyoteRuntime.AssignAsyncControlFlowRuntime(this.Runtime); // Notify the scheduler that the operation started. This will yield execution until // the operation is ready to get scheduled. this.Scheduler.StartOperation(op); if (context is AsyncOperationContext <TWork, TExecutor, TResult> asyncContext) { // If the operation is asynchronous, then set the executor task completion source, which // can be used by `UnwrapTask` to unwrap and return the task executing this operation. this.SetTaskCompletionSource(asyncContext.ExecutorSource, asyncContext.Executor, null, ct); } if (context.Predecessor != null) { // If there is a predecessor task, then wait until the predecessor completes. ct.ThrowIfCancellationRequested(); op.OnWaitTask(context.Predecessor); } // Check if the operation must be canceled before starting the work. ct.ThrowIfCancellationRequested(); // Start executing the (asynchronous) work. Task executor = null; if (context.Work is Func <Task <TResult> > funcWithTaskResult) { executor = funcWithTaskResult(); } else if (context.Work is Func <Task> funcWithTask) { executor = funcWithTask(); } else if (context.Work is Func <CoyoteTasks.Task <TResult> > funcWithCoyoteTaskResult) { // TODO: temporary until we remove the custom task type. executor = funcWithCoyoteTaskResult().UncontrolledTask; } else if (context.Work is Func <CoyoteTasks.Task> funcWithCoyoteTask) { // TODO: temporary until we remove the custom task type. executor = funcWithCoyoteTask().UncontrolledTask; } else if (context.Work is Func <TResult> func) { result = func(); } else { throw new NotSupportedException($"Unable to execute work with unsupported type {context.Work.GetType()}."); } if (executor != null) { // If the work is asynchronous, then wait until it completes. this.OnWaitTask(op.Id, executor); if (executor.IsFaulted) { // Propagate the failing exception by rethrowing it. ExceptionDispatchInfo.Capture(executor.Exception).Throw(); } else if (executor.IsCanceled) { if (op.Exception != null) { // An exception has been already captured, so propagate it. ExceptionDispatchInfo.Capture(op.Exception).Throw(); } else { // Wait the canceled executor (which is non-blocking as it has already completed) // to throw the generated `OperationCanceledException`. executor.Wait(); } } // Safely get the result without blocking as the work has completed. result = executor is Task <TResult> resultTask ? resultTask.Result : executor is TResult r ? r : default; } } catch (Exception ex) { // Unwrap and cache the exception to propagate it. exception = ControlledRuntime.UnwrapException(ex); this.ReportThrownException(exception); } finally { IO.Debug.WriteLine("<ScheduleDebug> Completed operation '{0}' on task '{1}'.", op.Name, Task.CurrentId); op.OnCompleted(); // Set the result task completion source to notify to the awaiters that the operation // has been completed, and schedule the next enabled operation. this.SetTaskCompletionSource(context.ResultSource, result, exception, default); this.Scheduler.ScheduleNextOperation(); } return(result); }
/// <summary> /// Execute the operation with the specified context. /// </summary> private void ExecuteOperation(object state) { // Extract the expected operation context from the task state. var context = state as OperationContext <Action, object>; TaskOperation op = context.Operation; CancellationToken ct = context.CancellationToken; Exception exception = null; try { // Update the current asynchronous control flow with the current runtime instance, // allowing future retrieval in the same asynchronous call stack. CoyoteRuntime.AssignAsyncControlFlowRuntime(this.Runtime); // Notify the scheduler that the operation started. This will yield execution until // the operation is ready to get scheduled. this.Scheduler.StartOperation(op); if (context.Predecessor != null) { // If there is a predecessor task, then wait until the predecessor completes. ct.ThrowIfCancellationRequested(); op.OnWaitTask(context.Predecessor); } if (context.Options.HasFlag(OperationExecutionOptions.YieldAtStart)) { // Try yield execution to the next operation. this.Scheduler.ScheduleNextOperation(true); } // Check if the operation must be canceled before starting the work. ct.ThrowIfCancellationRequested(); // Start executing the work. context.Work(); } catch (Exception ex) { if (context.Options.HasFlag(OperationExecutionOptions.FailOnException)) { this.Assert(false, "Unhandled exception. {0}", ex); } else { // Unwrap and cache the exception to propagate it. exception = ControlledRuntime.UnwrapException(ex); this.ReportThrownException(exception); } } finally { IO.Debug.WriteLine("<ScheduleDebug> Completed operation '{0}' on task '{1}'.", op.Name, Task.CurrentId); op.OnCompleted(); // Set the result task completion source to notify to the awaiters that the operation // has been completed, and schedule the next enabled operation. this.SetTaskCompletionSource(context.ResultSource, null, exception, default); this.Scheduler.ScheduleNextOperation(); } }
public CoyoteTasks.Task<TResult> ScheduleDelegate<TResult>(Delegate work, Task predecessor, CancellationToken cancellationToken) { // TODO: support cancellations during testing. this.Assert(work != null, "The task cannot execute a null delegate."); ulong operationId = this.Runtime.GetNextOperationId(); var op = new TaskOperation(operationId, this.Scheduler); this.Scheduler.RegisterOperation(op); op.OnEnabled(); var task = new Task<TResult>(() => { try { // Update the current asynchronous control flow with the current runtime instance, // allowing future retrieval in the same asynchronous call stack. CoyoteRuntime.AssignAsyncControlFlowRuntime(this.Runtime); OperationScheduler.StartOperation(op); if (predecessor != null) { op.OnWaitTask(predecessor); } if (work is Func<Task> funcWithTaskResult) { Task resultTask = funcWithTaskResult(); this.OnWaitTask(operationId, resultTask); if (resultTask is TResult typedResultTask) { return typedResultTask; } } else if (work is Func<Task<TResult>> funcWithGenericTaskResult) { Task<TResult> resultTask = funcWithGenericTaskResult(); this.OnWaitTask(operationId, resultTask); return resultTask.Result; } else if (work is Func<TResult> funcWithGenericResult) { return funcWithGenericResult(); } return default; } catch (Exception ex) { // Report the unhandled exception and rethrow it. ReportUnhandledExceptionInOperation(op, ex); throw; } finally { IO.Debug.WriteLine("<ScheduleDebug> Completed operation '{0}' on task '{1}'.", op.Name, Task.CurrentId); op.OnCompleted(); } }, cancellationToken); // Schedule a task continuation that will schedule the next enabled operation upon completion. task.ContinueWith(t => this.Scheduler.ScheduleNextEnabledOperation(), TaskScheduler.Current); IO.Debug.WriteLine("<CreateLog> Operation '{0}' was created to execute task '{1}'.", op.Name, task.Id); this.Scheduler.ScheduleOperation(op, task.Id); task.Start(); this.Scheduler.WaitOperationStart(op); this.Scheduler.ScheduleNextEnabledOperation(); return new CoyoteTasks.Task<TResult>(this, task); }
internal void RunTest(Delegate testMethod, string testName) { testName = string.IsNullOrEmpty(testName) ? string.Empty : $" '{testName}'"; this.Logger.WriteLine($"<TestLog> Running test{testName}."); this.Assert(testMethod != null, "Unable to execute a null test method."); this.Assert(Task.CurrentId != null, "The test must execute inside a controlled task."); TaskOperation op = this.CreateTaskOperation(); Task task = new Task(() => { try { // Update the current asynchronous control flow with the current runtime instance, // allowing future retrieval in the same asynchronous call stack. AssignAsyncControlFlowRuntime(this); this.Scheduler.StartOperation(op); Task testMethodTask = null; if (testMethod is Action <IActorRuntime> actionWithRuntime) { actionWithRuntime(this); } else if (testMethod is Action action) { action(); } else if (testMethod is Func <IActorRuntime, Task> functionWithRuntime) { testMethodTask = functionWithRuntime(this); } else if (testMethod is Func <Task> function) { testMethodTask = function(); } else if (testMethod is Func <IActorRuntime, CoyoteTasks.Task> functionWithRuntime2) { testMethodTask = functionWithRuntime2(this).UncontrolledTask; } else if (testMethod is Func <CoyoteTasks.Task> function2) { testMethodTask = function2().UncontrolledTask; } else { throw new InvalidOperationException($"Unsupported test delegate of type '{testMethod.GetType()}'."); } if (testMethodTask != null) { // The test method is asynchronous, so wait on the task to complete. op.OnWaitTask(testMethodTask); if (testMethodTask.Exception != null) { // The test method failed with an unhandled exception. ExceptionDispatchInfo.Capture(testMethodTask.Exception).Throw(); } else if (testMethodTask.IsCanceled) { throw new TaskCanceledException(testMethodTask); } } IO.Debug.WriteLine("<ScheduleDebug> Completed operation {0} on task '{1}'.", op.Name, Task.CurrentId); op.OnCompleted(); // Task has completed, schedule the next enabled operation, which terminates exploration. this.Scheduler.ScheduleNextOperation(); } catch (Exception ex) { this.ProcessUnhandledExceptionInOperation(op, ex); } }); task.Start(); this.Scheduler.WaitOperationStart(op); }