Skip to content

Commit 73c75cb

Browse files
committed
Allow for optimistic locking to be turned on post-production.
Data should be persisted in such a way that it's okay if optimistic locking is turned on later, and shouldn't require a data migration. This will require a migration for postgres, but will give a warning if it hasn't been run. Existing data will need to be migrated - maybe delay this for 2.0?
1 parent a0e438e commit 73c75cb

11 files changed

Lines changed: 76 additions & 14 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# frozen_string_literal: true
2+
class AddDefaultToLockVersion < ActiveRecord::Migration[5.1]
3+
def change
4+
change_column_default :orm_resources, :lock_version, from: nil, to: 0
5+
end
6+
end

lib/valkyrie/persistence/fedora/persister/model_converter.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,15 @@ def properties
3636
resource_attributes.keys - [:new_record]
3737
end
3838

39-
delegate :attributes, to: :resource, prefix: true
39+
# Always generate a token, in case it's enabled later.
40+
def resource_attributes
41+
@resource_attributes ||=
42+
begin
43+
attributes = resource.attributes.dup
44+
attributes[Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK] ||= [Valkyrie::Persistence::OptimisticLockToken.new(adapter_id: adapter.id, token: Time.now.to_r)]
45+
attributes
46+
end
47+
end
4048

4149
# Construct the LDP Basic Container modeling the Valkyrie Resource in Fedora
4250
# @see https://www.w3.org/TR/ldp/#ldpc

lib/valkyrie/persistence/fedora/persister/orm_converter.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def populate_native_lock(resource)
4242
return resource unless lastmod
4343

4444
token = Valkyrie::Persistence::OptimisticLockToken.new(adapter_id: "native-#{adapter.id}", token: DateTime.parse(lastmod.to_s).httpdate)
45-
resource.send(Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK) << token
45+
resource.set_value(Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK, resource[Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK] + [token])
4646
resource
4747
end
4848

lib/valkyrie/persistence/memory/metadata_adapter.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def cache
2727

2828
# @return [Valkyrie::ID] Identifier for this metadata adapter.
2929
def id
30-
@id ||= Valkyrie::ID.new(Digest::MD5.hexdigest(self.class.to_s))
30+
@id ||= Valkyrie::ID.new(Digest::MD5.hexdigest(self.class.to_s + SecureRandom.uuid))
3131
end
3232
end
3333
end

lib/valkyrie/persistence/memory/persister.rb

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,10 @@ def normalize_date_value(value)
8484

8585
# Create a new lock token based on the current timestamp.
8686
def generate_lock_token(resource)
87-
return unless resource.optimistic_locking_enabled?
8887
token = Valkyrie::Persistence::OptimisticLockToken.new(adapter_id: adapter.id, token: Time.now.to_r)
88+
cache[:versions] ||= {}
89+
cache[:versions][resource.id] = token
90+
return unless resource.optimistic_locking_enabled?
8991
resource.set_value(Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK, token)
9092
end
9193

@@ -96,11 +98,12 @@ def valid_lock?(resource)
9698
cached_resource = cache[resource.id]
9799
return true if cached_resource.blank?
98100

99-
resource_lock_tokens = resource[Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK]
101+
resource_lock_tokens = resource[Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK] || []
100102
resource_value = resource_lock_tokens.find { |lock_token| lock_token.adapter_id == adapter.id }
101-
return true if resource_value.blank?
103+
cached_token = cached_resource[Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK] || []
104+
cached_value = cached_token.find { |lock_token| lock_token.adapter_id == adapter.id }
105+
return true if resource_value.nil?
102106

103-
cached_value = cached_resource[Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK].first
104107
cached_value == resource_value
105108
end
106109
end

