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
case class User(username: Username, title: Option[String]) {
def addTitle(newTitle: String): User = {
if (title.nonEmpty) throw new ValidationException
copy(title = Some(newTitle)
}
}
object User {
implicit val usernameKey = key(props.username)
}
Now let’s suppose one thread is updating the user to have title "Ms."
:
// thread A:
for {
retrieved <- repo.retrieve[User](username)
modified = retrieved.modify(_.addTitle("Ms."))
updated <- repo.update(modified)
} yield updated
and another thread is updating the title to "Dr."
:
// thread B:
for {
retrieved <- repo.retrieve[User](username)
modified = retrieved.modify(_.addTitle("Dr."))
updated <- repo.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 modify
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.