From 550eb84c8ca8c33a7cce96f8e2660c80d04e70f1 Mon Sep 17 00:00:00 2001 From: Robert Mosolgo Date: Fri, 5 Dec 2025 12:03:30 -0500 Subject: [PATCH 1/6] Use top-level Sync instead of plain Fiber for AsyncDataloader --- lib/graphql/analysis/query_complexity.rb | 4 ++-- lib/graphql/dataloader/async_dataloader.rb | 10 +++------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/lib/graphql/analysis/query_complexity.rb b/lib/graphql/analysis/query_complexity.rb index 4a90824b4c..c953855de4 100644 --- a/lib/graphql/analysis/query_complexity.rb +++ b/lib/graphql/analysis/query_complexity.rb @@ -27,14 +27,14 @@ def result future_complexity end when nil - subject.logger.warn <<~GRAPHQL + subject.logger.warn <<~MESSAGE GraphQL-Ruby's complexity cost system is getting some "breaking fixes" in a future version. See the migration notes at https://graphql-ruby.org/api-doc/#{GraphQL::VERSION}/GraphQL/Schema.html#complexity_cost_calculation_mode_for-class_method To opt into the future behavior, configure your schema (#{subject.schema.name ? subject.schema.name : subject.schema.ancestors}) with: complexity_cost_calculation_mode(:future) # or `:legacy`, `:compare` - GRAPHQL + MESSAGE max_possible_complexity(mode: :legacy) else raise ArgumentError, "Expected `:future`, `:legacy`, `:compare`, or `nil` from `#{query.schema}.complexity_cost_calculation_mode_for` but got: #{query.schema.complexity_cost_calculation_mode.inspect}" diff --git a/lib/graphql/dataloader/async_dataloader.rb b/lib/graphql/dataloader/async_dataloader.rb index 9781dda03b..be2d08da93 100644 --- a/lib/graphql/dataloader/async_dataloader.rb +++ b/lib/graphql/dataloader/async_dataloader.rb @@ -23,7 +23,9 @@ def run(trace_query_lazy: nil) next_source_tasks = [] first_pass = true sources_condition = Async::Condition.new - manager = spawn_fiber do + fiber_vars = get_fiber_variables + Sync do + set_fiber_variables(fiber_vars) trace&.begin_dataloader(self) while first_pass || !job_fibers.empty? first_pass = false @@ -55,12 +57,6 @@ def run(trace_query_lazy: nil) end trace&.end_dataloader(self) end - - manager.resume - if manager.alive? - raise "Invariant: Manager didn't terminate successfully: #{manager}" - end - rescue UncaughtThrowError => e throw e.tag, e.value end From 983d1fbc383e9a4a029212be4449fff8c586e000 Mon Sep 17 00:00:00 2001 From: Robert Mosolgo Date: Fri, 5 Dec 2025 13:26:32 -0500 Subject: [PATCH 2/6] Remove EVT tests because they're making the test suite hang --- spec/graphql/dataloader/nonblocking_dataloader_spec.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/graphql/dataloader/nonblocking_dataloader_spec.rb b/spec/graphql/dataloader/nonblocking_dataloader_spec.rb index 9a3bcdf8b0..ad799fd1a8 100644 --- a/spec/graphql/dataloader/nonblocking_dataloader_spec.rb +++ b/spec/graphql/dataloader/nonblocking_dataloader_spec.rb @@ -251,11 +251,11 @@ def self.included(child_class) include NonblockingDataloaderAssertions end - describe "with evt" do - require "evt" - let(:scheduler_class) { Evt::Scheduler } - include NonblockingDataloaderAssertions - end + # describe "with evt" do + # require "evt" + # let(:scheduler_class) { Evt::Scheduler } + # include NonblockingDataloaderAssertions + # end end end end From 9f652e060b03d1dc8779a1c20676adc038aa22be Mon Sep 17 00:00:00 2001 From: Robert Mosolgo Date: Fri, 5 Dec 2025 15:34:23 -0500 Subject: [PATCH 3/6] Try removing evt and libuv altogether --- Gemfile | 5 ----- gemfiles/mongoid_8.gemfile | 2 -- gemfiles/mongoid_9.gemfile | 2 -- gemfiles/rails_7.2_postgresql.gemfile | 1 - gemfiles/rails_8.0.gemfile | 1 - gemfiles/rails_8.1.gemfile | 1 - gemfiles/rails_master.gemfile | 1 - .../dataloader/nonblocking_dataloader_spec.rb | 14 ------------- spec/graphql/dataloader_spec.rb | 21 ------------------- 9 files changed, 48 deletions(-) diff --git a/Gemfile b/Gemfile index 77180b4d3d..77742028ec 100644 --- a/Gemfile +++ b/Gemfile @@ -8,11 +8,6 @@ gem 'stackprof', platform: :ruby gem 'pry' gem 'pry-stack_explorer', platform: :ruby -if RUBY_VERSION >= "3.0" - gem "libev_scheduler" - gem "evt" -end - if RUBY_VERSION >= "3.2.0" gem "async", "~>2.0" end diff --git a/gemfiles/mongoid_8.gemfile b/gemfiles/mongoid_8.gemfile index 0639b55bfd..5536381b76 100644 --- a/gemfiles/mongoid_8.gemfile +++ b/gemfiles/mongoid_8.gemfile @@ -8,8 +8,6 @@ gem "ruby-prof", platform: :ruby gem "pry" gem "pry-stack_explorer", platform: :ruby gem "mongoid", "~> 8.0" -gem "libev_scheduler" -gem "evt" gem "async" gem "concurrent-ruby", "1.3.4" diff --git a/gemfiles/mongoid_9.gemfile b/gemfiles/mongoid_9.gemfile index b4d8679531..0bd71a82ee 100644 --- a/gemfiles/mongoid_9.gemfile +++ b/gemfiles/mongoid_9.gemfile @@ -7,8 +7,6 @@ gem "ruby-prof", platform: :ruby gem "pry" gem "pry-stack_explorer", platform: :ruby gem "mongoid", "~> 9.0" -gem "libev_scheduler" -gem "evt" gem "async" gemspec path: "../" diff --git a/gemfiles/rails_7.2_postgresql.gemfile b/gemfiles/rails_7.2_postgresql.gemfile index 25ebbae82b..ce33d7ca3a 100644 --- a/gemfiles/rails_7.2_postgresql.gemfile +++ b/gemfiles/rails_7.2_postgresql.gemfile @@ -9,7 +9,6 @@ gem "pry-stack_explorer", platform: :ruby gem "rails", "~> 7.2.0", require: "rails/all" gem "pg", platform: :ruby gem "sequel" -gem "evt" gem "async" gem "libev_scheduler" gem "google-protobuf" diff --git a/gemfiles/rails_8.0.gemfile b/gemfiles/rails_8.0.gemfile index 11c33d0ea5..109bf0220a 100644 --- a/gemfiles/rails_8.0.gemfile +++ b/gemfiles/rails_8.0.gemfile @@ -10,7 +10,6 @@ gem "rails", "~> 8.0.0", require: "rails/all" gem "sqlite3" gem "pg", platform: :ruby gem "sequel" -gem "evt" gem "async" gem "google-protobuf" diff --git a/gemfiles/rails_8.1.gemfile b/gemfiles/rails_8.1.gemfile index a55c305eaa..30f62960f0 100644 --- a/gemfiles/rails_8.1.gemfile +++ b/gemfiles/rails_8.1.gemfile @@ -10,7 +10,6 @@ gem "rails", "~> 8.1.0", require: "rails/all" gem "sqlite3" gem "pg", platform: :ruby gem "sequel" -gem "evt" gem "async" gem "google-protobuf" diff --git a/gemfiles/rails_master.gemfile b/gemfiles/rails_master.gemfile index 635da2eec6..e181203a87 100644 --- a/gemfiles/rails_master.gemfile +++ b/gemfiles/rails_master.gemfile @@ -10,7 +10,6 @@ gem "rails", github: "rails/rails", require: "rails/all", ref: "main" gem 'sqlite3' gem 'pg' gem "sequel" -gem "evt" if RUBY_ENGINE == "ruby" # This doesn't work on truffle-ruby because there's no `IO::READABLE` gem "libev_scheduler" end diff --git a/spec/graphql/dataloader/nonblocking_dataloader_spec.rb b/spec/graphql/dataloader/nonblocking_dataloader_spec.rb index ad799fd1a8..fa60ccfd19 100644 --- a/spec/graphql/dataloader/nonblocking_dataloader_spec.rb +++ b/spec/graphql/dataloader/nonblocking_dataloader_spec.rb @@ -243,19 +243,5 @@ def self.included(child_class) let(:scheduler_class) { ::DummyScheduler } include NonblockingDataloaderAssertions end - - if RUBY_ENGINE == "ruby" && !ENV["GITHUB_ACTIONS"] - describe "With libev_scheduler" do - require "libev_scheduler" - let(:scheduler_class) { Libev::Scheduler } - include NonblockingDataloaderAssertions - end - - # describe "with evt" do - # require "evt" - # let(:scheduler_class) { Evt::Scheduler } - # include NonblockingDataloaderAssertions - # end - end end end diff --git a/spec/graphql/dataloader_spec.rb b/spec/graphql/dataloader_spec.rb index a5020de012..0da7ae8110 100644 --- a/spec/graphql/dataloader_spec.rb +++ b/spec/graphql/dataloader_spec.rb @@ -1283,27 +1283,6 @@ def make_schema_from(schema) include DataloaderAssertions end - - if RUBY_ENGINE == "ruby" && !ENV["GITHUB_ACTIONS"] - describe "nonblocking: true with libev" do - require "libev_scheduler" - def make_schema_from(schema) - Class.new(schema) do - use GraphQL::Dataloader, nonblocking: true - end - end - - before do - Fiber.set_scheduler(Libev::Scheduler.new) - end - - after do - Fiber.set_scheduler(nil) - end - - include DataloaderAssertions - end - end end describe "example from #3314" do From ae4067bf7d3e88390acbbebbacf193a68c65149f Mon Sep 17 00:00:00 2001 From: Robert Mosolgo Date: Fri, 5 Dec 2025 16:11:10 -0500 Subject: [PATCH 4/6] Try moving condition inside loop --- lib/graphql/dataloader/async_dataloader.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/graphql/dataloader/async_dataloader.rb b/lib/graphql/dataloader/async_dataloader.rb index be2d08da93..21444cf485 100644 --- a/lib/graphql/dataloader/async_dataloader.rb +++ b/lib/graphql/dataloader/async_dataloader.rb @@ -22,9 +22,9 @@ def run(trace_query_lazy: nil) source_tasks = [] next_source_tasks = [] first_pass = true - sources_condition = Async::Condition.new fiber_vars = get_fiber_variables Sync do + sources_condition = Async::Condition.new set_fiber_variables(fiber_vars) trace&.begin_dataloader(self) while first_pass || !job_fibers.empty? @@ -56,6 +56,7 @@ def run(trace_query_lazy: nil) end end trace&.end_dataloader(self) + cleanup_fiber end rescue UncaughtThrowError => e throw e.tag, e.value From f9c78721085ac93de7bfe00c1039e657ffc04a57 Mon Sep 17 00:00:00 2001 From: Robert Mosolgo Date: Fri, 5 Dec 2025 16:25:08 -0500 Subject: [PATCH 5/6] Move dataloader fiber loop to not include Async --- lib/graphql/dataloader/async_dataloader.rb | 52 +++++++++++----------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/lib/graphql/dataloader/async_dataloader.rb b/lib/graphql/dataloader/async_dataloader.rb index 21444cf485..7f7dff43ac 100644 --- a/lib/graphql/dataloader/async_dataloader.rb +++ b/lib/graphql/dataloader/async_dataloader.rb @@ -22,42 +22,42 @@ def run(trace_query_lazy: nil) source_tasks = [] next_source_tasks = [] first_pass = true - fiber_vars = get_fiber_variables - Sync do - sources_condition = Async::Condition.new - set_fiber_variables(fiber_vars) - trace&.begin_dataloader(self) - while first_pass || !job_fibers.empty? - first_pass = false - fiber_vars = get_fiber_variables + sources_condition = Async::Condition.new + trace&.begin_dataloader(self) + while first_pass || !job_fibers.empty? + first_pass = false + + f = spawn_fiber do run_pending_steps(job_fibers, next_job_fibers, source_tasks, jobs_fiber_limit, trace) + end + run_fiber(f) - Sync do |root_task| - set_fiber_variables(fiber_vars) - while !source_tasks.empty? || @source_cache.each_value.any? { |group_sources| group_sources.each_value.any?(&:pending?) } - while (task = (source_tasks.shift || (((job_fibers.size + next_job_fibers.size + source_tasks.size + next_source_tasks.size) < total_fiber_limit) && spawn_source_task(root_task, sources_condition, trace)))) - if task.alive? - root_task.yield # give the source task a chance to run - next_source_tasks << task - end + fiber_vars = get_fiber_variables + Sync do |root_task| + set_fiber_variables(fiber_vars) + while !source_tasks.empty? || @source_cache.each_value.any? { |group_sources| group_sources.each_value.any?(&:pending?) } + while (task = (source_tasks.shift || (((job_fibers.size + next_job_fibers.size + source_tasks.size + next_source_tasks.size) < total_fiber_limit) && spawn_source_task(root_task, sources_condition, trace)))) + if task.alive? + root_task.yield # give the source task a chance to run + next_source_tasks << task end - sources_condition.signal - source_tasks.concat(next_source_tasks) - next_source_tasks.clear end + sources_condition.signal + source_tasks.concat(next_source_tasks) + next_source_tasks.clear end + end - if !@lazies_at_depth.empty? - with_trace_query_lazy(trace_query_lazy) do - run_next_pending_lazies(job_fibers, trace) - run_pending_steps(job_fibers, next_job_fibers, source_tasks, jobs_fiber_limit, trace) - end + if !@lazies_at_depth.empty? + with_trace_query_lazy(trace_query_lazy) do + run_next_pending_lazies(job_fibers, trace) + run_pending_steps(job_fibers, next_job_fibers, source_tasks, jobs_fiber_limit, trace) end end - trace&.end_dataloader(self) - cleanup_fiber end + trace&.end_dataloader(self) + rescue UncaughtThrowError => e throw e.tag, e.value end From 6c145292096ed67c3871cfb9169b3772760e76ed Mon Sep 17 00:00:00 2001 From: Robert Mosolgo Date: Fri, 5 Dec 2025 16:50:44 -0500 Subject: [PATCH 6/6] Update tests to accomodate short-lived jobs fibers --- lib/graphql/dataloader/async_dataloader.rb | 3 +++ spec/graphql/dataloader_spec.rb | 23 +++++++++++++++++++--- spec/spec_helper.rb | 2 +- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/lib/graphql/dataloader/async_dataloader.rb b/lib/graphql/dataloader/async_dataloader.rb index 7f7dff43ac..5f8b744170 100644 --- a/lib/graphql/dataloader/async_dataloader.rb +++ b/lib/graphql/dataloader/async_dataloader.rb @@ -32,6 +32,9 @@ def run(trace_query_lazy: nil) run_pending_steps(job_fibers, next_job_fibers, source_tasks, jobs_fiber_limit, trace) end run_fiber(f) + if f.alive? + raise "GraphQL-Ruby internal error: AsyncDataloader job fiber didn't terminate" + end fiber_vars = get_fiber_variables Sync do |root_task| diff --git a/spec/graphql/dataloader_spec.rb b/spec/graphql/dataloader_spec.rb index 0da7ae8110..743c843e7e 100644 --- a/spec/graphql/dataloader_spec.rb +++ b/spec/graphql/dataloader_spec.rb @@ -1191,17 +1191,34 @@ def assert_last_max_fiber_count(expected_last_max_fiber_count, message = nil) res = schema.execute(query_str, context: { dataloader: fiber_counting_dataloader_class.new }) assert_nil res.context.dataloader.fiber_limit - assert_equal 10, FiberCounting.last_spawn_fiber_count + + extra_shortlived_jobs_fibers = if fiber_counting_dataloader_class < GraphQL::Dataloader::AsyncDataloader + 3 + else + 0 + end + assert_equal 10 + extra_shortlived_jobs_fibers, FiberCounting.last_spawn_fiber_count assert_last_max_fiber_count(9, "No limit works as expected") + extra_shortlived_jobs_fibers = if fiber_counting_dataloader_class < GraphQL::Dataloader::AsyncDataloader + 10 # more here because there are fewer jobs fibers running at any one time + else + 0 + end res = schema.execute(query_str, context: { dataloader: fiber_counting_dataloader_class.new(fiber_limit: 4) }) assert_equal 4, res.context.dataloader.fiber_limit - assert_equal 12, FiberCounting.last_spawn_fiber_count + assert_equal 12 + extra_shortlived_jobs_fibers, FiberCounting.last_spawn_fiber_count assert_last_max_fiber_count(4, "Limit of 4 works as expected") + extra_shortlived_jobs_fibers = if fiber_counting_dataloader_class < GraphQL::Dataloader::AsyncDataloader + 4 + else + 0 + end + res = schema.execute(query_str, context: { dataloader: fiber_counting_dataloader_class.new(fiber_limit: 6) }) assert_equal 6, res.context.dataloader.fiber_limit - assert_equal 8, FiberCounting.last_spawn_fiber_count + assert_equal 8 + extra_shortlived_jobs_fibers, FiberCounting.last_spawn_fiber_count assert_last_max_fiber_count(6, "Limit of 6 works as expected") end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index cd5dd98d88..cf7f3e691e 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -43,7 +43,7 @@ end # C methods aren't fair game in non-main Ractors -RUN_RACTOR_TESTS = defined?(::Ractor) && !USING_C_PARSER +RUN_RACTOR_TESTS = defined?(::Ractor) && !USING_C_PARSER && !ENV["SKIP_RACTOR_TESTS"] require "rake" require "graphql/rake_task"