Skip to content

Commit ca09137

Browse files
committed
Batch list argument loading steps
1 parent 358f639 commit ca09137

4 files changed

Lines changed: 721 additions & 4 deletions

File tree

lib/graphql/execution/input_values.rb

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -209,14 +209,13 @@ def argument_value(argument_values, argument_key, argument_definition, arg_value
209209
if argument_definition.type.list?
210210
results = Array.new(arg_value.size, nil)
211211
argument_values[argument_key] = results
212-
arg_value.each_with_index do |inner_v, idx|
213-
loads_step = LoadArgumentStep.new(
212+
if !arg_value.empty?
213+
loads_step = LoadArgumentsStep.new(
214214
field_resolve_step: field_resolve_step,
215215
load_receiver: load_receiver,
216-
argument_value: inner_v,
216+
argument_values: arg_value,
217217
argument_definition: argument_definition,
218218
arguments: results,
219-
argument_key: idx,
220219
)
221220
ps.push(loads_step)
222221
@runner.add_step(loads_step)
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# frozen_string_literal: true
2+
module GraphQL
3+
module Execution
4+
class LoadArgumentsStep
5+
def initialize(field_resolve_step:, arguments:, load_receiver:, argument_values:, argument_definition:)
6+
@field_resolve_step = field_resolve_step
7+
@load_receiver = load_receiver
8+
@arguments = arguments
9+
@argument_values = argument_values
10+
@argument_definition = argument_definition
11+
@loaded_values = Array.new(argument_values.size)
12+
@authorization_states = Array.new(argument_values.size, true)
13+
@errors = Array.new(argument_values.size)
14+
@remaining_values = argument_values.size
15+
@remaining_loads = argument_values.size
16+
@phase = :start
17+
@next_index = 0
18+
@lazy_indexes = []
19+
end
20+
21+
def call
22+
case @phase
23+
when :start
24+
@phase = :loading
25+
enqueue_jobs(@argument_values.size)
26+
when :loading
27+
index = @next_index
28+
@next_index += 1
29+
load_value(index)
30+
when :resolving
31+
index = @lazy_indexes[@next_index]
32+
@next_index += 1
33+
resolve_lazy_value(index)
34+
else
35+
raise GraphQL::InvariantError, "Unexpected LoadArgumentsStep phase: #{@phase.inspect}"
36+
end
37+
nil
38+
end
39+
40+
def value
41+
@phase = :resolving
42+
@next_index = 0
43+
enqueue_jobs(@lazy_indexes.size)
44+
nil
45+
end
46+
47+
private
48+
49+
def load_value(index)
50+
@field_resolve_step.set_current_field
51+
context = @field_resolve_step.selections_step.query.context
52+
begin
53+
@loaded_values[index] = begin
54+
@load_receiver.load_and_authorize_application_object(
55+
@argument_definition,
56+
@argument_values[index],
57+
context,
58+
)
59+
rescue GraphQL::UnauthorizedError => auth_err
60+
@authorization_states[index] = false
61+
context.schema.unauthorized_object(auth_err)
62+
end
63+
complete_load(index)
64+
rescue GraphQL::RuntimeError => err
65+
handle_runtime_error(index, err, loading: true)
66+
complete_load(index)
67+
rescue StandardError => stderr
68+
handle_standard_error(index, stderr)
69+
complete_load(index)
70+
ensure
71+
@field_resolve_step.set_current_field(nil)
72+
end
73+
end
74+
75+
def resolve_lazy_value(index)
76+
@field_resolve_step.set_current_field
77+
schema = @field_resolve_step.runner.schema
78+
begin
79+
@loaded_values[index] = schema.sync_lazy(@loaded_values[index])
80+
complete_value(index)
81+
rescue GraphQL::UnauthorizedError => auth_err
82+
@authorization_states[index] = false
83+
schema.unauthorized_object(auth_err)
84+
rescue GraphQL::RuntimeError => err
85+
handle_runtime_error(index, err, loading: false)
86+
complete_value(index)
87+
rescue StandardError => stderr
88+
handle_standard_error(index, stderr)
89+
complete_value(index)
90+
ensure
91+
@field_resolve_step.set_current_field(nil)
92+
end
93+
end
94+
95+
def handle_runtime_error(index, error, loading:)
96+
if error.is_a?(Schema::Subscription::EarlyUnsubscribe)
97+
@authorization_states[index] = false if loading
98+
@loaded_values[index] = error.unsubscribed_result
99+
else
100+
@loaded_values[index] = @errors[index] = error
101+
end
102+
end
103+
104+
def handle_standard_error(index, error)
105+
query = @field_resolve_step.selections_step.query
106+
@loaded_values[index] = begin
107+
query.handle_or_reraise(
108+
error,
109+
field: @field_resolve_step.field_definition,
110+
arguments: @field_resolve_step.arguments, # rubocop:disable Development/ContextIsPassedCop
111+
object: nil,
112+
)
113+
rescue GraphQL::ExecutionError => execution_error
114+
execution_error
115+
end
116+
end
117+
118+
def record_error(index)
119+
loaded_value = @loaded_values[index]
120+
@errors[index] = loaded_value if loaded_value.is_a?(GraphQL::RuntimeError)
121+
end
122+
123+
def complete_load(index)
124+
record_error(index)
125+
load_completed(index)
126+
end
127+
128+
def complete_value(index)
129+
record_error(index)
130+
value_completed(index)
131+
end
132+
133+
def enqueue_jobs(count)
134+
dataloader = @field_resolve_step.runner.dataloader
135+
count.times { dataloader.append_job(self) }
136+
end
137+
138+
def load_completed(index)
139+
runner = @field_resolve_step.runner
140+
if runner.resolves_lazies && runner.lazy?(@loaded_values[index])
141+
@lazy_indexes << index
142+
else
143+
value_completed(index)
144+
end
145+
146+
@remaining_loads -= 1
147+
if @remaining_loads == 0 && !@lazy_indexes.empty?
148+
@phase = :waiting
149+
runner.dataloader.lazy_at_depth(@field_resolve_step.path.size, self)
150+
end
151+
end
152+
153+
def value_completed(index)
154+
query = @field_resolve_step.selections_step.query
155+
if (error = @errors[index])
156+
error.path = @field_resolve_step.path
157+
@field_resolve_step.arguments = error
158+
elsif @authorization_states[index]
159+
loaded_value = @loaded_values[index]
160+
query.current_trace.object_loaded(@argument_definition, loaded_value, query.context)
161+
@arguments[index] = loaded_value
162+
else
163+
@field_resolve_step.arguments = EmptyObjects::EMPTY_HASH
164+
@field_resolve_step.pending_steps.clear
165+
@field_resolve_step.build_errors_result(nil, nil)
166+
end
167+
168+
@remaining_values -= 1
169+
return if @remaining_values > 0 || (!@authorization_states[index] && error.nil?)
170+
171+
finish
172+
end
173+
174+
def finish
175+
@phase = :finished
176+
field_pending_steps = @field_resolve_step.pending_steps
177+
field_pending_steps.delete(self)
178+
if @field_resolve_step.arguments && field_pending_steps.empty? # rubocop:disable Development/ContextIsPassedCop
179+
@field_resolve_step.runner.add_step(@field_resolve_step)
180+
end
181+
end
182+
end
183+
end
184+
end

lib/graphql/execution/next.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
require "graphql/execution/field_resolve_step"
55
require "graphql/execution/finalize"
66
require "graphql/execution/load_argument_step"
7+
require "graphql/execution/load_arguments_step"
78
require "graphql/execution/resolve_type_step"
89
require "graphql/execution/runner"
910
require "graphql/execution/selections_step"

0 commit comments

Comments
 (0)