Skip to content

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

Description

@masterful

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions