Skip to content

Optimistic Locking

hackmastera edited this page Aug 6, 2018 · 12 revisions

Optimistic locking is a strategy for ensuring that data in systems being frequently updated are not lost or corrupted during concurrent updates. Using a relational database in an example, two clients or processes could attempt to simultaneously update the same record within the same span of time (less than milliseconds apart). Should the first process to complete the update do so after the second process has read the record into memory, the record data in the second process is now out of date. The second process would then be modifying old data, and committing the second update would result losing the data saved by the first update. Locking ensures that this situation does not occur. A more straightforward (but less performant) approach would be to simply ensure that only one process can modify a given record at any point in time. This approach is considered to be pessimistic, and is usually supported at the layer of the database management system in which the row for the logical record is actually locked (please see https://www.postgresql.org/docs/current/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE and https://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html). The approach considered to be optimistic is one in which multiple, concurrent processes can access the same record simultaneously, but with the added step of ensuring that the record was not updated. A process would initially read the record, prepare the update, and before committing this to the system, perform a second (and final) read operation in order to determine whether or not the record has changed after the initial read. If it has been, the update is rolled back (aborted).

Optimistic Locking on Valkyrie Resources

The Valkyrie::Resource Class has the method .enable_optimistic_locking which may be used when modeling one's repository resources:

class MyLockingResource < Valkyrie::Resource
  enable_optimistic_locking
  attribute :id, Valkyrie::Types::ID.optional
  attribute :title, Valkyrie::Types::Set
end

Given the structure of Valkyrie resources serialized within the persistence layer, optimistic locking is supported exclusively at the level of the entire resource (hence, there is only one record for any given resource when persisting to a relational database). By default, should any process attempt to update a resource which has been changed (and invalidated) before the update has been committed, Valkyrie::Persistence::StaleObjectError shall be raised during the operation. These updates are performed (as they have been) using ChangeSets:

resource = MyLockingResource.new(title: "test title 1")

change_set = MyChangeSet.new(resource)
change_set.validate(title: "test title 2")
change_set.sync

change_set_persister = ChangeSetPersister.new(
  metadata_adapter: Valkyrie.config.metadata_adapter,
  storage_adapter: Valkyrie.config.storage_adapter
)
updated_resource = change_set_persister.save(change_set: change_set)

second_change_set = MyChangeSet.new(resource)
second_change_set.validate(title: "test title 3")
second_change_set.sync

twice_updated_resource = change_set_persister.save(change_set: second_change_set)

where, if at any point an external process introduces conflicting updates, an error referencing the ID of the resource will be raised: Valkyrie::Persistence::StaleObjectError: 3a38c370-56fb-44ee-96ef-b241660b5d8f.

Supported Metadata Persistence Adapters

Currently, the following adapters support optimistic locking:

  • PostgreSQL
  • Solr
  • Fedora
  • Memory (single thread only)