From 4695f70a4761d953cc0f3a3758644b17e9886c2a Mon Sep 17 00:00:00 2001 From: Carolyn Cole Date: Fri, 3 Aug 2018 10:07:57 -0400 Subject: [PATCH] Adding optimistic locking to the memory persister fixes #455 Co-authored-by: Mike Tribone Co-authored-by: Anna Headley Co-authored-by: Adam Wead --- lib/valkyrie/persistence/memory/persister.rb | 39 ++++++++++++++++--- .../persistence/memory/persister_spec.rb | 2 + .../persistence/memory/query_service_spec.rb | 2 + 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/lib/valkyrie/persistence/memory/persister.rb b/lib/valkyrie/persistence/memory/persister.rb index 9004d94df..01e228e07 100644 --- a/lib/valkyrie/persistence/memory/persister.rb +++ b/lib/valkyrie/persistence/memory/persister.rb @@ -16,18 +16,25 @@ def initialize(adapter) # @return [Valkyrie::Resource] The resource with an `#id` value generated by the # persistence backend. def save(resource:) - resource = generate_id(resource) if resource.id.blank? - resource.created_at ||= Time.current - resource.updated_at = Time.current - resource.new_record = false - normalize_dates!(resource) - cache[resource.id] = resource + raise Valkyrie::Persistence::StaleObjectError, resource.id unless valid_lock?(resource) + + # duplicate the resource so we are not creating side effects on the caller's resource + internal_resource = resource.dup + + internal_resource = generate_id(internal_resource) if internal_resource.id.blank? + internal_resource.created_at ||= Time.current + internal_resource.updated_at = Time.current + internal_resource.new_record = false + generate_lock_token(internal_resource) + normalize_dates!(internal_resource) + cache[internal_resource.id] = internal_resource end # @param resources [Array] List of resources to save. # @return [Array] List of resources with an `#id` value # generated by the persistence backend. def save_all(resources:) + raise Valkyrie::Persistence::StaleObjectError, resources.map(&:id).join(', ') unless resources.reject { |resource| valid_lock?(resource) }.blank? resources.map do |resource| save(resource: resource) end @@ -64,5 +71,25 @@ def normalize_date_value(value) return value.to_datetime.utc if value.is_a?(Time) value end + + def generate_lock_token(resource) + return unless resource.optimistic_locking_enabled? + token = Valkyrie::Persistence::OptimisticLockToken.new(adapter_id: adapter.id, token: Time.now.to_r) + resource.send("#{Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK}=", token) + end + + def valid_lock?(resource) + return true unless resource.optimistic_locking_enabled? + + cached_resource = cache[resource.id] + return true if cached_resource.blank? + + resource_lock_tokens = resource[Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK] + resource_value = resource_lock_tokens.find { |lock_token| lock_token.adapter_id == adapter.id } + return true if resource_value.blank? + + cached_value = cached_resource[Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK].first + cached_value == resource_value + end end end diff --git a/spec/valkyrie/persistence/memory/persister_spec.rb b/spec/valkyrie/persistence/memory/persister_spec.rb index a1247e174..e40ef47f3 100644 --- a/spec/valkyrie/persistence/memory/persister_spec.rb +++ b/spec/valkyrie/persistence/memory/persister_spec.rb @@ -1,10 +1,12 @@ # frozen_string_literal: true require 'spec_helper' require 'valkyrie/specs/shared_specs' +require 'valkyrie/specs/shared_specs/locking_persister' RSpec.describe Valkyrie::Persistence::Memory::Persister do let(:adapter) { Valkyrie::Persistence::Memory::MetadataAdapter.new } let(:query_service) { adapter.query_service } let(:persister) { adapter.persister } it_behaves_like "a Valkyrie::Persister" + it_behaves_like "a Valkyrie locking persister" end diff --git a/spec/valkyrie/persistence/memory/query_service_spec.rb b/spec/valkyrie/persistence/memory/query_service_spec.rb index 8e2b8d4b8..2af6b6962 100644 --- a/spec/valkyrie/persistence/memory/query_service_spec.rb +++ b/spec/valkyrie/persistence/memory/query_service_spec.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true require 'spec_helper' require 'valkyrie/specs/shared_specs' +require 'valkyrie/specs/shared_specs/locking_query' RSpec.describe Valkyrie::Persistence::Memory::QueryService do let(:adapter) { Valkyrie::Persistence::Memory::MetadataAdapter.new } it_behaves_like "a Valkyrie query provider" + it_behaves_like "a Valkyrie locking query provider" end