Skip to content

Commit

Permalink
experimental scheduling wait and signal apis (microsoft#440)
Browse files Browse the repository at this point in the history
  • Loading branch information
pdeligia authored Dec 15, 2022
1 parent f50059a commit 4d063db
Show file tree
Hide file tree
Showing 5 changed files with 260 additions and 5 deletions.
16 changes: 16 additions & 0 deletions Source/Core/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,12 @@ public class Configuration
[DataMember]
internal bool IsActorQuiescenceCheckingEnabledOutsideTesting;

/// <summary>
/// If enabled, the runtime can bypass scheduling suppression to avoid deadlocking during testing.
/// </summary>
[DataMember]
internal bool IsSchedulingSuppressionWeak;

/// <summary>
/// Attaches the debugger during trace replay.
/// </summary>
Expand Down Expand Up @@ -347,6 +353,7 @@ protected Configuration()
this.IsImplicitProgramStateHashingEnabled = false;
this.IsMonitoringEnabledOutsideTesting = false;
this.IsActorQuiescenceCheckingEnabledOutsideTesting = false;
this.IsSchedulingSuppressionWeak = false;
this.AttachDebugger = false;

this.IsUncontrolledInvocationStackTraceLoggingEnabled = false;
Expand Down Expand Up @@ -866,6 +873,15 @@ internal Configuration WithMonitoringEnabledOutsideTesting(bool isEnabled = true
return this;
}

/// <summary>
/// Updates the configuration to enable weak scheduling suppression during testing.
/// </summary>
public Configuration WithWeakSchedulingSuppressionEnabled(bool isEnabled = true)
{
this.IsSchedulingSuppressionWeak = isEnabled;
return this;
}

/// <summary>
/// Updates the configuration to allow the runtime to check for actor quiescence
/// outside the scope of the testing engine.
Expand Down
118 changes: 118 additions & 0 deletions Source/Core/Runtime/CompilerServices/SignalAwaitable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Runtime.CompilerServices;

namespace Microsoft.Coyote.Runtime.CompilerServices
{
/// <summary>
/// Provides an awaitable object that pauses the executing operation until a signal happens.
/// </summary>
/// <remarks>This type is intended for compiler use only.</remarks>
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
public readonly struct SignalAwaitable
{
/// <summary>
/// The paused operation awaiter.
/// </summary>
private readonly SignalAwaiter Awaiter;

/// <summary>
/// Initializes a new instance of the <see cref="SignalAwaitable"/> struct.
/// </summary>
internal SignalAwaitable(CoyoteRuntime runtime, ControlledOperation op, string name) =>
this.Awaiter = new SignalAwaiter(runtime, op, name);

/// <summary>
/// Returns an awaiter for this awaitable object.
/// </summary>
/// <returns>The awaiter.</returns>
public SignalAwaiter GetAwaiter() => this.Awaiter;

/// <summary>
/// Provides an awaiter that that pauses the this.Operationly executing operation until a signal happens.
/// </summary>
/// <remarks>This type is intended for compiler use only.</remarks>
public struct SignalAwaiter : IControllableAwaiter, ICriticalNotifyCompletion, INotifyCompletion
{
/// <summary>
/// The runtime managing the paused operation.
/// </summary>
private readonly CoyoteRuntime Runtime;

/// <summary>
/// The paused operation.
/// </summary>
private readonly ControlledOperation Operation;

/// <summary>
/// The name of the signal being awaited.
/// </summary>
private readonly string Name;

/// <summary>
/// The yield awaiter.
/// </summary>
private readonly YieldAwaitable.YieldAwaiter Awaiter;

/// <summary>
/// True if the awaiter has completed, else false.
/// </summary>
#pragma warning disable CA1822 // Mark members as static
public bool IsCompleted => false;
#pragma warning restore CA1822 // Mark members as static

/// <inheritdoc/>
bool IControllableAwaiter.IsControlled => true;

/// <summary>
/// Initializes a new instance of the <see cref="SignalAwaiter"/> struct.
/// </summary>
internal SignalAwaiter(CoyoteRuntime runtime, ControlledOperation op, string name)
{
this.Runtime = runtime;
this.Operation = op;
this.Name = name;
this.Awaiter = default;
}

/// <summary>
/// Ends asynchronously waiting for the completion of the awaiter.
/// </summary>
public void GetResult() => this.Awaiter.GetResult();

/// <summary>
/// Sets the action to perform when the controlled task completes.
/// </summary>
public void OnCompleted(Action continuation)
{
if (this.Runtime is null || this.Operation is null)
{
this.Awaiter.OnCompleted(continuation);
}
else
{
this.Runtime.Schedule(continuation, preCondition: this.WaitSignal);
}
}

/// <summary>
/// Schedules the continuation action that is invoked when the controlled task completes.
/// </summary>
public void UnsafeOnCompleted(Action continuation)
{
if (this.Runtime is null || this.Operation is null)
{
this.Awaiter.UnsafeOnCompleted(continuation);
}
else
{
this.Runtime.Schedule(continuation, preCondition: this.WaitSignal);
}
}

private void WaitSignal() => SchedulingPoint.Wait(this.Name);
}
}
}
45 changes: 40 additions & 5 deletions Source/Core/Runtime/CoyoteRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,16 @@ internal sealed class CoyoteRuntime : ICoyoteRuntime, IDisposable
/// </summary>
private readonly HashSet<string> UncontrolledInvocations;

/// <summary>
/// Map from signal names to their corresponding counters.
/// </summary>
internal readonly Dictionary<string, int> SignalMap;

/// <summary>
/// Map from operation ids to the name of the signal they are awaiting.
/// </summary>
internal readonly Dictionary<ulong, string> OperationSignalAwaiters;

/// <summary>
/// The currently scheduled operation during systematic testing.
/// </summary>
Expand Down Expand Up @@ -320,6 +330,8 @@ private CoyoteRuntime(Configuration configuration, OperationScheduler scheduler,
this.ControlledTasks = new ConcurrentDictionary<Task, ControlledOperation>();
this.UncontrolledTasks = new ConcurrentDictionary<Task, string>();
this.UncontrolledInvocations = new HashSet<string>();
this.SignalMap = new Dictionary<string, int>();
this.OperationSignalAwaiters = new Dictionary<ulong, string>();
this.CompletionSource = new TaskCompletionSource<bool>();

if (this.SchedulingPolicy is SchedulingPolicy.Interleaving)
Expand Down Expand Up @@ -431,10 +443,10 @@ internal void Schedule(Task task)
/// <summary>
/// Schedules the specified continuation to execute on the controlled thread pool.
/// </summary>
internal void Schedule(Action continuation)
internal void Schedule(Action continuation, Action preCondition = null, Action postCondition = null)
{
ControlledOperation op = this.CreateControlledOperation(group: ExecutingOperation?.Group);
this.ScheduleOperation(op, continuation);
this.ScheduleOperation(op, continuation, preCondition, postCondition);
this.ScheduleNextOperation(default, SchedulingPointType.ContinueWith);
}

Expand All @@ -456,9 +468,6 @@ private void ScheduleOperation(ControlledOperation op, Action action, Action pre
{
try
{
// Execute the optional pre-condition.
preCondition?.Invoke();

// Start the operation.
this.StartOperation(op);

Expand All @@ -469,6 +478,9 @@ private void ScheduleOperation(ControlledOperation op, Action action, Action pre
this.DelayOperation(op);
}

// Execute the optional pre-condition.
preCondition?.Invoke();

// Execute the controlled action.
action.Invoke();

Expand Down Expand Up @@ -1313,6 +1325,27 @@ private bool TryEnableOperationsWithResolvedDependencies(ControlledOperation cur
break;
}

if (enabledOpsCount is 0 && this.OperationMap.Values.Any(op => op.Status is OperationStatus.Suppressed))
{
if (this.Configuration.IsSchedulingSuppressionWeak)
{
// Enable all suppressed operations, to avoid deadlocking the program.
foreach (var op in this.OperationMap.Values)
{
if (op.Status is OperationStatus.Suppressed)
{
op.Status = OperationStatus.Enabled;
}
}

this.OperationSignalAwaiters.Clear();
}
else
{
this.NotifyAssertionFailure("Only suppressed operations remain.");
}
}

this.LogWriter.LogDebug("[coyote::debug] There are {0} enabled operations in runtime '{1}'.",
enabledOpsCount, this.Id);
this.MaxConcurrencyDegree = Math.Max(this.MaxConcurrencyDegree, enabledOpsCount);
Expand Down Expand Up @@ -2604,6 +2637,8 @@ private void Dispose(bool disposing)
this.SpecificationMonitors.Clear();
this.TaskLivenessMonitors.Clear();
this.StateHashingFunctions.Clear();
this.SignalMap.Clear();
this.OperationSignalAwaiters.Clear();

if (!(this.Extension is NullRuntimeExtension))
{
Expand Down
5 changes: 5 additions & 0 deletions Source/Core/Runtime/Operations/OperationStatus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ internal enum OperationStatus
/// </summary>
PausedOnReceive,

/// <summary>
/// The operation is suppressed until it is resumed.
/// </summary>
Suppressed,

/// <summary>
/// The operation is completed.
/// </summary>
Expand Down
81 changes: 81 additions & 0 deletions Source/Core/Runtime/Scheduling/SchedulingPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// Licensed under the MIT License.

using System.Collections.Generic;
using System.Linq;
using Microsoft.Coyote.Runtime.CompilerServices;

namespace Microsoft.Coyote.Runtime
{
Expand Down Expand Up @@ -107,6 +109,85 @@ public static void Write(string state, IEqualityComparer<string> comparer = defa
}
}

/// <summary>
/// Waits for a signal to a resource with the specified name.
/// </summary>
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
public static void Wait(string name)
{
var runtime = CoyoteRuntime.Current;
if (runtime.SchedulingPolicy is SchedulingPolicy.Interleaving &&
runtime.TryGetExecutingOperation(out ControlledOperation current))
{
using (runtime.EnterSynchronizedSection())
{
if (!runtime.SignalMap.TryGetValue(name, out var signal))
{
runtime.SignalMap.Add(name, 0);
}

runtime.SignalMap[name]--;
if (runtime.SignalMap[name] < 0)
{
runtime.LogWriter.LogDebug("[coyote::debug] Operation '{0}' is waiting a signal for '{1}'.",
current.Name, name);
runtime.SignalMap[name] = 0;
current.Status = OperationStatus.Suppressed;
runtime.OperationSignalAwaiters.Add(current.Id, name);
runtime.ScheduleNextOperation(current, SchedulingPointType.Default, isSuppressible: false);
}
}
}
}

/// <summary>
/// Waits asynchronously for a signal to a resource with the specified name.
/// </summary>
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
public static SignalAwaitable WaitAsync(string name)
{
var runtime = CoyoteRuntime.Current;
if (runtime.SchedulingPolicy is SchedulingPolicy.Interleaving &&
runtime.TryGetExecutingOperation(out ControlledOperation current))
{
return new SignalAwaitable(runtime, current, name);
}

return new SignalAwaitable(null, null, name);
}

/// <summary>
/// Signals a resource with the specified name.
/// </summary>
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
public static void Signal(string name)
{
var runtime = CoyoteRuntime.Current;
if (runtime.SchedulingPolicy is SchedulingPolicy.Interleaving &&
runtime.TryGetExecutingOperation(out ControlledOperation current))
{
using (runtime.EnterSynchronizedSection())
{
foreach (var kvp in runtime.OperationSignalAwaiters.Where(kvp => kvp.Value == name).ToList())
{
var op = runtime.GetOperationWithId(kvp.Key);
runtime.LogWriter.LogDebug("[coyote::debug] Operation '{0}' is signaled for '{1}'.",
op.Name, name);
op.Status = OperationStatus.Enabled;
runtime.OperationSignalAwaiters.Remove(kvp.Key);
}

if (!runtime.SignalMap.TryGetValue(name, out var signal))
{
runtime.SignalMap.Add(name, 0);
}

runtime.SignalMap[name]++;
runtime.ScheduleNextOperation(current, SchedulingPointType.Default, isSuppressible: false);
}
}
}

/// <summary>
/// Suppresses interleavings until <see cref="Resume"/> is invoked.
/// </summary>
Expand Down

0 comments on commit 4d063db

Please sign in to comment.