Search code examples
postgresqlscalaslick

Avoiding race condition in postgres updates using slick


case class Item(id: String, count: Int).  

class ItemRepo(db: Database) {
  val query = TableQuery[ItemTable]


def updateAmount(id: String, incCount :Int) = {
   val currentRow = db.run(query.filter(_.id === id).result).head
   val updatedRow =  Item(currentRow.id, currentRow.count + incCount)
   db.run((query returning query).insertOrUpdate(updatedRow))
}

The code above has a race condition - if two threads run this in parallel they might both read the same count, and only the last updating thread will increment their incCount.

How can i avoid this case? I tried using .forUpdate in the line that does query.filter but it doesn't block the other thread. am i missing something?


Solution

  • There are a few tricks you can use to improve this situation.

    First, you're sending two independent queries to the database (two db.run calls). You can improve that by composing them into a single action and send that to the database. For example:

    // Danger: I've not tried to compile this. Please excuse typos.
    
    val lookupAction = query.filter(_.id === id).result
    
    
    val updateAction = lookupAction.flatMap { matchingRows =>
       val newItem = matchingRows.headOption match {
          case Some(Item(_, count)) => Item(id, count + incCount)
          case None => Item(id, 1) // or whatever your default is 
       }
       (query returning query).insertOrUpdate(newItem)
    }
    
    // and you db.run(updateAction.transactionally)
    
    

    That will get you some way, depending on the transaction guarantees for your database. I mention it because combining actions in Slick is an important concept. With that, your forUpdate (that Laurenz Albe noted) may behave as expected.

    However, you may prefer to send an update to the database. You'd need to do this using Slick's Plain SQL feature:

    val action = sqlu"UPDATE items SET count = count + $incCount WHERE id = $id"
    // And then you db.run(action)
    

    ...and allow you database to handle the concurrency (subject to database isolation levels).

    If you really want to do this all client-side, in Scala code on the JVM, there are concurrency concepts such as locks, actors, and refs. There's nothing in Slick itself to do the JVM locking for you.