6
\$\begingroup\$

.NET 8 finally introduced a time abstraction that can be used to fake advance a clock while testing certain components.

https://learn.microsoft.com/en-us/dotnet/api/system.timeprovider?view=net-9.0

The code below should behave exactly like Task.Delay and should be a 1:1 replacement.

  • Is this code fully non blocking in all cases?
  • Are there any other issues with this code?
public static class TimeProviderExtensions
{
    /// <summary>
    /// Creates a task that completes after the specified time delay using the given TimeProvider.
    /// </summary>
    /// <param name="timeProvider">The TimeProvider to schedule the timer with.</param>
    /// <param name="delay">The time to wait before completing the returned task, or Timeout.InfiniteTimeSpan.</param>
    /// <param name="cancellationToken">A cancellation token that can be used to cancel the delay.</param>
    /// <returns>A task that represents the time delay.</returns>
    /// <exception cref="ArgumentOutOfRangeException">
    /// Thrown if <paramref name="delay"/> is less than -1 milliseconds and not Timeout.InfiniteTimeSpan.
    /// </exception>
    public static Task Delay(this TimeProvider timeProvider, TimeSpan delay, CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(timeProvider);

        // Validate the delay argument similar to how Task.Delay would
        if (delay < TimeSpan.Zero && delay != Timeout.InfiniteTimeSpan)
            throw new ArgumentOutOfRangeException(nameof(delay), "Delay must be non-negative or Timeout.InfiniteTimeSpan.");

        // If token is already canceled, return a canceled task
        if (cancellationToken.IsCancellationRequested)
            return Task.FromCanceled(cancellationToken);

        // If no delay is needed, return a completed task
        if (delay == TimeSpan.Zero)
            return Task.CompletedTask;

        var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        CancellationTokenRegistration ctr = default;
        ITimer? timer = null;

        // Set up cancellation if requested
        if (cancellationToken.CanBeCanceled)
        {
            ctr = cancellationToken.Register(() =>
            {
                timer?.Dispose();
                tcs.SetCanceled(cancellationToken);
            });
        }

        // Create a one-shot timer that completes the TCS when elapsed
        timer = timeProvider.CreateTimer(_ =>
        {
            // Timer has fired, dispose cancellation registration and complete the task
            ctr.Dispose();
            tcs.TrySetResult();
        }, state: null, dueTime: delay, period: Timeout.InfiniteTimeSpan);

        return tcs.Task;
    }
}

\$\endgroup\$
1
  • \$\begingroup\$ Can you elaborate on the use case for this? Maybe give a concrete example? \$\endgroup\$ Commented Jan 23 at 11:58

1 Answer 1

8
\$\begingroup\$

You don't need this at all. The Task.Delay has overloads which anticipates a TimeProvider (for example: TimeProvider.System, FakeTimeProvider, etc.).

Here is a simplified example:

using System;
using System.Threading;
using System.Threading.Tasks;

using Microsoft.Extensions.Time.Testing;
using Polly;
using Polly.Timeout;
                    
public class Program
{
    public static async Task Main()
    {
        var timeProvider = new FakeTimeProvider();
        var pipeline = new ResiliencePipelineBuilder { TimeProvider = timeProvider }
            .AddTimeout(TimeSpan.FromMilliseconds(100))
            .Build();
        
        try
        {
            await pipeline.ExecuteAsync(async ct =>
            {
                timeProvider.Advance(TimeSpan.FromSeconds(1));
                await Task.Delay(TimeSpan.FromSeconds(10), timeProvider, ct);

            }, CancellationToken.None);
        } 
        catch(TimeoutRejectedException)
        {
            Console.WriteLine("Execution was interrupted by a timeout.");
            return;
        }
        
        Console.WriteLine("Finished without interruption.");
    }
}
  • We have defined a timeout Polly strategy for 100 milliseconds
  • We have advanced a second on the fake time provider
  • We have defined a Task.Delay with 10 seconds
  • Due to the time advancement and the timeout policy the execution of the Task.Delay is terminated and resulted in a Polly specific TimeoutRejectedException

Dotnet fiddle: https://dotnetfiddle.net/B2nZ78

\$\endgroup\$

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.