Skip to content

AsyncDataloader: ClosedQueueError ("Cannot enqueue items to a closed queue!") under Puma after #5479 (2.6.4) #5654

Description

@FoundRoleApp

AsyncDataloader: ClosedQueueError ("Cannot enqueue items to a closed queue!") under Puma after #5479 (2.6.4)

Summary

After upgrading graphql from 2.6.3 to 2.6.4, every GraphQL request that uses
dataloader sources started throwing Async::Queue::ClosedError: Cannot enqueue items to a closed queue!. Downgrading to 2.6.3 makes it disappear entirely.

The app runs GraphQL::Dataloader::AsyncDataloader under Puma (clustered,
threaded workers — not Falcon). The rework in #5479 ("Don't nest Async
primitives inside plain Ruby fibers") looks like the trigger: the error
originates entirely inside async_dataloader.rb, in spawn_source_task pushing
into an Async::Queue that has already been closed during request teardown.

Versions

  • graphql: 2.6.4 (regression; 2.6.3 is unaffected)
  • async: 2.39.0
  • puma: 8.0.2 (clustered mode, multiple workers, multiple threads per worker)
  • ruby: 3.4.x
  • Rails: 8.1.x
  • Server: Puma, not Falcon

Schema setup

class MySchema < GraphQL::Schema
  use GraphQL::Dataloader::AsyncDataloader, fiber_limit: <N>
  # ...
end

Standard GraphQL::Dataloader::Source subclasses, loaded via
dataloader.with(MySource).load(id). No custom fiber/Async code on the
application side — the entire backtrace is inside the gem.

Backtrace

Async::Queue::ClosedError: Cannot enqueue items to a closed queue!
  async-2.39.0/lib/async/queue.rb:65:in 'Async::Queue#push'
  graphql-2.6.4/lib/graphql/dataloader/async_dataloader.rb:262:in 'block (2 levels) in GraphQL::Dataloader::AsyncDataloader#spawn_source_task'
  async-2.39.0/lib/async/task.rb:224:in 'block in Async::Task#run'
  async-2.39.0/lib/async/task.rb:521:in 'block in Async::Task#schedule'
cause: ClosedQueueError: queue closed
  async-2.39.0/lib/async/queue.rb:63:in 'Thread::Queue#push'
  async-2.39.0/lib/async/queue.rb:63:in 'Async::Queue#push'
  graphql-2.6.4/lib/graphql/dataloader/async_dataloader.rb:262:in 'block (2 levels) in GraphQL::Dataloader::AsyncDataloader#spawn_source_task'
  async-2.39.0/lib/async/task.rb:224:in 'block in Async::Task#run'
  async-2.39.0/lib/async/task.rb:521:in 'block in Async::Task#schedule'

It surfaces as an unhandled Async::Task failure ("Task may have ended with
unhandled exception"), so the source task dies and the field resolution it
backed never completes.

Reproduction

No minimal repro yet, but in this app it happens on virtually every
dataloader-backed request under Puma. Happy to help build a minimal one if it
doesn't reproduce out of the box.

The decisive signal: zero occurrences across 7+ days on 2.6.3, then a
continuous flood starting within the first minute of deploying 2.6.4, with no
application-code changes in between — only the gem version changed.

Expected behavior

Source tasks scheduled by AsyncDataloader shouldn't attempt to enqueue into a
queue that's already been closed during request teardown. Under Puma the new
Async-primitive-based task lifecycle from #5479 seems to race the queue close.

Workaround

Pinning graphql to 2.6.3 fully resolves it.

Additional context

#5479 was reported as validated in real-world use, but the discussion doesn't
mention Puma specifically — this may be a Puma-vs-Falcon interaction (threaded
host + per-request fiber scheduler vs. a fully-async host) that the rework
didn't cover. Possibly related: #5463 (the deadlock this PR fixed) and #5407
(fiber leaking).

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