Implement polling using Polly
displayMode: compact
gantt
tickInterval 1second
dateFormat s.SSS
axisFormat %S
title Polling
section Retries
0 : t0, 0.000, 0.030
1 : t1, 1.000, 1.030
2 : t2, 2.000, 2.030
3 : t3, 3.000, 3.030
4 : t4, 4.000, 4.030
5 : t5, 5.000, 5.030
6 : t6, 6.000, 6.030
7 : t7, 7.000, 7.030
8 : t8, 8.000, 8.030
9 : t9, 9.000, 9.030
10 : t10, 10.000, 10.030
timeout : milestone, 10.000, 10.000
Although it's a good idea to avoid polling in your code if you can, there are still cases when it's necessary. While it's not too difficult to implement the polling loop manually, you can also use the Polly resilience library for that purpose.
The following criteria usually affect the polling implementation:
- The exit condition determines when to stop polling.
- The polling interval determines how much to wait between the calls if they have to be repeated because the exit condition wasn't met yet.
- The timeout determines when to stop polling even if the exit condition wasn't met.
Below is a typical manual implementation of such a polling loop:
public static readonly TimeSpan PollingInterval = TimeSpan.FromSeconds(1);
public static readonly TimeSpan Timeout = TimeSpan.FromSeconds(10);
public async Task<bool> Process()
{
var id = await backendService.Create();
var endTime = DateTime.UtcNow + Timeout;
while (DateTime.UtcNow < endTime)
{
var status = await backendService.GetStatus(id);
if (status != Status.Pending)
{
return status == Status.Success;
}
await Task.Delay(PollingInterval);
}
throw new TimeoutException();
}
The loop repeatedly polls the current status while it is Pending
(the exit condition). The delay between the attempts is 1 second (polling interval), and the loop exists after 10 seconds even if the status is still Pending
(timeout).
The Polly library was primarily designed for resilience purposes, i.e., to handle transient errors. However, polling a status for a short period of time until it changes isn't all that different. The Pending
status could be treated as a transient error. And the polling loop could be replaced with the retry resilience strategy:
public static readonly ResiliencePipeline pipeline = new ResiliencePipelineBuilder()
.AddRetry(
new RetryStrategyOptions
{
ShouldHandle = new PredicateBuilder().HandleResult(Status.Pending),
Delay = TimeSpan.FromSeconds(1),
MaxRetryAttempts = 10,
}
)
.Build();
public async Task<bool> Process()
{
var id = await backendService.Create();
var result = await pipeline.ExecuteAsync(
async _ => await backendService.GetStatus(id)
);
if (result == Status.Pending)
{
throw new TimeoutException();
}
return result == Status.Success;
}
In this case all three criteria are defined as part of the resilience pipeline:
ShouldHandle
specifies the exit criteria,Delay
specifies the poling interval, andMaxRetryAttempts
specifies the timeout.
For anyone familiar with the library, it's easier to see the polling criteria in this code than in the manually written loop. There's also no risk of having a hidden bug in your implementation. Last but not least, Polly gives you the option to easily make the retry pattern more sophisticated than just a simple retry at a fixed interval, e.g. you can add some jitter, or make the interval dynamic.
You can find full source code for a sample project using both approaches in my GitHub repository. I added some tests to make sure that both implementations work as expected.
There's always a balance to be found between writing the code yourself and taking dependencies on external libraries. In this particular case, I think there might be benefits to using a battle-proven library like Polly, especially if you're already using it in your project for resilience purposes or if a simple fixed polling interval doesn't cover your requirements.