0

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();
        }
    }
}
Community
  • 1
  • 1
Brian Vallelunga
  • 9,869
  • 15
  • 60
  • 87
  • What exactly is `Tenant`? Is it a class from some library (which one?) or your custom class? Would it make sense to modify it just for this purpose? Also, can one `Tenant` have more than one `ApiClient`? – svick Jul 15 '16 at 08:35
  • Tenant is just a small context class that contains the name of the tenant and some connection information used by the API client like username/password. I'm using the ASP.NET WebJobs SDK and Tenant is simply injected in when I handle a service bus message. You can think of the Tenant object as read-only. – Brian Vallelunga Jul 15 '16 at 13:41
  • I've updated the question with a Semaphore approach I came up with. – Brian Vallelunga Jul 15 '16 at 13:50

1 Answers1

1

Your SemaphoreSlim approach seems mostly reasonable to me.

One potential issue is that if Tenants can come and go over the lifetime of the application, then you'll be keeping semaphores even for Tenants 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.

svick
  • 236,525
  • 50
  • 385
  • 514
  • Thanks for this. Tenants are rarely removed from the system. Much less frequently than the service is restarted, so I think I'm safe. – Brian Vallelunga Jul 15 '16 at 15:28