Skip to content

Commit

Permalink
Add a failing test case to show why deadlocks are occurring
Browse files Browse the repository at this point in the history
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)>'
```
  • Loading branch information
MattFenelon committed Apr 12, 2024
1 parent 6829616 commit e4de93e
Show file tree
Hide file tree
Showing 2 changed files with 29 additions and 8 deletions.
6 changes: 5 additions & 1 deletion lib/graphiti/scope.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,20 +59,24 @@ def resolve_sideloads(results)
parent_resource = @resource
graphiti_context = Graphiti.context
resolve_sideload = -> {
puts "thread #{Thread.current.object_id}: running #{name}"
Graphiti.config.before_sideload&.call(graphiti_context)
Graphiti.context = graphiti_context
sideload.resolve(results, q, parent_resource)
@resource.adapter.close if concurrent
}
if concurrent
puts "thread #{Thread.current.object_id}: #{@resource.class.type} queuing #{name}"
promises << Concurrent::Promise.execute(executor: self.class.global_thread_pool_executor, &resolve_sideload)
else
resolve_sideload.call
end
end

if concurrent
Concurrent::Promise.zip(*promises, executor: self.class.global_thread_pool_executor).value!
puts "thread #{Thread.current.object_id}: #{@resource.class.type} waiting on #{@query.sideloads.map(&:first)}"
Concurrent::Promise.zip(*promises).value!
puts "thread #{Thread.current.object_id}: #{@resource.class.type} finished waiting on #{@query.sideloads.map(&:first)}"
end
end

Expand Down
31 changes: 24 additions & 7 deletions spec/scope_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@
describe "#resolve_sideloads" do
let(:sideload) { double(shared_remote?: false, name: :positions) }
let(:results) { [double.as_null_object] }
let(:params) { {include: {positions: {}}} }

before do
params[:include] = {positions: {}}
objekt = instance.instance_variable_get(:@object)
allow(resource).to receive(:resolve).with(objekt) { results }
end
Expand Down Expand Up @@ -119,14 +119,31 @@
expect { instance.resolve_sideloads(results) }.not_to raise_error
end

context 'with more sideloads than the thread pool size' do
before { allow(Graphiti.config).to receive(:concurrency_max_threads).and_return(0) }

it 'deadlocks' do
expect { instance.resolve_sideloads(results) }.to raise_error do |e|
expect(e.message).to start_with('No live threads left. Deadlock?')
context 'with nested sideloads greater than Graphiti.config.concurrency_max_threads' do
let(:params) { { include: { positions: { department: {} } } } }
let(:position_resource) { PORO::PositionResource.new }
let(:departments_sideload) { double(shared_remote?: false, name: :departments) }

before do
stub_const(
'Graphiti::Scope::GLOBAL_THREAD_POOL_EXECUTOR',
Concurrent::Delay.new {
Concurrent::ThreadPoolExecutor.new(max_threads: 1, fallback_policy: :caller_runs)
}
)

allow(position_resource.class).to receive(:sideload).with(:department) { departments_sideload }
allow(departments_sideload).to receive(:resolve).and_return(departments_sideload)

# make resolve just load the sideloads
allow(sideload).to receive(:resolve) do |results, q, _parent_resource|
described_class.new(sideload.as_null_object, position_resource, q).resolve_sideloads(results)
end
end

it 'does not deadlock' do
expect { instance.resolve_sideloads(results) }.not_to raise_error
end
end
end

Expand Down

0 comments on commit e4de93e

Please sign in to comment.