From 843864d357c7888fe175983e97b5e25cbc8ba6fb Mon Sep 17 00:00:00 2001 From: Robert Mosolgo Date: Wed, 2 Apr 2025 15:44:29 -0400 Subject: [PATCH 1/6] Add LazySource --- lib/graphql/dataloader.rb | 21 ++++++++--- lib/graphql/dataloader/lazy_source.rb | 27 ++++++++++++++ lib/graphql/dataloader/source.rb | 4 +++ lib/graphql/execution/interpreter/runtime.rb | 17 +++++---- lib/graphql/execution/lazy.rb | 35 +++++++++++++++++++ lib/graphql/query/context.rb | 5 +++ lib/graphql/schema.rb | 21 +++++++++-- spec/graphql/authorization_spec.rb | 3 ++ spec/graphql/execution/errors_spec.rb | 1 + spec/graphql/execution/interpreter_spec.rb | 2 ++ .../query/context/scoped_context_spec.rb | 1 + spec/graphql/query/context_spec.rb | 1 + spec/graphql/schema/argument_spec.rb | 1 + spec/graphql/schema/directive_spec.rb | 1 + spec/graphql/schema/input_object_spec.rb | 3 +- spec/graphql/schema/member/scoped_spec.rb | 1 + spec/graphql/schema/resolver_spec.rb | 1 + spec/graphql/tracing/appsignal_trace_spec.rb | 3 ++ spec/graphql/tracing/data_dog_trace_spec.rb | 2 ++ spec/graphql/tracing/trace_modes_spec.rb | 1 + spec/support/dummy/schema.rb | 2 +- spec/support/lazy_helpers.rb | 15 ++++++-- spec/support/star_wars/schema.rb | 2 +- 23 files changed, 152 insertions(+), 18 deletions(-) create mode 100644 lib/graphql/dataloader/lazy_source.rb diff --git a/lib/graphql/dataloader.rb b/lib/graphql/dataloader.rb index 92ddac4f6c..97a4209467 100644 --- a/lib/graphql/dataloader.rb +++ b/lib/graphql/dataloader.rb @@ -6,6 +6,7 @@ require "graphql/dataloader/source" require "graphql/dataloader/active_record_association_source" require "graphql/dataloader/active_record_source" +require "graphql/dataloader/lazy_source" module GraphQL # This plugin supports Fiber-based concurrency, along with {GraphQL::Dataloader::Source}. @@ -69,6 +70,8 @@ def initialize(nonblocking: self.class.default_nonblocking, fiber_limit: self.cl # @return [Integer, nil] attr_reader :fiber_limit + attr_reader :source_cache + def nonblocking? @nonblocking end @@ -211,8 +214,18 @@ def run end join_queues(job_fibers, next_job_fibers) - while (!source_fibers.empty? || @source_cache.each_value.any? { |group_sources| group_sources.each_value.any?(&:pending?) }) - while (f = source_fibers.shift || (((job_fibers.size + source_fibers.size + next_source_fibers.size + next_job_fibers.size) < total_fiber_limit) && spawn_source_fiber(trace))) + defer_sources = nil + @source_cache.each_value do |group_sources| + group_sources.each_value do |source_inst| + if source_inst.defer? + defer_sources ||= [] + defer_sources << source_inst + end + end + end + + while (!source_fibers.empty? || @source_cache.each_value.any? { |group_sources| group_sources.each_value.any? { |s| s.pending? && (defer_sources ? !defer_sources.include?(s) : true) } }) + while (f = source_fibers.shift || (((job_fibers.size + source_fibers.size + next_source_fibers.size + next_job_fibers.size) < total_fiber_limit) && spawn_source_fiber(trace, defer_sources))) if f.alive? finished = run_fiber(f) if !finished @@ -304,11 +317,11 @@ def spawn_job_fiber(trace) end end - def spawn_source_fiber(trace) + def spawn_source_fiber(trace, defer_sources) pending_sources = nil @source_cache.each_value do |source_by_batch_params| source_by_batch_params.each_value do |source| - if source.pending? + if source.pending? && !defer_sources&.include?(source) pending_sources ||= [] pending_sources << source end diff --git a/lib/graphql/dataloader/lazy_source.rb b/lib/graphql/dataloader/lazy_source.rb new file mode 100644 index 0000000000..44ae1deb91 --- /dev/null +++ b/lib/graphql/dataloader/lazy_source.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true +require "graphql/dataloader/source" + +module GraphQL + class Dataloader + class LazySource < GraphQL::Dataloader::Source + def initialize(phase, context) + @context = context + @phase = phase + end + + def fetch(lazies) + lazies.map do |l| + @context.schema.sync_lazy(l) + rescue StandardError => err + err + end + end + + attr_reader :phase + + def defer? + @phase == :field_resolve && dataloader.source_cache[self.class].any? { |k, v| v.phase == :object_wrap && v.pending? } + end + end + end +end diff --git a/lib/graphql/dataloader/source.rb b/lib/graphql/dataloader/source.rb index 86ce88726e..9732bf60c3 100644 --- a/lib/graphql/dataloader/source.rb +++ b/lib/graphql/dataloader/source.rb @@ -80,6 +80,10 @@ def load_all(values) result_keys.map { |k| result_for(k) } end + def defer? + false + end + # Subclasses must implement this method to return a value for each of `keys` # @param keys [Array] keys passed to {#load}, {#load_all}, {#request}, or {#request_all} # @return [Array] A loaded value for each of `keys`. The array must match one-for-one to the list of `keys`. diff --git a/lib/graphql/execution/interpreter/runtime.rb b/lib/graphql/execution/interpreter/runtime.rb index d5b700391b..b1cf29e29f 100644 --- a/lib/graphql/execution/interpreter/runtime.rb +++ b/lib/graphql/execution/interpreter/runtime.rb @@ -54,6 +54,7 @@ def initialize(query:, lazies_at_depth:) end # { Class => Boolean } @lazy_cache = {}.compare_by_identity + @default_lazy_legacy = @schema.legacy_sync_lazy end def final_result @@ -884,13 +885,17 @@ def resolve_type(type, value) end end - def lazy?(object) - obj_class = object.class - is_lazy = @lazy_cache[obj_class] - if is_lazy.nil? - is_lazy = @lazy_cache[obj_class] = @schema.lazy?(object) + def lazy?(object, legacy: @default_lazy_legacy) + if legacy + obj_class = object.class + is_lazy = @lazy_cache[obj_class] + if is_lazy.nil? + is_lazy = @lazy_cache[obj_class] = @schema.lazy?(object, legacy: true) + end + is_lazy + else + false end - is_lazy end end end diff --git a/lib/graphql/execution/lazy.rb b/lib/graphql/execution/lazy.rb index 41849f5f50..cf787ae53f 100644 --- a/lib/graphql/execution/lazy.rb +++ b/lib/graphql/execution/lazy.rb @@ -12,6 +12,41 @@ module Execution # - It has no error-catching functionality # @api private class Lazy + module FieldIntegration + def resolve(_obj, _args, ctx) + result = super + if ctx.runtime.lazy?(result, legacy: true) + ctx.dataloader.with(GraphQL::Dataloader::LazySource, :field_resolve, ctx).load(result) + else + result + end + end + end + + module ObjectIntegration + def self.included(child_class) + child_class.extend(ClassMethods) + child_class.prepend(Authorized) + end + + module ClassMethods + def inherited(child_class) + child_class.prepend(Authorized) + super + end + end + + module Authorized + def authorized?(obj, ctx) + result = super + if ctx.runtime.lazy?(result, legacy: true) + ctx.dataloader.with(GraphQL::Dataloader::LazySource, :object_wrap, ctx).load(result) + else + result + end + end + end + end attr_reader :field # Create a {Lazy} which will get its inner value by calling the block diff --git a/lib/graphql/query/context.rb b/lib/graphql/query/context.rb index 538a03adaa..bf699004d7 100644 --- a/lib/graphql/query/context.rb +++ b/lib/graphql/query/context.rb @@ -270,6 +270,11 @@ def scoped Scoped.new(@scoped_context, current_path) end + # @api private + def runtime + @runtime ||= namespace(:interpreter_runtime)[:runtime] + end + class Scoped def initialize(scoped_context, path) @path = path diff --git a/lib/graphql/schema.rb b/lib/graphql/schema.rb index 7c61856bd2..91d9e5b907 100644 --- a/lib/graphql/schema.rb +++ b/lib/graphql/schema.rb @@ -1322,6 +1322,9 @@ def parse_error(parse_err, ctx) end def lazy_resolve(lazy_class, value_method) + if !legacy_sync_lazy && self.dataloader_class == GraphQL::Dataloader::NullDataloader + warn "`#{self}.lazy_resolve` requires `self.legacy_sync_lazy` or `use GraphQL::Dataloader` to be configured first\n #{caller(1, 1).first}" + end lazy_methods.set(lazy_class, value_method) end @@ -1618,6 +1621,16 @@ def after_lazy(value, &block) end end + def legacy_sync_lazy(new_value = NOT_CONFIGURED) + if NOT_CONFIGURED.equal?(new_value) + @legacy_sync_lazy = new_value + elsif superclass.respond_to?(:legacy_sync_lazy) + superclass.legacy_sync_lazy + else + false + end + end + # Override this method to handle lazy objects in a custom way. # @param value [Object] an instance of a class registered with {.lazy_resolve} # @return [Object] A GraphQL-ready (non-lazy) object @@ -1638,8 +1651,12 @@ def lazy_method_name(obj) end # @return [Boolean] True if this object should be lazily resolved - def lazy?(obj) - !!lazy_method_name(obj) + def lazy?(obj, legacy: false) + if legacy == true || legacy_sync_lazy + !!lazy_method_name(obj) + else + false + end end # Return a lazy if any of `maybe_lazies` are lazy, diff --git a/spec/graphql/authorization_spec.rb b/spec/graphql/authorization_spec.rb index b49af2aa44..b761b218bc 100644 --- a/spec/graphql/authorization_spec.rb +++ b/spec/graphql/authorization_spec.rb @@ -351,6 +351,7 @@ class Schema < GraphQL::Schema mutation(Mutation) directive(Nothing) use GraphQL::Schema::Warden if ADD_WARDEN + legacy_sync_lazy(true) lazy_resolve(Box, :value) def self.unauthorized_object(err) @@ -369,6 +370,7 @@ def self.unauthorized_object(err) class SchemaWithFieldHook < GraphQL::Schema query(Query) use GraphQL::Schema::Warden if ADD_WARDEN + legacy_sync_lazy(true) lazy_resolve(Box, :value) def self.unauthorized_field(err) @@ -651,6 +653,7 @@ def auth_execute(*args, **kwargs) it "returns nil if not authorized" do query = "{ unauthorized }" response = AuthTest::SchemaWithFieldHook.execute(query, root_value: 34, context: { lazy_field_authorized: false }) + assert_nil response["data"].fetch("unauthorized") assert_equal ["Unauthorized field unauthorized on Query: 34"], response["errors"].map { |e| e["message"] } end diff --git a/spec/graphql/execution/errors_spec.rb b/spec/graphql/execution/errors_spec.rb index 7433d1bbf6..bd787cc231 100644 --- a/spec/graphql/execution/errors_spec.rb +++ b/spec/graphql/execution/errors_spec.rb @@ -171,6 +171,7 @@ def error_in_each end query(Query) + legacy_sync_lazy(true) lazy_resolve(Proc, :call) def self.object_from_id(id, ctx) diff --git a/spec/graphql/execution/interpreter_spec.rb b/spec/graphql/execution/interpreter_spec.rb index 87c4fd0345..3f85912fc6 100644 --- a/spec/graphql/execution/interpreter_spec.rb +++ b/spec/graphql/execution/interpreter_spec.rb @@ -276,6 +276,7 @@ def increment_counter class Schema < GraphQL::Schema query(Query) mutation(Mutation) + legacy_sync_lazy(true) lazy_resolve(Box, :value) use GraphQL::Schema::AlwaysVisible @@ -674,6 +675,7 @@ class Subscription < GraphQL::Schema::Object query Query subscription Subscription use InMemoryBackend::Subscriptions, extra: nil + legacy_sync_lazy(true) lazy_resolve Proc, :call end diff --git a/spec/graphql/query/context/scoped_context_spec.rb b/spec/graphql/query/context/scoped_context_spec.rb index a752114ac5..2dabb0b19e 100644 --- a/spec/graphql/query/context/scoped_context_spec.rb +++ b/spec/graphql/query/context/scoped_context_spec.rb @@ -78,6 +78,7 @@ def things end query(Query) + legacy_sync_lazy(true) lazy_resolve(Promise, :value) end diff --git a/spec/graphql/query/context_spec.rb b/spec/graphql/query/context_spec.rb index 56150903f1..f36e214295 100644 --- a/spec/graphql/query/context_spec.rb +++ b/spec/graphql/query/context_spec.rb @@ -227,6 +227,7 @@ def set_scoped_int class ContextSchema < GraphQL::Schema query(ContextQuery) + legacy_sync_lazy(true) lazy_resolve(LazyBlock, :value) use GraphQL::Dataloader end diff --git a/spec/graphql/schema/argument_spec.rb b/spec/graphql/schema/argument_spec.rb index b156c1cbe5..c3ee2f9b10 100644 --- a/spec/graphql/schema/argument_spec.rb +++ b/spec/graphql/schema/argument_spec.rb @@ -87,6 +87,7 @@ def context_arg_test(input:) class Schema < GraphQL::Schema query(Query) + legacy_sync_lazy(true) lazy_resolve(Proc, :call) def self.object_from_id(id, ctx) diff --git a/spec/graphql/schema/directive_spec.rb b/spec/graphql/schema/directive_spec.rb index e59c351708..644a51007e 100644 --- a/spec/graphql/schema/directive_spec.rb +++ b/spec/graphql/schema/directive_spec.rb @@ -184,6 +184,7 @@ def fetch(names) class Schema < GraphQL::Schema query(Query) directive(CountFields) + legacy_sync_lazy(true) lazy_resolve(Proc, :call) use GraphQL::Dataloader end diff --git a/spec/graphql/schema/input_object_spec.rb b/spec/graphql/schema/input_object_spec.rb index 4948bf31f0..1c64db4d1a 100644 --- a/spec/graphql/schema/input_object_spec.rb +++ b/spec/graphql/schema/input_object_spec.rb @@ -222,6 +222,7 @@ def resolve(list:) class Schema < GraphQL::Schema query(Query) mutation(Mutation) + legacy_sync_lazy(true) lazy_resolve(Proc, :call) def self.object_from_id(id, ctx) @@ -1275,7 +1276,7 @@ class Query < GraphQL::Schema::Object def self.object_from_id(id, ctx) -> { nil } end - + legacy_sync_lazy(true) lazy_resolve(Proc, :call) rescue_from(StandardError) { diff --git a/spec/graphql/schema/member/scoped_spec.rb b/spec/graphql/schema/member/scoped_spec.rb index 8b5fc76074..4b4ad2f4a9 100644 --- a/spec/graphql/schema/member/scoped_spec.rb +++ b/spec/graphql/schema/member/scoped_spec.rb @@ -120,6 +120,7 @@ def lazy_items end query(Query) + legacy_sync_lazy(true) lazy_resolve(Proc, :call) end diff --git a/spec/graphql/schema/resolver_spec.rb b/spec/graphql/schema/resolver_spec.rb index 49e38a8059..b1b5dbaf34 100644 --- a/spec/graphql/schema/resolver_spec.rb +++ b/spec/graphql/schema/resolver_spec.rb @@ -500,6 +500,7 @@ def resolve(*) class Schema < GraphQL::Schema query(Query) mutation(Mutation) + legacy_sync_lazy(true) lazy_resolve LazyBlock, :value orphan_types IntegerWrapper diff --git a/spec/graphql/tracing/appsignal_trace_spec.rb b/spec/graphql/tracing/appsignal_trace_spec.rb index 4e7c9ab96e..bd07b6404e 100644 --- a/spec/graphql/tracing/appsignal_trace_spec.rb +++ b/spec/graphql/tracing/appsignal_trace_spec.rb @@ -69,6 +69,7 @@ def thing; :thing; end class TestSchema < GraphQL::Schema query(Query) trace_with(GraphQL::Tracing::AppsignalTrace) + legacy_sync_lazy(true) lazy_resolve(IntBox, :value) end end @@ -100,6 +101,7 @@ class AppsignalAndDatadogTestSchema < GraphQL::Schema query(AppsignalTraceTest::Query) trace_with(GraphQL::Tracing::DataDogTrace) trace_with(GraphQL::Tracing::AppsignalTrace) + legacy_sync_lazy(true) lazy_resolve(IntBox, :value) end @@ -108,6 +110,7 @@ class AppsignalAndDatadogReverseOrderTestSchema < GraphQL::Schema # Include these modules in different order than above: trace_with(GraphQL::Tracing::AppsignalTrace) trace_with(GraphQL::Tracing::DataDogTrace) + legacy_sync_lazy(true) lazy_resolve(IntBox, :value) end diff --git a/spec/graphql/tracing/data_dog_trace_spec.rb b/spec/graphql/tracing/data_dog_trace_spec.rb index d103b4e335..352b31e77c 100644 --- a/spec/graphql/tracing/data_dog_trace_spec.rb +++ b/spec/graphql/tracing/data_dog_trace_spec.rb @@ -33,6 +33,7 @@ def thing; :thing; end class TestSchema < GraphQL::Schema query(Query) trace_with(GraphQL::Tracing::DataDogTrace) + legacy_sync_lazy(true) lazy_resolve(Box, :value) end @@ -45,6 +46,7 @@ def prepare_span(trace_key, object, span) end query(Query) trace_with(CustomDataDogTracing) + legacy_sync_lazy(true) lazy_resolve(Box, :value) end end diff --git a/spec/graphql/tracing/trace_modes_spec.rb b/spec/graphql/tracing/trace_modes_spec.rb index f668df6381..a35e1a7689 100644 --- a/spec/graphql/tracing/trace_modes_spec.rb +++ b/spec/graphql/tracing/trace_modes_spec.rb @@ -286,6 +286,7 @@ def execute_query(query) CustomTraceClass = Class.new(GraphQL::Tracing::Trace) class BaseSchemaWithCustomTraceClass < GraphQL::Schema + legacy_sync_lazy(true) use(GraphQL::Batch) trace_class(CustomTraceClass) trace_with(SomeTraceMod) diff --git a/spec/support/dummy/schema.rb b/spec/support/dummy/schema.rb index 0fbcb6c7f1..9235e6c47c 100644 --- a/spec/support/dummy/schema.rb +++ b/spec/support/dummy/schema.rb @@ -569,7 +569,7 @@ def self.type_error(err, ctx) end use GraphQL::Dataloader - + legacy_sync_lazy(true) lazy_resolve(Proc, :call) end diff --git a/spec/support/lazy_helpers.rb b/spec/support/lazy_helpers.rb index b6f3d60581..411466ae92 100644 --- a/spec/support/lazy_helpers.rb +++ b/spec/support/lazy_helpers.rb @@ -49,7 +49,16 @@ def self.all end end - class LazySum < GraphQL::Schema::Object + class BaseField < GraphQL::Schema::Field + include GraphQL::Execution::Lazy::FieldIntegration + end + + class BaseObject < GraphQL::Schema::Object + include GraphQL::Execution::Lazy::ObjectIntegration + field_class(BaseField) + end + + class LazySum < BaseObject field :value, Integer def value if object == MAGIC_NUMBER_THAT_RAISES_ERROR @@ -85,7 +94,7 @@ def nested_sum(value:) alias :nullable_nested_sum :nested_sum end - class LazyQuery < GraphQL::Schema::Object + class LazyQuery < BaseObject field :int, Integer, null: false do argument :value, Integer argument :plus, Integer, required: false, default_value: 0 @@ -178,11 +187,11 @@ def add_check(object, text) class LazySchema < GraphQL::Schema query(LazyQuery) mutation(LazyQuery) + use GraphQL::Dataloader lazy_resolve(Wrapper, :item) lazy_resolve(SumAll, :value) trace_with(SumAllInstrumentation2) trace_with(SumAllInstrumentation) - def self.sync_lazy(lazy) if lazy.is_a?(SumAll) && lazy.own_value > 1000 lazy.value # clear the previous set diff --git a/spec/support/star_wars/schema.rb b/spec/support/star_wars/schema.rb index 4d73d1e0a5..04d4ca7088 100644 --- a/spec/support/star_wars/schema.rb +++ b/spec/support/star_wars/schema.rb @@ -420,7 +420,7 @@ def self.object_from_id(node_id, ctx) def self.id_from_object(object, type, ctx) GraphQL::Schema::UniqueWithinType.encode(type.graphql_name, object.id) end - + legacy_sync_lazy(true) lazy_resolve(LazyWrapper, :value) lazy_resolve(LazyLoader, :value) end From 44c256684c75afcd3b92ff6e5396eff2870a0ce7 Mon Sep 17 00:00:00 2001 From: Robert Mosolgo Date: Wed, 2 Apr 2025 16:29:13 -0400 Subject: [PATCH 2/6] Fix module mix-in --- lib/graphql/execution/lazy.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/graphql/execution/lazy.rb b/lib/graphql/execution/lazy.rb index cf787ae53f..9169b1d4d4 100644 --- a/lib/graphql/execution/lazy.rb +++ b/lib/graphql/execution/lazy.rb @@ -26,12 +26,12 @@ def resolve(_obj, _args, ctx) module ObjectIntegration def self.included(child_class) child_class.extend(ClassMethods) - child_class.prepend(Authorized) + child_class.singleton_class.prepend(Authorized) end module ClassMethods def inherited(child_class) - child_class.prepend(Authorized) + child_class.singleton_class.prepend(Authorized) super end end From c2eb2b4f50b78dbdb46af463454acf964d1e595b Mon Sep 17 00:00:00 2001 From: Robert Mosolgo Date: Mon, 14 Apr 2025 14:43:35 -0400 Subject: [PATCH 3/6] Add lazy_compat_mode and GRAPHQL_FUTURE option --- .github/workflows/ci.yaml | 8 ++--- lib/graphql/dataloader.rb | 36 ++++++++++++++++++---- spec/graphql/execution/interpreter_spec.rb | 6 +++- spec/spec_helper.rb | 10 +++++- spec/support/dummy/schema.rb | 3 +- 5 files changed, 49 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9153791a03..3a05bac1eb 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -57,18 +57,18 @@ jobs: ruby: 3.3 - gemfile: gemfiles/rails_master.gemfile ruby: 3.3 - graphql_reject_numbers_followed_by_names: 1 + graphql_future: 1 redis: 1 - gemfile: gemfiles/rails_master.gemfile ruby: 3.4 - graphql_reject_numbers_followed_by_names: 1 + graphql_future: 1 isolation_level_fiber: 1 redis: 1 runs-on: ubuntu-latest steps: - run: echo BUNDLE_GEMFILE=${{ matrix.gemfile }} > $GITHUB_ENV - - run: echo GRAPHQL_REJECT_NUMBERS_FOLLOWED_BY_NAMES=1 > $GITHUB_ENV - if: ${{ !!matrix.graphql_reject_numbers_followed_by_names }} + - run: echo GRAPHQL_FUTURE=1 > $GITHUB_ENV + if: ${{ !!matrix.graphql_future }} - run: echo ISOLATION_LEVEL_FIBER=1 > $GITHUB_ENV if: ${{ !!matrix.isolation_level_fiber }} - uses: shogo82148/actions-setup-redis@v1 diff --git a/lib/graphql/dataloader.rb b/lib/graphql/dataloader.rb index 97a4209467..b15e90db72 100644 --- a/lib/graphql/dataloader.rb +++ b/lib/graphql/dataloader.rb @@ -27,10 +27,10 @@ module GraphQL # class Dataloader class << self - attr_accessor :default_nonblocking, :default_fiber_limit + attr_accessor :default_nonblocking, :default_fiber_limit, :default_lazy_compat_mode end - def self.use(schema, nonblocking: nil, fiber_limit: nil) + def self.use(schema, nonblocking: nil, fiber_limit: nil, lazy_compat_mode: false) dataloader_class = if nonblocking warn("`nonblocking: true` is deprecated from `GraphQL::Dataloader`, please use `GraphQL::Dataloader::AsyncDataloader` instead. Docs: https://graphql-ruby.org/dataloader/async_dataloader.") Class.new(self) { self.default_nonblocking = true } @@ -43,6 +43,13 @@ def self.use(schema, nonblocking: nil, fiber_limit: nil) dataloader_class.default_fiber_limit = fiber_limit end + if lazy_compat_mode + if dataloader_class == self + dataloader_class = Class.new(dataloader_class) + end + dataloader_class.default_lazy_compat_mode = lazy_compat_mode + end + schema.dataloader_class = dataloader_class end @@ -58,13 +65,15 @@ def self.with_dataloading(&block) result end - def initialize(nonblocking: self.class.default_nonblocking, fiber_limit: self.class.default_fiber_limit) + def initialize(nonblocking: self.class.default_nonblocking, fiber_limit: self.class.default_fiber_limit, lazy_compat_mode: self.class.default_lazy_compat_mode) @source_cache = Hash.new { |h, k| h[k] = {} } @pending_jobs = [] if !nonblocking.nil? @nonblocking = nonblocking end @fiber_limit = fiber_limit + @running_jobs = false + @lazy_compat_mode = lazy_compat_mode end # @return [Integer, nil] @@ -144,9 +153,17 @@ def yield(source = Fiber[:__graphql_current_dataloader_source]) # @api private Nothing to see here def append_job(&job) - # Given a block, queue it up to be worked through when `#run` is called. - # (If the dataloader is already running, than a Fiber will pick this up later.) - @pending_jobs.push(job) + # This is to match GraphQL-Batch-type execution. + # In that approach, a field's children would be resolved right after the parent. + # But the default dataloader approach is to run siblings, then "cousin" fields -- each depth in a wave. + # This option restores the flow of lazy_resolve. + if @lazy_compat_mode && @running_jobs + job.call + else + # Given a block, queue it up to be worked through when `#run` is called. + # (If the dataloader is already running, than a Fiber will pick this up later.) + @pending_jobs.push(job) + end nil end @@ -163,6 +180,7 @@ def clear_cache def run_isolated prev_queue = @pending_jobs prev_pending_keys = {} + prev_running_jobs = @running_jobs @source_cache.each do |source_class, batched_sources| batched_sources.each do |batch_args, batched_source_instance| if batched_source_instance.pending? @@ -173,6 +191,7 @@ def run_isolated end @pending_jobs = [] + @running_jobs = false res = nil # Make sure the block is inside a Fiber, so it can `Fiber.yield` append_job { @@ -182,6 +201,7 @@ def run_isolated res ensure @pending_jobs = prev_queue + @running_jobs = prev_running_jobs prev_pending_keys.each do |source_instance, pending| pending.each do |key, value| if !source_instance.results.key?(key) @@ -204,6 +224,7 @@ def run while first_pass || !job_fibers.empty? first_pass = false + @running_jobs = true while (f = (job_fibers.shift || (((next_job_fibers.size + job_fibers.size) < jobs_fiber_limit) && spawn_job_fiber(trace)))) if f.alive? finished = run_fiber(f) @@ -212,6 +233,7 @@ def run end end end + @running_jobs = false join_queues(job_fibers, next_job_fibers) defer_sources = nil @@ -255,6 +277,8 @@ def run rescue UncaughtThrowError => e throw e.tag, e.value + ensure + @running_jobs = false end def run_fiber(f) diff --git a/spec/graphql/execution/interpreter_spec.rb b/spec/graphql/execution/interpreter_spec.rb index 4aa74fad31..83da04a147 100644 --- a/spec/graphql/execution/interpreter_spec.rb +++ b/spec/graphql/execution/interpreter_spec.rb @@ -241,6 +241,10 @@ class Counter < GraphQL::Schema::Object field :value, Integer, null: false field :lazy_value, Integer, null: false + def value + object.value + end + def lazy_value Box.new { object.value } end @@ -266,7 +270,7 @@ def increment_counter class Schema < GraphQL::Schema query(Query) mutation(Mutation) - legacy_sync_lazy(true) + dataloader_lazy_setup(self) lazy_resolve(Box, :value) use GraphQL::Schema::AlwaysVisible diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 26f41e0492..b5ce908df3 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -32,7 +32,15 @@ USING_C_PARSER = false end -if ENV["GRAPHQL_REJECT_NUMBERS_FOLLOWED_BY_NAMES"] +def dataloader_lazy_setup(schema) + if ENV["GRAPHQL_FUTURE"] + schema.use(GraphQL::Dataloader, lazy_compat_mode: true) + else + legacy_sync_lazy(true) + end +end + +if ENV["GRAPHQL_FUTURE"] puts "Opting into GraphQL.reject_numbers_followed_by_names" GraphQL.reject_numbers_followed_by_names = true puts "Opting into GraphQL::Schema::Visibility::Profile" diff --git a/spec/support/dummy/schema.rb b/spec/support/dummy/schema.rb index 9235e6c47c..c4651457cf 100644 --- a/spec/support/dummy/schema.rb +++ b/spec/support/dummy/schema.rb @@ -568,8 +568,7 @@ def self.type_error(err, ctx) end end - use GraphQL::Dataloader - legacy_sync_lazy(true) + dataloader_lazy_setup(self) lazy_resolve(Proc, :call) end From b9dd15af908791f0ae5caf59aead0a88c53ea8a1 Mon Sep 17 00:00:00 2001 From: Robert Mosolgo Date: Mon, 14 Apr 2025 15:40:07 -0400 Subject: [PATCH 4/6] Migrate some examples to use lazies-by-dataloader --- lib/graphql/analysis.rb | 1 + lib/graphql/execution/interpreter.rb | 9 +---- lib/graphql/execution/interpreter/runtime.rb | 3 +- lib/graphql/query.rb | 5 +++ lib/graphql/schema.rb | 39 ++++++++++++------- lib/graphql/schema/member/has_arguments.rb | 3 ++ spec/graphql/authorization_spec.rb | 4 +- spec/graphql/execution/errors_spec.rb | 2 +- spec/graphql/execution/interpreter_spec.rb | 2 +- .../query/context/scoped_context_spec.rb | 2 +- spec/graphql/query/context_spec.rb | 2 +- spec/graphql/schema/argument_spec.rb | 4 +- spec/graphql/schema/directive_spec.rb | 2 +- spec/graphql/schema/input_object_spec.rb | 4 +- spec/graphql/schema/member/scoped_spec.rb | 2 +- spec/graphql/schema/resolver_spec.rb | 2 +- spec/graphql/tracing/appsignal_trace_spec.rb | 6 +-- spec/graphql/tracing/data_dog_trace_spec.rb | 4 +- spec/graphql/tracing/trace_modes_spec.rb | 2 +- spec/spec_helper.rb | 4 +- spec/support/dummy/schema.rb | 2 +- spec/support/star_wars/schema.rb | 2 +- 22 files changed, 62 insertions(+), 44 deletions(-) diff --git a/lib/graphql/analysis.rb b/lib/graphql/analysis.rb index b90ebf8e25..0b30577f3a 100644 --- a/lib/graphql/analysis.rb +++ b/lib/graphql/analysis.rb @@ -54,6 +54,7 @@ def analyze_multiplex(multiplex, analyzers) # @param analyzers [Array] # @return [Array] Results from those analyzers def analyze_query(query, analyzers, multiplex_analyzers: []) + query.init_runtime(lazies_at_depth: nil) query.current_trace.analyze_query(query: query) do query_analyzers = analyzers .map { |analyzer| analyzer.new(query) } diff --git a/lib/graphql/execution/interpreter.rb b/lib/graphql/execution/interpreter.rb index 4b6a60099e..01ab389f04 100644 --- a/lib/graphql/execution/interpreter.rb +++ b/lib/graphql/execution/interpreter.rb @@ -43,6 +43,7 @@ def run_all(schema, query_options, context: {}, max_complexity: schema.max_compl schema = multiplex.schema queries = multiplex.queries lazies_at_depth = Hash.new { |h, k| h[k] = [] } + queries.each { |q| q.init_runtime(lazies_at_depth: lazies_at_depth) } multiplex_analyzers = schema.multiplex_analyzers if multiplex.max_complexity multiplex_analyzers += [GraphQL::Analysis::MaxQueryComplexity] @@ -70,14 +71,8 @@ def run_all(schema, query_options, context: {}, max_complexity: schema.max_compl NO_OPERATION else begin - # Although queries in a multiplex _share_ an Interpreter instance, - # they also have another item of state, which is private to that query - # in particular, assign it here: - runtime = Runtime.new(query: query, lazies_at_depth: lazies_at_depth) - query.context.namespace(:interpreter_runtime)[:runtime] = runtime - query.current_trace.execute_query(query: query) do - runtime.run_eager + query.context.runtime.run_eager end rescue GraphQL::ExecutionError => err query.context.errors << err diff --git a/lib/graphql/execution/interpreter/runtime.rb b/lib/graphql/execution/interpreter/runtime.rb index 972c448a7b..401f5b4317 100644 --- a/lib/graphql/execution/interpreter/runtime.rb +++ b/lib/graphql/execution/interpreter/runtime.rb @@ -38,7 +38,7 @@ def current_object def initialize(query:, lazies_at_depth:) @query = query @current_trace = query.current_trace - @dataloader = query.multiplex.dataloader + @dataloader = query.context.dataloader @lazies_at_depth = lazies_at_depth @schema = query.schema @context = query.context @@ -877,7 +877,6 @@ def resolve_type(type, value) query.resolve_type(type, value) end @current_trace.end_resolve_type(type, value, context, resolved_type) - if lazy?(resolved_type) GraphQL::Execution::Lazy.new do @current_trace.begin_resolve_type(type, value, context) diff --git a/lib/graphql/query.rb b/lib/graphql/query.rb index bf4813a406..a48862e485 100644 --- a/lib/graphql/query.rb +++ b/lib/graphql/query.rb @@ -419,6 +419,11 @@ def after_lazy(value, &block) attr_reader :logger + def init_runtime(lazies_at_depth:) + runtime = Execution::Interpreter::Runtime.new(query: self, lazies_at_depth: lazies_at_depth) + context.namespace(:interpreter_runtime)[:runtime] = runtime + end + private def find_operation(operations, operation_name) diff --git a/lib/graphql/schema.rb b/lib/graphql/schema.rb index eb7e13fc72..562272baf8 100644 --- a/lib/graphql/schema.rb +++ b/lib/graphql/schema.rb @@ -1154,20 +1154,33 @@ def resolve_type(type, obj, ctx) super end - after_lazy(maybe_lazy_resolve_type_result) do |resolve_type_result| - if resolve_type_result.is_a?(Array) && resolve_type_result.size == 2 - resolved_type = resolve_type_result[0] - resolved_value = resolve_type_result[1] - else - resolved_type = resolve_type_result - resolved_value = obj + if legacy_sync_lazy + after_lazy(maybe_lazy_resolve_type_result) do |resolve_type_result| + process_resolve_type_result(type, obj, ctx, resolve_type_result) end + elsif lazy?(maybe_lazy_resolve_type_result, legacy: true) + res = ctx.dataloader.with(Dataloader::LazySource, :resolve_type, ctx).load(maybe_lazy_resolve_type_result) + process_resolve_type_result(type, obj, ctx, res) + else + process_resolve_type_result(type, obj, ctx, maybe_lazy_resolve_type_result) + end + end - if resolved_type.nil? || (resolved_type.is_a?(Module) && resolved_type.respond_to?(:kind)) - [resolved_type, resolved_value] - else - raise ".resolve_type should return a type definition, but got #{resolved_type.inspect} (#{resolved_type.class}) from `resolve_type(#{type}, #{obj}, #{ctx})`" - end + private + + def process_resolve_type_result(type, obj, ctx, resolve_type_result) + if resolve_type_result.is_a?(Array) && resolve_type_result.size == 2 + resolved_type = resolve_type_result[0] + resolved_value = resolve_type_result[1] + else + resolved_type = resolve_type_result + resolved_value = obj + end + + if resolved_type.nil? || (resolved_type.is_a?(Module) && resolved_type.respond_to?(:kind)) + [resolved_type, resolved_value] + else + raise ".resolve_type should return a type definition, but got #{resolved_type.inspect} (#{resolved_type.class}) from `resolve_type(#{type}, #{obj}, #{ctx})`" end end end @@ -1633,7 +1646,7 @@ def after_lazy(value, &block) end def legacy_sync_lazy(new_value = NOT_CONFIGURED) - if NOT_CONFIGURED.equal?(new_value) + if !NOT_CONFIGURED.equal?(new_value) @legacy_sync_lazy = new_value elsif superclass.respond_to?(:legacy_sync_lazy) superclass.legacy_sync_lazy diff --git a/lib/graphql/schema/member/has_arguments.rb b/lib/graphql/schema/member/has_arguments.rb index 323850b997..8dd423fa1a 100644 --- a/lib/graphql/schema/member/has_arguments.rb +++ b/lib/graphql/schema/member/has_arguments.rb @@ -345,6 +345,9 @@ def load_application_object(argument, id, context) def load_and_authorize_application_object(argument, id, context) loaded_application_object = load_application_object(argument, id, context) + if context.runtime.lazy?(loaded_application_object, legacy: true) + loaded_application_object = context.dataloader.with(Dataloader::LazySource, :loads, context).load(loaded_application_object) + end authorize_application_object(argument, id, context, loaded_application_object) end diff --git a/spec/graphql/authorization_spec.rb b/spec/graphql/authorization_spec.rb index b761b218bc..f6c803a8fc 100644 --- a/spec/graphql/authorization_spec.rb +++ b/spec/graphql/authorization_spec.rb @@ -351,7 +351,7 @@ class Schema < GraphQL::Schema mutation(Mutation) directive(Nothing) use GraphQL::Schema::Warden if ADD_WARDEN - legacy_sync_lazy(true) + dataloader_lazy_setup(self) lazy_resolve(Box, :value) def self.unauthorized_object(err) @@ -370,7 +370,7 @@ def self.unauthorized_object(err) class SchemaWithFieldHook < GraphQL::Schema query(Query) use GraphQL::Schema::Warden if ADD_WARDEN - legacy_sync_lazy(true) + dataloader_lazy_setup(self) lazy_resolve(Box, :value) def self.unauthorized_field(err) diff --git a/spec/graphql/execution/errors_spec.rb b/spec/graphql/execution/errors_spec.rb index 8fe81280f9..3ec452c1f3 100644 --- a/spec/graphql/execution/errors_spec.rb +++ b/spec/graphql/execution/errors_spec.rb @@ -171,7 +171,7 @@ def error_in_each end query(Query) - legacy_sync_lazy(true) + dataloader_lazy_setup(self) lazy_resolve(Proc, :call) def self.object_from_id(id, ctx) diff --git a/spec/graphql/execution/interpreter_spec.rb b/spec/graphql/execution/interpreter_spec.rb index 83da04a147..4d12f6ac5b 100644 --- a/spec/graphql/execution/interpreter_spec.rb +++ b/spec/graphql/execution/interpreter_spec.rb @@ -652,7 +652,7 @@ class Subscription < GraphQL::Schema::Object query Query subscription Subscription use InMemoryBackend::Subscriptions, extra: nil - legacy_sync_lazy(true) + dataloader_lazy_setup(self) lazy_resolve Proc, :call end diff --git a/spec/graphql/query/context/scoped_context_spec.rb b/spec/graphql/query/context/scoped_context_spec.rb index 2dabb0b19e..ffb0975227 100644 --- a/spec/graphql/query/context/scoped_context_spec.rb +++ b/spec/graphql/query/context/scoped_context_spec.rb @@ -78,7 +78,7 @@ def things end query(Query) - legacy_sync_lazy(true) + dataloader_lazy_setup(self) lazy_resolve(Promise, :value) end diff --git a/spec/graphql/query/context_spec.rb b/spec/graphql/query/context_spec.rb index f36e214295..7ea5eda946 100644 --- a/spec/graphql/query/context_spec.rb +++ b/spec/graphql/query/context_spec.rb @@ -227,7 +227,7 @@ def set_scoped_int class ContextSchema < GraphQL::Schema query(ContextQuery) - legacy_sync_lazy(true) + dataloader_lazy_setup(self) lazy_resolve(LazyBlock, :value) use GraphQL::Dataloader end diff --git a/spec/graphql/schema/argument_spec.rb b/spec/graphql/schema/argument_spec.rb index c3ee2f9b10..c8aaf94368 100644 --- a/spec/graphql/schema/argument_spec.rb +++ b/spec/graphql/schema/argument_spec.rb @@ -87,7 +87,8 @@ def context_arg_test(input:) class Schema < GraphQL::Schema query(Query) - legacy_sync_lazy(true) + dataloader_lazy_setup(self) + lazy_resolve(Proc, :call) def self.object_from_id(id, ctx) @@ -746,6 +747,7 @@ def test; end end query(query_type) + use GraphQL::Dataloader lazy_resolve(Proc, :call) end end diff --git a/spec/graphql/schema/directive_spec.rb b/spec/graphql/schema/directive_spec.rb index 58536870d3..7592264ae2 100644 --- a/spec/graphql/schema/directive_spec.rb +++ b/spec/graphql/schema/directive_spec.rb @@ -184,7 +184,7 @@ def fetch(names) class Schema < GraphQL::Schema query(Query) directive(CountFields) - legacy_sync_lazy(true) + dataloader_lazy_setup(self) lazy_resolve(Proc, :call) use GraphQL::Dataloader end diff --git a/spec/graphql/schema/input_object_spec.rb b/spec/graphql/schema/input_object_spec.rb index 84122b376a..294f4fc471 100644 --- a/spec/graphql/schema/input_object_spec.rb +++ b/spec/graphql/schema/input_object_spec.rb @@ -227,7 +227,7 @@ def resolve(list:) class Schema < GraphQL::Schema query(Query) mutation(Mutation) - legacy_sync_lazy(true) + dataloader_lazy_setup(self) lazy_resolve(Proc, :call) def self.object_from_id(id, ctx) @@ -1288,7 +1288,7 @@ class Query < GraphQL::Schema::Object def self.object_from_id(id, ctx) -> { nil } end - legacy_sync_lazy(true) + dataloader_lazy_setup(self) lazy_resolve(Proc, :call) rescue_from(StandardError) { diff --git a/spec/graphql/schema/member/scoped_spec.rb b/spec/graphql/schema/member/scoped_spec.rb index 4b4ad2f4a9..1ade73a6ed 100644 --- a/spec/graphql/schema/member/scoped_spec.rb +++ b/spec/graphql/schema/member/scoped_spec.rb @@ -120,7 +120,7 @@ def lazy_items end query(Query) - legacy_sync_lazy(true) + dataloader_lazy_setup(self) lazy_resolve(Proc, :call) end diff --git a/spec/graphql/schema/resolver_spec.rb b/spec/graphql/schema/resolver_spec.rb index 2106ee3045..689acd74bd 100644 --- a/spec/graphql/schema/resolver_spec.rb +++ b/spec/graphql/schema/resolver_spec.rb @@ -500,7 +500,7 @@ def resolve(*) class Schema < GraphQL::Schema query(Query) mutation(Mutation) - legacy_sync_lazy(true) + dataloader_lazy_setup(self) lazy_resolve LazyBlock, :value orphan_types IntegerWrapper diff --git a/spec/graphql/tracing/appsignal_trace_spec.rb b/spec/graphql/tracing/appsignal_trace_spec.rb index bd07b6404e..b5d878079c 100644 --- a/spec/graphql/tracing/appsignal_trace_spec.rb +++ b/spec/graphql/tracing/appsignal_trace_spec.rb @@ -69,7 +69,7 @@ def thing; :thing; end class TestSchema < GraphQL::Schema query(Query) trace_with(GraphQL::Tracing::AppsignalTrace) - legacy_sync_lazy(true) + dataloader_lazy_setup(self) lazy_resolve(IntBox, :value) end end @@ -101,7 +101,7 @@ class AppsignalAndDatadogTestSchema < GraphQL::Schema query(AppsignalTraceTest::Query) trace_with(GraphQL::Tracing::DataDogTrace) trace_with(GraphQL::Tracing::AppsignalTrace) - legacy_sync_lazy(true) + dataloader_lazy_setup(self) lazy_resolve(IntBox, :value) end @@ -110,7 +110,7 @@ class AppsignalAndDatadogReverseOrderTestSchema < GraphQL::Schema # Include these modules in different order than above: trace_with(GraphQL::Tracing::AppsignalTrace) trace_with(GraphQL::Tracing::DataDogTrace) - legacy_sync_lazy(true) + dataloader_lazy_setup(self) lazy_resolve(IntBox, :value) end diff --git a/spec/graphql/tracing/data_dog_trace_spec.rb b/spec/graphql/tracing/data_dog_trace_spec.rb index 219f8e1694..a11ee3f218 100644 --- a/spec/graphql/tracing/data_dog_trace_spec.rb +++ b/spec/graphql/tracing/data_dog_trace_spec.rb @@ -45,7 +45,7 @@ class TestSchema < GraphQL::Schema query(Query) use GraphQL::Dataloader trace_with(GraphQL::Tracing::DataDogTrace) - legacy_sync_lazy(true) + dataloader_lazy_setup(self) lazy_resolve(Box, :value) end @@ -58,7 +58,7 @@ def prepare_span(trace_key, object, span) end query(Query) trace_with(CustomDataDogTracing) - legacy_sync_lazy(true) + dataloader_lazy_setup(self) lazy_resolve(Box, :value) end end diff --git a/spec/graphql/tracing/trace_modes_spec.rb b/spec/graphql/tracing/trace_modes_spec.rb index a35e1a7689..40602931bf 100644 --- a/spec/graphql/tracing/trace_modes_spec.rb +++ b/spec/graphql/tracing/trace_modes_spec.rb @@ -286,7 +286,7 @@ def execute_query(query) CustomTraceClass = Class.new(GraphQL::Tracing::Trace) class BaseSchemaWithCustomTraceClass < GraphQL::Schema - legacy_sync_lazy(true) + dataloader_lazy_setup(self) use(GraphQL::Batch) trace_class(CustomTraceClass) trace_with(SomeTraceMod) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b5ce908df3..ac9959d0a7 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -32,9 +32,9 @@ USING_C_PARSER = false end -def dataloader_lazy_setup(schema) +def dataloader_lazy_setup(schema, lazy_compat_mode: true) if ENV["GRAPHQL_FUTURE"] - schema.use(GraphQL::Dataloader, lazy_compat_mode: true) + schema.use(GraphQL::Dataloader, lazy_compat_mode: lazy_compat_mode) else legacy_sync_lazy(true) end diff --git a/spec/support/dummy/schema.rb b/spec/support/dummy/schema.rb index c4651457cf..c79c1ccb62 100644 --- a/spec/support/dummy/schema.rb +++ b/spec/support/dummy/schema.rb @@ -568,7 +568,7 @@ def self.type_error(err, ctx) end end - dataloader_lazy_setup(self) + dataloader_lazy_setup(self, lazy_compat_mode: false) lazy_resolve(Proc, :call) end diff --git a/spec/support/star_wars/schema.rb b/spec/support/star_wars/schema.rb index 04d4ca7088..1c8f82f092 100644 --- a/spec/support/star_wars/schema.rb +++ b/spec/support/star_wars/schema.rb @@ -420,7 +420,7 @@ def self.object_from_id(node_id, ctx) def self.id_from_object(object, type, ctx) GraphQL::Schema::UniqueWithinType.encode(type.graphql_name, object.id) end - legacy_sync_lazy(true) + dataloader_lazy_setup(self) lazy_resolve(LazyWrapper, :value) lazy_resolve(LazyLoader, :value) end From bb6d9c4faa14caa117498adea7033f8fd376b6f4 Mon Sep 17 00:00:00 2001 From: Robert Mosolgo Date: Mon, 14 Apr 2025 16:46:02 -0400 Subject: [PATCH 5/6] Fix some tests --- lib/graphql/analysis.rb | 5 +++- lib/graphql/execution/interpreter.rb | 6 ++--- lib/graphql/schema.rb | 39 ++++++++++------------------ spec/graphql/query/context_spec.rb | 4 +++ spec/support/jazz.rb | 1 + 5 files changed, 26 insertions(+), 29 deletions(-) diff --git a/lib/graphql/analysis.rb b/lib/graphql/analysis.rb index 0b30577f3a..08f3d70011 100644 --- a/lib/graphql/analysis.rb +++ b/lib/graphql/analysis.rb @@ -54,7 +54,10 @@ def analyze_multiplex(multiplex, analyzers) # @param analyzers [Array] # @return [Array] Results from those analyzers def analyze_query(query, analyzers, multiplex_analyzers: []) - query.init_runtime(lazies_at_depth: nil) + # If called outside of execution: + if query.context.runtime.nil? + query.init_runtime(lazies_at_depth: nil) + end query.current_trace.analyze_query(query: query) do query_analyzers = analyzers .map { |analyzer| analyzer.new(query) } diff --git a/lib/graphql/execution/interpreter.rb b/lib/graphql/execution/interpreter.rb index 01ab389f04..288f219b62 100644 --- a/lib/graphql/execution/interpreter.rb +++ b/lib/graphql/execution/interpreter.rb @@ -22,6 +22,7 @@ class << self # @param max_complexity [Integer, nil] # @return [Array] One result per query def run_all(schema, query_options, context: {}, max_complexity: schema.max_complexity) + lazies_at_depth = Hash.new { |h, k| h[k] = [] } queries = query_options.map do |opts| query = case opts when Hash @@ -40,10 +41,9 @@ def run_all(schema, query_options, context: {}, max_complexity: schema.max_compl trace = multiplex.current_trace Fiber[:__graphql_current_multiplex] = multiplex trace.execute_multiplex(multiplex: multiplex) do - schema = multiplex.schema queries = multiplex.queries - lazies_at_depth = Hash.new { |h, k| h[k] = [] } - queries.each { |q| q.init_runtime(lazies_at_depth: lazies_at_depth) } + queries.each { |query| query.init_runtime(lazies_at_depth: lazies_at_depth) } + schema = multiplex.schema multiplex_analyzers = schema.multiplex_analyzers if multiplex.max_complexity multiplex_analyzers += [GraphQL::Analysis::MaxQueryComplexity] diff --git a/lib/graphql/schema.rb b/lib/graphql/schema.rb index 562272baf8..b74423573c 100644 --- a/lib/graphql/schema.rb +++ b/lib/graphql/schema.rb @@ -1154,33 +1154,20 @@ def resolve_type(type, obj, ctx) super end - if legacy_sync_lazy - after_lazy(maybe_lazy_resolve_type_result) do |resolve_type_result| - process_resolve_type_result(type, obj, ctx, resolve_type_result) + after_lazy(maybe_lazy_resolve_type_result) do |resolve_type_result| + if resolve_type_result.is_a?(Array) && resolve_type_result.size == 2 + resolved_type = resolve_type_result[0] + resolved_value = resolve_type_result[1] + else + resolved_type = resolve_type_result + resolved_value = obj end - elsif lazy?(maybe_lazy_resolve_type_result, legacy: true) - res = ctx.dataloader.with(Dataloader::LazySource, :resolve_type, ctx).load(maybe_lazy_resolve_type_result) - process_resolve_type_result(type, obj, ctx, res) - else - process_resolve_type_result(type, obj, ctx, maybe_lazy_resolve_type_result) - end - end - - private - def process_resolve_type_result(type, obj, ctx, resolve_type_result) - if resolve_type_result.is_a?(Array) && resolve_type_result.size == 2 - resolved_type = resolve_type_result[0] - resolved_value = resolve_type_result[1] - else - resolved_type = resolve_type_result - resolved_value = obj - end - - if resolved_type.nil? || (resolved_type.is_a?(Module) && resolved_type.respond_to?(:kind)) - [resolved_type, resolved_value] - else - raise ".resolve_type should return a type definition, but got #{resolved_type.inspect} (#{resolved_type.class}) from `resolve_type(#{type}, #{obj}, #{ctx})`" + if resolved_type.nil? || (resolved_type.is_a?(Module) && resolved_type.respond_to?(:kind)) + [resolved_type, resolved_value] + else + raise ".resolve_type should return a type definition, but got #{resolved_type.inspect} (#{resolved_type.class}) from `resolve_type(#{type}, #{obj}, #{ctx})`" + end end end end @@ -1648,6 +1635,8 @@ def after_lazy(value, &block) def legacy_sync_lazy(new_value = NOT_CONFIGURED) if !NOT_CONFIGURED.equal?(new_value) @legacy_sync_lazy = new_value + elsif defined?(@legacy_sync_lazy) + @legacy_sync_lazy elsif superclass.respond_to?(:legacy_sync_lazy) superclass.legacy_sync_lazy else diff --git a/spec/graphql/query/context_spec.rb b/spec/graphql/query/context_spec.rb index 7ea5eda946..eb266e4ee5 100644 --- a/spec/graphql/query/context_spec.rb +++ b/spec/graphql/query/context_spec.rb @@ -176,7 +176,11 @@ def fetch(keys) end end + class BaseField < GraphQL::Schema::Field + include GraphQL::Execution::Lazy::FieldIntegration + end class ContextQuery < GraphQL::Schema::Object + field_class(BaseField) field :get_scoped_context, String do argument :key, String argument :lazy, Boolean, required: false, default_value: false diff --git a/spec/support/jazz.rb b/spec/support/jazz.rb index ded347eee3..208a16f16e 100644 --- a/spec/support/jazz.rb +++ b/spec/support/jazz.rb @@ -916,6 +916,7 @@ def self.object_from_id(id, ctx) BlogPost.has_no_fields(true) extra_types BlogPost use GraphQL::Dataloader + legacy_sync_lazy(true) # for literal use of `Execution::Lazy` above use GraphQL::Schema::Warden if ADD_WARDEN end From 877d5b05c62050008be3ab795d80d50f46492c7a Mon Sep 17 00:00:00 2001 From: Robert Mosolgo Date: Mon, 14 Apr 2025 17:40:39 -0400 Subject: [PATCH 6/6] Fix more tests --- lib/graphql/schema/member/has_arguments.rb | 3 --- spec/graphql/testing/helpers_spec.rb | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/graphql/schema/member/has_arguments.rb b/lib/graphql/schema/member/has_arguments.rb index 8dd423fa1a..323850b997 100644 --- a/lib/graphql/schema/member/has_arguments.rb +++ b/lib/graphql/schema/member/has_arguments.rb @@ -345,9 +345,6 @@ def load_application_object(argument, id, context) def load_and_authorize_application_object(argument, id, context) loaded_application_object = load_application_object(argument, id, context) - if context.runtime.lazy?(loaded_application_object, legacy: true) - loaded_application_object = context.dataloader.with(Dataloader::LazySource, :loads, context).load(loaded_application_object) - end authorize_application_object(argument, id, context, loaded_application_object) end diff --git a/spec/graphql/testing/helpers_spec.rb b/spec/graphql/testing/helpers_spec.rb index a371363ded..f5971af13d 100644 --- a/spec/graphql/testing/helpers_spec.rb +++ b/spec/graphql/testing/helpers_spec.rb @@ -110,6 +110,7 @@ def lookahead_selections(lookahead:) query(Query) use GraphQL::Dataloader + legacy_sync_lazy(true) # TODO this shouldn't be required lazy_resolve Proc, :call def self.unauthorized_object(err)