Don't allow parallel API invocation based on parameter

72 Views Asked by At

I'm required to do not allow more than one invocation at a time for an API, based on single query param. Parallel invocation with different values of the query param must be allowed.

I invoke the api through

await client.GetAsync(QueryHelpers.AddQueryString(path, parameters))

So, there is an async invocation that do not allows me to use System.Threading.Monitor.TryEnter beacause when I try to release the lock I got the exception:

System.Threading.SynchronizationLockException: Object synchronization method was called from an unsynchronized block of code.

Here's the snippet

            try
            {
                Monitor.TryEnter(lockObj, timeout, ref lockTaken);
                if (lockTaken)
                {
                    List<PatientData> patients = await RetrievePatientsDataAsync(client, new Dictionary<string, string>
                    {
                        ["customerNo"] = customerNo,
                        ["offset"] = _lindeAPIOptions.Offset,
                        ["pagesize"] = _lindeAPIOptions.Pagesize

                    });

                    data.Patients = patients;
                    return data;

                }

            }
            finally
            {
                // Ensure that the lock is released.
                if (lockTaken)
                {
                    Monitor.Exit(lockObj);
                }

            }

The parameter is customerNo. In method RetrievePatientsDataAsync i call the aforementioned

await client.GetAsync(QueryHelpers.AddQueryString(path, parameters))
1

There are 1 best solutions below

2
On

You could possibly keep a list of all the ids that are "locked", something like this:

private HashSet<long> lockedIds = new();

public async Task ExclusiveAccessById(long id)
{
    bool hasExclusiveAccess = true;
    lock (lockedIds)
    {
        hasExclusiveAccess = lockedIds.Add(id);
    }
    if (!hasExclusiveAccess)
    {
        // handle failure
        return;
    }

    try
    {
        // Handle success
        await Task.Delay(1);
        return;
    }
    finally
    {
        lock (lockedIds)
        {
            lockedIds.Remove(id);
        }
    }
}

The key part here is to not hold the lock while the actual method is running, or when doing any awaiting etc. You could probably do something similar with a ConcurrentDictionary, but the overhead of a lock should be low if there is low contention.