Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fixes #1705 by locking node while Assigning rel #1706

Closed
wants to merge 7 commits into from

Conversation

mrhardikjoshi
Copy link
Member

Fixes #1705

This pull introduces/changes:

  • makes #replace_with method safe for concurrent use by locking node before read/update

@@ -62,12 +63,13 @@ def add_rels(node_or_nodes, original_ids)
end

def delete_rels_for_nodes(original_ids, new_ids)
ids = original_ids.select { |id| !new_ids.include?(id) }
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using - is faster compared to using select loop and then checking with include? again

@codecov-commenter
Copy link

codecov-commenter commented Dec 5, 2023

Codecov Report

All modified and coverable lines are covered by tests ✅

Comparison is base (3c89aef) 93.43% compared to head (9b6b215) 93.26%.

❗ Current head 9b6b215 differs from pull request most recent head ea53581. Consider uploading reports for the commit ea53581 to get more accurate results

❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1706      +/-   ##
==========================================
- Coverage   93.43%   93.26%   -0.18%     
==========================================
  Files         128      128              
  Lines        5909     5923      +14     
==========================================
+ Hits         5521     5524       +3     
- Misses        388      399      +11     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

lib/active_graph/transactions.rb Outdated Show resolved Hide resolved
@@ -28,6 +28,10 @@ def read_transaction(**config, &block)

alias transaction write_transaction

def lock_node(node)
node.as(:n).query.remove('n._LOCK_').exec if tx&.open? || explicit_session&.open?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be good to ensure that your specs fail with this code:

Suggested change
node.as(:n).query.remove('n._LOCK_').exec if tx&.open? || explicit_session&.open?
node.as(:n).query.exec if tx&.open? || explicit_session&.open?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is the tx boundary set in specs? What tx do new threads use?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The specs are too complex to be confident without failing version.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with disabling my code in lock_node I get following error so we now have a simpler specs with proper failure without fix.

  1) ActiveGraph::Relationship::Persistence::QueryFactory#build! non-subclassed concurrent update does not create duplicate has_one relationship
     Failure/Error: expect(ActiveGraph::Base.query("MATCH (node2:`ToClass`)<-[rel1:`HAS_REL`]-(from_class:`FromClass`) return from_class").to_a.size).to eq(1)
     
       expected: 1
            got: 19
     
       (compared using ==)
     # ./spec/e2e/relationship/persistence/query_factory_spec.rb:126:in `block (5 levels) in <top (required)>'
     ```

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@klobuczek every thread has unique tx, I checked their object_id.

@@ -28,6 +28,10 @@ def read_transaction(**config, &block)

alias transaction write_transaction

def lock_node(node)
node.as(:n).query.remove('n._AGLOCK_').exec if tx&.open? || explicit_session&.open?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why the condition? No harm would happen if the condition is not met.

before do
allow_any_instance_of(ActiveGraph::Node::Query::QueryProxy).to receive(:replace_with).and_wrap_original do |original, *args|
$concurrency_queue << 'ready'
Thread.stop
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the right thing would be to stop at delete_rels_for_nodes after the rels are read so you can force failure with just 2 threads not 100. And then after adding lock see it passing.

Comment on lines 106 to 128
context 'concurrent update' do
before do
allow_any_instance_of(ActiveGraph::Node::Query::QueryProxy).to receive(:replace_with).and_wrap_original do |original, *args|
$concurrency_queue << 'ready'
Thread.stop
original.call(*args)
end
end
after { $concurrency_queue = nil }
let!(:from_node) { FromClass.create(name: 'foo') }
let!(:to_node) { ToClass.create(name: 'bar') }

it 'does not create duplicate has_one relationship' do
count = 100
$concurrency_queue = Thread::Queue.new
threads = count.times.map { Thread.new { to_node.update(from_class: from_node) } }
sleep(0.1) until $concurrency_queue.size == count
$concurrency_queue.clear
threads.each(&:run)
threads.each(&:join)
expect(ActiveGraph::Base.query("MATCH (node2:`ToClass`)<-[rel1:`HAS_REL`]-(from_class:`FromClass`) return from_class").to_a.size).to eq(1)
end
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not thrilled about this spec. sleep, 100 threads, no guarantee of failure without lock, low level api. I think this would be possible async.

@klobuczek
Copy link
Member

closing as already merged to zeitwerk branch

@klobuczek klobuczek closed this Dec 29, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

has_one association allows creation of two association when done concurrently
3 participants