I have a durable functions orchestration, where I need to rate-limit the requests to an external API. There are a bunch of tasks in the orchestrator that invoke a 'DownloadData' function with different parameters, but I couldn't figure out how to limit the requests to the external service globally, for all instances of 'DownloadData'. I figured I'd try using a Durable Entity to keep track of the remaining calls within a certain timeframe.
Here is the DelegatingHandler I use for authentication and was hoping to also use for rate-limiting via the durable entity, however the app crashes as it cannot set the DurableTaskClient via DI and I don't know how to fix this.
public class AuthenticationHandler : DelegatingHandler
{
private readonly IKeyVaultService _keyVaultService;
private readonly ILogger<AuthenticationHandler> _logger;
private readonly DurableTaskClient _durableClient;
public AuthenticationHandler(IKeyVaultService keyVaultService, ILogger<AuthenticationHandler> logger, DurableTaskClient durableClient)
{
_keyVaultService = keyVaultService ?? throw new ArgumentNullException(nameof(keyVaultService));
_logger = logger;
_durableClient = durableClient;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
string token = await _keyVaultService.GetAccessTokenAsync();
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
var entityId = new EntityInstanceId(nameof(RateLimiterEntity), "RateLimiter");
EntityMetadata<RateLimiterEntity>? entity = await _durableClient.Entities.GetEntityAsync<RateLimiterEntity>(entityId, cancellation: cancellationToken);
_logger.LogInformation("Remaining requests: {rem}", entity.State.RemainingRequests.ToString());
if (entity != null && entity.State.RemainingRequests <= 25)
{
var delay = entity.State.ResetTime - DateTime.UtcNow;
if (delay > TimeSpan.Zero)
{
_logger.LogWarning("Rate limit reached. Delaying for {Delay} seconds", delay.TotalSeconds);
await Task.Delay(delay, cancellationToken);
}
await _durableClient.Entities.SignalEntityAsync(entityId, nameof(RateLimiterEntity.Reset), cancellationToken);
}
await _durableClient.Entities.SignalEntityAsync(entityId, nameof(RateLimiterEntity.Decrement), cancellationToken);
var response = await base.SendAsync(request, cancellationToken);
return response;
}
}
Here is the durable entity (isolated, .NET 8):
[DurableTask(nameof(RateLimiterEntity))]
public class RateLimiterEntity
{
public int RemainingRequests { get; set; } = 100;
public DateTime ResetTime { get; set; } = DateTime.UtcNow.AddSeconds(60);
public void Reset()
{
RemainingRequests = 100;
ResetTime = DateTime.UtcNow.AddSeconds(60);
}
public void Decrement()
{
RemainingRequests--;
}
[Function(nameof(RateLimiterEntity))]
public static Task Run([EntityTrigger] TaskEntityDispatcher dispatcher)
=> dispatcher.DispatchAsync<RateLimiterEntity>();
}
So how to properly register the DurableTaskClient and access the entity from within the DelegatingHandler?