graphql 2.6.2 → 2.6.5

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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/field_extractor.rb +17 -1
  3. data/lib/graphql/analysis/query_complexity.rb +31 -15
  4. data/lib/graphql/backtrace/table.rb +10 -1
  5. data/lib/graphql/current.rb +7 -1
  6. data/lib/graphql/dataloader/async_dataloader.rb +310 -60
  7. data/lib/graphql/dataloader/source.rb +18 -10
  8. data/lib/graphql/dataloader.rb +1 -1
  9. data/lib/graphql/execution/directive_checks.rb +2 -0
  10. data/lib/graphql/execution/field_resolve_step.rb +84 -18
  11. data/lib/graphql/execution/finalize.rb +10 -9
  12. data/lib/graphql/execution/interpreter/arguments_cache.rb +3 -0
  13. data/lib/graphql/execution/interpreter/runtime.rb +0 -6
  14. data/lib/graphql/execution/load_argument_step.rb +6 -0
  15. data/lib/graphql/execution/prepare_object_step.rb +14 -4
  16. data/lib/graphql/execution/runner.rb +7 -1
  17. data/lib/graphql/execution/selections_step.rb +6 -2
  18. data/lib/graphql/execution_error.rb +4 -0
  19. data/lib/graphql/language.rb +8 -2
  20. data/lib/graphql/schema/argument.rb +1 -1
  21. data/lib/graphql/schema/interface.rb +1 -1
  22. data/lib/graphql/schema/introspection_system.rb +6 -21
  23. data/lib/graphql/schema/printer.rb +1 -1
  24. data/lib/graphql/schema/validator/allow_blank_validator.rb +3 -3
  25. data/lib/graphql/schema/validator/allow_null_validator.rb +3 -3
  26. data/lib/graphql/schema/validator/exclusion_validator.rb +2 -2
  27. data/lib/graphql/schema/validator/format_validator.rb +3 -3
  28. data/lib/graphql/schema/validator/inclusion_validator.rb +2 -2
  29. data/lib/graphql/schema/validator/length_validator.rb +6 -6
  30. data/lib/graphql/schema/validator/numericality_validator.rb +19 -19
  31. data/lib/graphql/schema/validator/required_validator.rb +6 -4
  32. data/lib/graphql/schema/validator.rb +9 -0
  33. data/lib/graphql/schema/visibility/profile.rb +6 -4
  34. data/lib/graphql/schema/visibility.rb +30 -22
  35. data/lib/graphql/schema.rb +1 -1
  36. data/lib/graphql/version.rb +1 -1
  37. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a0c0b5ff2af84e07dab381443b1b62137074324bb3063c8387bf6dc777d51d00
4
- data.tar.gz: 4e5383e45c9d62ea5a04782867a743d01d6db0d0bc853deadc7a229f26d440e4
3
+ metadata.gz: 8ece536c0702c8789a3a5c79de82af4bd2ff806a3209d0ac4da88fc968629d19
4
+ data.tar.gz: 0063f1ea552e7782029083eb027138a0725af89a65265dc643469f2227184134
5
5
  SHA512:
6
- metadata.gz: 86019205d2eb2dabd464dc615af28ab078c1a48e43c0701a1852dddfd13eded8954e42e91b7176c685eed10679b15bf4bcef2c86c48d9f9ad4b870cdc78bb19a
7
- data.tar.gz: b95880ff00058b167fe7574ca5e267814fe7aa9be65df0d2691d658344a796dd1fdafedad6cc35b58c3299c5401b39a964353cb5ab0e3974296ca4c26f9ab504
6
+ metadata.gz: 2ee406b8ad164004af45d98e3b5dc70d69396f008a80cc68ef83a4bff212698c0a181644dc2b513e8bd1e71d2baf7d1ba2901ad05f497199556b171a6dfb11ab
7
+ data.tar.gz: 2ffd4e01742bd3b0feb207a316b1935d1edff05be8b28afb1c42e5ba47b4df916df802ee15617bb30bd45cdf6fd4f7a59c22b5d73cbabb3d8ee6658f3246d8ef
@@ -6,7 +6,23 @@ module Graphql
6
6
  module FieldExtractor
