From 5513f0d0c3d3a28191c7c746e92a857051848de4 Mon Sep 17 00:00:00 2001 From: Pan Thomakos Date: Sat, 24 Nov 2018 21:34:30 -0800 Subject: [PATCH 1/3] Lazy Concurrency Per Evaluation Layer This change adds support for the concurrent resolution of lazy objects. While it is currently possible to return something like a `::Concurrent::Promise.execute` from a GraphQL method, there is currently no way to make this work in tandem with lazy objects. Consider the case in which we would like to use a Gem like `graphql-batch` (or a thread-safe alternative) but execute queries in parallel whenever possible: ``` { post(id: 10) { author { name } comments { count } } } ``` In this query you could imagine that the each of `post`, `author`, and `comments` are separate DB calls. While it may not be possible for `post` and `author` to be executed in parallel, certainly `author` and `comments` can. But we would still like to perform this operation lazily because if this query is expanded: ``` { a: post(id: 10) { author { name } comments { count } } b: post(id: 11) { author { name } comments { count } } } ``` We would like to be able to load the authors for post 10 and 11 in a single query. I have implemented this solution by allowing the `lazy_resolve` directive to accept an additional method name (the concurrent execution method). This method will be called on all layers (breadth first) before the `value` method is called. This ensures that concurrent execution can be delayed until the last possible moment (to enable batching) but it also ensures that multiple batches can be run in parallel if they are resolved in the same graph layer. Although intended for concurrent execution, it is not necessary for this new method to actually perform an operation concurrently (i.e it does not need to return a Thread or anything like that). This allows `graphql-ruby` to not enforce any specific parallel execution primitive (threads or `concurrent-ruby` could be used interchangeably). I know this is a large PR, so I am happy to split it up into multiple PRs if the overall approach is agreeable. --- lib/graphql/execution/execute.rb | 45 ++++++++---- lib/graphql/execution/lazy.rb | 45 +++++++++--- lib/graphql/execution/lazy/lazy_method_map.rb | 7 +- lib/graphql/execution/lazy/resolve.rb | 17 +++-- lib/graphql/field.rb | 27 +++++++- lib/graphql/schema.rb | 66 ++++++++++++++---- .../execution/lazy/lazy_method_map_spec.rb | 16 +++-- spec/graphql/execution/lazy_spec.rb | 32 +++++++-- spec/integration/mongoid/star_trek/schema.rb | 12 +++- spec/integration/rails/graphql/schema_spec.rb | 18 ++++- spec/support/lazy_helpers.rb | 69 +++++++++++++++++++ 11 files changed, 289 insertions(+), 65 deletions(-) diff --git a/lib/graphql/execution/execute.rb b/lib/graphql/execution/execute.rb index 47881b2bb8..f4aad7bff6 100644 --- a/lib/graphql/execution/execute.rb +++ b/lib/graphql/execution/execute.rb @@ -115,20 +115,29 @@ def resolve_field(object, field_ctx) end if field_ctx.schema.lazy?(raw_value) - field_ctx.value = Execution::Lazy.new { - inner_value = field_ctx.trace("execute_field_lazy", {context: field_ctx}) { - begin + field_ctx.value = Execution::Lazy.new( + value: -> { + inner_value = field_ctx.trace("execute_field_lazy", {context: field_ctx}) { begin - field_ctx.field.lazy_resolve(raw_value, arguments, field_ctx) - rescue GraphQL::UnauthorizedError => err - field_ctx.schema.unauthorized_object(err) + begin + field_ctx.field.lazy_resolve(raw_value, arguments, field_ctx) + rescue GraphQL::UnauthorizedError => err + field_ctx.schema.unauthorized_object(err) + end + rescue GraphQL::ExecutionError => err + err end - rescue GraphQL::ExecutionError => err - err + } + continue_or_wait(inner_value, field_ctx.type, field_ctx) + }, + exec: -> { + if field_ctx.schema.concurrent?(raw_value) + field_ctx.trace("execute_field_concurrent", {context: field_ctx}) { + field_ctx.field.concurrent_exec(raw_value, arguments, field_ctx) + } end } - continue_or_wait(inner_value, field_ctx.type, field_ctx) - } + ) else continue_or_wait(raw_value, field_ctx.type, field_ctx) end @@ -143,8 +152,10 @@ def resolve_field(object, field_ctx) # and resolve child fields def continue_or_wait(raw_value, field_type, field_ctx) if field_ctx.schema.lazy?(raw_value) - field_ctx.value = Execution::Lazy.new { - inner_value = begin + field_ctx.value = Execution::Lazy.new( + value: -> { + inner_value = + begin begin field_ctx.schema.sync_lazy(raw_value) rescue GraphQL::UnauthorizedError => err @@ -154,8 +165,14 @@ def continue_or_wait(raw_value, field_type, field_ctx) err end - field_ctx.value = continue_or_wait(inner_value, field_type, field_ctx) - } + field_ctx.value = continue_or_wait(inner_value, field_type, field_ctx) + }, + exec: -> { + if field_ctx.schema.concurrent?(raw_value) + field_ctx.schema.exec_concurrent(raw_value) + end + } + ) else field_ctx.value = continue_resolve_field(raw_value, field_type, field_ctx) end diff --git a/lib/graphql/execution/lazy.rb b/lib/graphql/execution/lazy.rb index 4bb3cd08c7..2f04bbf684 100644 --- a/lib/graphql/execution/lazy.rb +++ b/lib/graphql/execution/lazy.rb @@ -20,18 +20,39 @@ def self.resolve(val) end # Create a {Lazy} which will get its inner value by calling the block - # @param get_value_func [Proc] a block to get the inner value (later) - def initialize(&get_value_func) - @get_value_func = get_value_func + # @param value_proc [Proc] a block to get the inner value (later) + def initialize(original = nil, value:, exec:) + @original = original + @value_proc = value + @exec_proc = exec @resolved = false end + def execute + return if @resolved + + exec = + begin + e = @exec_proc.call + if e.is_a?(Lazy) + e = e.execute + end + e + rescue GraphQL::ExecutionError => err + err + end + + if exec.is_a?(StandardError) + raise exec + end + end + # @return [Object] The wrapped value, calling the lazy block if necessary def value if !@resolved @resolved = true @value = begin - v = @get_value_func.call + v = @value_proc.call if v.is_a?(Lazy) v = v.value end @@ -50,22 +71,24 @@ def value # @return [Lazy] A {Lazy} whose value depends on another {Lazy}, plus any transformations in `block` def then - self.class.new { - yield(value) - } + self.class.new( + value: -> { yield(value) }, + exec: -> { execute } + ) end # @param lazies [Array] Maybe-lazy objects # @return [Lazy] A lazy which will sync all of `lazies` def self.all(lazies) - self.new { - lazies.map { |l| l.is_a?(Lazy) ? l.value : l } - } + self.new( + value: -> { lazies.map { |l| l.is_a?(Lazy) ? l.value : l } }, + exec: -> { lazies.each { |l| l.is_a?(Lazy) ? l.execute : l } } + ) end # This can be used for fields which _had no_ lazy results # @api private - NullResult = Lazy.new(){} + NullResult = Lazy.new(value: -> {}, exec: -> {}) NullResult.value end end diff --git a/lib/graphql/execution/lazy/lazy_method_map.rb b/lib/graphql/execution/lazy/lazy_method_map.rb index e5809b3a8e..4231ae19ca 100644 --- a/lib/graphql/execution/lazy/lazy_method_map.rb +++ b/lib/graphql/execution/lazy/lazy_method_map.rb @@ -24,10 +24,13 @@ def initialize_copy(other) @storage = other.storage.dup end + LazySpec = Struct.new(:value_method, :exec_method) + private_constant :LazySpec + # @param lazy_class [Class] A class which represents a lazy value (subclasses may also be used) # @param lazy_value_method [Symbol] The method to call on this class to get its value - def set(lazy_class, lazy_value_method) - @storage[lazy_class] = lazy_value_method + def set(lazy_class, lazy_value_method, concurrent_exec_method) + @storage[lazy_class] = LazySpec.new(lazy_value_method, concurrent_exec_method) end # @param value [Object] an object which may have a `lazy_value_method` registered for its class or superclasses diff --git a/lib/graphql/execution/lazy/resolve.rb b/lib/graphql/execution/lazy/resolve.rb index c19415f117..a2876d2bae 100644 --- a/lib/graphql/execution/lazy/resolve.rb +++ b/lib/graphql/execution/lazy/resolve.rb @@ -35,12 +35,17 @@ def self.resolve_in_place(value) if acc.empty? Lazy::NullResult else - Lazy.new { - acc.each_with_index { |ctx, idx| - acc[idx] = ctx.value.value - } - resolve_in_place(acc) - } + Lazy.new( + value: -> { + acc.each { |ctx| ctx.value.execute } + + acc.each_with_index { |ctx, idx| + acc[idx] = ctx.value.value + } + resolve_in_place(acc) + }, + exec: -> {} + ) end end diff --git a/lib/graphql/field.rb b/lib/graphql/field.rb index d39ee1c306..479ddd2d38 100644 --- a/lib/graphql/field.rb +++ b/lib/graphql/field.rb @@ -123,6 +123,7 @@ class Field include GraphQL::Define::InstanceDefinable accepts_definitions :name, :description, :deprecation_reason, :resolve, :lazy_resolve, + :concurrent_exec, :type, :arguments, :property, :hash_key, :complexity, :mutation, :function, @@ -138,6 +139,7 @@ class Field :name, :deprecation_reason, :description, :description=, :property, :hash_key, :mutation, :arguments, :complexity, :function, :resolve, :resolve=, :lazy_resolve, :lazy_resolve=, :lazy_resolve_proc, :resolve_proc, + :concurrent_exec, :concurrent_exec=, :concurrent_exec_proc, :type, :type=, :name=, :property=, :hash_key=, :relay_node_field, :relay_nodes_field, :edges?, :edge_class, :subscription_scope, :introspection? @@ -155,6 +157,9 @@ class Field # @return [<#call(obj, args, ctx)>] A proc-like object which can be called trigger a lazy resolution attr_reader :lazy_resolve_proc + # @return [<#call(obj, args, ctx)>] A proc-like object which can be called trigger a concurrent execution + attr_reader :concurrent_exec_proc + # @return [String] The name of this field on its {GraphQL::ObjectType} (or {GraphQL::InterfaceType}) attr_reader :name alias :graphql_name :name @@ -218,6 +223,7 @@ def initialize @arguments = {} @resolve_proc = build_default_resolver @lazy_resolve_proc = DefaultLazyResolve + @concurrent_exec_proc = DefaultConcurrentExec @relay_node_field = false @connection = false @connection_max_page_size = nil @@ -307,12 +313,21 @@ def lazy_resolve=(new_lazy_resolve_proc) @lazy_resolve_proc = new_lazy_resolve_proc end + def concurrent_exec(obj, args, ctx) + @concurrent_exec_proc.call(obj, args, ctx) + end + + def concurrent_exec=(new_concurrent_exec_proc) + @concurrent_exec_proc = new_concurrent_exec_proc + end + # Prepare a lazy value for this field. It may be `then`-ed and resolved later. # @return [GraphQL::Execution::Lazy] A lazy wrapper around `obj` and its registered method name def prepare_lazy(obj, args, ctx) - GraphQL::Execution::Lazy.new { - lazy_resolve(obj, args, ctx) - } + GraphQL::Execution::Lazy.new( + value: -> { lazy_resolve(obj, args, ctx) }, + exec: -> { concurrent_exec(obj, args, ctx) } + ) end private @@ -326,5 +341,11 @@ def self.call(obj, args, ctx) ctx.schema.sync_lazy(obj) end end + + module DefaultConcurrentExec + def self.call(obj, args, ctx) + ctx.schema.exec_concurrent(obj) + end + end end end diff --git a/lib/graphql/schema.rb b/lib/graphql/schema.rb index 258ed40b83..b594c09e34 100644 --- a/lib/graphql/schema.rb +++ b/lib/graphql/schema.rb @@ -92,7 +92,9 @@ class Schema query_analyzer: ->(schema, analyzer) { schema.query_analyzers << analyzer }, multiplex_analyzer: ->(schema, analyzer) { schema.multiplex_analyzers << analyzer }, middleware: ->(schema, middleware) { schema.middleware << middleware }, - lazy_resolve: ->(schema, lazy_class, lazy_value_method) { schema.lazy_methods.set(lazy_class, lazy_value_method) }, + lazy_resolve: ->(schema, lazy_class, lazy_value_method, concurrent_exec_method = nil) { + schema.lazy_methods.set(lazy_class, lazy_value_method, concurrent_exec_method) + }, rescue_from: ->(schema, err_class, &block) { schema.rescue_from(err_class, &block)}, tracer: ->(schema, tracer) { schema.tracers.push(tracer) } @@ -157,7 +159,7 @@ def initialize @parse_error_proc = DefaultParseError @instrumenters = Hash.new { |h, k| h[k] = [] } @lazy_methods = GraphQL::Execution::Lazy::LazyMethodMap.new - @lazy_methods.set(GraphQL::Execution::Lazy, :value) + @lazy_methods.set(GraphQL::Execution::Lazy, :value, :execute) @cursor_encoder = Base64Encoder # Default to the built-in execution strategy: @query_execution_strategy = self.class.default_execution_strategy @@ -604,7 +606,14 @@ class InvalidDocumentError < Error; end; # @return [Symbol, nil] The method name to lazily resolve `obj`, or nil if `obj`'s class wasn't registered wtih {#lazy_resolve}. def lazy_method_name(obj) - @lazy_methods.get(obj) + spec = @lazy_methods.get(obj) + spec && spec.value_method + end + + # @return [Symbol, nil] The method name to concurrently resolve `obj`, or nil if `obj`'s class wasn't registered wtih {#lazy_resolve} with a concurrent method. + def concurrent_method_name(obj) + spec = @lazy_methods.get(obj) + spec && spec.exec_method end # @return [Boolean] True if this object should be lazily resolved @@ -612,6 +621,11 @@ def lazy?(obj) !!lazy_method_name(obj) end + # @return [Boolean] True if this object should be concurrently executed + def concurrent?(obj) + !!concurrent_method_name(obj) + end + # Return the GraphQL IDL for the schema # @param context [Hash] # @param only [<#call(member, ctx)>] @@ -656,7 +670,8 @@ class << self :execute, :multiplex, :static_validator, :introspection_system, :query_analyzers, :tracers, :instrumenters, - :validate, :multiplex_analyzers, :lazy?, :lazy_method_name, :after_lazy, :sync_lazy, + :validate, :multiplex_analyzers, + :lazy?, :lazy_method_name, :concurrent_method_name, :after_lazy, :sync_lazy, # Configuration :max_complexity=, :max_depth=, :metadata, @@ -717,8 +732,8 @@ def to_graphql end end schema_defn.instrumenters[:query] << GraphQL::Schema::Member::Instrumentation - lazy_classes.each do |lazy_class, value_method| - schema_defn.lazy_methods.set(lazy_class, value_method) + lazy_classes.each do |lazy_class, (value_method, exec_method)| + schema_defn.lazy_methods.set(lazy_class, value_method, exec_method) end if @rescues @rescues.each do |err_class, handler| @@ -915,8 +930,8 @@ def type_error(type_err, ctx) DefaultTypeError.call(type_err, ctx) end - def lazy_resolve(lazy_class, value_method) - lazy_classes[lazy_class] = value_method + def lazy_resolve(lazy_class, value_method, exec_method = nil) + lazy_classes[lazy_class] = [value_method, exec_method] end def instrument(instrument_step, instrumenter, options = {}) @@ -1028,13 +1043,16 @@ def resolve_type(type, obj, ctx = :__undefined__) # @api private def after_lazy(value) if lazy?(value) - GraphQL::Execution::Lazy.new do - result = sync_lazy(value) - # The returned result might also be lazy, so check it, too - after_lazy(result) do |final_result| - yield(final_result) if block_given? - end - end + GraphQL::Execution::Lazy.new( + value: -> { + result = sync_lazy(value) + # The returned result might also be lazy, so check it, too + after_lazy(result) do |final_result| + yield(final_result) if block_given? + end + }, + exec: -> { exec_concurrent(value) } + ) else yield(value) if block_given? end @@ -1062,6 +1080,24 @@ def sync_lazy(value) } end + # Override this method to handle lazy concurrent objects in a custom way. + # @param value [Object] an instance of a class registered with {.lazy_resolve} + # @param ctx [GraphQL::Query::Context] the context for this query + # @return [Object] A GraphQL-ready (non-lazy) object + def self.exec_concurrent(value) + yield(value) + end + + # @see Schema.exec_concurrent for a hook to override + # @api private + def exec_concurrent(value) + self.class.exec_concurrent(value) do |v| + if concurrent_method = concurrent_method_name(v) + value.public_send(concurrent_method) + end + end + end + protected def rescues? diff --git a/spec/graphql/execution/lazy/lazy_method_map_spec.rb b/spec/graphql/execution/lazy/lazy_method_map_spec.rb index 94c4a73e04..f9c2af75dd 100644 --- a/spec/graphql/execution/lazy/lazy_method_map_spec.rb +++ b/spec/graphql/execution/lazy/lazy_method_map_spec.rb @@ -7,11 +7,12 @@ def self.test_lazy_method_map a = Class.new b = Class.new(a) c = Class.new(b) - lazy_method_map.set(a, :a) + lazy_method_map.set(a, :a, :b) threads = 1000.times.map do |i| Thread.new { d = Class.new(c) - assert_equal :a, lazy_method_map.get(d.new) + assert_equal :a, lazy_method_map.get(d.new).value_method + assert_equal :b, lazy_method_map.get(d.new).exec_method } end threads.map(&:join) @@ -21,15 +22,18 @@ def self.test_lazy_method_map a = Class.new b = Class.new(a) c = Class.new(b) - lazy_method_map.set(a, :a) + lazy_method_map.set(a, :a, :b) lazy_method_map.get(b.new) lazy_method_map.get(c.new) dup_map = lazy_method_map.dup assert_equal 3, dup_map.instance_variable_get(:@storage).size - assert_equal :a, dup_map.get(a.new) - assert_equal :a, dup_map.get(b.new) - assert_equal :a, dup_map.get(c.new) + assert_equal :a, dup_map.get(a.new).value_method + assert_equal :b, dup_map.get(a.new).exec_method + assert_equal :a, dup_map.get(b.new).value_method + assert_equal :b, dup_map.get(b.new).exec_method + assert_equal :a, dup_map.get(c.new).value_method + assert_equal :b, dup_map.get(c.new).exec_method end end diff --git a/spec/graphql/execution/lazy_spec.rb b/spec/graphql/execution/lazy_spec.rb index 687795b4cc..dce639a7d2 100644 --- a/spec/graphql/execution/lazy_spec.rb +++ b/spec/graphql/execution/lazy_spec.rb @@ -10,6 +10,11 @@ assert_equal 3, res["data"]["int"] end + it 'calls concurrent handlers' do + res = run_query('{ concurrentInt(value: 2, plus: 1) }') + assert_equal 3, res['data']['concurrentInt'] + end + it "can do nested lazy values" do res = run_query %| { @@ -43,6 +48,13 @@ value } } + + d: concurrentNestedSum(value: 1) { + value + concurrentNestedSum(value: 2) { + value + } + } } | @@ -65,6 +77,9 @@ {"nestedSum"=>{"value"=>14}}, {"nestedSum"=>{"value"=>14}} ], + "d"=>{"value"=>1, "concurrentNestedSum"=>{ + "value"=>3 + }} } assert_equal expected_data, res["data"] @@ -162,6 +177,7 @@ a: nullableNestedSum(value: 1001) { value } b: nullableNestedSum(value: 1013) { value } c: nullableNestedSum(value: 1002) { value } + d: concurrentNestedSum(value: 2) { value } } GRAPHQL @@ -169,6 +185,7 @@ assert_equal 101, res["data"]["a"]["value"] assert_equal 113, res["data"]["b"]["value"] assert_equal 102, res["data"]["c"]["value"] + assert_equal 2, res["data"]["d"]["value"] end end @@ -178,14 +195,19 @@ class SubWrapper < LazyHelpers::Wrapper; end let(:map) { GraphQL::Execution::Lazy::LazyMethodMap.new } it "finds methods for classes and subclasses" do - map.set(LazyHelpers::Wrapper, :item) - map.set(LazyHelpers::SumAll, :value) + map.set(LazyHelpers::Wrapper, :item, :exec) + map.set(LazyHelpers::SumAll, :value, :exec) b = LazyHelpers::Wrapper.new(1) sub_b = LazyHelpers::Wrapper.new(2) s = LazyHelpers::SumAll.new(3) - assert_equal(:item, map.get(b)) - assert_equal(:item, map.get(sub_b)) - assert_equal(:value, map.get(s)) + assert_equal(:item, map.get(b).value_method) + assert_equal(:exec, map.get(b).exec_method) + + assert_equal(:item, map.get(sub_b).value_method) + assert_equal(:exec, map.get(sub_b).exec_method) + + assert_equal(:value, map.get(s).value_method) + assert_equal(:exec, map.get(s).exec_method) end end end diff --git a/spec/integration/mongoid/star_trek/schema.rb b/spec/integration/mongoid/star_trek/schema.rb index 48ae8e918d..e0b7d82c71 100644 --- a/spec/integration/mongoid/star_trek/schema.rb +++ b/spec/integration/mongoid/star_trek/schema.rb @@ -262,6 +262,10 @@ def value loaded[@id] end + + def execute + # no-op + end end class LazyWrapper @@ -276,6 +280,10 @@ def initialize(value = nil, &block) def value @resolved_value = @value || @lazy_value.call end + + def execute + # no-op + end end LazyNodesWrapper = Struct.new(:relation) @@ -394,8 +402,8 @@ def self.id_from_object(object, type, ctx) GraphQL::Schema::UniqueWithinType.encode(type.name, object.id) end - lazy_resolve(LazyWrapper, :value) - lazy_resolve(LazyLoader, :value) + lazy_resolve(LazyWrapper, :value, :execute) + lazy_resolve(LazyLoader, :value, :execute) instrument(:field, ClassNameRecorder.new(:before_built_ins)) instrument(:field, ClassNameRecorder.new(:after_built_ins), after_built_ins: true) diff --git a/spec/integration/rails/graphql/schema_spec.rb b/spec/integration/rails/graphql/schema_spec.rb index f179afbf87..71a92722dc 100644 --- a/spec/integration/rails/graphql/schema_spec.rb +++ b/spec/integration/rails/graphql/schema_spec.rb @@ -373,9 +373,10 @@ def instrument(type, field) end end - describe "#lazy? / #lazy_method_name" do + describe "#lazy? / #lazy_method_name / #concurrent? / #concurrent_method_name" do class LazyObj; end class LazyObjChild < LazyObj; end + class ConcurrentObj; end let(:schema) { query_type = GraphQL::ObjectType.define(name: "Query") @@ -383,18 +384,33 @@ class LazyObjChild < LazyObj; end query(query_type) lazy_resolve(Integer, :itself) lazy_resolve(LazyObj, :dup) + lazy_resolve(ConcurrentObj, :dup, :exec) end } it "returns registered lazy method names by class/superclass, or returns nil" do assert_equal :itself, schema.lazy_method_name(68) assert_equal true, schema.lazy?(77) + assert_equal false, schema.concurrent?(77) + assert_equal :dup, schema.lazy_method_name(LazyObj.new) + assert_nil schema.concurrent_method_name(LazyObj.new) assert_equal true, schema.lazy?(LazyObj.new) + assert_equal false, schema.concurrent?(LazyObj.new) + assert_equal :dup, schema.lazy_method_name(LazyObjChild.new) + assert_nil schema.concurrent_method_name(LazyObjChild.new) assert_equal true, schema.lazy?(LazyObjChild.new) + assert_equal false, schema.concurrent?(LazyObjChild.new) + + assert_equal :dup, schema.lazy_method_name(ConcurrentObj.new) + assert_equal :exec, schema.concurrent_method_name(ConcurrentObj.new) + assert_equal true, schema.lazy?(ConcurrentObj.new) + assert_equal true, schema.concurrent?(ConcurrentObj.new) + assert_nil schema.lazy_method_name({}) assert_equal false, schema.lazy?({}) + assert_equal false, schema.concurrent?({}) end end diff --git a/spec/support/lazy_helpers.rb b/spec/support/lazy_helpers.rb index fbc00ae0a0..7af8ebadf9 100644 --- a/spec/support/lazy_helpers.rb +++ b/spec/support/lazy_helpers.rb @@ -18,6 +18,24 @@ def item end end + # This is like the `Wrapper` but it will only evaluate a `value` if the block + # has been executed. This allows for testing that the `execute` block has in + # fact been called before the value has been accessed. While this is not a + # requirement in real applications (the `value` method could also call + # `execute` if it has not yet been called) this simplified class makes testing + # easier. + class ConcurrentWrapper + attr_reader :value + + def initialize(&block) + @block = block + end + + def execute + @value = @block.call + end + end + class SumAll attr_reader :own_value attr_writer :value @@ -46,6 +64,33 @@ def self.all end end + class ConcurrentSumAll + attr_reader :own_value + attr_accessor :value + + def initialize(own_value) + @own_value = own_value + all << self + end + + def execute + @value = begin + total_value = all.map(&:own_value).reduce(&:+) + all.each { |v| v.value = total_value} + all.clear + total_value + end + end + + def all + self.class.all + end + + def self.all + @all ||= [] + end + end + class LazySum < GraphQL::Schema::Object field :value, Integer, null: true, resolve: ->(o, a, c) { o == 13 ? nil : o } field :nestedSum, LazySum, null: false do @@ -66,6 +111,17 @@ def nested_sum(value:) alias :nullable_nested_sum :nested_sum end + class ConcurrentSum < GraphQL::Schema::Object + field :value, Integer, null: true, resolve: ->(o, a, c) { o } + field :concurrentNestedSum, ConcurrentSum, null: false do + argument :value, Integer, required: true + end + + def concurrent_nested_sum(value:) + ConcurrentWrapper.new { @object + value } + end + end + using GraphQL::DeprecatedDSL if RUBY_ENGINE == "jruby" # JRuby doesn't support refinements, so the `using` above won't work @@ -80,11 +136,22 @@ def nested_sum(value:) resolve ->(o, a, c) { Wrapper.new(a[:value] + a[:plus])} end + field :concurrentInt, !types.Int do + argument :value, !types.Int + argument :plus, types.Int, default_value: 0 + resolve ->(o, a, c) { ConcurrentWrapper.new { a[:value] + a[:plus] } } + end + field :nestedSum, !LazySum do argument :value, !types.Int resolve ->(o, args, c) { SumAll.new(args[:value]) } end + field :concurrentNestedSum, !ConcurrentSum do + argument :value, !types.Int + resolve ->(o, args, c) { ConcurrentSumAll.new(args[:value]) } + end + field :nullableNestedSum, LazySum do argument :value, types.Int resolve ->(o, args, c) { @@ -138,7 +205,9 @@ class LazySchema < GraphQL::Schema query(LazyQuery) mutation(LazyQuery) lazy_resolve(Wrapper, :item) + lazy_resolve(ConcurrentWrapper, :value, :execute) lazy_resolve(SumAll, :value) + lazy_resolve(ConcurrentSumAll, :value, :execute) instrument(:query, SumAllInstrumentation.new(counter: nil)) instrument(:multiplex, SumAllInstrumentation.new(counter: 1)) instrument(:multiplex, SumAllInstrumentation.new(counter: 2)) From bf4560252504dd88d99dae1996af11317d631de6 Mon Sep 17 00:00:00 2001 From: Robert Mosolgo Date: Mon, 28 Jan 2019 12:49:32 -0500 Subject: [PATCH 2/3] Fix merge - update field to use keyword args --- spec/support/lazy_helpers.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/support/lazy_helpers.rb b/spec/support/lazy_helpers.rb index 61b4f040b1..324980136a 100644 --- a/spec/support/lazy_helpers.rb +++ b/spec/support/lazy_helpers.rb @@ -170,7 +170,7 @@ def concurrent_int(value:, plus:) end def concurrent_nested_sum(value:) - ConcurrentSumAll.new(args[:value]) + ConcurrentSumAll.new(value) end field :nested_sum, LazySum, null: false do From 58245bff2727eb4df393e3d3c750ec03451f2e5d Mon Sep 17 00:00:00 2001 From: Robert Mosolgo Date: Mon, 28 Jan 2019 12:58:01 -0500 Subject: [PATCH 3/3] Update call signature to accomodate legacy usage in interpreter --- lib/graphql/execution/lazy.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/graphql/execution/lazy.rb b/lib/graphql/execution/lazy.rb index 37d226a277..34a75d4914 100644 --- a/lib/graphql/execution/lazy.rb +++ b/lib/graphql/execution/lazy.rb @@ -26,9 +26,15 @@ def self.resolve(val) # @param value_proc [Proc] a block to get the inner value (later) # @param path [Array] # @param field [GraphQL::Schema::Field] - def initialize(original = nil, path: nil, field: nil, value:, exec:) + def initialize(original = nil, path: nil, field: nil, value: nil, exec: nil) @original = original - @value_proc = value + @value_proc = if value + value + elsif block_given? + Proc.new + else + raise ArgumentError, "A block to call later is required as `value:` ora block" + end @exec_proc = exec @resolved = false @path = path