How to "lock" an route per user?

Hello,

What is the recommended way to “lock” per user some routes in Phoenix?

For example, let’s say I have a route that is used to subscribe a user to a subscription plan.

Since I only want the user to subscribe once (any other call to the same route would be recognized as a “change plan” call and not a subscribe one), I need to avoid race conditions when more than one call to the same route is being made at the same time.

The solution to me seems something like a “mutex”, the first call blocks the function until it is done. But I’m not sure how to implement this based on the user (since I want to block the call for the same user only).

A workaround to that is maybe to create an agent with a process name unique for that user and function to run the logic inside so any other call would fail since the agent creation would fail with an already exist error.

Do you have any other suggestions and good practices to handle this kind of scenario?

Thanks.

Instead of forcing the user to find the one in a million tabs that has the URL open already I just let them do their thing in this new window. Once they have submitted, I won’t allow them to create a new subscription due to database constraints from the other tabs, if their csrf token is still valid anyway…

1 Like

That would be my go-to solution, yes, but there is a problem that I forgot to add to my example that would make that solution not work.

Basically, this call doesn’t only do database updates atomically, it also run some external API queries for third-party servers.

So, for example, following the example I did before, to create a valid subscription to a user, I would first call a REST query for a third party payment system asking them to create the subscription and charge it, the result would be the subscription_id which I would store in my database and after that not allow it to change via database constrains.

But there is a small window for a race condition in this case between the REST call and the database write. In a scenario that a user called the same API at the same time, it could be possible for it to call the REST call twice creating 2 credit card charges for 2 subscriptions when I only can allow 1.

This is why I need to lock the whole process, so I can guarantee that this call will not cause this kind of race-conditions

Couldn’t you just mark the transaction as ‘Pending’ before doing the API call? That way you could check if a transaction is already in process and just don’t call the API again. Also, not sure which payment API you are using but it seems like Stripe has some idempotency ‘tools’ that you could use so the payment isn’t made twice.

1 Like

You mean write into the database a flag “pending” for that user and then remove it when the call is done?

The payment system I’m using is BlueSnap, I didn’t saw anything similar to the Stripe idempotency feature in their documentation.

Something along those lines, not sure if your database structure allows you to do that but it could be one way to solve it. You’d have to save the state of the subscription somewhere, the db seems like a good place. Keep in mind that I’m just trying to help you out with some ideas, I can’t be absolutely sure that this will work for you.

Yeah, that is actually my current solution.

I created a SubscriptionTransaction table that is uniquely related to a user, so the first thing I do is create a new row to that table, and when I’m done I just delete it.

The issue that I see with that approach is that I need to treat all possible errors (process crash, exception, etc) to make sure that I always call the SubscriptionTransaction delete function at the end because if not, all other calls will simply fail or be deadlocked.

That’s why I was trying to find another way or simply a better way to do that.

I will try the Agent solution I talked about in the first post, it seems like a better solution since it allows the function to be serialized and at the same time I will not have to worry about cleanup.

I think I found a good solution, there is a very nice library called Mutex, with it I could do the same thing I wanted with the Agent but cleaner

1 Like

I’ve just updated mutex because of some bug. You should use {:mutex, "~> 1.3"} in your dependencies.

2 Likes