Rate-limit your actions and funcs by throttling and debouncing them. Retry when an exception is thrown.
This is a .NET library that lets you rate-limit delegates so they are only executed at most once in a given interval, even if they are invoked multiple times in that interval. You can also invoke a delegate and automatically retry it if it fails.
This package is available on NuGet Gallery.
dotnet add package ThrottleDebounce
Install-Package ThrottleDebounce
It targets .NET Standard 2.0 and .NET Framework 4.5.2, so it should be compatible with many runtimes.
Action originalAction;
Func<int> originalFunc;
TimeSpan wait = TimeSpan.FromMilliseconds(50);
using RateLimitedAction throttledAction = Throttler.Throttle(originalAction, wait, leading: true, trailing: true);
using RateLimitedFunc<int> debouncedFunc = Debouncer.Debounce(originalFunc, wait, leading: false, trailing: true);
throttledAction.Invoke();
int? result = debouncedFunc.Invoke();
- Call
Throttler.Throttle()
to throttle your delegate, orDebouncer.Debounce()
to debounce it. PassAction action
/Func func
— your delegate to rate-limitTimeSpan wait
— how long to wait between executionsbool leading
—true
if the first invocation should be executed immediately, orfalse
if it should be queued. Optional, defaults totrue
for throttling andfalse
for debouncing.bool trailing
—true
if subsequent invocations in the waiting period should be enqueued for later execution once the waiting interval is over, orfalse
if they should be discarded. Optional, defaults totrue
.
- Call the resulting
RateLimitedAction
/RateLimitedFunc
object'sInvoke()
method to enqueue an invocation.RateLimitedFunc.Invoke
will returndefault
(e.g.null
) ifleading
isfalse
and the rate-limitedFunc
has not been executed before. Otherwise, it will return theFunc
's most recent return value.
- Your delegate will be executed at the desired rate.
- Optionally call the
RateLimitedAction
/RateLimitedFunc
object'sDispose()
method to prevent all queued executions from running when you are done.
Throttling and debouncing both restrict a function to not execute too often, no matter how frequently you invoke it.
This is useful if the function is invoked very frequently, like whenever the mouse moves, but you don't want to it to run every single time the pointer moves 1 pixel, because the function is expensive, such as rendering a user interface.
Throttling allows the function to still be executed periodically, even with a constant stream of invocations.
Debouncing prevents the function from being executed at all until it hasn't been invoked for a while.
An invocation can result in at most one execution. For example, if both leading
and trailing
are true
, one single invocation will execute once on the leading edge and not on the trailing edge.
Debouncing and Throttling Explained Through Examples by David Corbacho
Action throttled = Throttler.Throttle(() => Console.WriteLine("hi"), TimeSpan.FromSeconds(1)).Invoke;
throttled(); //logs at 0s
throttled(); //logs at 1s
Thread.Sleep(1000);
throttled(); //logs at 2s
Func<double, double, double> debounced = Debouncer.Debounce((double x, double y) => Math.Sqrt(x * x + y * y),
TimeSpan.FromMilliseconds(200)).Invoke;
double? result;
result = debounced(1, 1); //never runs
result = debounced(2, 2); //never runs
result = debounced(3, 4); //runs at 200ms
RateLimitedAction rateLimited = Throttler.Throttle(() => Console.WriteLine("hello"), TimeSpan.FromSeconds(1));
rateLimited.Invoke(); //runs at 0s
rateLimited.Dispose();
rateLimited.Invoke(); //never runs
static void SaveWindowLocation(double x, double y) => Registry.SetValue(@"HKEY_CURRENT_USER\Software\My Program",
"Window Location", $"{x},{y}");
Action<double, double> saveWindowLocationThrottled = Throttler.Throttle<double, double>(saveWindowLocation,
TimeSpan.FromSeconds(1)).Invoke;
LocationChanged += (sender, args) => SaveWindowLocationThrottled(Left, Top);
public MainWindow(){
InitializeComponent();
Action<object, RoutedEventArgs> onButtonClickDebounced = Debouncer.Debounce<object, RoutedEventArgs>(
OnButtonClick, TimeSpan.FromMilliseconds(40), true, false).Invoke;
MyButton.Click += new RoutedEventHandler(onButtonClickDebounced);
}
private void OnButtonClick(object sender, RoutedEventArgs e) {
MessageBox.Show("Button clicked");
}
Given a function or action, you can execute it and, if it threw an exception, automatically execute it again until it succeeds.
Retrier.Attempt(attempt => MyErrorProneAction(), maxAttempts: 2);
- Call
Retrier.Attempt()
. PassAction<int> action
/Func<int, T> func
— your delegate to attempt, and possibly retry if it throws exceptions. The attempt number will be passed as theint
parameter, starting with0
before the first attempt. If this func returns aTask
, it will be awaited to determine if it threw an exception.int maxAttempts
— the total number of times the delegate is allowed to run in this invocation, equal to1
initial attempt plus up tomaxAttempts - 1
retries if it throws an exception. Must be at least 1, if you pass 0 it will clip to 1. Defaults to 2. For infinite retries, passnull
.Func<int, TimeSpan> delay
— how long to wait between attempts, as a function of the attempt number. The upcoming attempt number will be passed as a parameter, starting with1
before the second attempt. You can return a constantTimeSpan
for a fixed delay, or pass longer values for subsequent attempts to implement, for example, exponential backoff. Optional, defaults tonull
, which means no delay. The minimum value is0
, the maximum value isint.MaxValue
(uint.MaxValue - 1
starting in .NET 6), and values outside this range will be clipped.Func<Exception, bool> isRetryAllowed
— whether the delegate is permitted to execute again after a givenException
instance. Returntrue
to allow orfalse
to deny retries. For example, you may want to retry after HTTP 500 errors since subsequent requests may succeed, but stop after the first failure for an HTTP 403 error which probably won't succeed if the same request is sent again. Optional, defaults to retrying on all exceptions besidesOutOfMemoryException
.Action beforeRetry
/Func<Task> beforeRetry
— a delegate to run extra logic between attempts, for example, if you want to log a message or perform any cleanup before the next attempt. Optional, defaults to not running anything between attempts.CancellationToken cancellationToken
— used to cancel the attempts and delays before they have all completed. Optional, defaults to no cancellation token. When cancelled,Attempt()
throws aTaskCancelledException
.
- If your delegate returns a value, it will be returned by
Attempt()
.
using HttpClient httpClient = new();
HttpStatusCode statusCode = await Retrier.Attempt(async attempt => {
Console.WriteLine($"Attempt #{attempt:N0}...");
HttpResponseMessage response = await httpClient.GetAsync("https://httpbin.org/status/200%2C500");
Console.WriteLine($"Received response status code {(int) response.StatusCode}.");
response.EnsureSuccessStatusCode(); // throws HttpRequestException for status codes outside the range [200, 300)
return response.StatusCode;
}, 5, _ => TimeSpan.FromSeconds(2));
Console.WriteLine($"Final response: {(int) statusCode}")
Attempt #0...
Received response status code 500
Attempt #1...
Received response status code 500
Attempt #2...
Received response status code 200
Final response: 200