I have an API endpoint with which authenticated users can place reservations for an event. Once a reservation call to the API is successful, that number of spots is "consumed" and cannot be reserved by another user.
What I am trying to achieve is concurrent calls getting organized in order to avoid having the following scenario occur:
- The venue has 10 spots available
- User A makes an API call to reserve 6 spots
- User B at about the same time (milliseconds later) makes an API call to reserve 5 spots
- Before the call from User A finishes and locks the 6 spots, the call from User B proceeds as if it will succeed because it sees 10 spots available.
Essentially I am trying to organize my calls serially.
For further considerations:
- I am using Microsoft Azure - should I consider using a queue of sorts?
- I am also using EF Core for my back-end data if that could make a difference in a way that I cannot think of.
- Is there a library I can use to do this semaphore-like approach?
I fully understand that in as an alternative I may be able to soft-reserve one spot at a time for each user and only if all spots get soft-reserved successfully I then go ahead and hard-reserve them. Before I go down some route like this one, I am curious if there was some less clunky-approach.
Thank you
CodePudding user response:
This is a common problem in multithreaded systems. There are lots of ways it could be done, but if the call is fast and the load isn't extreme you could probably get away with something like ReaderWriterLockSlim. I'm not well versed in Azure, but that would be my quick-and-dirty solution if I were writing the server myself.
CodePudding user response:
Synchronous
You can solve this problem without a queue or asynchrony if you're ok with pessimistic locking at the database level. Pessimistic locking is inherently a means of serializing. I do not recommend locking with in-process locks such as the lock
keyword/mutex and such. Modern APIs have more than one instance so that precludes single-machine/memory mutexes.
You can make an immediate (synchronous) call to a persistent store of some kind (e.g. a table dedicated to reserving slots) to reserve M
slots. This might be something like, "update the record where NAvailable - M >= 0
". "The record" will be identified by some criteria like a room ID.
- If that transaction succeeds, it succeeds while that record is locked for update meaning other calls will block until you return from the transaction. Those will then be able to proceed with their attempt. This is safe, so you can return 201 CREATED from your API
POST
(it's aPOST
because you are asking to create a reservation). - If that transaction fails, it simply means there are not enough slots for that call. You can return a 409 CONFLICT, meaning the client's request is legit, but can't be filled due to the state of the system (not enough slots).
Asynchronous
This is necessarily more complex, but may increase the concurrency of the solution. In this case, you generally accept all the calls from the client and enqueue them to a queue. Again, you could use an in-memory solution like ConcurrentQueue
to serialize the requests, but it would be better to use a distributed solution using a "real" queueing mechanism such as Azure Queues.
In this scenario, every client request immediately gets placed on the queue, and a 202 ACCEPTED is returned by the API. The queue's consumer attempts to fulfill the request. The whole workflow would conform to the Asynchronous Request-Reply pattern.
This requires more work on the server (keeping track of the work and fulfillment) and the client (polling to find out if the work actually succeed, or reacting to a push notification ala SignalR).
Summarizing
I am using Microsoft Azure - should I consider using a queue of sorts?
Yes. This is reasonable and is more optimistic from a concurrency perspective. Going with the "soft-reserve" extends this approach, and is not only reasonable, but common; consider reserving seats on a plane. You get the seats for a small window of time, but not forever. The workflow is more complex, but allows for compensating transactions if the optimistic concurrency fails.
I am also using EF Core for my back-end data if that could make a difference in a way that I cannot think of.
I don't think this will make a difference. It's an abstraction over the database.
Is there a library I can use to do this semaphore-like approach?
Perhaps. Most of this falls into pattern territory meaning you can probably make use of some boilerplate code (I know I've done this a few times!) or a framework that codifies the patterns.