Skip to content

Optimistic Locking

Trey Pendragon edited this page Jun 26, 2020 · 12 revisions

Optimistic locking is a strategy for ensuring that data in systems being frequently updated are not lost or corrupted during concurrent updates. For example, one client could read a record and spend time making local updates. Meanwhile, a second client could read the same record and save an update. If the first client saves its changes, the second client's change would be lost. Locking ensures that this situation does not occur.

Pessimistic locking is a straightforward approach that ensures that only one client can modify a record at any given time. This approach is usually supported by databases which lock the row for the record and block other clients (see docs from Postgres and MySQL). But pessimistic locking has the disadvantage of blocking updates and locking users out of records.

Optimistic locking aims to address these problems by allowing multiple clients to update the same record simultaneously, but checking that each client has the current version of the record before applying their updates, to make sure they are not unintentionally overwriting an update they haven't seen.

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.

Optimistic locks in change sets

In general updates to Valkyrie resources are performed using ChangeSets. Once locking is enabled on a resource, you should add the lock field to your changesets for the resource:

property :optimistic_lock_token, multiple: true, required: true,
  type: Valkyrie::Types::Set.of(Valkyrie::Types::OptimisticLockToken)

Including a lock in a form

To enforce locking when users are editing a resource, the form must be submitted with the lock the resource had when it was retrieved. Add the lock into your form as a hidden field. For example, with plain rails:

<%= form.hidden_field :optimistic_lock_token, multiple: true, value: form.resource.optimistic_lock_token %>

if you use simple_form:

<%= f.input :optimistic_lock_token, as: :hidden, input_html: { value: f.object.optimistic_lock_token, multiple: f.object.multiple?(key) } %>

Valkyrie's use of Dry::Types will take care of casting the lock token objects to/from strings when it is sent to the form and retrieved from the form params.

Example functionality

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.

# Create a resource
resource = MyLockingResource.new(title: "test title 1")
resource = Valkyrie.config.metadata_adapter.persister.save(resource: resource)

# Create a change set for the resource
change_set = MyChangeSet.new(resource)
change_set.validate(title: "test title 2")
change_set.sync

# Save the resource
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)

# Create a new change set from the now-stale resource
second_change_set = MyChangeSet.new(resource)
second_change_set.validate(title: "test title 3")
second_change_set.sync

# attempt to change persist the new change set
twice_updated_resource = change_set_persister.save(change_set: second_change_set)
# => Valkyrie::Persistence::StaleObjectError

Implementation Notes

  • Minimizing the time between loading an object and modifying it will reduce the possibility of Valkyrie::Persistence::StaleObjectError happening.
  • One way to gracefully recover from Valkyrie::Persistence::StaleObjectError is to rescue the error, reload the resource being updated, and try to apply the updates to the resource.
  • To avoid conflicts between metadata updates (typically user-initiated) and file-processing updates (often done in background jobs), some implementations use a FileSet resource (attached as members to the metadata resource) to hold metadata about files associated with a resource.