longevity

A Persistence Framework for Scala and NoSQL

View project on GitHub

optimistic locking

TLDR: Add longevity.optimisticLocking = true to your config.

Optimistic locking, or optimistic concurrency control, is a technique used to prevent writes from overwriting changes from a write issued on another thread. Suppose User has a method for setting the title that disallows overwriting an existing title:

import longevity.model.annotations.persistent

@persistent(keySet = Set(key(props.username)))
case class User(
  username: Username,
  title: Option[String]) {

  def addTitle(newTitle: String): User = {
    if (title.nonEmpty) throw new ValidationException
    copy(title = Some(newTitle)
  }
}

Now let’s suppose one thread is updating the user to have title "Ms.":

// thread A:
for {
  retrieved <- userRepo.retrieve(username)
  modified = retrieved.map(_.addTitle("Ms."))
  updated <- userRepo.update(modified)
} yield updated

and another thread is updating the title to "Dr.":

// thread B:
for {
  retrieved <- userRepo.retrieve(username)
  modified = retrieved.map(_.addTitle("Dr."))
  updated <- userRepo.update(modified)
} yield updated

If both of the calls to Repo.retrieve happen before the calls to Repo.update, then we have a conflict. Without any sort of locking, whichever update call happens last “wins”, and the title from other thread will be lost. With optimistic locking, the second update will be kicked out with a longevity.exceptions.persistence.WriteConflictException. The title from the first update will prevail, and the other thread will have a chance to respond to the WriteConflictException, such as by retrying the operation, or returning a 409 Conflict HTTP code.

In order to turn on optimistic locking in longevity, set the following configuration variable:

longevity.optimisticLocking = true

Turning on optimistic locking will introduce a slight, but probably negligible, performance penalty. For details on a specific back end, please see the appropriate section in the chapter on database translation. You do not need to know how optimistic locking works in order to use it, but we describe our implementation briefly here for those who are interested.

We implement optimistic locking by storing a rowVersion value in every database row. Every create or update operation will increment the rowVersion by one.

The persistent state keeps track of the value of rowVersion for the object it encloses. The repository methods all return PStates with the rowVersion matching what is in the database at that moment. PState methods set and map preserve the rowVersion.

When Repo methods update or delete are called, the repository qualifies the database write command that gets issued. The command will only make a database change when the rowVersion in the database matches the rowVersion in the PState. The repository method checks the return value from the database to confirm the change was made. If it was not, then there was a write conflict, and a WriteConflictException is thrown.

prev: configuring your longevity context
up: the longevity context
next: write timestamps