7
7
  def fields
8
8
  columns = []
9
- columns += (klass&.columns&.map { |c| generate_column_string(c) } || [])
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
 
@@ -9,6 +9,8 @@ module GraphQL
9
9
  super
10
10
  @skip_introspection_fields = !query.schema.max_complexity_count_introspection_fields
11
11
  @complexities_on_type_by_query = {}
12
+ @intersect_cache = Hash.new { |h, k| h[k] = {}.compare_by_identity }.compare_by_identity
13
+ @possible_types_cache = {}.compare_by_identity
12
14
  end
13
15
 
14
16
  # Override this method to use the complexity result
@@ -27,14 +29,14 @@ module GraphQL
27
29
  future_complexity
28
30
  end
29
31
  when nil
30
- subject.logger.warn <<~GRAPHQL
32
+ subject.logger.warn <<~MESSAGE
31
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
32
34
 
33
35
  To opt into the future behavior, configure your schema (#{subject.schema.name ? subject.schema.name : subject.schema.ancestors}) with:
34
36
 
35
37
  complexity_cost_calculation_mode(:future) # or `:legacy`, `:compare`
36
38
 
37
- GRAPHQL
39
+ MESSAGE
38
40
  max_possible_complexity(mode: :legacy)
39
41
  else
40
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}"
@@ -159,8 +161,22 @@ module GraphQL
159
161
 
160
162
  def types_intersect?(query, a, b)
161
163
  return true if a == b
162
- a_types = query.types.possible_types(a)
163
- query.types.possible_types(b).any? { |t| a_types.include?(t) }
164
+
165
+ if a.object_id < b.object_id
166
+ first_cache = @intersect_cache[a]
167
+ second_key = b
168
+ else
169
+ first_cache = @intersect_cache[b]
170
+ second_key = a
171
+ end
172
+
173
+ if first_cache.key?(second_key)
174
+ first_cache[second_key]
175
+ else
176
+ a_types = @possible_types_cache[a] ||= query.types.possible_types(a).to_set
177
+ b_types = @possible_types_cache[b] ||= query.types.possible_types(b).to_set
178
+ first_cache[second_key] = a_types.intersect?(b_types)
179
+ end
164
180
  end
165
181
 
166
182
  # A hook which is called whenever a field's max complexity is calculated.
@@ -175,18 +191,16 @@ module GraphQL
175
191
  # @param inner_selections [Array<Hash<String, ScopedTypeComplexity>>] Field selections for a scope
176
192
  # @return [Integer] Total complexity value for all these selections in the parent scope
177
193
  def merged_max_complexity(query, inner_selections)
178
- # Aggregate a set of all unique field selection keys across all scopes.
179
- # Use a hash, but ignore the values; it's just a fast way to work with the keys.
180
- unique_field_keys = inner_selections.each_with_object({}) do |inner_selection, memo|
181
- memo.merge!(inner_selection)
194
+ child_scopes_by_key = {}
195
+ inner_selections.each do |inner_selection|
196
+ inner_selection.each do |k, v|
197
+ scopes = child_scopes_by_key[k] ||= []
198
+ scopes << v
199
+ end
182
200
  end
183
-
184
201
  # Add up the total cost for each unique field name's coalesced selections
185
- unique_field_keys.each_key.reduce(0) do |total, field_key|
186
- # Collect all child scopes for this field key;
187
- # all keys come with at least one scope.
188
- child_scopes = inner_selections.filter_map { _1[field_key] }
189
-
202
+ total = 0
203
+ child_scopes_by_key.each do |field_key, child_scopes|
190
204
  # Compute maximum possible cost of child selections;
191
205
  # composites merge their maximums, while leaf scopes are always zero.
192
206
  # FieldsWillMerge validation assures all scopes are uniformly composite or leaf.
@@ -214,8 +228,10 @@ module GraphQL
214
228
  child_complexity: maximum_children_cost,
215
229
  )
216
230
 
217
- total + maximum_cost
231
+ total += maximum_cost
218
232
  end
233
+
234
+ total
219
235
  end
220
236
 
221
237
  def legacy_merged_max_complexity(query, inner_selections)
