Reputation: 10201
I'm writing a class that uses HttpClient to access an API and I want to throttle the number of concurrent calls that can be made to a certain function in this class. The trick is though that the limit is per tenant and multiple tenants might be using their own instance of the class at a time.
My Tenant class is just a container for read-only context information.
public class Tenant
{
public string Name { get; }
public string ApiKey { get; }
}
Here's the ApiClient:
public class ApiClient
{
private readonly Tenant tenant;
public ApiClient(Tenant tenant)
{
this.tenant = tenant;
}
public async Task<string> DoSomething()
{
var response = await this.SendCoreAsync();
return response.ToString();
}
private Task<XElement> SendCore()
{
using (var httpClient = new HttpClient())
{
var httpRequest = this.BuildHttpRequest();
var httpResponse = await httpClient.SendAsync(httpRequest);
return XElement.Parse(await httpResponse.Content.ReadAsStringAsync());
}
}
}
What I want to do is throttle the SendCore
method and limit it to two concurrent requests per tenant. I've read suggestions of using TPL
or SemaphoreSlim
to do basic throttling (such as here: Throttling asynchronous tasks), but I'm not clear on how to add in the further complication of the tenant.
Thanks for the suggestions.
UPDATE
I've attempted to use a set of SemaphoreSlim
objects (one per tenant) contained in a ConcurrentDictionary
. This seems to work, but I'm not sure if this is ideal. The new code is:
public class ApiClient
{
private static readonly ConcurrentDictionary<string, SemaphoreSlim> Semaphores = new ConcurrentDictionary<string, SemaphoreSlim>();
private readonly Tenant tenant;
private readonly SemaphoreSlim semaphore;
public ApiClient(Tenant tenant)
{
this.tenant = tenant;
this.semaphore = Semaphores.GetOrAdd(this.tenant.Name, k => new SemaphoreSlim(2));
}
public async Task<string> DoSomething()
{
var response = await this.SendCoreAsync);
return response.ToString();
}
private Task<XElement> SendCore()
{
await this.semaphore.WaitAsync();
try
{
using (var httpClient = new HttpClient())
{
var httpRequest = this.BuildHttpRequest();
var httpResponse = await httpClient.SendAsync(httpRequest);
return XElement.Parse(await httpResponse.Content.ReadAsStringAsync());
}
}
finally
{
this.semaphore.Release();
}
}
}
Upvotes: 0
Views: 1997
Reputation: 244908
Your SemaphoreSlim
approach seems mostly reasonable to me.
One potential issue is that if Tenant
s can come and go over the lifetime of the application, then you'll be keeping semaphores even for Tenant
s that don't exist anymore.
A solution to that would be to use ConditionalWeakTable<Tenant, SemaphoreSlim>
instead of your ConcurrentDictionary
, which makes sure its keys can be garbage collected and when they are, it releases the value.
Upvotes: 1