diff --git a/CodeJam.Main.Tests/TestTools.cs b/CodeJam.Main.Tests/TestTools.cs index cc4738251..bce21086e 100644 --- a/CodeJam.Main.Tests/TestTools.cs +++ b/CodeJam.Main.Tests/TestTools.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; +using System.Threading.Tasks; using CodeJam.Strings; using CodeJam.Targeting; @@ -69,6 +70,40 @@ private static void PrintProps([NotNull] string typeName) Console.WriteLine($"\t * {prop.Name}: {prop.GetValue(null, null)}"); } } + + public static void WaitForResult([NotNull] this Task source) + { +#if NET45_OR_GREATER || TARGETS_NETCOREAPP + source.GetAwaiter().GetResult(); +#else + // Workaround for Theraot cancellation logic + try + { + source.GetAwaiter().GetResult(); + } + catch (TaskCanceledException ex) + { + throw new OperationCanceledException(ex.Message, ex); + } +#endif + } + + public static T WaitForResult([NotNull] this Task source) + { +#if NET45_OR_GREATER || TARGETS_NETCOREAPP + return source.GetAwaiter().GetResult(); +#else + // Workaround for Theraot cancellation logic + try + { + return source.GetAwaiter().GetResult(); + } + catch (TaskCanceledException ex) + { + throw new OperationCanceledException(ex.Message, ex); + } +#endif + } } public class Holder diff --git a/CodeJam.Main.Tests/Threading/TaskHelperTests.WithTimeout.cs b/CodeJam.Main.Tests/Threading/TaskHelperTests.WithTimeout.cs new file mode 100644 index 000000000..94859127a --- /dev/null +++ b/CodeJam.Main.Tests/Threading/TaskHelperTests.WithTimeout.cs @@ -0,0 +1,413 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +using CodeJam.Collections; + +using NUnit.Framework; + +#if NET45_OR_GREATER || TARGETS_NETCOREAPP +using TaskEx = System.Threading.Tasks.Task; +#elif NET40_OR_GREATER +using TaskEx = System.Threading.Tasks.TaskEx; +#else +using TaskEx = System.Threading.Tasks.Task; +#endif + +namespace CodeJam.Threading +{ + partial class TaskHelperTests + { + private enum SampleResult + { + FromCallback, + FromCancellation + } + + private enum SampleEvent + { + CallbackStarted, + CallbackCompleted, + CallbackCanceled, + CallbackCanceledOnStart, + CancellationStarted, + CancellationCompleted, + CancellationCanceled, + CancellationCanceledOnStart + } + + private class TimedOutSample + { + private readonly TaskCompletionSource _callbackCompletion = new TaskCompletionSource(); + + private readonly TaskCompletionSource _cancellationCompletion = new TaskCompletionSource(); + + private readonly List _events = new List(); + + public TimeSpan CallbackDelay { get; set; } + + public TimeSpan CancellationDelay { get; set; } + + public async Task WaitForCallbackCompletion() + { + await _callbackCompletion.Task; + return _events.ToArray(); + } + + public async Task WaitForCancellationCompletion() + { + await _cancellationCompletion.Task; + return _events.ToArray(); + } + + public async Task WaitForFullCompletion() + { + await TaskEx.WhenAll(_callbackCompletion.Task, _cancellationCompletion.Task); + return _events.ToArray(); + } + + public async Task OnCallback(CancellationToken cancellation = default) + { + if (cancellation.IsCancellationRequested) + { + _events.Add(SampleEvent.CallbackCanceledOnStart); + } + else + { + _events.Add(SampleEvent.CallbackStarted); + try + { + await TaskEx.Delay(CallbackDelay, cancellation); + _events.Add(SampleEvent.CallbackCompleted); + } + catch (OperationCanceledException) + { + _events.Add(SampleEvent.CallbackCanceled); + } + } + _callbackCompletion.SetResult(SampleResult.FromCallback); + return SampleResult.FromCallback; + } + + public async Task OnCancellation(CancellationToken cancellation = default) + { + if (cancellation.IsCancellationRequested) + { + _events.Add(SampleEvent.CancellationCanceledOnStart); + } + else + { + _events.Add(SampleEvent.CancellationStarted); + try + { + await TaskEx.Delay(CancellationDelay, cancellation); + _events.Add(SampleEvent.CancellationCompleted); + } + catch (Exception) + { + _events.Add(SampleEvent.CancellationCanceled); + } + } + + _cancellationCompletion.SetResult(SampleResult.FromCancellation); + return SampleResult.FromCancellation; + } + } + + private static readonly TimeSpan _timeout1 = TimeSpan.FromSeconds(1); + private static readonly TimeSpan _timeout2 = TimeSpan.FromSeconds(2); + private static readonly TimeSpan _timeout10 = TimeSpan.FromSeconds(10); + + [Test] + public void TestWithTimeoutSuccess() + { + var task = TaskEx.FromResult(SampleResult.FromCallback); + var taskWithTimeout = task.WithTimeout(_timeout1, CancellationToken.None); + + taskWithTimeout.Wait(); + Assert.AreEqual(taskWithTimeout.Result, SampleResult.FromCallback); + } + + [Test] + public void TestWithTimeoutFailure() + { + var task = TaskEx.Delay(_timeout10); + var taskWithTimeout = task.WithTimeout(_timeout1, CancellationToken.None); + + Assert.Throws(() => taskWithTimeout.WaitForResult()); + Assert.IsFalse(task.IsCompleted); + } + + [Test] + public void TestWithTimeoutCallbackSuccess() + { + var sample = new TimedOutSample(); + + var task = sample.OnCallback().WithTimeout( + _timeout1, + sample.OnCancellation, + CancellationToken.None); + + task.Wait(); + var events = sample.WaitForCallbackCompletion().WaitForResult(); + Assert.AreEqual(task.Result, SampleResult.FromCallback); + Assert.AreEqual( + events, + new[] + { + SampleEvent.CallbackStarted, + SampleEvent.CallbackCompleted + }); + } + + [Test] + public void TestWithTimeoutCallbackThrows() + { + var task = TaskEx.Run(() => throw new ArgumentNullException(nameof(_timeout1))); + var taskWithTimeout = task.WithTimeout(_timeout1, CancellationToken.None); + + Assert.Throws(() => taskWithTimeout.WaitForResult()); + } + + [Test] + public void TestWithTimeoutCallbackFailure() + { + var sample = new TimedOutSample + { + CallbackDelay = _timeout10 + }; + + var task = sample.OnCallback().WithTimeout( + _timeout1, + sample.OnCancellation, + CancellationToken.None); + + task.Wait(); + var events = sample.WaitForCancellationCompletion().WaitForResult(); + Assert.AreEqual(task.Result, SampleResult.FromCancellation); + Assert.AreEqual( + events, + new[] + { + SampleEvent.CallbackStarted, + SampleEvent.CancellationStarted, + SampleEvent.CancellationCompleted + }); + } + + [Test] +#if LESSTHAN_NET45 + [Ignore("https://github.com/theraot/Theraot/issues/120")] +#endif + public void TestWithTimeoutCallbackCancellation() + { + var sample = new TimedOutSample + { + CallbackDelay = _timeout10, + CancellationDelay = _timeout10 + }; + + var cts = new CancellationTokenSource(); + var task = sample.OnCallback(cts.Token).WithTimeout( + _timeout2, + sample.OnCancellation, + cts.Token); + cts.CancelAfter(_timeout1); + + Assert.Throws(() => task.WaitForResult()); + var events = sample.WaitForCallbackCompletion().WaitForResult(); + Assert.AreEqual( + events, + new[] + { + SampleEvent.CallbackStarted, + SampleEvent.CallbackCanceled + }); + } + + [Test] +#if LESSTHAN_NET45 + [Ignore("https://github.com/theraot/Theraot/issues/120")] +#endif + public void TestWithTimeoutCallbackTimeoutCancellation() + { + var sample = new TimedOutSample + { + CallbackDelay = _timeout10, + CancellationDelay = _timeout10 + }; + + var cts = new CancellationTokenSource(); + var task = sample.OnCallback(cts.Token).WithTimeout( + _timeout1, + sample.OnCancellation, + cts.Token); + cts.CancelAfter(_timeout2); + + task.Wait(CancellationToken.None); + var events = sample.WaitForCallbackCompletion().WaitForResult(); + events.Sort(); + Assert.AreEqual( + events, + new[] + { + SampleEvent.CallbackStarted, + SampleEvent.CallbackCanceled, + SampleEvent.CancellationStarted, + SampleEvent.CancellationCanceled, + }); + } + + [Test] + public void TestRunWithTimeoutSuccess() + { + var taskWithTimeout = TaskHelper.RunWithTimeout( + ct => TaskEx.FromResult(SampleResult.FromCallback), + _timeout1, + CancellationToken.None); + + Assert.AreEqual(taskWithTimeout.WaitForResult(), SampleResult.FromCallback); + } + + [Test] + public void TestRunWithTimeoutFailure() + { + var taskWithTimeout = TaskHelper.RunWithTimeout( + ct => TaskEx.Delay(_timeout10, CancellationToken.None), + _timeout1, + CancellationToken.None); + + Assert.Throws(() => taskWithTimeout.WaitForResult()); + } + + [Test] + public void TestRunWithTimeoutCallbackSuccess() + { + var sample = new TimedOutSample(); + + var task = TaskHelper.RunWithTimeout( + sample.OnCallback, + _timeout1, + sample.OnCancellation, + CancellationToken.None); + + task.Wait(); + var events = sample.WaitForCallbackCompletion().WaitForResult(); + Assert.AreEqual(task.Result, SampleResult.FromCallback); + Assert.AreEqual( + events, + new[] + { + SampleEvent.CallbackStarted, + SampleEvent.CallbackCompleted + }); + } + + [Test] + public void TestRunWithTimeoutCallbackThrows() + { + var taskWithTimeout = TaskHelper.RunWithTimeout( + ct => throw new ArgumentNullException(nameof(_timeout1)), + _timeout1, + CancellationToken.None); + + Assert.Throws(() => taskWithTimeout.WaitForResult()); + } + + [Test] + public void TestRunWithTimeoutCallbackFailure() + { + var sample = new TimedOutSample + { + CallbackDelay = _timeout10 + }; + + var task = TaskHelper.RunWithTimeout( + sample.OnCallback, + _timeout1, + sample.OnCancellation, + CancellationToken.None); + + task.Wait(); + var events = sample.WaitForCancellationCompletion().WaitForResult(); + events.Sort(); + Assert.AreEqual(task.Result, SampleResult.FromCancellation); + Assert.AreEqual( + events, + new[] + { + SampleEvent.CallbackStarted, + SampleEvent.CallbackCanceled, + SampleEvent.CancellationStarted, + SampleEvent.CancellationCompleted + }); + } + + [Test] +#if LESSTHAN_NET45 + [Ignore("https://github.com/theraot/Theraot/issues/120")] +#endif + public void TestRunWithTimeoutCallbackCancellation() + { + var sample = new TimedOutSample + { + CallbackDelay = _timeout10, + CancellationDelay = _timeout10 + }; + + var cts = new CancellationTokenSource(); + var task = TaskHelper.RunWithTimeout( + sample.OnCallback, + _timeout2, + sample.OnCancellation, + cts.Token); + cts.CancelAfter(_timeout1); + + Assert.Throws(() => task.WaitForResult()); + var events = sample.WaitForCallbackCompletion().WaitForResult(); + events.Sort(); + Assert.AreEqual( + events, + new[] + { + SampleEvent.CallbackStarted, + SampleEvent.CallbackCanceled + }); + } + + [Test] +#if LESSTHAN_NET45 + [Ignore("https://github.com/theraot/Theraot/issues/120")] +#endif + public void TestRunWithTimeoutCallbackTimeoutCancellation() + { + var sample = new TimedOutSample + { + CallbackDelay = _timeout10, + CancellationDelay = _timeout10 + }; + + var cts = new CancellationTokenSource(); + var task = TaskHelper.RunWithTimeout( + sample.OnCallback, + _timeout1, + sample.OnCancellation, + cts.Token); + cts.CancelAfter(_timeout2); + + task.Wait(CancellationToken.None); + var events = sample.WaitForFullCompletion().WaitForResult(); + events.Sort(); + Assert.AreEqual( + events, + new[] + { + SampleEvent.CallbackStarted, + SampleEvent.CallbackCanceled, + SampleEvent.CancellationStarted, + SampleEvent.CancellationCanceled + }); + } + } +} \ No newline at end of file diff --git a/CodeJam.Main.Tests/Threading/TaskHelperTests.cs b/CodeJam.Main.Tests/Threading/TaskHelperTests.cs index 9a1527dc2..5bd8097f6 100644 --- a/CodeJam.Main.Tests/Threading/TaskHelperTests.cs +++ b/CodeJam.Main.Tests/Threading/TaskHelperTests.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -12,98 +11,93 @@ #else using TaskEx = System.Threading.Tasks.Task; #endif - using SuppressMessageAttribute = System.Diagnostics.CodeAnalysis.SuppressMessageAttribute; namespace CodeJam.Threading { [TestFixture] - public class TaskHelperTests + [Parallelizable(ParallelScope.All)] + public partial class TaskHelperTests { [Test] - public void TestWhenCanceled() + public void TestWaitForCancellation() { - TaskEx.Run(() => TestWhenCanceledCore()).Wait(); + TaskEx.Run(() => TesWaitForCancellationCore()).Wait(); Assert.IsTrue(true); } - [SuppressMessage("ReSharper", "MethodSupportsCancellation")] - public async Task TestWhenCanceledCore() + public async Task TesWaitForCancellationCore() { + // Empty cancellation case + Assert.Throws( + () => CancellationToken.None + .WaitForCancellationAsync() + .WaitForResult()); + // No cancellation case var cts = new CancellationTokenSource(); - var delayTask = TaskEx.Delay(TimeSpan.FromMilliseconds(500)); - var whenCanceledTask = cts.Token.WhenCanceled(); + var delayTask = TaskEx.Delay(TimeSpan.FromMilliseconds(500), CancellationToken.None); + var waitForCancellationTask = cts.Token.WaitForCancellationAsync(); var completedTask = await TaskEx.WhenAny( delayTask, - whenCanceledTask); + waitForCancellationTask); Assert.AreEqual(completedTask, delayTask); // Token canceled case - delayTask = TaskEx.Delay(TimeSpan.FromMinutes(1)); + delayTask = TaskEx.Delay(TimeSpan.FromMinutes(1), CancellationToken.None); var whenAny = TaskEx.WhenAny( delayTask, - whenCanceledTask); + waitForCancellationTask); cts.Cancel(); completedTask = await whenAny; - Assert.AreEqual(completedTask, whenCanceledTask); -#if NET40_OR_GREATER || TARGETS_NETCOREAPP - Assert.ThrowsAsync(async () => await whenCanceledTask); - Assert.ThrowsAsync(() => whenCanceledTask); -#endif - var ex = Assert.Throws(() => whenCanceledTask.Wait()); - Assert.AreEqual(ex.InnerExceptions.Single().GetType(), typeof(TaskCanceledException)); + Assert.AreEqual(completedTask, waitForCancellationTask); + await waitForCancellationTask; } [Test] - public void TestWhenCanceledTimeout() + public void TestWaitForCancellationTimeout() { - TaskEx.Run(() => TestWhenCanceledTimeoutCore()).Wait(); + TaskEx.Run(() => TestWaitForCancellationTimeoutCore()).Wait(); Assert.IsTrue(true); } [SuppressMessage("ReSharper", "AccessToModifiedClosure")] - [SuppressMessage("ReSharper", "MethodSupportsCancellation")] - public async Task TestWhenCanceledTimeoutCore() + public async Task TestWaitForCancellationTimeoutCore() { + // Empty cancellation case + Assert.Throws( + () => CancellationToken.None + .WaitForCancellationAsync(TimeoutHelper.InfiniteTimeSpan) + .WaitForResult()); + // No cancellation case var neverTimeout = TimeSpan.FromDays(1); var cts = new CancellationTokenSource(); - var whenCanceledTask = cts.Token.WhenCanceled(neverTimeout); - var delayTask = TaskEx.Delay(TimeSpan.FromMilliseconds(500)); + var waitForCancellationTask = cts.Token.WaitForCancellationAsync(neverTimeout); + var delayTask = TaskEx.Delay(TimeSpan.FromMilliseconds(500), CancellationToken.None); var completedTask = await TaskEx.WhenAny( - whenCanceledTask, + waitForCancellationTask, delayTask); Assert.AreEqual(completedTask, delayTask); // Token canceled case - delayTask = TaskEx.Delay(TimeSpan.FromMinutes(1)); + delayTask = TaskEx.Delay(TimeSpan.FromMinutes(1), CancellationToken.None); var whenAny = TaskEx.WhenAny( - whenCanceledTask, + waitForCancellationTask, delayTask); cts.Cancel(); completedTask = await whenAny; - Assert.AreEqual(completedTask, whenCanceledTask); -#if NET40_OR_GREATER || TARGETS_NETCOREAPP - Assert.ThrowsAsync(async () => await whenCanceledTask); - Assert.ThrowsAsync(() => whenCanceledTask); -#endif - var ex = Assert.Throws(() => whenCanceledTask.Wait()); - Assert.AreEqual(ex.InnerExceptions.Single().GetType(), typeof(TaskCanceledException)); + Assert.AreEqual(completedTask, waitForCancellationTask); + await waitForCancellationTask; // Token cancellation timeout case - whenCanceledTask = new CancellationToken().WhenCanceled(TimeSpan.FromMilliseconds(500)); - delayTask = TaskEx.Delay(TimeSpan.FromMinutes(1)); + waitForCancellationTask = new CancellationToken().WaitForCancellationAsync(TimeSpan.FromMilliseconds(500)); + delayTask = TaskEx.Delay(TimeSpan.FromMinutes(1), CancellationToken.None); completedTask = await TaskEx.WhenAny( - whenCanceledTask, + waitForCancellationTask, delayTask); - Assert.AreEqual(completedTask, whenCanceledTask); -#if NET40_OR_GREATER || TARGETS_NETCOREAPP - Assert.ThrowsAsync(async () => await whenCanceledTask); - Assert.ThrowsAsync(() => whenCanceledTask); -#endif - ex = Assert.Throws(() => whenCanceledTask.Wait()); - Assert.AreEqual(ex.InnerExceptions.Single().GetType(), typeof(TimeoutException)); + Assert.AreEqual(completedTask, waitForCancellationTask); + Assert.Throws(() => waitForCancellationTask.WaitForResult()); } } -} +} \ No newline at end of file diff --git a/CodeJam.Main.Tests/Threading/TimeoutHelperTests.cs b/CodeJam.Main.Tests/Threading/TimeoutHelperTests.cs new file mode 100644 index 000000000..19a42ce37 --- /dev/null +++ b/CodeJam.Main.Tests/Threading/TimeoutHelperTests.cs @@ -0,0 +1,78 @@ +using System; + +using NUnit.Framework; + +namespace CodeJam.Threading +{ + [TestFixture] + public class TimeoutHelperTests + { + [Test] + public void TestAdjustTimeout() + { + var d1 = TimeSpan.FromDays(1); + var dMinus1 = TimeSpan.FromDays(-1); + + Assert.AreEqual(TimeSpan.Zero.AdjustTimeout(), TimeSpan.Zero); + Assert.AreEqual(d1.AdjustTimeout(), d1); + Assert.AreEqual(dMinus1.AdjustTimeout(), TimeoutHelper.InfiniteTimeSpan); + } + + [Test] + public void TestAdjustTimeoutInfinite() + { + const bool infiniteIfDefault = true; + var d1 = TimeSpan.FromDays(1); + var dMinus1 = TimeSpan.FromDays(-1); + + Assert.AreEqual(TimeSpan.Zero.AdjustTimeout(infiniteIfDefault), TimeoutHelper.InfiniteTimeSpan); + Assert.AreEqual(d1.AdjustTimeout(infiniteIfDefault), d1); + Assert.AreEqual(dMinus1.AdjustTimeout(infiniteIfDefault), TimeoutHelper.InfiniteTimeSpan); + } + + [Test] + public void TestAdjustTimeoutLimit() + { + var d1 = TimeSpan.FromDays(1); + var d2 = TimeSpan.FromDays(2); + var dMinus1 = TimeSpan.FromDays(-1); + + Assert.AreEqual(TimeSpan.Zero.AdjustTimeout(TimeSpan.Zero), TimeSpan.Zero); + Assert.AreEqual(TimeSpan.Zero.AdjustTimeout(d1), TimeSpan.Zero); + Assert.AreEqual(TimeSpan.Zero.AdjustTimeout(dMinus1), TimeSpan.Zero); + + Assert.AreEqual(d1.AdjustTimeout(TimeSpan.Zero), TimeSpan.Zero); + Assert.AreEqual(d1.AdjustTimeout(d1), d1); + Assert.AreEqual(d1.AdjustTimeout(d2), d1); + Assert.AreEqual(d2.AdjustTimeout(d1), d1); + Assert.AreEqual(d1.AdjustTimeout(dMinus1), d1); + + Assert.AreEqual(dMinus1.AdjustTimeout(TimeSpan.Zero), TimeSpan.Zero); + Assert.AreEqual(dMinus1.AdjustTimeout(d1), d1); + Assert.AreEqual(dMinus1.AdjustTimeout(dMinus1), TimeoutHelper.InfiniteTimeSpan); + } + + [Test] + public void TestAdjustTimeoutLimitInfiniteIfDefault() + { + const bool infiniteIfDefault = true; + var d1 = TimeSpan.FromDays(1); + var d2 = TimeSpan.FromDays(2); + var dMinus1 = TimeSpan.FromDays(-1); + + Assert.AreEqual(TimeSpan.Zero.AdjustTimeout(TimeSpan.Zero, infiniteIfDefault), TimeoutHelper.InfiniteTimeSpan); + Assert.AreEqual(TimeSpan.Zero.AdjustTimeout(d1, infiniteIfDefault), d1); + Assert.AreEqual(TimeSpan.Zero.AdjustTimeout(dMinus1, infiniteIfDefault), TimeoutHelper.InfiniteTimeSpan); + + Assert.AreEqual(d1.AdjustTimeout(TimeSpan.Zero, infiniteIfDefault), d1); + Assert.AreEqual(d1.AdjustTimeout(d1, infiniteIfDefault), d1); + Assert.AreEqual(d1.AdjustTimeout(d2, infiniteIfDefault), d1); + Assert.AreEqual(d2.AdjustTimeout(d1, infiniteIfDefault), d1); + Assert.AreEqual(d1.AdjustTimeout(dMinus1, infiniteIfDefault), d1); + + Assert.AreEqual(dMinus1.AdjustTimeout(TimeSpan.Zero, infiniteIfDefault), TimeoutHelper.InfiniteTimeSpan); + Assert.AreEqual(dMinus1.AdjustTimeout(d1, infiniteIfDefault), d1); + Assert.AreEqual(dMinus1.AdjustTimeout(dMinus1, infiniteIfDefault), TimeoutHelper.InfiniteTimeSpan); + } + } +} \ No newline at end of file diff --git a/CodeJam.Main/Assertions/CodeExceptions.cs b/CodeJam.Main/Assertions/CodeExceptions.cs index a2a9a1940..2f6a41aa1 100644 --- a/CodeJam.Main/Assertions/CodeExceptions.cs +++ b/CodeJam.Main/Assertions/CodeExceptions.cs @@ -167,6 +167,20 @@ public static IndexOutOfRangeException IndexOutOfRange( $"The value of '{argumentName}' ({value}) should be greater than or equal to {startIndex} and less than {length}.")) .LogToCodeTraceSourceBeforeThrow(); } + + /// Creates for non-cancellable tokens. + /// Name of the argument. + /// Initialized instance of . + [DebuggerHidden, NotNull, MustUseReturnValue] + public static ArgumentException ArgumentWaitCancellationRequired( + [NotNull, InvokerParameterName] string argumentName) + { + BreakIfAttached(); + return new ArgumentException( + Invariant($"This method requires '{argumentName}' to be cancellable; otherwise method may wait indefinitely."), + argumentName) + .LogToCodeTraceSourceBeforeThrow(); + } #endregion #region General purpose exceptions @@ -221,6 +235,33 @@ public static OverflowException Overflow( #endregion #region Exceptions for specific scenarios + /// Creates . + /// The message format. + /// The arguments. + /// Initialized instance of . + [DebuggerHidden, NotNull, MustUseReturnValue] + [StringFormatMethod("messageFormat")] + public static TimeoutException Timeout( + [NotNull] string messageFormat, + [CanBeNull] params object[] args) + { + BreakIfAttached(); + return new TimeoutException(InvariantFormat(messageFormat, args)) + .LogToCodeTraceSourceBeforeThrow(); + } + + /// Creates . + /// The timeout. + /// Initialized instance of . + [DebuggerHidden, NotNull, MustUseReturnValue] + public static TimeoutException Timeout(TimeSpan timeout) + { + BreakIfAttached(); + return new TimeoutException( + Invariant($"Operation timed out in {timeout}.")) + .LogToCodeTraceSourceBeforeThrow(); + } + /// /// Creates . /// Used to be thrown from the default: switch clause diff --git a/CodeJam.Main/CodeJam.Main.csproj b/CodeJam.Main/CodeJam.Main.csproj index dcc1dfce3..9a04d2d0b 100644 --- a/CodeJam.Main/CodeJam.Main.csproj +++ b/CodeJam.Main/CodeJam.Main.csproj @@ -60,19 +60,19 @@ - + - + - + - + @@ -91,7 +91,7 @@ - + @@ -104,7 +104,7 @@ - + @@ -119,7 +119,7 @@ - + diff --git a/CodeJam.Main/Collections/ComparerBuilder.cs b/CodeJam.Main/Collections/ComparerBuilder.cs index 9c2488896..861bf5d63 100644 --- a/CodeJam.Main/Collections/ComparerBuilder.cs +++ b/CodeJam.Main/Collections/ComparerBuilder.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using System.Reflection; using JetBrains.Annotations; diff --git a/CodeJam.Main/Dates/TimeSpanHelper.cs b/CodeJam.Main/Dates/TimeSpanHelper.cs index c15b36619..e2151ceb1 100644 --- a/CodeJam.Main/Dates/TimeSpanHelper.cs +++ b/CodeJam.Main/Dates/TimeSpanHelper.cs @@ -63,36 +63,6 @@ public static TimeSpan FromMicroseconds(double microseconds) => public static TimeSpan FromNanoseconds(double nanoseconds) => FromTicksChecked(nanoseconds * _ticksPerNanosecond); - /// - /// Returns a TimeSpan that represents value multiplied to specified multiplier. - /// - /// The time span. - /// The multiplier. - /// A System.TimeSpan that represents value multiplied to specified multiplier. - /// value is less than or greater than . - /// -or- - /// value is. - /// -or- - /// value is . - /// value is equal to . - public static TimeSpan Multiply(this TimeSpan timeSpan, double multiplier) => - FromTicksChecked(timeSpan.Ticks * multiplier); - - /// - /// Returns a TimeSpan that represents value divided to specified divisor. - /// - /// The time span. - /// The divisor. - /// A System.TimeSpan that represents value divided to specified divisor. - /// value is less than or greater than . - /// -or- - /// value is. - /// -or- - /// value is . - /// value is equal to . - public static TimeSpan Divide(this TimeSpan timeSpan, double divisor) => - FromTicksChecked(timeSpan.Ticks / divisor); - /// /// Gets the value of the current TimeSpan structure expressed in whole /// and fractional microseconds. @@ -130,5 +100,35 @@ public static double TotalNanoseconds(this TimeSpan timeSpan) return temp; } + + /// + /// Returns a TimeSpan that represents value multiplied to specified multiplier. + /// + /// The time span. + /// The multiplier. + /// A System.TimeSpan that represents value multiplied to specified multiplier. + /// value is less than or greater than . + /// -or- + /// value is. + /// -or- + /// value is . + /// value is equal to . + public static TimeSpan Multiply(this TimeSpan timeSpan, double multiplier) => + FromTicksChecked(timeSpan.Ticks * multiplier); + + /// + /// Returns a TimeSpan that represents value divided to specified divisor. + /// + /// The time span. + /// The divisor. + /// A System.TimeSpan that represents value divided to specified divisor. + /// value is less than or greater than . + /// -or- + /// value is. + /// -or- + /// value is . + /// value is equal to . + public static TimeSpan Divide(this TimeSpan timeSpan, double divisor) => + FromTicksChecked(timeSpan.Ticks / divisor); } } \ No newline at end of file diff --git a/CodeJam.Main/Threading/TaskHelper.NonGenerated.cs b/CodeJam.Main/Threading/TaskHelper.NonGenerated.cs index 3f1efa326..f210d6894 100644 --- a/CodeJam.Main/Threading/TaskHelper.NonGenerated.cs +++ b/CodeJam.Main/Threading/TaskHelper.NonGenerated.cs @@ -19,27 +19,105 @@ namespace CodeJam.Threading public static partial class TaskHelper { /// - /// Allows to await for the cancellation. - /// IMPORTANT: this method completes on token cancellation only - /// and always throws . + /// Creates derived cancellation. + /// + /// Parent token1. + /// Parent token2. + [Pure] + public static CancellationTokenSource CreateCancellation(CancellationToken token1, CancellationToken token2) => + CancellationTokenSource.CreateLinkedTokenSource(token1, token2); + + /// + /// Creates derived cancellation. + /// + /// Parent cancellations. + [Pure] + public static CancellationTokenSource CreateCancellation(params CancellationToken[] cancellations) => + CancellationTokenSource.CreateLinkedTokenSource(cancellations); + + /// + /// Creates derived cancellation with specified timeout. + /// + /// The timeout. + /// Parent token1. + /// Parent token2. + [Pure] + public static CancellationTokenSource CreateCancellation( + TimeSpan timeout, + CancellationToken token1, + CancellationToken token2) + { + var cancellation = CancellationTokenSource.CreateLinkedTokenSource(token1, token2); + if (timeout != TimeoutHelper.InfiniteTimeSpan) + cancellation.CancelAfter(timeout); + return cancellation; + } + + /// + /// Creates derived cancellation with specified timeout. + /// + /// The timeout. + /// Parent cancellations. + [Pure] + public static CancellationTokenSource CreateCancellation( + TimeSpan timeout, + params CancellationToken[] cancellations) + { + var cancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellations); + if (timeout != TimeoutHelper.InfiniteTimeSpan) + cancellation.CancelAfter(timeout); + return cancellation; + } + + /// + /// Creates cancellation scope. + /// The will be canceled on scope exit + /// + /// The cancellation token source. + /// + [Pure] + public static IDisposable CancellationScope( + this CancellationTokenSource cancellationTokenSource) => + Disposable.Create(cancellationTokenSource.Cancel); + + /// + /// Allows to await for the cancellation without throwing a . /// /// The cancellation token to await for cancellation. /// Task that completes (canceled) on token cancellation. - /// was canceled. - public static Task WhenCanceled(this CancellationToken cancellationToken) => - TaskEx.Delay(-1, cancellationToken); + public static async Task WaitForCancellationAsync(this CancellationToken cancellationToken) + { + if (!cancellationToken.CanBeCanceled) + throw CodeExceptions.ArgumentWaitCancellationRequired(nameof(cancellationToken)); + + try + { + await TaskEx.Delay(TimeoutHelper.InfiniteTimeSpan, cancellationToken); + } + catch (OperationCanceledException) + { + } + } /// - /// Allows to await for the cancellation with await timeout. + /// Allows to await for the cancellation with await timeout without throwing a . /// /// The cancellation token to await for cancellation. /// Cancellation wait wait timeout. - /// was canceled. /// elapsed and was not canceled. - public static async Task WhenCanceled(this CancellationToken cancellationToken, TimeSpan timeout) + public static async Task WaitForCancellationAsync(this CancellationToken cancellationToken, TimeSpan timeout) { - await TaskEx.Delay(timeout, cancellationToken); - throw new TimeoutException($"Wait for cancellation timed out in {timeout}"); + if (timeout == TimeoutHelper.InfiniteTimeSpan && !cancellationToken.CanBeCanceled) + throw CodeExceptions.ArgumentWaitCancellationRequired(nameof(cancellationToken)); + + try + { + await TaskEx.Delay(timeout, cancellationToken); + throw new TimeoutException($"Wait for cancellation timed out in {timeout}"); + } + catch (OperationCanceledException) + { + } } } } \ No newline at end of file diff --git a/CodeJam.Main/Threading/TaskHelper.WithTimeout.cs b/CodeJam.Main/Threading/TaskHelper.WithTimeout.cs new file mode 100644 index 000000000..be93ea7a3 --- /dev/null +++ b/CodeJam.Main/Threading/TaskHelper.WithTimeout.cs @@ -0,0 +1,258 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +using JetBrains.Annotations; + +#if NET45_OR_GREATER || TARGETS_NETSTANDARD || TARGETS_NETCOREAPP +using TaskEx = System.Threading.Tasks.Task; +#else +using TaskEx = System.Threading.Tasks.TaskEx; +#endif + +namespace CodeJam.Threading +{ + /// + /// Helper methods for and . + /// + partial class TaskHelper + { + /// + /// Awaits passed task or throws on timeout. + /// + /// The task. + /// The timeout. + /// The cancellation. + public static Task WithTimeout( + [NotNull] this Task task, + TimeSpan timeout, + CancellationToken cancellation = default) => + task.WithTimeout( + timeout, + _ => throw CodeExceptions.Timeout(timeout), + cancellation); + + /// + /// Awaits passed task or calls on timeout. + /// + /// The task. + /// The timeout. + /// Callback that will be called on timeout. + /// The cancellation. + public static async Task WithTimeout( + [NotNull] this Task task, + TimeSpan timeout, + [NotNull, InstantHandle] Func timeoutCallback, + CancellationToken cancellation = default) + { + Code.NotNull(task, nameof(task)); + Code.NotNull(timeoutCallback, nameof(timeoutCallback)); + if (timeout == TimeoutHelper.InfiniteTimeSpan) + { + await task; + return; + } + + var timeoutTask = TaskEx.Delay(timeout, cancellation); + var taskOrTimeout = await TaskEx.WhenAny(task, timeoutTask); + cancellation.ThrowIfCancellationRequested(); + + if (taskOrTimeout == timeoutTask) + { + await timeoutCallback(cancellation); + return; + } + + // Await will rethrow exception from the task, if any. + // There's no additional cost as FW has optimization for await over completed task: + // continuation will run synchronously + await task; + } + + /// + /// Awaits passed task or throws on timeout. + /// + /// The type of the completed Task. + /// The task. + /// The timeout. + /// The cancellation. + public static Task WithTimeout( + [NotNull] this Task task, + TimeSpan timeout, + CancellationToken cancellation = default) => + task.WithTimeout( + timeout, + _ => throw CodeExceptions.Timeout(timeout), + cancellation); + + /// + /// Awaits passed task or calls on timeout. + /// + /// The type of the completed Task. + /// The task. + /// The timeout. + /// Callback that will be called on timeout. + /// The cancellation. + public static async Task WithTimeout( + [NotNull] this Task task, + TimeSpan timeout, + [NotNull, InstantHandle] Func> timeoutCallback, + CancellationToken cancellation = default) + { + Code.NotNull(task, nameof(task)); + Code.NotNull(timeoutCallback, nameof(timeoutCallback)); + if (timeout == TimeoutHelper.InfiniteTimeSpan) + return await task; + + var timeoutTask = TaskEx.Delay(timeout, cancellation); + var taskOrTimeout = await TaskEx.WhenAny(task, timeoutTask); + cancellation.ThrowIfCancellationRequested(); + + if (taskOrTimeout == timeoutTask) + return await timeoutCallback(cancellation); + + // Await will rethrow exception from the task, if any. + // There's no additional cost as FW has optimization for await over completed task: + // continuation will run synchronously + return await task; + } + + /// + /// Awaits passed task or throws on timeout. + /// + /// The task factory. Accepts that will be canceled on timeout or . + /// The timeout. + /// The cancellation. + public static Task RunWithTimeout( + [NotNull, InstantHandle] Func taskFactory, + TimeSpan timeout, + CancellationToken cancellation = default) => + RunWithTimeout( + taskFactory, + timeout, + _ => throw CodeExceptions.Timeout(timeout), + cancellation); + + /// + /// Awaits passed task or calls on timeout. + /// + /// The task factory. Accepts that will be canceled on timeout or . + /// The timeout. + /// Callback that will be called on timeout. Accepts . + /// The cancellation. + public static Task RunWithTimeout( + [NotNull, InstantHandle] Func taskFactory, + TimeSpan timeout, + [NotNull, InstantHandle] Func timeoutCallback, + CancellationToken cancellation = default) + { + /* + Implementation logic: + 1. Await for taskFactory task, cancellation, or timeout. + 2. Force taskFactory task cancellation on timeout + 3. Ensure that we cancel taskFactory task even on unhandled exceptions via timeoutOrCancellation.CancellationScope() + + NB: We can not use CreateCancellation(timeout) overload here as it will result in a race in WithTimeout() logic. + Here's why: + ```cs + var timeoutTask = TaskEx.Delay(timeout, cancellation); + var taskOrTimeout = await TaskEx.WhenAny(task, timeoutTask); + if (taskOrTimeout == timeoutTask) + return await timeoutCallback(cancellation); + ``` + The task may be canceled first and therefore timeoutCallback will not be called. + */ + Code.NotNull(taskFactory, nameof(taskFactory)); + Code.NotNull(timeoutCallback, nameof(timeoutCallback)); + return TaskEx.Run( + async () => + { + using (var timeoutOrCancellation = CreateCancellation(cancellation)) + using (timeoutOrCancellation.CancellationScope()) + { + await taskFactory(timeoutOrCancellation.Token) + .WithTimeout( + timeout, + ct => + { + // ReSharper disable once AccessToDisposedClosure + timeoutOrCancellation.Cancel(); + return timeoutCallback(ct); + }, + cancellation); + } + }, + cancellation); + } + + /// + /// Awaits passed task or throws on timeout. + /// + /// The type of the completed Task. + /// The task factory. Accepts that will be canceled on timeout or . + /// The timeout. + /// The cancellation. + public static Task RunWithTimeout( + [NotNull, InstantHandle] Func> taskFactory, + TimeSpan timeout, + CancellationToken cancellation = default) => + RunWithTimeout( + taskFactory, + timeout, + _ => throw CodeExceptions.Timeout(timeout), + cancellation); + + /// + /// Awaits passed task or calls on timeout. + /// + /// The type of the completed Task. + /// The task factory. Accepts that will be canceled on timeout or . + /// The timeout. + /// Callback that will be called on timeout. Accepts . + /// The cancellation. + public static Task RunWithTimeout( + [NotNull, InstantHandle] Func> taskFactory, + TimeSpan timeout, + [NotNull, InstantHandle] Func> timeoutCallback, + CancellationToken cancellation = default) + { + /* + Implementation logic: + 1. Await for taskFactory task, cancellation, or timeout. + 2. Force taskFactory task cancellation on timeout + 3. Ensure that we cancel taskFactory task even on unhandled exceptions via timeoutOrCancellation.CancellationScope() + + NB: We can not use CreateCancellation(timeout) overload here as it will result in a race in WithTimeout() logic. + Here's why: + ```cs + var timeoutTask = TaskEx.Delay(timeout, cancellation); + var taskOrTimeout = await TaskEx.WhenAny(task, timeoutTask); + if (taskOrTimeout == timeoutTask) + return await timeoutCallback(cancellation); + ``` + The task may be canceled first and therefore timeoutCallback will not be called. + */ + Code.NotNull(taskFactory, nameof(taskFactory)); + Code.NotNull(timeoutCallback, nameof(timeoutCallback)); + return TaskEx.Run( + async () => + { + using (var timeoutOrCancellation = CreateCancellation(cancellation)) + using (timeoutOrCancellation.CancellationScope()) + { + return await taskFactory(timeoutOrCancellation.Token) + .WithTimeout( + timeout, + ct => + { + // ReSharper disable once AccessToDisposedClosure + timeoutOrCancellation.Cancel(); + return timeoutCallback(ct); + }, + cancellation); + } + }, + cancellation); + } + } +} \ No newline at end of file diff --git a/CodeJam.Main/Threading/TimeoutHelper.cs b/CodeJam.Main/Threading/TimeoutHelper.cs new file mode 100644 index 000000000..d8814b871 --- /dev/null +++ b/CodeJam.Main/Threading/TimeoutHelper.cs @@ -0,0 +1,74 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +using JetBrains.Annotations; + +namespace CodeJam.Threading +{ + /// + /// Helper methods for timespan timeouts + /// + [PublicAPI] + public static class TimeoutHelper + { + /// + /// A constant used to specify an infinite waiting period, for methods that accept a TimeSpan parameter + /// + public static readonly TimeSpan InfiniteTimeSpan = +#if NET45_OR_GREATER || TARGETS_NETSTANDARD || TARGETS_NETCOREAPP + Timeout.InfiniteTimeSpan; +#else + new TimeSpan(0, 0, 0, 0, -1); +#endif + + /// + /// Replaces negative value with . + /// If is true, value is treated as + /// + /// + /// Use case scenario: methods that accept timeout often accept only + /// but not other negative values. Check as example. + /// Motivation for : + /// default timeout in configs often means 'infinite timeout', not 'do not wait and return immediately'. + /// + public static TimeSpan AdjustTimeout(this TimeSpan timeout, bool infiniteIfDefault = false) + { + if (infiniteIfDefault) + return timeout <= TimeSpan.Zero + ? InfiniteTimeSpan + : timeout; + + return timeout < TimeSpan.Zero + ? InfiniteTimeSpan + : timeout; + } + + /// + /// Limits timeout by upper limit. + /// Replaces negative value with . + /// If is true, value is treated as + /// + /// + /// Use case scenario: methods that accept timeout often accept only + /// but not other negative values. Check as example. + /// Motivation for : + /// default timeout in configs often means 'infinite timeout', not 'do not wait and return immediately'. + /// + public static TimeSpan AdjustTimeout(this TimeSpan timeout, TimeSpan upperLimit, bool infiniteIfDefault = false) + { + timeout = timeout.AdjustTimeout(infiniteIfDefault); + upperLimit = upperLimit.AdjustTimeout(infiniteIfDefault); + + // Ignore upper limit if negative + if (upperLimit < TimeSpan.Zero) + return timeout; + + // Ignore timeout if negative or exceeds upper limit + if (timeout < TimeSpan.Zero || timeout > upperLimit) + return upperLimit; + + return timeout; + } + } +} \ No newline at end of file