From a68360f9e4bbb998e21cf6fc90867fc2f2eab9fa Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Thu, 30 Dec 2021 13:43:42 -0800 Subject: [PATCH 1/2] Add failing test doing list manipulations inside a transaction on the default repository implementation. This works with serialized transactions. --- spec/repository_spec.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/spec/repository_spec.rb b/spec/repository_spec.rb index 2e15bea5..f3363a0a 100644 --- a/spec/repository_spec.rb +++ b/spec/repository_spec.rb @@ -62,4 +62,20 @@ expect(subject.statements.first).to eq existing_statement expect(subject.statements.first).to be_inferred end + + it "performs coherent list updates" do + # List updates require reading from the repository mid-transaction, which requires a SerializedTransaction + repo = RDF::Repository.new + lh = RDF::Node.new("o") + repo << RDF::Statement.new(RDF::URI('s'), RDF::URI('p'), lh) + expect(repo.count).to eq 1 + repo.transaction(mutable: true) do |tx| + list = RDF::List.new(subject: lh, graph: tx, values: %w(a b c)) + expect(tx.count).to eq 7 + list[0, 2] = %(d) + expect(tx.count).to eq 5 + end + expect(repo.count).to eq 5 + + end end From 0f69e30a6b8bd15c17b7a78f8adb36448107f438 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Thu, 30 Dec 2021 13:43:52 -0800 Subject: [PATCH 2/2] Add back SerializedTransaction as a sub-class of Transaction, and use in default Repository implementation. --- VERSION | 2 +- lib/rdf/repository.rb | 2 +- lib/rdf/transaction.rb | 76 +++++++++++++++++++++++++++++++++++++++- spec/transaction_spec.rb | 7 ++++ 4 files changed, 84 insertions(+), 3 deletions(-) diff --git a/VERSION b/VERSION index 944880fa..e4604e3a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.2.0 +3.2.1 diff --git a/lib/rdf/repository.rb b/lib/rdf/repository.rb index 69d49b47..05b09931 100644 --- a/lib/rdf/repository.rb +++ b/lib/rdf/repository.rb @@ -252,7 +252,7 @@ def self.extend_object(obj) Hash.new) obj.instance_variable_set(:@tx_class, obj.options.delete(:transaction_class) || - DEFAULT_TX_CLASS) + RDF::Transaction::SerializedTransaction) super end diff --git a/lib/rdf/transaction.rb b/lib/rdf/transaction.rb index 6c66f209..02b7988c 100644 --- a/lib/rdf/transaction.rb +++ b/lib/rdf/transaction.rb @@ -322,7 +322,81 @@ def read_target end public - + + ## + # A transaction with full serializability. + # + # @todo refactor me! + # @see RDF::Transaction + class SerializedTransaction < Transaction + ## + # @see Transaction#initialize + def initialize(*args, **options, &block) + super(*args, **options, &block) + @base_snapshot = @snapshot + end + + ## + # Inserts the statement to the transaction's working snapshot. + # + # @see Transaction#insert_statement + def insert_statement(statement) + @snapshot = @snapshot.class + .new(data: @snapshot.send(:insert_to, + @snapshot.send(:data), + process_statement(statement))) + end + + ## + # Deletes the statement from the transaction's working snapshot. + # + # @see Transaction#insert_statement + def delete_statement(statement) + @snapshot = @snapshot.class + .new(data: @snapshot.send(:delete_from, + @snapshot.send(:data), + process_statement(statement))) + end + + ## + # @see RDF::Dataset#isolation_level + def isolation_level + :serializable + end + + ## + # @note this is a simple object equality check. + # + # @see RDF::Transaction#mutated? + def mutated? + !@snapshot.send(:data).equal?(repository.send(:data)) + end + + ## + # Replaces repository data with the transaction's snapshot in a safely + # serializable fashion. + # + # @note this transaction uses a pessimistic merge strategy which + # fails the transaction if any data has changed in the repository + # since transaction start time. However, the specific guarantee is + # softer: multiple concurrent conflicting transactions will not + # succeed. We may choose to implement a less pessimistic merge + # strategy as a non-breaking change. + # + # @raise [TransactionError] when the transaction can't be merged. + # @see Transaction#execute + def execute + raise TransactionError, 'Cannot execute a rolled back transaction. ' \ + 'Open a new one instead.' if instance_variable_defined?(:@rolledback) && @rolledback + + raise TransactionError, 'Error merging transaction. Repository' \ + 'has changed during transaction time.' unless + repository.send(:data).equal? @base_snapshot.send(:data) + + repository.send(:data=, @snapshot.send(:data)) + end + end # SerializedTransaction + ## # An error class for transaction failures. # diff --git a/spec/transaction_spec.rb b/spec/transaction_spec.rb index ecd5c5c9..51220627 100644 --- a/spec/transaction_spec.rb +++ b/spec/transaction_spec.rb @@ -76,3 +76,10 @@ class MyTx < described_class; end end end end + +describe RDF::Transaction::SerializedTransaction do + let(:repository) { RDF::Repository.new } + + # @see lib/rdf/spec/transaction.rb in rdf-spec + it_behaves_like "an RDF::Transaction", RDF::Transaction::SerializedTransaction +end