From e060bc2b1afa82ae8ae3d2f2ca74eca6a1e6eb5f Mon Sep 17 00:00:00 2001 From: Hardik Joshi Date: Thu, 30 Nov 2023 20:15:25 +0530 Subject: [PATCH 1/7] rel concurrency issue proving test cases --- .../query_proxy_methods_of_mass_updating.rb | 4 ++ .../persistence/query_factory_spec.rb | 53 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/lib/active_graph/node/query/query_proxy_methods_of_mass_updating.rb b/lib/active_graph/node/query/query_proxy_methods_of_mass_updating.rb index e3d1ae71a..e5c5f7e8f 100644 --- a/lib/active_graph/node/query/query_proxy_methods_of_mass_updating.rb +++ b/lib/active_graph/node/query/query_proxy_methods_of_mass_updating.rb @@ -51,6 +51,10 @@ def delete_all_rels def replace_with(node_or_nodes) node_or_nodes = Array(node_or_nodes).map { |arg| arg.is_a?(ActiveGraph::Node) ? arg : @model.find(arg) } original_ids = self.pluck(:id) + $concurrency_queue << 'ready' if $concurrency_test # completed read above so send signal in the queue + while $concurrency_test && $concurrency_test_wait # wait till other thread has also completed its read + sleep 1 + end delete_rels_for_nodes(original_ids, node_or_nodes.collect(&:id)) add_rels(node_or_nodes, original_ids) end diff --git a/spec/e2e/relationship/persistence/query_factory_spec.rb b/spec/e2e/relationship/persistence/query_factory_spec.rb index ac461bab0..49de7cb92 100644 --- a/spec/e2e/relationship/persistence/query_factory_spec.rb +++ b/spec/e2e/relationship/persistence/query_factory_spec.rb @@ -103,6 +103,59 @@ def self.count expect(from_node.reload.to_classes).to be_empty end + it 'does not create duplicate has_one relationship' do + from_node.save + to_node.save + begin + $concurrency_test = true + $concurrency_queue = Queue.new + $concurrency_test_wait = true + t1 = Thread.new { to_node.update(from_class: from_node) } + t2 = Thread.new { to_node.update(from_class: from_node) } + while $concurrency_queue.size < 2 + # wait till both thread have complted read query + sleep 1 + end + $concurrency_test_wait = false # make threads resume their work + [t1, t2].join # wait for the threads to finish + + # to_node.from_class only returns 1 result but in db there are two rels associated with this node + # this assertion fails with with result 2 proving the presence of bug + expect(ActiveGraph::Base.query("MATCH (node2:`ToClass`)<-[rel1:`HAS_REL`]-(from_class:`FromClass`) return from_class").to_a.size).to eq(1) + ensure + $concurrency_test = nil + $concurrency_test_wait = nil + $concurrency_queue = nil + end + end + + it 'does not create two rels with different nodes in has_one relationship' do + from_node.save + to_node.save + from_node_two = FromClass.create(name: 'foo-2') + begin + $concurrency_test = true + $concurrency_queue = Queue.new + $concurrency_test_wait = true + t1 = Thread.new { to_node.update(from_class: from_node) } + t2 = Thread.new { to_node.update(from_class: from_node_two) } + while $concurrency_queue.size < 2 + # wait till both thread have complted read query + sleep 1 + end + $concurrency_test_wait = false # make threads resume their work + [t1, t2].join # wait for the threads to finish + + # to_node.from_class only returns 1 result but in db there are two rels associated with this node + # this assertion fails with with result 2 proving the presence of bug + expect(ActiveGraph::Base.query("MATCH (node2:`ToClass`)<-[rel1:`HAS_REL`]-(from_class:`FromClass`) return from_class").to_a.size).to eq(1) + ensure + $concurrency_test = nil + $concurrency_test_wait = nil + $concurrency_queue = nil + end + end + it 'delets has_one rel from from_node when new relation is created' do to_node_two = ToClass.new(name: 'bar-2') Rel2Class.new(from_node: from_node, to_node: to_node, score: 10).save From 772525bfcb376b1e7b64801c0f6b3b105ca4496f Mon Sep 17 00:00:00 2001 From: Hardik Joshi Date: Tue, 5 Dec 2023 10:32:02 +0530 Subject: [PATCH 2/7] fixes #1705 --- .../query_proxy_methods_of_mass_updating.rb | 14 ++-- lib/active_graph/transactions.rb | 4 + .../persistence/query_factory_spec.rb | 79 +++++++++---------- 3 files changed, 49 insertions(+), 48 deletions(-) diff --git a/lib/active_graph/node/query/query_proxy_methods_of_mass_updating.rb b/lib/active_graph/node/query/query_proxy_methods_of_mass_updating.rb index e5c5f7e8f..14065a73f 100644 --- a/lib/active_graph/node/query/query_proxy_methods_of_mass_updating.rb +++ b/lib/active_graph/node/query/query_proxy_methods_of_mass_updating.rb @@ -50,11 +50,8 @@ def delete_all_rels # Executed in the database, callbacks will not be run. def replace_with(node_or_nodes) node_or_nodes = Array(node_or_nodes).map { |arg| arg.is_a?(ActiveGraph::Node) ? arg : @model.find(arg) } + ActiveGraph::Base.lock_node(start_object) unless start_object.new_record? original_ids = self.pluck(:id) - $concurrency_queue << 'ready' if $concurrency_test # completed read above so send signal in the queue - while $concurrency_test && $concurrency_test_wait # wait till other thread has also completed its read - sleep 1 - end delete_rels_for_nodes(original_ids, node_or_nodes.collect(&:id)) add_rels(node_or_nodes, original_ids) end @@ -66,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) } - return unless ids.present? + ids_to_be_removed = original_ids - new_ids + return unless ids_to_be_removed.present? + if association.dependent - start_object.public_send("dependent_#{association.dependent}_callback", association, ids) + start_object.public_send("dependent_#{association.dependent}_callback", association, ids_to_be_removed) else - self.where(id: ids).delete_all_rels + self.where(id: ids_to_be_removed).delete_all_rels end end diff --git a/lib/active_graph/transactions.rb b/lib/active_graph/transactions.rb index 44f01cf2e..5ad72f384 100644 --- a/lib/active_graph/transactions.rb +++ b/lib/active_graph/transactions.rb @@ -28,6 +28,10 @@ def read_transaction(**config, &block) alias transaction write_transaction + def lock_node(node) + node.as(:n).query.set('n._LOCK_ = null').exec if tx&.open? || explicit_session&.open? + end + private def send_transaction(method, **config, &block) diff --git a/spec/e2e/relationship/persistence/query_factory_spec.rb b/spec/e2e/relationship/persistence/query_factory_spec.rb index 49de7cb92..6b696e360 100644 --- a/spec/e2e/relationship/persistence/query_factory_spec.rb +++ b/spec/e2e/relationship/persistence/query_factory_spec.rb @@ -103,56 +103,55 @@ def self.count expect(from_node.reload.to_classes).to be_empty end - it 'does not create duplicate has_one relationship' do - from_node.save - to_node.save - begin - $concurrency_test = true - $concurrency_queue = Queue.new - $concurrency_test_wait = true + context 'concurrent update' do + before do + allow(ActiveGraph::Base).to receive(:lock_node).and_wrap_original do |original, *args| + $concurrency_queue << 'ready' + Thread.stop + original.call(*args) + $concurrency_queue << Thread.current + Thread.stop + end + end + after { $concurrency_queue = nil } + let!(:from_node) { FromClass.create(name: 'foo') } + let!(:to_node) { ToClass.create(name: 'bar') } + let(:from_node_two) { FromClass.create(name: 'foo-2') } + + it 'does not create duplicate has_one relationship' do + $concurrency_queue = Thread::Queue.new t1 = Thread.new { to_node.update(from_class: from_node) } t2 = Thread.new { to_node.update(from_class: from_node) } - while $concurrency_queue.size < 2 - # wait till both thread have complted read query - sleep 1 - end - $concurrency_test_wait = false # make threads resume their work - [t1, t2].join # wait for the threads to finish + sleep(0.1) until $concurrency_queue.size == 2 + $concurrency_queue.clear + [t1, t2].each(&:run) + sleep(0.1) until $concurrency_queue.size == 1 && t1.status == 'sleep' && t2.status == 'sleep' + $concurrency_queue.pop.run + sleep(0.1) until !(t1.status && t2.status) - # to_node.from_class only returns 1 result but in db there are two rels associated with this node - # this assertion fails with with result 2 proving the presence of bug expect(ActiveGraph::Base.query("MATCH (node2:`ToClass`)<-[rel1:`HAS_REL`]-(from_class:`FromClass`) return from_class").to_a.size).to eq(1) - ensure - $concurrency_test = nil - $concurrency_test_wait = nil - $concurrency_queue = nil + (t1.status == 'sleep' ? t1.run : t2.run).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 - it 'does not create two rels with different nodes in has_one relationship' do - from_node.save - to_node.save - from_node_two = FromClass.create(name: 'foo-2') - begin - $concurrency_test = true - $concurrency_queue = Queue.new - $concurrency_test_wait = true + it 'does not create two rels with different nodes in has_one relationship' do + $concurrency_queue = Thread::Queue.new t1 = Thread.new { to_node.update(from_class: from_node) } t2 = Thread.new { to_node.update(from_class: from_node_two) } - while $concurrency_queue.size < 2 - # wait till both thread have complted read query - sleep 1 - end - $concurrency_test_wait = false # make threads resume their work - [t1, t2].join # wait for the threads to finish + sleep(0.1) until $concurrency_queue.size == 2 + $concurrency_queue.clear + [t1, t2].each(&:run) + sleep(0.1) until $concurrency_queue.size == 1 && t1.status == 'sleep' && t2.status == 'sleep' + $concurrency_queue.pop.run + sleep(0.1) until !(t1.status && t2.status) + + first_assigned_from_class, second_assigned_from_class = t1.status == 'sleep' ? [from_node_two, from_node] : [from_node, from_node_two] + + expect(ToClass.find(to_node.id).from_class.id).to eq(first_assigned_from_class.id) + (t1.status == 'sleep' ? t1.run : t2.run).join - # to_node.from_class only returns 1 result but in db there are two rels associated with this node - # this assertion fails with with result 2 proving the presence of bug + expect(to_node.reload.from_class.id).to eq(second_assigned_from_class.id) expect(ActiveGraph::Base.query("MATCH (node2:`ToClass`)<-[rel1:`HAS_REL`]-(from_class:`FromClass`) return from_class").to_a.size).to eq(1) - ensure - $concurrency_test = nil - $concurrency_test_wait = nil - $concurrency_queue = nil end end From d885f664f37a899af5f9e477af092ac4a1cbd525 Mon Sep 17 00:00:00 2001 From: Hardik Joshi Date: Tue, 5 Dec 2023 20:45:35 +0530 Subject: [PATCH 3/7] removing the specs --- .../persistence/query_factory_spec.rb | 52 ------------------- 1 file changed, 52 deletions(-) diff --git a/spec/e2e/relationship/persistence/query_factory_spec.rb b/spec/e2e/relationship/persistence/query_factory_spec.rb index 6b696e360..ac461bab0 100644 --- a/spec/e2e/relationship/persistence/query_factory_spec.rb +++ b/spec/e2e/relationship/persistence/query_factory_spec.rb @@ -103,58 +103,6 @@ def self.count expect(from_node.reload.to_classes).to be_empty end - context 'concurrent update' do - before do - allow(ActiveGraph::Base).to receive(:lock_node).and_wrap_original do |original, *args| - $concurrency_queue << 'ready' - Thread.stop - original.call(*args) - $concurrency_queue << Thread.current - Thread.stop - end - end - after { $concurrency_queue = nil } - let!(:from_node) { FromClass.create(name: 'foo') } - let!(:to_node) { ToClass.create(name: 'bar') } - let(:from_node_two) { FromClass.create(name: 'foo-2') } - - it 'does not create duplicate has_one relationship' do - $concurrency_queue = Thread::Queue.new - t1 = Thread.new { to_node.update(from_class: from_node) } - t2 = Thread.new { to_node.update(from_class: from_node) } - sleep(0.1) until $concurrency_queue.size == 2 - $concurrency_queue.clear - [t1, t2].each(&:run) - sleep(0.1) until $concurrency_queue.size == 1 && t1.status == 'sleep' && t2.status == 'sleep' - $concurrency_queue.pop.run - sleep(0.1) until !(t1.status && t2.status) - - expect(ActiveGraph::Base.query("MATCH (node2:`ToClass`)<-[rel1:`HAS_REL`]-(from_class:`FromClass`) return from_class").to_a.size).to eq(1) - (t1.status == 'sleep' ? t1.run : t2.run).join - expect(ActiveGraph::Base.query("MATCH (node2:`ToClass`)<-[rel1:`HAS_REL`]-(from_class:`FromClass`) return from_class").to_a.size).to eq(1) - end - - it 'does not create two rels with different nodes in has_one relationship' do - $concurrency_queue = Thread::Queue.new - t1 = Thread.new { to_node.update(from_class: from_node) } - t2 = Thread.new { to_node.update(from_class: from_node_two) } - sleep(0.1) until $concurrency_queue.size == 2 - $concurrency_queue.clear - [t1, t2].each(&:run) - sleep(0.1) until $concurrency_queue.size == 1 && t1.status == 'sleep' && t2.status == 'sleep' - $concurrency_queue.pop.run - sleep(0.1) until !(t1.status && t2.status) - - first_assigned_from_class, second_assigned_from_class = t1.status == 'sleep' ? [from_node_two, from_node] : [from_node, from_node_two] - - expect(ToClass.find(to_node.id).from_class.id).to eq(first_assigned_from_class.id) - (t1.status == 'sleep' ? t1.run : t2.run).join - - expect(to_node.reload.from_class.id).to eq(second_assigned_from_class.id) - expect(ActiveGraph::Base.query("MATCH (node2:`ToClass`)<-[rel1:`HAS_REL`]-(from_class:`FromClass`) return from_class").to_a.size).to eq(1) - end - end - it 'delets has_one rel from from_node when new relation is created' do to_node_two = ToClass.new(name: 'bar-2') Rel2Class.new(from_node: from_node, to_node: to_node, score: 10).save From c3f6a8f50cd14cd177f2bc4f40070db36b235958 Mon Sep 17 00:00:00 2001 From: Hardik Joshi Date: Tue, 5 Dec 2023 21:01:08 +0530 Subject: [PATCH 4/7] adding tests back --- .../persistence/query_factory_spec.rb | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/spec/e2e/relationship/persistence/query_factory_spec.rb b/spec/e2e/relationship/persistence/query_factory_spec.rb index ac461bab0..6b696e360 100644 --- a/spec/e2e/relationship/persistence/query_factory_spec.rb +++ b/spec/e2e/relationship/persistence/query_factory_spec.rb @@ -103,6 +103,58 @@ def self.count expect(from_node.reload.to_classes).to be_empty end + context 'concurrent update' do + before do + allow(ActiveGraph::Base).to receive(:lock_node).and_wrap_original do |original, *args| + $concurrency_queue << 'ready' + Thread.stop + original.call(*args) + $concurrency_queue << Thread.current + Thread.stop + end + end + after { $concurrency_queue = nil } + let!(:from_node) { FromClass.create(name: 'foo') } + let!(:to_node) { ToClass.create(name: 'bar') } + let(:from_node_two) { FromClass.create(name: 'foo-2') } + + it 'does not create duplicate has_one relationship' do + $concurrency_queue = Thread::Queue.new + t1 = Thread.new { to_node.update(from_class: from_node) } + t2 = Thread.new { to_node.update(from_class: from_node) } + sleep(0.1) until $concurrency_queue.size == 2 + $concurrency_queue.clear + [t1, t2].each(&:run) + sleep(0.1) until $concurrency_queue.size == 1 && t1.status == 'sleep' && t2.status == 'sleep' + $concurrency_queue.pop.run + sleep(0.1) until !(t1.status && t2.status) + + expect(ActiveGraph::Base.query("MATCH (node2:`ToClass`)<-[rel1:`HAS_REL`]-(from_class:`FromClass`) return from_class").to_a.size).to eq(1) + (t1.status == 'sleep' ? t1.run : t2.run).join + expect(ActiveGraph::Base.query("MATCH (node2:`ToClass`)<-[rel1:`HAS_REL`]-(from_class:`FromClass`) return from_class").to_a.size).to eq(1) + end + + it 'does not create two rels with different nodes in has_one relationship' do + $concurrency_queue = Thread::Queue.new + t1 = Thread.new { to_node.update(from_class: from_node) } + t2 = Thread.new { to_node.update(from_class: from_node_two) } + sleep(0.1) until $concurrency_queue.size == 2 + $concurrency_queue.clear + [t1, t2].each(&:run) + sleep(0.1) until $concurrency_queue.size == 1 && t1.status == 'sleep' && t2.status == 'sleep' + $concurrency_queue.pop.run + sleep(0.1) until !(t1.status && t2.status) + + first_assigned_from_class, second_assigned_from_class = t1.status == 'sleep' ? [from_node_two, from_node] : [from_node, from_node_two] + + expect(ToClass.find(to_node.id).from_class.id).to eq(first_assigned_from_class.id) + (t1.status == 'sleep' ? t1.run : t2.run).join + + expect(to_node.reload.from_class.id).to eq(second_assigned_from_class.id) + expect(ActiveGraph::Base.query("MATCH (node2:`ToClass`)<-[rel1:`HAS_REL`]-(from_class:`FromClass`) return from_class").to_a.size).to eq(1) + end + end + it 'delets has_one rel from from_node when new relation is created' do to_node_two = ToClass.new(name: 'bar-2') Rel2Class.new(from_node: from_node, to_node: to_node, score: 10).save From 4191932c71ea1cba1b259535d63db50143bb2f58 Mon Sep 17 00:00:00 2001 From: Hardik Joshi Date: Tue, 5 Dec 2023 23:29:00 +0530 Subject: [PATCH 5/7] enhanced locking logic --- lib/active_graph/transactions.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/active_graph/transactions.rb b/lib/active_graph/transactions.rb index 5ad72f384..6e125879f 100644 --- a/lib/active_graph/transactions.rb +++ b/lib/active_graph/transactions.rb @@ -29,7 +29,7 @@ def read_transaction(**config, &block) alias transaction write_transaction def lock_node(node) - node.as(:n).query.set('n._LOCK_ = null').exec if tx&.open? || explicit_session&.open? + node.as(:n).query.remove('n._LOCK_').exec if tx&.open? || explicit_session&.open? end private From a958daba9fba92b7c42c78e4ff6143364c53c5f5 Mon Sep 17 00:00:00 2001 From: Hardik Joshi Date: Tue, 12 Dec 2023 21:23:34 +0530 Subject: [PATCH 6/7] Cleanup --- lib/active_graph/transactions.rb | 2 +- .../persistence/query_factory_spec.rb | 40 +++---------------- 2 files changed, 7 insertions(+), 35 deletions(-) diff --git a/lib/active_graph/transactions.rb b/lib/active_graph/transactions.rb index 6e125879f..e8ef4ae4f 100644 --- a/lib/active_graph/transactions.rb +++ b/lib/active_graph/transactions.rb @@ -29,7 +29,7 @@ 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? + node.as(:n).query.remove('n._AGLOCK_').exec if tx&.open? || explicit_session&.open? end private diff --git a/spec/e2e/relationship/persistence/query_factory_spec.rb b/spec/e2e/relationship/persistence/query_factory_spec.rb index 6b696e360..f2911ea94 100644 --- a/spec/e2e/relationship/persistence/query_factory_spec.rb +++ b/spec/e2e/relationship/persistence/query_factory_spec.rb @@ -105,52 +105,24 @@ def self.count context 'concurrent update' do before do - allow(ActiveGraph::Base).to receive(:lock_node).and_wrap_original do |original, *args| + 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) - $concurrency_queue << Thread.current - Thread.stop end end after { $concurrency_queue = nil } let!(:from_node) { FromClass.create(name: 'foo') } let!(:to_node) { ToClass.create(name: 'bar') } - let(:from_node_two) { FromClass.create(name: 'foo-2') } it 'does not create duplicate has_one relationship' do + count = 100 $concurrency_queue = Thread::Queue.new - t1 = Thread.new { to_node.update(from_class: from_node) } - t2 = Thread.new { to_node.update(from_class: from_node) } - sleep(0.1) until $concurrency_queue.size == 2 + threads = count.times.map { Thread.new { to_node.update(from_class: from_node) } } + sleep(0.1) until $concurrency_queue.size == count $concurrency_queue.clear - [t1, t2].each(&:run) - sleep(0.1) until $concurrency_queue.size == 1 && t1.status == 'sleep' && t2.status == 'sleep' - $concurrency_queue.pop.run - sleep(0.1) until !(t1.status && t2.status) - - expect(ActiveGraph::Base.query("MATCH (node2:`ToClass`)<-[rel1:`HAS_REL`]-(from_class:`FromClass`) return from_class").to_a.size).to eq(1) - (t1.status == 'sleep' ? t1.run : t2.run).join - expect(ActiveGraph::Base.query("MATCH (node2:`ToClass`)<-[rel1:`HAS_REL`]-(from_class:`FromClass`) return from_class").to_a.size).to eq(1) - end - - it 'does not create two rels with different nodes in has_one relationship' do - $concurrency_queue = Thread::Queue.new - t1 = Thread.new { to_node.update(from_class: from_node) } - t2 = Thread.new { to_node.update(from_class: from_node_two) } - sleep(0.1) until $concurrency_queue.size == 2 - $concurrency_queue.clear - [t1, t2].each(&:run) - sleep(0.1) until $concurrency_queue.size == 1 && t1.status == 'sleep' && t2.status == 'sleep' - $concurrency_queue.pop.run - sleep(0.1) until !(t1.status && t2.status) - - first_assigned_from_class, second_assigned_from_class = t1.status == 'sleep' ? [from_node_two, from_node] : [from_node, from_node_two] - - expect(ToClass.find(to_node.id).from_class.id).to eq(first_assigned_from_class.id) - (t1.status == 'sleep' ? t1.run : t2.run).join - - expect(to_node.reload.from_class.id).to eq(second_assigned_from_class.id) + 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 From ea53581d82b5658b762b01762b53c597f4bb31a0 Mon Sep 17 00:00:00 2001 From: Hardik Joshi Date: Sun, 24 Dec 2023 08:09:33 +0530 Subject: [PATCH 7/7] Review feedback --- lib/active_graph/transactions.rb | 2 +- .../persistence/query_factory_spec.rb | 24 ------------------- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/lib/active_graph/transactions.rb b/lib/active_graph/transactions.rb index e8ef4ae4f..3ba5e888f 100644 --- a/lib/active_graph/transactions.rb +++ b/lib/active_graph/transactions.rb @@ -29,7 +29,7 @@ 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? + node.as(:n).query.remove('n._AGLOCK_').exec if tx&.open? end private diff --git a/spec/e2e/relationship/persistence/query_factory_spec.rb b/spec/e2e/relationship/persistence/query_factory_spec.rb index f2911ea94..ac461bab0 100644 --- a/spec/e2e/relationship/persistence/query_factory_spec.rb +++ b/spec/e2e/relationship/persistence/query_factory_spec.rb @@ -103,30 +103,6 @@ def self.count expect(from_node.reload.to_classes).to be_empty end - 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 - it 'delets has_one rel from from_node when new relation is created' do to_node_two = ToClass.new(name: 'bar-2') Rel2Class.new(from_node: from_node, to_node: to_node, score: 10).save