-
Notifications
You must be signed in to change notification settings - Fork 20
Optimistic Locking
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.
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.
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)
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.
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
- 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.