@@ -90,7 +90,16 @@ module GraphQL
90
90
 
91
91
  if ast_node
92
92
  field_defn = query.get_field(result.graphql_result_type, ast_node.name)
93
- args = query.arguments_for(ast_node, field_defn).to_h
93
+ args = begin
94
+ if (cached_args = query.arguments_cache.cached_arguments_for(ast_node, field_defn))
95
+ cached_args.to_h
96
+ else
97
+ EmptyObjects::EMPTY_HASH
98
+ end
99
+ rescue StandardError => err
100
+ "Failed to load arguments, #{err.class}: #{err.message}"
101
+ end
102
+
94
103
  field_path = field_defn.path
95
104
  if ast_node.alias
96
105
  field_path += " as #{ast_node.alias}"
@@ -41,7 +41,13 @@ module GraphQL
41
41
  # @see GraphQL::Field#path for a string identifying this field
42
42
  # @return [GraphQL::Field, nil] The currently-running field, if there is one.
43
43
  def self.field
44
- Fiber[:__graphql_runtime_info]&.values&.first&.current_field
44
+ if (interpreter_info = Fiber[:__graphql_runtime_info])
45
+ interpreter_info.values&.first&.current_field
46
+ elsif (field = Fiber[:__graphql_current_field])
47
+ field
48
+ else
49
+ nil
50
+ end
45
51
  end
46
52
 
47
53
  # @return [Class, nil] The currently-running {Dataloader::Source} class, if there is one.
@@ -1,86 +1,324 @@
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
+ if !Async::Task.method_defined?(:cancel)
8
+ Async::Task.alias_method(:cancel, :stop)
9
+ end
10
+ if !Async::Task.method_defined?(:graphql_async_dataloader_run)
11
+ Async::Task.attr_accessor(:graphql_async_dataloader_run)
12
+ Async::Task.attr_accessor(:graphql_async_dataloader_condition)
13
+ end
14
+ super
15
+ end
16
+
17
+ def initialize(...)
18
+ super
19
+ create_pending_run
20
+ end
21
+
22
+ def create_pending_run
23
+ jobs_fiber_limit, total_fiber_limit = calculate_fiber_limit
24
+ @pending_run = Run.new(total_fiber_limit, jobs_fiber_limit)
25
+ end
26
+
5
27
  def yield(source = Fiber[:__graphql_current_dataloader_source])
6
- trace = Fiber[:__graphql_current_multiplex]&.current_trace
28
+ task = Async::Task.current
29
+ run = task.graphql_async_dataloader_run
30
+ trace = run.trace
7
31
  trace&.dataloader_fiber_yield(source)
8
- if (condition = Fiber[:graphql_dataloader_next_tick])
9
- condition.wait
10
- else
11
- Fiber.yield
12
- end
32
+ run.finished_tasks.push(task)
33
+ condition = task.graphql_async_dataloader_condition
34
+ condition.wait
35
+ run.started_tasks.push(task)
13
36
  trace&.dataloader_fiber_resume(source)
14
37
  nil
15
38
  end
16
39
 
17
- def run(trace_query_lazy: nil)
18
- trace = Fiber[:__graphql_current_multiplex]&.current_trace
19
- jobs_fiber_limit, total_fiber_limit = calculate_fiber_limit
20
- job_fibers = []
21
- next_job_fibers = []
22
- source_tasks = []
23
- next_source_tasks = []
24
- first_pass = true
25
- sources_condition = Async::Condition.new
26
- manager = spawn_fiber do
27
- trace&.begin_dataloader(self)
28
- while first_pass || !job_fibers.empty?
29
- first_pass = false
30
- fiber_vars = get_fiber_variables
40
+ class Run
41
+ def initialize(total_fiber_limit, jobs_fiber_limit)
42
+ @root_task = nil
43
+ @trace = nil
44
+ @jobs = []
31
45
 
32
- run_pending_steps(job_fibers, next_job_fibers, source_tasks, jobs_fiber_limit, trace)
46
+ @total_fiber_limit = total_fiber_limit
47
+ @jobs_fiber_limit = jobs_fiber_limit
48
+ @lazies_at_depth = Hash.new { |h, k| h[k] = [] }
33
49
 