lib/valkyrie/persistence/memory/query_service.rb

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,14 @@ def initialize(adapter:)
2424
def find_by(id:)
2525
id = Valkyrie::ID.new(id.to_s) if id.is_a?(String)
2626
validate_id(id)
27-
cache[id] || raise(::Valkyrie::Persistence::ObjectNotFoundError)
27+
fetch_from_cache(id) || raise(::Valkyrie::Persistence::ObjectNotFoundError)
28+
end
29+
30+
def fetch_from_cache(id)
31+
result = cache[id]
32+
return result if result.nil? || !result.optimistic_locking_enabled? || result[Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK].present?
33+
result.set_value(Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK, cache[:versions][id])
34+
result
2835
end
2936

3037
# Get a single resource by `alternate_identifier`. Alternate identifiers are identifiers (like NOIDs,
@@ -62,15 +69,17 @@ def find_many_by_ids(ids:)
6269
# Get all objects.
6370
# @return [Array<Valkyrie::Resource>] All objects in the persistence backend.
6471
def find_all
65-
cache.values
72+
cache.except(:versions).values.map do |resource|
73+
fetch_from_cache(resource.id)
74+
end
6675
end
6776

6877
# Get all objects of a given model.
6978
# @param model [Class] Class to query for.
7079
# @return [Array<Valkyrie::Resource>] All objects in the persistence backend
7180
# with the given class.
7281
def find_all_of_model(model:)
73-
cache.values.select do |obj|
82+
find_all.select do |obj|
7483
obj.is_a?(model)
7584
end
7685
end

lib/valkyrie/persistence/postgres/orm_converter.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def resource
3535
# Construct the optimistic lock token using the adapter and lock version for the Resource
3636
# @return [Valkyrie::Persistence::OptimisticLockToken]
3737
def lock_token
38-
return lock_token_warning unless orm_object.class.column_names.include?("lock_version")
38+
return lock_token_warning unless orm_object.class.column_names.include?("lock_version") && orm_object.class.columns.find { |x| x.name == "lock_version" }.default == "0"
3939
@lock_token ||=
4040
Valkyrie::Persistence::OptimisticLockToken.new(
4141
adapter_id: resource_factory.adapter_id,

lib/valkyrie/specs/shared_specs/persister.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,33 @@ class CustomResource < Valkyrie::Resource
313313
expect(query_service.find_all.to_a.length).to eq 0
314314
end
315315

316+
context "optimistic locking enabled later" do
317+
before do
318+
class MyLockingResource < Valkyrie::Resource
319+
attribute :title
320+
end
321+
end
322+
describe "#save" do
323+
it "will error appropriately if optimistic locking is enabled later" do
324+
resource = MyLockingResource.new
325+
saved_resource = persister.save(resource: resource)
326+
327+
MyLockingResource.enable_optimistic_locking
328+
329+
saved_resource = query_service.find_by(id: saved_resource.id)
330+
reloaded = query_service.find_by(id: saved_resource.id)
331+
saved_resource.title = "Don't Save Me"
332+
reloaded.title = "Save Me"
333+
334+
persister.save(resource: reloaded)
335+
expect { persister.save(resource: saved_resource) }.to raise_error Valkyrie::Persistence::StaleObjectError
336+
end
337+
end
338+
after do
339+
Object.send(:remove_const, :MyLockingResource)
340+
end
341+
end
342+
316343
context "optimistic locking" do
317344
before do
318345
class MyLockingResource < Valkyrie::Resource

spec/spec_helper.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
WebMock.disable_net_connect!(allow_localhost: true)
2525

2626
RSpec.configure do |config|
27+
config.example_status_persistence_file_path = "tmp/rspec_examples.txt"
2728
config.order = :random
2829
Kernel.srand config.seed
2930

spec/valkyrie/persistence/memory/metadata_adapter_spec.rb

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@
77
it_behaves_like "a Valkyrie::MetadataAdapter"
88

99
describe "#id" do
10-
it "creates an md5 hash from the class name" do
11-
expected = Digest::MD5.hexdigest described_class.to_s
12-
expect(adapter.id.to_s).to eq expected
10+
it "creates a unique identifier for each instance" do
11+
expect(adapter.id.to_s).not_to eq described_class.new.id.to_s
1312
end
1413
end
1514
end

0 commit comments

Comments
 (0)