-
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. 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).
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")
# 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