34
- Sync do |root_task|
35
- set_fiber_variables(fiber_vars)
36
- while !source_tasks.empty? || @source_cache.each_value.any? { |group_sources| group_sources.each_value.any?(&:pending?) }
37
- while (task = (source_tasks.shift || (((job_fibers.size + next_job_fibers.size + source_tasks.size + next_source_tasks.size) < total_fiber_limit) && spawn_source_task(root_task, sources_condition, trace))))
38
- if task.alive?
39
- root_task.yield # give the source task a chance to run
40
- next_source_tasks << task
41
- end
42
- end
43
- sources_condition.signal
44
- source_tasks.concat(next_source_tasks)
45
- next_source_tasks.clear
50
+ @finished_tasks = nil
51
+ @started_tasks = nil
52
+ @started_count_task = nil
53
+ @finished_count_task = nil
54
+ @finished_all_tasks = nil
55
+ @finished_first_pass = nil
56
+
57
+ @snoozed_jobs_condition = Async::Condition.new
58
+ @snoozed_sources_condition = Async::Condition.new
59
+ end
60
+
61
+ attr_accessor :trace, :root_task
62
+
63
+ attr_reader :jobs, :lazies_at_depth, :jobs_fiber_limit, :total_fiber_limit, :finished_tasks, :started_tasks, :snoozed_jobs_condition, :snoozed_sources_condition
64
+
65
+ def jobs_bandwidth?
66
+ running_count < jobs_fiber_limit
67
+ end
68
+
69
+ def allowed_sources_tasks
70
+ within_limit = total_fiber_limit - running_count
71
+ if within_limit < 1
72
+ 1
73
+ else
74
+ within_limit
75
+ end
76
+ end
77
+
78
+ def close_queues
79
+ @finished_tasks.close
80
+ @finished_count_task.cancel
81
+
82
+ @started_tasks.close
83
+ @started_count_task.cancel
84
+ end
85
+
86
+ def running_count
87
+ @snoozed_jobs_condition.instance_variable_get(:@ready).num_waiting +
88
+ @snoozed_sources_condition.instance_variable_get(:@ready).num_waiting +
89
+ @started_count +
90
+ @started_tasks.size -
91
+ @finished_count
92
+ end
93
+
94
+ def wait_for_queues
95
+ if !@finished_first_pass.resolved?
96
+ @finished_first_pass.resolve(true)
97
+ end
98
+
99
+ @finished_all_tasks.wait
100
+ @finished_all_tasks = Async::Promise.new
101
+ end
102
+
103
+ def new_queues
104
+ @finished_tasks = Async::Queue.new
105
+ @finished_count = 0
106
+ @started_tasks = Async::Queue.new
107
+ @started_count = 0
108
+ @finished_first_pass = Async::Promise.new
109
+ @finished_all_tasks = Async::Promise.new
110
+
111
+ @started_count_task = @root_task.async do |task|
112
+ @finished_first_pass.wait
113
+ while task = @started_tasks.wait
114
+ @started_count += 1
115
+ if task.status == :initialized # could also be resumed after waiting
116
+ task.run
46
117
  end
47
118
  end
119
+ end
48
120
 
49
- if !@lazies_at_depth.empty?
50
- with_trace_query_lazy(trace_query_lazy) do
51
- run_next_pending_lazies(job_fibers, trace)
52
- run_pending_steps(job_fibers, next_job_fibers, source_tasks, jobs_fiber_limit, trace)
121
+ @finished_count_task = @root_task.async do |task|
122
+ while t_or_err = @finished_tasks.wait
123
+ if t_or_err.is_a?(StandardError)
124
+ @finished_all_tasks.reject(t_or_err)
125
+ else
126
+ @finished_count += 1
127
+ if @finished_count == @started_count
128
+ @finished_all_tasks.resolve(true)
129
+ end
53
130
  end
54
131
  end
55
132
  end
56
- trace&.end_dataloader(self)
57
133
  end
58
134
 
59
- manager.resume
60
- if manager.alive?
61
- raise "Invariant: Manager didn't terminate successfully: #{manager}"
135
+ def running?
136
+ @snoozed_jobs_condition.waiting? || @snoozed_sources_condition.waiting?
62
137
  end
