graphql 2.6.3 → 2.6.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/generators/graphql/field_extractor.rb +17 -1
- data/lib/generators/graphql/templates/schema.erb +2 -1
- data/lib/graphql/analysis/query_complexity.rb +2 -2
- data/lib/graphql/dashboard/application_controller.rb +5 -1
- data/lib/graphql/dashboard.rb +3 -0
- data/lib/graphql/dataloader/async_dataloader.rb +333 -68
- data/lib/graphql/dataloader/source.rb +32 -19
- data/lib/graphql/dataloader.rb +52 -36
- data/lib/graphql/execution/field_resolve_step.rb +79 -19
- data/lib/graphql/execution/finalize.rb +7 -8
- data/lib/graphql/execution/interpreter/runtime.rb +1 -8
- data/lib/graphql/execution/interpreter.rb +9 -1
- data/lib/graphql/execution/prepare_object_step.rb +8 -4
- data/lib/graphql/execution/runner.rb +17 -5
- data/lib/graphql/execution/selections_step.rb +10 -6
- data/lib/graphql/execution_error.rb +4 -0
- data/lib/graphql/float_decoding_error.rb +13 -0
- data/lib/graphql/float_encoding_error.rb +28 -0
- data/lib/graphql/language/block_string.rb +6 -11
- data/lib/graphql/language/cache.rb +84 -14
- data/lib/graphql/language/lexer.rb +15 -9
- data/lib/graphql/language/nodes.rb +2 -1
- data/lib/graphql/language.rb +3 -3
- data/lib/graphql/pagination/relation_connection.rb +10 -1
- data/lib/graphql/query/variable_validation_error.rb +16 -1
- data/lib/graphql/query/variables.rb +1 -1
- data/lib/graphql/railtie.rb +2 -1
- data/lib/graphql/schema/build_from_definition.rb +11 -3
- data/lib/graphql/schema/directive.rb +3 -0
- data/lib/graphql/schema/input_object.rb +2 -2
- data/lib/graphql/schema/resolver.rb +1 -1
- data/lib/graphql/schema/wrapper.rb +4 -0
- data/lib/graphql/schema.rb +5 -5
- data/lib/graphql/static_validation/rules/fields_will_merge.rb +66 -29
- data/lib/graphql/subscriptions/serialize.rb +10 -4
- data/lib/graphql/types/float.rb +18 -4
- data/lib/graphql/version.rb +1 -1
- data/lib/graphql.rb +2 -0
- metadata +4 -58
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a6c191887acba06a4030f962822e9106e9d3aa9d5550c8cd25ea76f343927f1b
|
|
4
|
+
data.tar.gz: b984ad9a61a391b4b8a42ade3bb9c63dc79ab2b3ee987845d655dc7038df1255
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7deac13ce0bbc579609e28575a309caa46086196f1381cfc5fbd6a5770b2059e123cb047cc835c573ad76be1df264a3b0d52d8692dbf161ad0d0248f8cfed2a3
|
|
7
|
+
data.tar.gz: 1362c0867ad8bbbe086b1999c710f447de6ea16b53b8a05a83dc39cba6c7b7d8d5cccdc388a3dff2f42c1b5135ff59a4c78dd2d53badd34b28718a73aa331b92
|
|
@@ -6,7 +6,23 @@ module Graphql
|
|
|
6
6
|
module FieldExtractor
|
|
7
7
|
def fields
|
|
8
8
|
columns = []
|
|
9
|
-
|
|
9
|
+
if (model_columns = klass&.columns)
|
|
10
|
+
filter = if defined?(ActiveSupport::ParameterFilter)
|
|
11
|
+
fp = if defined?(Rails) && Rails.application && (app_config = Rails.application.config.filter_parameters).present? && !app_config.empty?
|
|
12
|
+
app_config
|
|
13
|
+
elsif ActiveSupport.respond_to?(:filter_parameters)
|
|
14
|
+
ActiveSupport.filter_parameters
|
|
15
|
+
else
|
|
16
|
+
[]
|
|
17
|
+
end
|
|
18
|
+
ActiveSupport::ParameterFilter.new(fp, mask: nil)
|
|
19
|
+
else
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
columns += model_columns
|
|
23
|
+
.select { |c| filter ? filter.filter_param(c.name, c.name) : true }
|
|
24
|
+
.map { |c| generate_column_string(c) }
|
|
25
|
+
end
|
|
10
26
|
columns + custom_fields
|
|
11
27
|
end
|
|
12
28
|
|
|
@@ -26,7 +26,8 @@ class <%= schema_name %> < GraphQL::Schema
|
|
|
26
26
|
raise(GraphQL::RequiredImplementationMissingError)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
# Limit the size of incoming queries:
|
|
29
|
+
# Limit the depth and size of incoming queries:
|
|
30
|
+
max_depth(15)
|
|
30
31
|
max_query_string_tokens(5000)
|
|
31
32
|
|
|
32
33
|
# Stop validating when it encounters this many errors:
|
|
@@ -29,14 +29,14 @@ module GraphQL
|
|
|
29
29
|
future_complexity
|
|
30
30
|
end
|
|
31
31
|
when nil
|
|
32
|
-
subject.logger.warn <<~
|
|
32
|
+
subject.logger.warn <<~MESSAGE
|
|
33
33
|
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
|
|
34
34
|
|
|
35
35
|
To opt into the future behavior, configure your schema (#{subject.schema.name ? subject.schema.name : subject.schema.ancestors}) with:
|
|
36
36
|
|
|
37
37
|
complexity_cost_calculation_mode(:future) # or `:legacy`, `:compare`
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
MESSAGE
|
|
40
40
|
max_possible_complexity(mode: :legacy)
|
|
41
41
|
else
|
|
42
42
|
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}"
|
|
@@ -22,7 +22,11 @@ module Graphql
|
|
|
22
22
|
|
|
23
23
|
def schema_class
|
|
24
24
|
@schema_class ||= begin
|
|
25
|
-
|
|
25
|
+
configured_schemas = Array(request.path_parameters[:schema] || request.path_parameters["schema"])
|
|
26
|
+
schema_param = request.query_parameters["schema"]
|
|
27
|
+
schema_param = configured_schemas.find { |schema| schema.to_s == schema_param } if schema_param
|
|
28
|
+
schema_param ||= configured_schemas.first
|
|
29
|
+
|
|
26
30
|
case schema_param
|
|
27
31
|
when Class
|
|
28
32
|
schema_param
|
data/lib/graphql/dashboard.rb
CHANGED
|
@@ -10,6 +10,9 @@ module Graphql
|
|
|
10
10
|
# @example Mounting the Dashboard in your app
|
|
11
11
|
# mount GraphQL::Dashboard, at: "graphql_dashboard", schema: "MySchema"
|
|
12
12
|
#
|
|
13
|
+
# Pass an array to allow selecting from multiple schemas with the `schema` query parameter.
|
|
14
|
+
# mount GraphQL::Dashboard, at: "graphql_dashboard", schema: ["MySchema", "OtherSchema"]
|
|
15
|
+
#
|
|
13
16
|
# @example Authenticating the Dashboard with HTTP Basic Auth
|
|
14
17
|
# # config/initializers/graphql_dashboard.rb
|
|
15
18
|
# GraphQL::Dashboard.middleware.use(Rack::Auth::Basic) do |username, password|
|
|
@@ -1,110 +1,375 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module GraphQL
|
|
3
4
|
class Dataloader
|
|
4
5
|
class AsyncDataloader < Dataloader
|
|
6
|
+
def self.use(...)
|
|
7
|
+
install_graphql_methods
|
|
8
|
+
super
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.install_graphql_methods
|
|
12
|
+
if !Async::Task.method_defined?(:cancel)
|
|
13
|
+
Async::Task.alias_method(:cancel, :stop)
|
|
14
|
+
end
|
|
15
|
+
if !Async::Task.method_defined?(:graphql_async_dataloader_run)
|
|
16
|
+
Async::Task.attr_accessor(:graphql_async_dataloader_run)
|
|
17
|
+
Async::Task.attr_accessor(:graphql_async_dataloader_condition)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(...)
|
|
22
|
+
super
|
|
23
|
+
create_pending_run
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @api private
|
|
27
|
+
attr_reader :pending_sources
|
|
28
|
+
|
|
29
|
+
def create_pending_run
|
|
30
|
+
jobs_fiber_limit, total_fiber_limit = calculate_fiber_limit
|
|
31
|
+
@pending_run = Run.new(self, total_fiber_limit, jobs_fiber_limit)
|
|
32
|
+
end
|
|
33
|
+
|
|
5
34
|
def yield(source = Fiber[:__graphql_current_dataloader_source])
|
|
6
|
-
|
|
35
|
+
task = Async::Task.current
|
|
36
|
+
run = task.graphql_async_dataloader_run
|
|
37
|
+
trace = run.trace
|
|
7
38
|
trace&.dataloader_fiber_yield(source)
|
|
8
|
-
if (
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
39
|
+
if !run.push_task_message(:paused_task, task)
|
|
40
|
+
task.stop
|
|
41
|
+
end
|
|
42
|
+
condition = task.graphql_async_dataloader_condition
|
|
43
|
+
condition.wait
|
|
44
|
+
if !run.push_task_message(:resumed_task, task)
|
|
45
|
+
task.stop
|
|
12
46
|
end
|
|
13
47
|
trace&.dataloader_fiber_resume(source)
|
|
14
48
|
nil
|
|
15
49
|
end
|
|
16
50
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
51
|
+
class Run
|
|
52
|
+
def initialize(dataloader, total_fiber_limit, jobs_fiber_limit)
|
|
53
|
+
@dataloader = dataloader
|
|
54
|
+
@root_task = nil
|
|
55
|
+
@trace = nil
|
|
56
|
+
@jobs = []
|
|
57
|
+
|
|
58
|
+
@total_fiber_limit = total_fiber_limit
|
|
59
|
+
@jobs_fiber_limit = jobs_fiber_limit
|
|
60
|
+
@lazies_at_depth = Hash.new { |h, k| h[k] = [] }
|
|
61
|
+
|
|
62
|
+
@running_tasks = nil
|
|
63
|
+
@tasks_channel = nil
|
|
64
|
+
@tasks_channel_task = nil
|
|
65
|
+
@activity = nil
|
|
66
|
+
@task_error = nil
|
|
67
|
+
@expected_resumes = 0
|
|
68
|
+
@mode = nil
|
|
69
|
+
|
|
70
|
+
@snoozed_jobs_condition = Async::Condition.new
|
|
71
|
+
@snoozed_sources_condition = Async::Condition.new
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
attr_accessor :trace, :root_task
|
|
75
|
+
|
|
76
|
+
attr_reader :dataloader, :jobs, :lazies_at_depth, :jobs_fiber_limit, :snoozed_jobs_condition, :snoozed_sources_condition
|
|
77
|
+
|
|
78
|
+
def jobs_bandwidth?
|
|
79
|
+
running_count < @jobs_fiber_limit
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def sources_bandwidth?
|
|
83
|
+
running_count < current_sources_fiber_limit
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def close_queues
|
|
87
|
+
@tasks_channel.close
|
|
88
|
+
@tasks_channel_task.cancel
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Push to the tasks_channel, tolerating a closed channel: on the error path, `run_queue`
|
|
92
|
+
# closes the channel while sibling tasks can still run one more slice before
|
|
93
|
+
# `root_task.cancel` reaches them. Record `:task_error` payloads so they aren't lost, and
|
|
94
|
+
# return false so the caller can stop the task instead of raising `ClosedError` into user code.
|
|
95
|
+
def push_task_message(msg, data)
|
|
96
|
+
@tasks_channel.push([msg, data])
|
|
97
|
+
true
|
|
98
|
+
rescue Async::Queue::ClosedError
|
|
99
|
+
if msg == :task_error
|
|
100
|
+
@task_error ||= data
|
|
101
|
+
end
|
|
102
|
+
false
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def wait_for_activity
|
|
106
|
+
@activity.wait
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def quiesced?
|
|
110
|
+
@running_tasks.empty? && @tasks_channel.empty? && @expected_resumes == 0
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def has_pending_work?
|
|
114
|
+
@mode == :jobs ? @jobs.any? : @dataloader.pending_sources.any?(&:pending?) # rubocop:disable Development/NoneWithoutBlockCop
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def has_bandwidth?
|
|
118
|
+
@mode == :jobs ? jobs_bandwidth? : sources_bandwidth?
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Signalled tasks don't appear in any accounting until their first slice
|
|
122
|
+
# pushes `:resumed_task`, so they have to be counted at signal time:
|
|
123
|
+
def expect_resumes(count)
|
|
124
|
+
@expected_resumes = count
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def check_error!
|
|
128
|
+
if (err = @task_error)
|
|
129
|
+
@task_error = nil
|
|
130
|
+
raise err
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def new_queues(mode)
|
|
135
|
+
@mode = mode
|
|
136
|
+
@tasks_channel = Async::Queue.new(parent: @root_task)
|
|
137
|
+
@activity = Async::Condition.new
|
|
138
|
+
@task_error = nil
|
|
139
|
+
@expected_resumes = 0
|
|
140
|
+
@running_tasks = []
|
|
141
|
+
@tasks_channel_task = @root_task.async do |_t|
|
|
142
|
+
while ((msg, data) = @tasks_channel.wait)
|
|
143
|
+
case msg
|
|
144
|
+
when :started_task
|
|
145
|
+
@running_tasks.push(data)
|
|
146
|
+
data.run
|
|
147
|
+
when :resumed_task
|
|
148
|
+
if @expected_resumes > 0
|
|
149
|
+
@expected_resumes -= 1
|
|
42
150
|
end
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
151
|
+
@running_tasks.push(data)
|
|
152
|
+
when :finished_task, :paused_task
|
|
153
|
+
@running_tasks.delete(data)
|
|
154
|
+
when :task_error
|
|
155
|
+
@task_error ||= data
|
|
156
|
+
else
|
|
157
|
+
raise ArgumentError, "Unknown tasks_channel action: #{msg.inspect}"
|
|
46
158
|
end
|
|
159
|
+
@activity.signal
|
|
47
160
|
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
48
163
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
164
|
+
def running?
|
|
165
|
+
@snoozed_jobs_condition.waiting? || @snoozed_sources_condition.waiting?
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def current_sources_fiber_limit
|
|
169
|
+
within_limit = @total_fiber_limit - running_count
|
|
170
|
+
if within_limit < 1
|
|
171
|
+
1
|
|
172
|
+
else
|
|
173
|
+
within_limit
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
private
|
|
178
|
+
|
|
179
|
+
def running_count
|
|
180
|
+
@snoozed_jobs_condition.instance_variable_get(:@ready).num_waiting +
|
|
181
|
+
@snoozed_sources_condition.instance_variable_get(:@ready).num_waiting +
|
|
182
|
+
(@running_tasks&.size || 0)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def append_job(callable = nil, &block)
|
|
187
|
+
active_run.jobs.push(callable || block)
|
|
188
|
+
nil
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def lazy_at_depth(depth, lazy)
|
|
192
|
+
active_run.lazies_at_depth[depth] << lazy
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def active_run
|
|
196
|
+
@pending_run || current_task_run || raise(GraphQL::Error, "No available Run to append to, GraphQL-Ruby bug")
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# The current task's run, but only if it belongs to this dataloader. A different
|
|
200
|
+
# dataloader may be running inside one of our tasks (or vice versa), e.g. a query
|
|
201
|
+
# executed from a resolver or a subscription trigger; its run must not be reused.
|
|
202
|
+
def current_task_run
|
|
203
|
+
run = Async::Task.current?&.graphql_async_dataloader_run
|
|
204
|
+
run if run&.dataloader.equal?(self)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def run_isolated
|
|
208
|
+
previous_run = current_task_run
|
|
209
|
+
prev_pending_keys = {}
|
|
210
|
+
# Clear pending loads but keep already-cached records
|
|
211
|
+
# in case they are useful to the given block.
|
|
212
|
+
@source_cache.each do |source_class, batched_sources|
|
|
213
|
+
batched_sources.each do |batch_args, batched_source_instance|
|
|
214
|
+
if batched_source_instance.pending?
|
|
215
|
+
prev_pending_keys[batched_source_instance] = batched_source_instance.pending.dup
|
|
216
|
+
batched_source_instance.pending.clear
|
|
54
217
|
end
|
|
55
218
|
end
|
|
56
|
-
trace&.end_dataloader(self)
|
|
57
219
|
end
|
|
58
220
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
221
|
+
res = nil
|
|
222
|
+
create_pending_run
|
|
223
|
+
@pending_run.jobs << -> { res = yield }
|
|
224
|
+
run
|
|
225
|
+
res
|
|
226
|
+
ensure
|
|
227
|
+
if previous_run
|
|
228
|
+
Async::Task.current.graphql_async_dataloader_run = previous_run
|
|
229
|
+
# clear the one created in #run:
|
|
230
|
+
@pending_run = nil
|
|
62
231
|
end
|
|
232
|
+
prev_pending_keys.each do |source_instance, pending|
|
|
233
|
+
pending.each do |key, value|
|
|
234
|
+
next if source_instance.results.key?(key)
|
|
235
|
+
|
|
236
|
+
queue_pending_source(source_instance) if source_instance.pending.empty?
|
|
237
|
+
source_instance.pending[key] = value
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def run(trace_query_lazy: nil)
|
|
243
|
+
trace = Fiber[:__graphql_current_multiplex]&.current_trace
|
|
244
|
+
run = @pending_run || current_task_run || raise(GraphQL::Error, "No available Run, GraphQL-Ruby internal bug")
|
|
245
|
+
@pending_run = nil
|
|
246
|
+
run.trace = trace
|
|
247
|
+
first_pass = true
|
|
248
|
+
trace&.begin_dataloader(self)
|
|
249
|
+
fiber_vars = get_fiber_variables
|
|
250
|
+
raised_error = nil
|
|
251
|
+
jobs = run.jobs
|
|
252
|
+
Sync do |_maybe_new_task|
|
|
253
|
+
# Make sure there's a new task instance to hold `.graphql_...` state:
|
|
254
|
+
task = Async::Task.new do |root_task|
|
|
255
|
+
run.root_task = root_task
|
|
256
|
+
root_task.graphql_async_dataloader_run = run
|
|
257
|
+
set_fiber_variables(fiber_vars)
|
|
258
|
+
|
|
259
|
+
while first_pass || run.running? || !jobs.empty?
|
|
260
|
+
first_pass = false
|
|
261
|
+
run_queue(run, run.snoozed_jobs_condition, :jobs)
|
|
262
|
+
run_queue(run, run.snoozed_sources_condition, :sources)
|
|
63
263
|
|
|
264
|
+
if !run.lazies_at_depth.empty?
|
|
265
|
+
with_trace_query_lazy(trace_query_lazy) do
|
|
266
|
+
if enqueue_next_pending_lazies(run.lazies_at_depth)
|
|
267
|
+
run_queue(run, run.snoozed_jobs_condition, :jobs)
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
rescue StandardError => err
|
|
273
|
+
raised_error = err
|
|
274
|
+
root_task.cancel
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
task.run
|
|
278
|
+
task.wait
|
|
279
|
+
end
|
|
280
|
+
create_pending_run
|
|
281
|
+
if raised_error
|
|
282
|
+
raise raised_error
|
|
283
|
+
end
|
|
284
|
+
trace&.end_dataloader(self)
|
|
64
285
|
rescue UncaughtThrowError => e
|
|
65
286
|
throw e.tag, e.value
|
|
66
287
|
end
|
|
67
288
|
|
|
68
289
|
private
|
|
69
290
|
|
|
70
|
-
def
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
291
|
+
def run_queue(run, condition, mode)
|
|
292
|
+
opened_queues = false
|
|
293
|
+
|
|
294
|
+
if condition.waiting?
|
|
295
|
+
opened_queues = true
|
|
296
|
+
run.new_queues(mode)
|
|
297
|
+
run.expect_resumes(condition.instance_variable_get(:@ready).num_waiting)
|
|
298
|
+
condition.signal
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
loop do
|
|
302
|
+
pending_work = (mode == :jobs) ? (!run.jobs.empty? && run.jobs_bandwidth? ? run.jobs : nil) : (drain_pending_sources)
|
|
303
|
+
if pending_work
|
|
304
|
+
if opened_queues == false
|
|
305
|
+
opened_queues = true
|
|
306
|
+
run.new_queues(mode)
|
|
307
|
+
end
|
|
308
|
+
num_tasks = mode == :sources ? run.current_sources_fiber_limit : 1
|
|
309
|
+
if num_tasks > pending_work.size
|
|
310
|
+
num_tasks = pending_work.size
|
|
76
311
|
end
|
|
312
|
+
spawn_tasks(run, mode, condition, pending_work, num_tasks)
|
|
77
313
|
end
|
|
78
|
-
end
|
|
79
|
-
job_fibers.concat(next_job_fibers)
|
|
80
|
-
next_job_fibers.clear
|
|
81
|
-
end
|
|
82
314
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
315
|
+
if !opened_queues
|
|
316
|
+
break
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
run.check_error!
|
|
320
|
+
|
|
321
|
+
if run.quiesced?
|
|
322
|
+
if !run.has_pending_work? || !run.has_bandwidth?
|
|
323
|
+
break
|
|
90
324
|
end
|
|
325
|
+
# Quiesced, but more work appeared - loop around to drain it.
|
|
326
|
+
else
|
|
327
|
+
run.wait_for_activity
|
|
91
328
|
end
|
|
92
329
|
end
|
|
330
|
+
ensure
|
|
331
|
+
if opened_queues
|
|
332
|
+
run.close_queues
|
|
333
|
+
end
|
|
334
|
+
end
|
|
93
335
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
336
|
+
# Use a separate method for this so that the outer loop's reassignment of `pending_work`
|
|
337
|
+
# doesn't affect already-running tasks which (would) close over that variable
|
|
338
|
+
def spawn_tasks(run, mode, condition, pending_work, num_tasks)
|
|
339
|
+
fiber_vars = get_fiber_variables
|
|
340
|
+
trace = run.trace
|
|
341
|
+
num_tasks.times do
|
|
342
|
+
new_task = Async::Task.new(run.root_task) do |task|
|
|
343
|
+
task.graphql_async_dataloader_run = run
|
|
344
|
+
task.graphql_async_dataloader_condition = condition
|
|
98
345
|
set_fiber_variables(fiber_vars)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
trace&.
|
|
102
|
-
|
|
103
|
-
|
|
346
|
+
case mode
|
|
347
|
+
when :jobs
|
|
348
|
+
trace&.dataloader_spawn_execution_fiber(pending_work)
|
|
349
|
+
while job = pending_work.shift
|
|
350
|
+
job.call
|
|
351
|
+
end
|
|
352
|
+
when :sources
|
|
353
|
+
trace&.dataloader_spawn_source_fiber(pending_work)
|
|
354
|
+
while (source = pending_work.shift)
|
|
355
|
+
Fiber[:__graphql_current_dataloader_source] = source
|
|
356
|
+
trace&.begin_dataloader_source(source)
|
|
357
|
+
source.run_pending_keys
|
|
358
|
+
trace&.end_dataloader_source(source)
|
|
359
|
+
end
|
|
360
|
+
else
|
|
361
|
+
raise ArgumentError, "Unknown mode: #{mode.inspect}"
|
|
104
362
|
end
|
|
363
|
+
nil
|
|
364
|
+
rescue StandardError => err
|
|
365
|
+
run.push_task_message(:task_error, err)
|
|
366
|
+
else
|
|
367
|
+
run.push_task_message(:finished_task, task)
|
|
368
|
+
ensure
|
|
105
369
|
cleanup_fiber
|
|
106
370
|
trace&.dataloader_fiber_exit
|
|
107
371
|
end
|
|
372
|
+
run.push_task_message(:started_task, new_task)
|
|
108
373
|
end
|
|
109
374
|
end
|
|
110
375
|
end
|
|
@@ -20,9 +20,7 @@ module GraphQL
|
|
|
20
20
|
# @return [Dataloader::Request] a pending request for a value from `key`. Call `.load` on that object to wait for the result.
|
|
21
21
|
def request(value)
|
|
22
22
|
res_key = result_key_for(value)
|
|
23
|
-
|
|
24
|
-
@pending[res_key] ||= normalize_fetch_key(value)
|
|
25
|
-
end
|
|
23
|
+
add_pending_key(res_key, value)
|
|
26
24
|
Dataloader::Request.new(self, value)
|
|
27
25
|
end
|
|
28
26
|
|
|
@@ -51,9 +49,7 @@ module GraphQL
|
|
|
51
49
|
def request_all(values)
|
|
52
50
|
values.each do |v|
|
|
53
51
|
res_key = result_key_for(v)
|
|
54
|
-
|
|
55
|
-
@pending[res_key] ||= normalize_fetch_key(v)
|
|
56
|
-
end
|
|
52
|
+
add_pending_key(res_key, v)
|
|
57
53
|
end
|
|
58
54
|
Dataloader::RequestAll.new(self, values)
|
|
59
55
|
end
|
|
@@ -65,7 +61,7 @@ module GraphQL
|
|
|
65
61
|
if @results.key?(result_key)
|
|
66
62
|
result_for(result_key)
|
|
67
63
|
else
|
|
68
|
-
|
|
64
|
+
add_pending_key(result_key, value)
|
|
69
65
|
sync([result_key])
|
|
70
66
|
result_for(result_key)
|
|
71
67
|
end
|
|
@@ -79,8 +75,7 @@ module GraphQL
|
|
|
79
75
|
values.each { |v|
|
|
80
76
|
k = result_key_for(v)
|
|
81
77
|
result_keys << k
|
|
82
|
-
if
|
|
83
|
-
@pending[k] ||= normalize_fetch_key(v)
|
|
78
|
+
if add_pending_key(k, v)
|
|
84
79
|
pending_keys << k
|
|
85
80
|
end
|
|
86
81
|
}
|
|
@@ -89,7 +84,8 @@ module GraphQL
|
|
|
89
84
|
sync(pending_keys)
|
|
90
85
|
end
|
|
91
86
|
|
|
92
|
-
result_keys.map { |k| result_for(k) }
|
|
87
|
+
result_keys.map! { |k| result_for(k) }
|
|
88
|
+
result_keys
|
|
93
89
|
end
|
|
94
90
|
|
|
95
91
|
# Subclasses must implement this method to return a value for each of `keys`
|
|
@@ -105,6 +101,7 @@ module GraphQL
|
|
|
105
101
|
# Then run the batch and update the cache.
|
|
106
102
|
# @return [void]
|
|
107
103
|
def sync(pending_result_keys)
|
|
104
|
+
@dataloader.queue_pending_source(self) if pending?
|
|
108
105
|
@dataloader.yield(self)
|
|
109
106
|
iterations = 0
|
|
110
107
|
while pending_result_keys.any? { |key| !@results.key?(key) }
|
|
@@ -138,22 +135,25 @@ module GraphQL
|
|
|
138
135
|
# @api private
|
|
139
136
|
# @return [void]
|
|
140
137
|
def run_pending_keys
|
|
141
|
-
|
|
142
|
-
@fetching.each_key { |k| @pending.delete(k) }
|
|
143
|
-
end
|
|
138
|
+
@fetching.each_key { |k| @pending.delete(k) }
|
|
144
139
|
return if @pending.empty?
|
|
145
140
|
fetch_h = @pending
|
|
146
|
-
@pending = {}
|
|
147
141
|
@fetching.merge!(fetch_h)
|
|
142
|
+
@pending = {}
|
|
148
143
|
results = fetch(fetch_h.values)
|
|
149
|
-
|
|
144
|
+
idx = 0
|
|
145
|
+
|
|
146
|
+
fetch_h.each_key do |key|
|
|
150
147
|
@results[key] = results[idx]
|
|
148
|
+
@fetching.delete(key)
|
|
149
|
+
idx += 1
|
|
151
150
|
end
|
|
152
151
|
nil
|
|
153
152
|
rescue StandardError => error
|
|
154
|
-
fetch_h.each_key { |key|
|
|
155
|
-
|
|
156
|
-
|
|
153
|
+
fetch_h.each_key { |key|
|
|
154
|
+
@results[key] = error
|
|
155
|
+
@fetching.delete(key)
|
|
156
|
+
}
|
|
157
157
|
end
|
|
158
158
|
|
|
159
159
|
# These arguments are given to `dataloader.with(source_class, ...)`. The object
|
|
@@ -171,7 +171,11 @@ module GraphQL
|
|
|
171
171
|
# @param batch_kwargs [Hash]
|
|
172
172
|
# @return [Object]
|
|
173
173
|
def self.batch_key_for(*batch_args, **batch_kwargs)
|
|
174
|
-
|
|
174
|
+
if batch_kwargs.any? # rubocop:disable Development/NoneWithoutBlockCop
|
|
175
|
+
[*batch_args, **batch_kwargs]
|
|
176
|
+
else
|
|
177
|
+
batch_args
|
|
178
|
+
end
|
|
175
179
|
end
|
|
176
180
|
|
|
177
181
|
# Clear any already-loaded objects for this source
|
|
@@ -185,6 +189,15 @@ module GraphQL
|
|
|
185
189
|
|
|
186
190
|
private
|
|
187
191
|
|
|
192
|
+
def add_pending_key(result_key, value)
|
|
193
|
+
return false if @results.key?(result_key)
|
|
194
|
+
|
|
195
|
+
was_empty = @pending.empty?
|
|
196
|
+
@pending[result_key] ||= normalize_fetch_key(value)
|
|
197
|
+
@dataloader.queue_pending_source(self) if was_empty
|
|
198
|
+
true
|
|
199
|
+
end
|
|
200
|
+
|
|
188
201
|
# Reads and returns the result for the key from the internal cache, or raises an error if the result was an error
|
|
189
202
|
# @param key [Object] key passed to {#load} or {#load_all}
|
|
190
203
|
# @return [Object] The result from {#fetch} for `key`.
|