rmosolgo / rmosolgo/graphql-ruby

[pro] Rails @defer setup can deadlock with development code reloading

Open
#5,707 10 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Ruby
Stars
5.4k
Forks
1.4k
Avg merge
23h 19m
Merged PRs (30d)
28

Description

Describe the bug

The current GraphQL Pro @defer server setup guide recommends using ActionController::Live to stream multipart responses from Rails.

In development, an ActionController::Live request can deadlock with Rails' code reloader. The problem is in the Rails integration rather than GraphQL execution itself, but following the documented setup exposes GraphQL controllers to it. Because ActionController::Live wraps the whole controller action,
non-deferred requests handled by the same controller are exposed too.

I have opened rails/rails#58486 with a proposed fix. Until a Rails fix lands, could the GraphQL-Ruby docs warn Rails users about this problem or recommend a streaming implementation that does not require ActionController::Live? I would also appreciate feedback on whether the Rails fix preserves the behavior GraphQL-Ruby expects from a streaming controller.

Versions

graphql version: 2.6.7
graphql-pro version: 1.30.2
graphql-enterprise version: 1.7.0
graphql-batch version: 0.6.1
rails: 8.1.3.1
ruby: 4.0.5

GraphQL schema / query

No particular schema/query is required. The deadlock occurs before the Live child thread can run the controller action and commit the response. Any request handled by a controller that includes ActionController::Live can enter the lock cycle.

Steps to reproduce

It's not technically an issue with this gem, but if you're curious, you can reproduce the behaviour by leveraging ActionController::Live...

Instructions here if you're interested

A minimal reproduction is available at:

https://github.com/GetJobber/reproduce-rails-deadlock-behaviour

Set it up with:

 git clone https://github.com/GetJobber/reproduce-rails-deadlock-behaviour.git
 cd reproduce-rails-deadlock-behaviour
 bundle install

Start Rails in development:

 bundle exec rails server -p 3011 -e development

In another terminal, run:

 PORT=3011 ./script/repro.sh

The script performs this sequence:

  1. Start an ActionController::Live request.
  2. Touch the controller file to request a reload.
  3. Start two more Live requests.
  4. Report request timeouts and capture /rails/locks.

The reproduction adds a short delay before the Live child reaches the running interlock. This makes the scheduling race deterministic. Thread-pool scheduling, pool pressure, GVL contention, or other work before the child acquires its lock can expose the same window without the artificial delay.

Expected behavior

The streaming controller should complete without deadlocking when Rails detects a code change.

If ActionController::Live cannot safely support development reloading, the GraphQL-Ruby guide should document that limitation and, if possible, recommend another way to serve deferred multipart responses?

Actual behavior

The Rails server can become permanently stuck. Requests time out, and even stopping the development server may become difficult.

Additional context

The Rails tracking issue is rails/rails#56888.

My current proposed fix is rails/rails#58486. It adds a one-shot running continuation for work scheduled by an execution context that already holds a running share.

Under that approach:

  • The Live child may claim its continuation ahead of an unload that queued after its parent request was admitted.
  • Unrelated new requests remain blocked behind the queued unload.
  • A continuation cannot bypass an unload that already holds the exclusive lock.
  • Once claimed, the child holds an ordinary running share for the complete action.
  • The unload therefore cannot replace constants underneath an active controller action.

An earlier approach in rails/rails#56942 allowed the unload to overtake the parent while it waited for the child. That broke the deadlock, but testing showed that a controller instance created before the reload could begin executing after its constants had been replaced. The continuation approach avoids that mixed-generation behavior.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the GraphQL Pro @defer setup guide's “sending streaming responses” section and review the linked Rails reproduction and Rails tracking issue. Verify the documented ActionController::Live limitation and whether a supported alternative exists; the documentation is done when Rails users are warned about development reload deadlocks and given accurate streaming guidance.

Written by the indexing model from the issue text.

Assessment

Tech stack
rails, ruby
Domain
documentation
Issue type
Documentation
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.