138
+ end
139
+
140
+ def append_job(callable = nil, &block)
141
+ active_run.jobs.push(callable || block)
142
+ nil
143
+ end
144
+
145
+ def lazy_at_depth(depth, lazy)
146
+ active_run.lazies_at_depth[depth] << lazy
147
+ end
148
+
149
+ def active_run
150
+ @pending_run || Async::Task.current?&.graphql_async_dataloader_run || raise(GraphQL::Error, "No available Run to append to, GraphQL-Ruby bug")
151
+ end
63
152
 
153
+ def run_isolated
154
+ previous_run = Async::Task.current?&.graphql_async_dataloader_run
155
+ prev_pending_keys = {}
156
+ # Clear pending loads but keep already-cached records
157
+ # in case they are useful to the given block.
158
+ @source_cache.each do |source_class, batched_sources|
159
+ batched_sources.each do |batch_args, batched_source_instance|
160
+ if batched_source_instance.pending?
161
+ prev_pending_keys[batched_source_instance] = batched_source_instance.pending.dup
162
+ batched_source_instance.pending.clear
163
+ end
164
+ end
165
+ end
166
+
167
+ res = nil
168
+ create_pending_run
169
+ @pending_run.jobs << -> { res = yield }
170
+ run
171
+ res
172
+ ensure
173
+ if previous_run
174
+ Async::Task.current.graphql_async_dataloader_run = previous_run
175
+ # clear the one created in #run:
176
+ @pending_run = nil
177
+ end
178
+ prev_pending_keys.each do |source_instance, pending|
179
+ pending.each do |key, value|
180
+ if !source_instance.results.key?(key)
181
+ source_instance.pending[key] = value
182
+ end
183
+ end
184
+ end
185
+ end
186
+
187
+ def run(trace_query_lazy: nil)
188
+ trace = Fiber[:__graphql_current_multiplex]&.current_trace
189
+ run = @pending_run || Async::Task.current?&.graphql_async_dataloader_run || raise(GraphQL::Error, "No available Run, GraphQL-Ruby internal bug")
190
+ @pending_run = nil
191
+ run.trace = trace
192
+ first_pass = true
193
+ trace&.begin_dataloader(self)
194
+ fiber_vars = get_fiber_variables
195
+ raised_error = nil
196
+ jobs = run.jobs
197
+ Sync do |_maybe_new_task|
198
+ # Make sure there's a new task instance to hold `.graphql_...` state:
199
+ task = Async::Task.new do |root_task|
200
+ run.root_task = root_task
201
+ root_task.graphql_async_dataloader_run = run
202
+ set_fiber_variables(fiber_vars)
203
+
204
+ while first_pass || run.running? || !jobs.empty?
205
+ first_pass = false
206
+ run_pending_steps(run)
207
+ run_sources(run)
208
+
209
+ if !run.lazies_at_depth.empty?
210
+ with_trace_query_lazy(trace_query_lazy) do
211
+ run_next_pending_lazies(run)
212
+ run_pending_steps(run)
213
+ end
214
+ end
215
+ end
216
+ rescue StandardError => err
217
+ raised_error = err
218
+ root_task.cancel
219
+ end
220
+
221
+ task.run
222
+ task.wait
223
+ end
224
+ create_pending_run
225
+ if raised_error
226
+ raise raised_error
227
+ end
228
+ trace&.end_dataloader(self)
64
229
  rescue UncaughtThrowError => e
65
230
  throw e.tag, e.value
66
231
  end
67
232
 
68
233
  private
69
234
 
70
- def run_pending_steps(job_fibers, next_job_fibers, source_tasks, jobs_fiber_limit, trace)
71
- while (f = (job_fibers.shift || (((job_fibers.size + next_job_fibers.size + source_tasks.size) < jobs_fiber_limit) && spawn_job_fiber(trace))))
72
- if f.alive?
73
- finished = run_fiber(f)
74
- if !finished
75
- next_job_fibers << f
235
+ def run_pending_steps(run)
236
+ run.new_queues
237
+
238
+ if (unsnoozed = run.snoozed_jobs_condition.waiting?)
239
+ run.snoozed_jobs_condition.signal
240
+ end
241
+ pending_jobs = run.jobs
242
+ while (!pending_jobs.empty? && (has_limit = run.jobs_bandwidth?)) || (unsnoozed)
243
+ unsnoozed = false
244
+ if has_limit
245
+ spawn_job_task(run)
246
+ end
247
+ run.wait_for_queues
248
+ end
249
+ ensure
250
+ run.close_queues
251
+ end
252
+
253
+ def spawn_job_task(run)
254
+ pending_jobs = run.jobs
255
+ if !pending_jobs.empty?
256
+ fiber_vars = get_fiber_variables
257
+ new_task = Async::Task.new(run.root_task) do |task|
258
+ run.trace&.dataloader_spawn_execution_fiber(pending_jobs)
259
+ task.graphql_async_dataloader_run = run
260
+ task.graphql_async_dataloader_condition = run.snoozed_jobs_condition
261
+ set_fiber_variables(fiber_vars)
262
+ while job = pending_jobs.shift
263
+ job.call
76
264
  end
265
+ ensure
266
+ cleanup_fiber
267
+ run.finished_tasks.push($! || task)
268
+ run.trace&.dataloader_fiber_exit
77
269
  end
270
+ run.started_tasks.push(new_task)
271
+ new_task
78
272
  end
79
- job_fibers.concat(next_job_fibers)
80
- next_job_fibers.clear
81
273
  end
82
274
 
83
- def spawn_source_task(parent_task, condition, trace)
275
+ def run_sources(run)
276
+ run.new_queues
277
+
278
+ if (unsnoozed = run.snoozed_sources_condition.waiting?)
279
+ run.snoozed_sources_condition.signal
280
+ end
281
+
282
+ allowed_tasks = run.allowed_sources_tasks
283
+ while (has_pending = @source_cache.each_value.any? { |group_sources| group_sources.each_value.any?(&:pending?) } ) || unsnoozed
284
+ unsnoozed = false
285
+ if has_pending
286
+ spawn_source_task(run, allowed_tasks)
287
+ end
288
+ run.wait_for_queues
289
+ end
290
+ ensure
291
+ run.close_queues
292
+ end
293
+
294
+ #### TODO DRY Had to duplicate to remove spawn_job_fiber
295
+ def run_next_pending_lazies(run)
296
+ smallest_depth = nil
297
+ run.lazies_at_depth.each_key do |depth_key|
298
+ smallest_depth ||= depth_key
299
+ if depth_key < smallest_depth
300
+ smallest_depth = depth_key
301
+ end
302
+ end
303
+
304
+ if smallest_depth
305
+ lazies = run.lazies_at_depth.delete(smallest_depth)
306
+ if !lazies.empty?
307
+ begin
308
+ run.new_queues
309
+ lazies.each_with_index do |l, idx|
310
+ append_job { l.value }
311
+ end
312
+ spawn_job_task(run) # Todo what was the last `true` condition?
313
+ run.wait_for_queues
314
+ ensure
315
+ run.close_queues
316
+ end
317
+ end
318
+ end
319
+ end
320
+
321
+ def spawn_source_task(run, num_tasks)
84
322
  pending_sources = nil
85
323
  @source_cache.each_value do |source_by_batch_params|
86
324
  source_by_batch_params.each_value do |source|
@@ -92,18 +330,30 @@ module GraphQL
92
330
  end
93
331
 
94
332
  if pending_sources
333
+ if num_tasks == Float::INFINITY
334
+ num_tasks = pending_sources.size
335
+ end
95
336
  fiber_vars = get_fiber_variables
96
- parent_task.async do
97
- trace&.dataloader_spawn_source_fiber(pending_sources)
98
- set_fiber_variables(fiber_vars)
99
- Fiber[:graphql_dataloader_next_tick] = condition
100
- pending_sources.each do |s|
101
- trace&.begin_dataloader_source(s)
102
- s.run_pending_keys
103
- trace&.end_dataloader_source(s)
337
+ trace = run.trace
338
+ num_tasks.times do
339
+ new_task = Async::Task.new(run.root_task) do |task|
340
+ task.graphql_async_dataloader_run = run
341
+ task.graphql_async_dataloader_condition = run.snoozed_sources_condition
342
+ trace&.dataloader_spawn_source_fiber(pending_sources)
343
+ set_fiber_variables(fiber_vars)
344
+ while (source = pending_sources.shift)
345
+ trace&.begin_dataloader_source(source)
346
+ source.run_pending_keys
347
+ trace&.end_dataloader_source(source)
348
+ end
349
+ nil
350
+ ensure
351
+ run.finished_tasks.push($! || task)
352
+ cleanup_fiber
353
+ trace&.dataloader_fiber_exit
104
354
  end
105
- cleanup_fiber
106
- trace&.dataloader_fiber_exit
355
+ run.started_tasks.push(new_task)
356
+ new_task
107
357
  end
108
358
  end
109
359
  end
@@ -89,7 +89,8 @@ module GraphQL
89
89
  sync(pending_keys)
90
90
  end
91
91
 
92
- result_keys.map { |k| result_for(k) }
92
+ result_keys.map! { |k| result_for(k) }
93
+ result_keys
93
94
  end
94
95
 
95
96
  # Subclasses must implement this method to return a value for each of `keys`
@@ -138,22 +139,25 @@ module GraphQL
138
139
  # @api private
139
140
  # @return [void]
140
141
  def run_pending_keys
141
- if !@fetching.empty?
142
- @fetching.each_key { |k| @pending.delete(k) }
143
- end
142
+ @fetching.each_key { |k| @pending.delete(k) }
144
143
  return if @pending.empty?
145
144
  fetch_h = @pending
146
- @pending = {}
147
145
  @fetching.merge!(fetch_h)
146
+ @pending = {}
148
147
  results = fetch(fetch_h.values)
149
- fetch_h.each_with_index do |(key, _value), idx|
148
+ idx = 0
149
+
150
+ fetch_h.each_key do |key|
150
151
  @results[key] = results[idx]
152
+ @fetching.delete(key)
153
+ idx += 1
151
154
  end
152
155
  nil
153
156
  rescue StandardError => error
154
- fetch_h.each_key { |key| @results[key] = error }
155
- ensure
156
- fetch_h && fetch_h.each_key { |k| @fetching.delete(k) }
157
+ fetch_h.each_key { |key|
158
+ @results[key] = error
159
+ @fetching.delete(key)
160
+ }
157
161
  end
158
162
 
159
163
  # These arguments are given to `dataloader.with(source_class, ...)`. The object
@@ -171,7 +175,11 @@ module GraphQL
171
175
  # @param batch_kwargs [Hash]
172
176
  # @return [Object]
173
177
  def self.batch_key_for(*batch_args, **batch_kwargs)
174
- [*batch_args, **batch_kwargs]
178
+ if batch_kwargs.any? # rubocop:disable Development/NoneWithoutBlockCop
179
+ [*batch_args, **batch_kwargs]
180
+ else
181
+ batch_args
182
+ end
175
183
  end
176
184
 
177
185
  # Clear any already-loaded objects for this source
@@ -58,7 +58,7 @@ module GraphQL
58
58
  end
59
59
 
60
60
  def initialize(nonblocking: self.class.default_nonblocking, fiber_limit: self.class.default_fiber_limit)
61
- @source_cache = Hash.new { |h, k| h[k] = {} }
61
+ @source_cache = Hash.new { |h, k| h[k] = {} }.compare_by_identity
62
62
  @pending_jobs = []
63
63
  if !nonblocking.nil?
64
64
  @nonblocking = nonblocking
@@ -18,11 +18,13 @@ module GraphQL
18
18
  case name
19
19
  when SKIP
20
20
  args = query.arguments_for(directive_ast_node, directive_defn)
21
+ next if args.is_a?(GraphQL::ExecutionError)
21
22
  if args[:if] == true
22
23
  return false
23
24
  end
24
25
  when INCLUDE
25
26
  args = query.arguments_for(directive_ast_node, directive_defn)
27
+ next if args.is_a?(GraphQL::ExecutionError)
26
28
  if args[:if] == false
27
29
  return false
28
30
  end