Skip to content
Advertisement

waitlist in postgres, deadlock

I’m trying to create a waiting list in Postgres. Minimal code:

status is important, because I want to be able to notify users when they are applied after waitlisted. But don’t want to notify them based on that they are in the first n position if they wasn’t waitlisted at all. Services are limited to a given number of users. There are multiple choices how the system decides which user gets the service. The simplest is first come first served. That’s the reason it has two phases (but it shouldn’t change the use case in any way).

Possible requests are:

  1. Insert: a user applies/enrolls for a service
    1. Inserting (user_id, service_id, CURRENT TIMESTAMP, state: 0 (new))
    2. a Updating state to 1 (waitlisted) or 2 (applied) based on the COUNT(*) with the same service_id
  2. Remove: a user cancels his/her application for a service
    1. Removing the given application
    2. If there’s someone in waitlisted state, move to applied, and send notification about it

My first try was a naive implementation. Let’s say service limit is 10.

1/2 OnAdd:

2/2 OnRemove:

and then ( I know they could be joined)

It worked for a sequential test, but the multi-threading test showed cases when there were more than 10 rows with applied state.

So I put them in a transaction started with SET TRANSACTION ISOLATION LEVEL SERIALIZABLE, then REPEATABLE READ, they gave me a lot of ERROR #40001 could not serialize access due to concurrent update. Then the same with READ COMMITTED. It was way much better, than the naive version, but it ended in over-applications as well.

Then I started using FOR NO KEY UPDATE in selects instead, but they always ended in a deadlock very soon. I searched a lot on deadlocks, but couldn’t find anything useful.

So I came up with a version, where OnAdd and OnRemove had very similar queries, only differing in selecting user_id, where I didn’t add FOR UPDATE. I had to change 1/1 Insert so the default state is waitlisted.

OnAdd:

OnRemove:

But in the multi-threading test it ended up in a deadlock too.

Edit:

As melcher suggested below, I added a column instead of counting. Not to a separate table, but inside services.

I added a transaction to OnAdd and OnRemove that starts with

Sometimes in the multithreading test it under-applied. So I joined Remove to be in the same transaction with OnRemove, and that works finally.

Advertisement

Answer

Based on my understanding of what you’re trying to do and how databases work – you’re going to need a shared resource that gets locked OnAdd.

The reason is that two threads that are both trying to ‘add’ at the same time must compete for a shared resource so that only one of them wins and the other errors / fails. You cannot accomplish your goal using a count of rows.

One solution would be a locks table:

And then:

  1. Open transaction (if outside of a procedure)
  2. Acquire an exclusive/write lock to services row in the lock table
  3. IF the limit/constraint is satisfied (e.g. applied_count < 10) then…
  4. Mark the “applies” as applied UPDATE applies SET status = 2
  5. Update lock table (e.g. SET applied_count = applied_count + 1)
  6. Commit transaction
User contributions licensed under: CC BY-SA
6 People found this is helpful
Advertisement