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).
AsyncDataloader:
ClosedQueueError("Cannot enqueue items to a closed queue!") under Puma after #5479 (2.6.4)Summary
After upgrading
graphqlfrom 2.6.3 to 2.6.4, every GraphQL request that usesdataloader 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::AsyncDataloaderunder 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, inspawn_source_taskpushinginto an
Async::Queuethat has already been closed during request teardown.Versions
graphql: 2.6.4 (regression; 2.6.3 is unaffected)async: 2.39.0puma: 8.0.2 (clustered mode, multiple workers, multiple threads per worker)Schema setup
Standard
GraphQL::Dataloader::Sourcesubclasses, loaded viadataloader.with(MySource).load(id). No custom fiber/Async code on theapplication side — the entire backtrace is inside the gem.
Backtrace
It surfaces as an unhandled
Async::Taskfailure ("Task may have ended withunhandled 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
AsyncDataloadershouldn't attempt to enqueue into aqueue 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
graphqlto2.6.3fully 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).