Skip to content

Instantly share code, notes, and snippets.

@reisenberger
Created December 3, 2015 12:25
Show Gist options
  • Save reisenberger/48b4e8e0102d0d0f6290 to your computer and use it in GitHub Desktop.
Save reisenberger/48b4e8e0102d0d0f6290 to your computer and use it in GitHub Desktop.
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Polly;
namespace PollyThreadSleepVersusTaskDelay
{
class Program
{
static void Main()
{
// This small C#.NET console app demonstrates an issue (29/11/2015) with the Polly method WaitAndRetryAsync(), described at https://github.com/App-vNext/Polly/issues/56
// The underlying implementation of WaitAndRetryAsync() currently (29/11/2015) uses Thread.Sleep(). This unnecessarily blocks threads and can lead to thread pool starvation:
// - With WaitAndRetryAsync() implemented via Thread.Sleep(), running below code shows the overall elapsed time to service all requests wildly exceeding waitAndRetrySeconds, as .NET throttles the creation of new threads to service the burst of requests. The overall number of threads used to service the requests is also very high.
// - With WaitAndRetryAsync() implemented via await Task.Delay() (see https://github.com/App-vNext/Polly/issues/56 or https://github.com/App-vNext/Polly/pull/53), overall completion is (as expected) just over waitAndRetrySeconds, while the overall number of threads used to service the requests remains low.
Console.WriteLine("Threads in use before making requests: " + Process.GetCurrentProcess().Threads.Count);
// Simulate a service receiving a number of closely-spaced calls, ie under load.
int simultaneousCalls = 60;
TimeSpan slightDelayBetweenCalls = TimeSpan.FromMilliseconds(5);
// Retry delay for a WaitAndRetryAsync() policy.
int waitAndRetrySeconds = 2;
// Track results of all calls, in order that we can also pretend we are the callers, and wait on results of all calls.
bool[] results = new bool[simultaneousCalls];
Stopwatch watch = new Stopwatch();
watch.Start();
// Place requests.
for (int i = 0; i < simultaneousCalls; i++)
{
int n = i;
Console.WriteLine("Making request " + n);
Task.Factory.StartNew(async () =>
{
var requestHandler = new ActionFailsThenSucceeds();
results[n] = await Policy.Handle<Exception>().WaitAndRetryAsync(new[] { TimeSpan.FromSeconds(waitAndRetrySeconds) })
.ExecuteAsync(async () => await requestHandler.Invoke());
});
Thread.Sleep(slightDelayBetweenCalls);
}
// Wait for all requests to return (meanwhile periodically reporting threads used).
while (results.Any(success => !success))
{
Console.WriteLine("Threads in use: " + Process.GetCurrentProcess().Threads.Count);
Thread.Sleep(TimeSpan.FromSeconds(1));
}
// Output results.
watch.Stop();
Console.WriteLine("{0} simultaneous requests; wait-and-retry set to {1} seconds; elapsed time for requests to all succeed: ~ {2} seconds.", simultaneousCalls, waitAndRetrySeconds, watch.Elapsed.Seconds);
Console.WriteLine("Threads in use at completion: " + Process.GetCurrentProcess().Threads.Count);
Console.ReadKey();
}
private class ActionFailsThenSucceeds
{
private bool firstRun = true;
public async Task<bool> Invoke() // Unusual to have an async method without any await. In this case, intentional. This method intentionally as lightweight as possible, so that thread usage and any observed elapsed-time delay can clearly be attributed to WaitAndRetryAsync().
{
if (firstRun)
{
firstRun = false;
throw new Exception("Process faulted.");
}
Console.WriteLine("Doing imaginary work!");
return true;
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment