Skip to content

Commit e4de93e

Browse files
committed
Add a failing test case to show why deadlocks are occurring
This include some logging for diagnostics to show what's happening when the deadlock occurs. ``` Graphiti::Scope #resolve_sideloads when the requested sideload exists on the resource with concurrency with nested sideloads greater than Graphiti.config.concurrency_max_threads thread 6220: employees queuing positions thread 6220: employees waiting on [:positions] thread 6240: running positions thread 6240: positions queuing department thread 6240: positions waiting on [:department] does not deadlock (FAILED - 1) Failures: 1) Graphiti::Scope#resolve_sideloads when the requested sideload exists on the resource with concurrency with nested sideloads greater than Graphiti.config.concurrency_max_threads does not deadlock Failure/Error: expect { instance.resolve_sideloads(results) }.not_to raise_error expected no Exception, got #<fatal:"No live threads left. Deadlock?\n2 threads, 2 sleeps current:0x00007f7e6f7b1780 main thread:...or.rb:339 sleep_forever>\n rb_thread_t:0x00007f7e6f7b1780 native:0x000070000cfb4000 int:0\n \n"> with backtrace: # ./lib/graphiti/scope.rb:78:in `resolve_sideloads' # ./spec/scope_spec.rb:145:in `block (7 levels) in <top (required)>' # ./spec/scope_spec.rb:145:in `block (6 levels) in <top (required)>' # ./spec/scope_spec.rb:145:in `block (6 levels) in <top (required)>' ```
1 parent 6829616 commit e4de93e

2 files changed

Lines changed: 29 additions & 8 deletions

File tree

lib/graphiti/scope.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,20 +59,24 @@ def resolve_sideloads(results)
5959
parent_resource = @resource
6060
graphiti_context = Graphiti.context
6161
resolve_sideload = -> {
62+
puts "thread #{Thread.current.object_id}: running #{name}"
6263
Graphiti.config.before_sideload&.call(graphiti_context)
6364
Graphiti.context = graphiti_context
6465
sideload.resolve(results, q, parent_resource)
6566
@resource.adapter.close if concurrent
6667
}
6768
if concurrent
69+
puts "thread #{Thread.current.object_id}: #{@resource.class.type} queuing #{name}"
6870
promises << Concurrent::Promise.execute(executor: self.class.global_thread_pool_executor, &resolve_sideload)
6971
else
7072
resolve_sideload.call
7173
end
7274
end
7375

7476
if concurrent
75-
Concurrent::Promise.zip(*promises, executor: self.class.global_thread_pool_executor).value!
77+
puts "thread #{Thread.current.object_id}: #{@resource.class.type} waiting on #{@query.sideloads.map(&:first)}"
78+
Concurrent::Promise.zip(*promises).value!
79+
puts "thread #{Thread.current.object_id}: #{@resource.class.type} finished waiting on #{@query.sideloads.map(&:first)}"
7680
end
7781
end
7882

spec/scope_spec.rb

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,9 @@
7575
describe "#resolve_sideloads" do
7676
let(:sideload) { double(shared_remote?: false, name: :positions) }
7777
let(:results) { [double.as_null_object] }
78+
let(:params) { {include: {positions: {}}} }
7879

7980
before do
80-
params[:include] = {positions: {}}
8181
objekt = instance.instance_variable_get(:@object)
8282
allow(resource).to receive(:resolve).with(objekt) { results }
8383
end
@@ -119,14 +119,31 @@
119119
expect { instance.resolve_sideloads(results) }.not_to raise_error
120120
end
121121

122-
context 'with more sideloads than the thread pool size' do
123-
before { allow(Graphiti.config).to receive(:concurrency_max_threads).and_return(0) }
124-
125-
it 'deadlocks' do
126-
expect { instance.resolve_sideloads(results) }.to raise_error do |e|
127-
expect(e.message).to start_with('No live threads left. Deadlock?')
122+
context 'with nested sideloads greater than Graphiti.config.concurrency_max_threads' do
123+
let(:params) { { include: { positions: { department: {} } } } }
124+
let(:position_resource) { PORO::PositionResource.new }
125+
let(:departments_sideload) { double(shared_remote?: false, name: :departments) }
126+
127+
before do
128+
stub_const(
129+
'Graphiti::Scope::GLOBAL_THREAD_POOL_EXECUTOR',
130+
Concurrent::Delay.new {
131+
Concurrent::ThreadPoolExecutor.new(max_threads: 1, fallback_policy: :caller_runs)
132+
}
133+
)
134+
135+
allow(position_resource.class).to receive(:sideload).with(:department) { departments_sideload }
136+
allow(departments_sideload).to receive(:resolve).and_return(departments_sideload)
137+
138+
# make resolve just load the sideloads
139+
allow(sideload).to receive(:resolve) do |results, q, _parent_resource|
140+
described_class.new(sideload.as_null_object, position_resource, q).resolve_sideloads(results)
128141
end
129142
end
143+
144+
it 'does not deadlock' do
145+
expect { instance.resolve_sideloads(results) }.not_to raise_error
146+
end
130147
end
131148
end
132149

0 commit comments

Comments
 (0)