graphql 2.5.11 → 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.
Files changed (154) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/detailed_trace_generator.rb +77 -0
  3. data/lib/generators/graphql/field_extractor.rb +17 -1
  4. data/lib/generators/graphql/templates/create_graphql_detailed_traces.erb +10 -0
  5. data/lib/generators/graphql/templates/schema.erb +2 -1
  6. data/lib/graphql/analysis/query_complexity.rb +31 -15
  7. data/lib/graphql/analysis.rb +20 -13
  8. data/lib/graphql/backtrace/table.rb +10 -1
  9. data/lib/graphql/current.rb +7 -1
  10. data/lib/graphql/dashboard/application_controller.rb +45 -0
  11. data/lib/graphql/dashboard/landings_controller.rb +9 -0
  12. data/lib/graphql/dashboard/statics_controller.rb +31 -0
  13. data/lib/graphql/dashboard/subscriptions.rb +2 -1
  14. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/_form.html.erb +1 -0
  15. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/edit.html.erb +2 -2
  16. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/index.html.erb +1 -1
  17. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/new.html.erb +1 -1
  18. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/index.html.erb +1 -1
  19. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/show.html.erb +1 -1
  20. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/show.html.erb +1 -1
  21. data/lib/graphql/dashboard/views/layouts/graphql/dashboard/application.html.erb +7 -7
  22. data/lib/graphql/dashboard.rb +14 -73
  23. data/lib/graphql/dataloader/async_dataloader.rb +336 -60
  24. data/lib/graphql/dataloader/null_dataloader.rb +48 -10
  25. data/lib/graphql/dataloader/source.rb +32 -19
  26. data/lib/graphql/dataloader.rb +107 -39
  27. data/lib/graphql/date_encoding_error.rb +1 -1
  28. data/lib/graphql/execution/directive_checks.rb +2 -0
  29. data/lib/graphql/execution/field_resolve_step.rb +804 -0
  30. data/lib/graphql/execution/finalize.rb +229 -0
  31. data/lib/graphql/execution/input_values.rb +333 -0
  32. data/lib/graphql/execution/interpreter/arguments_cache.rb +3 -0
  33. data/lib/graphql/execution/interpreter/handles_raw_value.rb +6 -0
  34. data/lib/graphql/execution/interpreter/resolve.rb +7 -13
  35. data/lib/graphql/execution/interpreter/runtime/graphql_result.rb +13 -0
  36. data/lib/graphql/execution/interpreter/runtime.rb +60 -40
  37. data/lib/graphql/execution/interpreter.rb +17 -23
  38. data/lib/graphql/execution/lazy.rb +1 -1
  39. data/lib/graphql/execution/load_argument_step.rb +102 -0
  40. data/lib/graphql/execution/multiplex.rb +1 -1
  41. data/lib/graphql/execution/next.rb +98 -0
  42. data/lib/graphql/execution/prepare_object_step.rb +151 -0
  43. data/lib/graphql/execution/resolve_type_step.rb +27 -0
  44. data/lib/graphql/execution/runner.rb +457 -0
  45. data/lib/graphql/execution/selections_step.rb +95 -0
  46. data/lib/graphql/execution.rb +15 -4
  47. data/lib/graphql/execution_error.rb +17 -12
  48. data/lib/graphql/float_decoding_error.rb +13 -0
  49. data/lib/graphql/float_encoding_error.rb +28 -0
  50. data/lib/graphql/introspection/directive_type.rb +7 -3
  51. data/lib/graphql/introspection/dynamic_fields.rb +5 -1
  52. data/lib/graphql/introspection/entry_points.rb +11 -3
  53. data/lib/graphql/introspection/enum_value_type.rb +5 -5
  54. data/lib/graphql/introspection/field_type.rb +13 -5
  55. data/lib/graphql/introspection/input_value_type.rb +21 -13
  56. data/lib/graphql/introspection/schema_type.rb +6 -2
  57. data/lib/graphql/introspection/type_type.rb +64 -28
  58. data/lib/graphql/invalid_null_error.rb +11 -5
  59. data/lib/graphql/language/block_string.rb +6 -11
  60. data/lib/graphql/language/cache.rb +84 -14
  61. data/lib/graphql/language/document_from_schema_definition.rb +2 -1
  62. data/lib/graphql/language/lexer.rb +27 -17
  63. data/lib/graphql/language/nodes.rb +2 -1
  64. data/lib/graphql/language/parser.rb +1 -1
  65. data/lib/graphql/language.rb +32 -17
  66. data/lib/graphql/pagination/connection.rb +2 -0
  67. data/lib/graphql/pagination/connections.rb +30 -0
  68. data/lib/graphql/pagination/relation_connection.rb +10 -1
  69. data/lib/graphql/query/context.rb +10 -3
  70. data/lib/graphql/query/null_context.rb +9 -3
  71. data/lib/graphql/query/partial.rb +18 -3
  72. data/lib/graphql/query/variable_validation_error.rb +16 -1
  73. data/lib/graphql/query/variables.rb +1 -1
  74. data/lib/graphql/query.rb +12 -3
  75. data/lib/graphql/railtie.rb +2 -1
  76. data/lib/graphql/runtime_error.rb +6 -0
  77. data/lib/graphql/schema/argument.rb +13 -1
  78. data/lib/graphql/schema/build_from_definition.rb +28 -1
  79. data/lib/graphql/schema/directive/feature.rb +4 -0
  80. data/lib/graphql/schema/directive/transform.rb +20 -0
  81. data/lib/graphql/schema/directive.rb +48 -13
  82. data/lib/graphql/schema/field/connection_extension.rb +4 -37
  83. data/lib/graphql/schema/field/scope_extension.rb +18 -13
  84. data/lib/graphql/schema/field.rb +79 -48
  85. data/lib/graphql/schema/field_extension.rb +11 -8
  86. data/lib/graphql/schema/has_single_input_argument.rb +24 -13
  87. data/lib/graphql/schema/input_object.rb +6 -2
  88. data/lib/graphql/schema/interface.rb +26 -0
  89. data/lib/graphql/schema/introspection_system.rb +6 -21
  90. data/lib/graphql/schema/list.rb +5 -1
  91. data/lib/graphql/schema/member/base_dsl_methods.rb +1 -11
  92. data/lib/graphql/schema/member/has_arguments.rb +43 -14
  93. data/lib/graphql/schema/member/has_authorization.rb +35 -0
  94. data/lib/graphql/schema/member/has_dataloader.rb +37 -0
  95. data/lib/graphql/schema/member/has_fields.rb +86 -5
  96. data/lib/graphql/schema/member.rb +5 -0
  97. data/lib/graphql/schema/non_null.rb +1 -1
  98. data/lib/graphql/schema/object.rb +1 -0
  99. data/lib/graphql/schema/printer.rb +1 -1
  100. data/lib/graphql/schema/ractor_shareable.rb +1 -0
  101. data/lib/graphql/schema/relay_classic_mutation.rb +16 -2
  102. data/lib/graphql/schema/resolver.rb +76 -1
  103. data/lib/graphql/schema/subscription.rb +53 -10
  104. data/lib/graphql/schema/timeout.rb +2 -2
  105. data/lib/graphql/schema/validator/allow_blank_validator.rb +3 -3
  106. data/lib/graphql/schema/validator/allow_null_validator.rb +3 -3
  107. data/lib/graphql/schema/validator/exclusion_validator.rb +2 -2
  108. data/lib/graphql/schema/validator/format_validator.rb +3 -3
  109. data/lib/graphql/schema/validator/inclusion_validator.rb +2 -2
  110. data/lib/graphql/schema/validator/length_validator.rb +6 -6
  111. data/lib/graphql/schema/validator/numericality_validator.rb +19 -19
  112. data/lib/graphql/schema/validator/required_validator.rb +38 -5
  113. data/lib/graphql/schema/validator.rb +9 -0
  114. data/lib/graphql/schema/visibility/profile.rb +74 -53
  115. data/lib/graphql/schema/visibility/visit.rb +1 -1
  116. data/lib/graphql/schema/visibility.rb +32 -24
  117. data/lib/graphql/schema/wrapper.rb +11 -1
  118. data/lib/graphql/schema.rb +93 -27
  119. data/lib/graphql/static_validation/base_visitor.rb +90 -66
  120. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +1 -1
  121. data/lib/graphql/static_validation/rules/argument_names_are_unique.rb +18 -6
  122. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +5 -2
  123. data/lib/graphql/static_validation/rules/directives_are_defined.rb +5 -2
  124. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +4 -3
  125. data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +14 -4
  126. data/lib/graphql/static_validation/rules/fields_will_merge.rb +351 -248
  127. data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +4 -4
  128. data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +3 -3
  129. data/lib/graphql/static_validation/rules/fragment_types_exist.rb +10 -7
  130. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +27 -7
  131. data/lib/graphql/static_validation/rules/variables_are_input_types.rb +12 -9
  132. data/lib/graphql/static_validation/validation_context.rb +1 -1
  133. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +1 -0
  134. data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +40 -10
  135. data/lib/graphql/subscriptions/serialize.rb +10 -4
  136. data/lib/graphql/subscriptions.rb +36 -1
  137. data/lib/graphql/testing/helpers.rb +12 -9
  138. data/lib/graphql/testing/mock_action_cable.rb +111 -0
  139. data/lib/graphql/testing.rb +1 -0
  140. data/lib/graphql/tracing/detailed_trace/active_record_backend.rb +74 -0
  141. data/lib/graphql/tracing/detailed_trace.rb +70 -7
  142. data/lib/graphql/tracing/perfetto_trace.rb +213 -81
  143. data/lib/graphql/tracing/sentry_trace.rb +3 -1
  144. data/lib/graphql/tracing/trace.rb +6 -0
  145. data/lib/graphql/types/float.rb +18 -4
  146. data/lib/graphql/types/relay/connection_behaviors.rb +8 -6
  147. data/lib/graphql/types/relay/edge_behaviors.rb +4 -3
  148. data/lib/graphql/types/relay/has_node_field.rb +13 -8
  149. data/lib/graphql/types/relay/has_nodes_field.rb +13 -8
  150. data/lib/graphql/types/relay/node_behaviors.rb +13 -2
  151. data/lib/graphql/unauthorized_error.rb +9 -1
  152. data/lib/graphql/version.rb +1 -1
  153. data/lib/graphql.rb +8 -2
  154. metadata +23 -59
@@ -1,99 +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
- trace = Fiber[:__graphql_current_multiplex]&.current_trace
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 (condition = Fiber[:graphql_dataloader_next_tick])
9
- condition.wait
10
- else
11
- Fiber.yield
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
- def run
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
31
-
32
- while (f = (job_fibers.shift || (((job_fibers.size + next_job_fibers.size + source_tasks.size) < jobs_fiber_limit) && spawn_job_fiber(trace))))
33
- if f.alive?
34
- finished = run_fiber(f)
35
- if !finished
36
- next_job_fibers << f
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
37
150
  end
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}"
38
158
  end
159
+ @activity.signal
160
+ end
161
+ end
162
+ end
163
+
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
39
217
  end
40
- job_fibers.concat(next_job_fibers)
41
- next_job_fibers.clear
42
-
43
- Sync do |root_task|
44
- set_fiber_variables(fiber_vars)
45
- while !source_tasks.empty? || @source_cache.each_value.any? { |group_sources| group_sources.each_value.any?(&:pending?) }
46
- 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))))
47
- if task.alive?
48
- root_task.yield # give the source task a chance to run
49
- next_source_tasks << task
218
+ end
219
+ end
220
+
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
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)
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)
50
268
  end
51
269
  end
52
- sources_condition.signal
53
- source_tasks.concat(next_source_tasks)
54
- next_source_tasks.clear
55
270
  end
56
271
  end
272
+ rescue StandardError => err
273
+ raised_error = err
274
+ root_task.cancel
57
275
  end
58
- trace&.end_dataloader(self)
59
- end
60
276
 
61
- manager.resume
62
- if manager.alive?
63
- raise "Invariant: Manager didn't terminate successfully: #{manager}"
277
+ task.run
278
+ task.wait
64
279
  end
65
-
280
+ create_pending_run
281
+ if raised_error
282
+ raise raised_error
283
+ end
284
+ trace&.end_dataloader(self)
66
285
  rescue UncaughtThrowError => e
67
286
  throw e.tag, e.value
68
287
  end
69
288
 
70
289
  private
71
290
 
72
- def spawn_source_task(parent_task, condition, trace)
73
- pending_sources = nil
74
- @source_cache.each_value do |source_by_batch_params|
75
- source_by_batch_params.each_value do |source|
76
- if source.pending?
77
- pending_sources ||= []
78
- pending_sources << source
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
311
+ end
312
+ spawn_tasks(run, mode, condition, pending_work, num_tasks)
313
+ end
314
+
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
79
324
  end
325
+ # Quiesced, but more work appeared - loop around to drain it.
326
+ else
327
+ run.wait_for_activity
80
328
  end
81
329
  end
330
+ ensure
331
+ if opened_queues
332
+ run.close_queues
333
+ end
334
+ end
82
335
 
83
- if pending_sources
84
- fiber_vars = get_fiber_variables
85
- parent_task.async do
86
- trace&.dataloader_spawn_source_fiber(pending_sources)
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
87
345
  set_fiber_variables(fiber_vars)
88
- Fiber[:graphql_dataloader_next_tick] = condition
89
- pending_sources.each do |s|
90
- trace&.begin_dataloader_source(s)
91
- s.run_pending_keys
92
- trace&.end_dataloader_source(s)
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}"
93
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
94
369
  cleanup_fiber
95
370
  trace&.dataloader_fiber_exit
96
371
  end
372
+ run.push_task_message(:started_task, new_task)
97
373
  end
98
374
  end
99
375
  end
@@ -2,24 +2,62 @@
2
2
 
3
3
  module GraphQL
4
4
  class Dataloader
5
- # The default implementation of dataloading -- all no-ops.
5
+ # GraphQL-Ruby uses this when Dataloader isn't enabled.
6
6
  #
7
- # The Dataloader interface isn't public, but it enables
8
- # simple internal code while adding the option to add Dataloader.
7
+ # It runs execution code inline and gathers lazy objects (eg. Promises)
8
+ # and resolves them during {#run}.
9
9
  class NullDataloader < Dataloader
10
- # These are all no-ops because code was
11
- # executed synchronously.
10
+ def initialize(*)
11
+ @lazies_at_depth = Hash.new { |h,k| h[k] = [] }
12
+ end
13
+
14
+ def freeze
15
+ @lazies_at_depth.default_proc = nil
16
+ @lazies_at_depth.freeze
17
+ super
18
+ end
19
+
20
+ def run(trace_query_lazy: nil)
21
+ with_trace_query_lazy(trace_query_lazy) do
22
+ while !@lazies_at_depth.empty?
23
+ smallest_depth = nil
24
+ @lazies_at_depth.each_key do |depth_key|
25
+ smallest_depth ||= depth_key
26
+ if depth_key < smallest_depth
27
+ smallest_depth = depth_key
28
+ end
29
+ end
30
+
31
+ if smallest_depth
32
+ lazies = @lazies_at_depth.delete(smallest_depth)
33
+ lazies.each(&:value) # resolve these Lazy instances
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ def run_isolated
40
+ # Reuse this instance because execution code may already have a reference to _this_ `dataloader` inside the given block.
41
+ prev_lazies_at_depth = @lazies_at_depth
42
+ @lazies_at_depth = @lazies_at_depth.dup.clear
43
+ res = nil
44
+ append_job {
45
+ res = yield
46
+ }
47
+ run
48
+ res
49
+ ensure
50
+ @lazies_at_depth = prev_lazies_at_depth
51
+ end
12
52
 
13
- def initialize(*); end
14
- def run; end
15
- def run_isolated; yield; end
16
53
  def clear_cache; end
54
+
17
55
  def yield(_source)
18
56
  raise GraphQL::Error, "GraphQL::Dataloader is not running -- add `use GraphQL::Dataloader` to your schema to use Dataloader sources."
19
57
  end
20
58
 
21
- def append_job
22
- yield
59
+ def append_job(callable = nil)
60
+ callable ? callable.call : yield
23
61
  nil
24
62
  end
25
63
 
@@ -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
- if !@results.key?(res_key)
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
- if !@results.key?(res_key)
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
- @pending[result_key] ||= normalize_fetch_key(value)
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 !@results.key?(k)
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
- if !@fetching.empty?
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
- fetch_h.each_with_index do |(key, _value), idx|
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| @results[key] = error }
155
- ensure
156
- fetch_h && fetch_h.each_key { |k| @fetching.delete(k) }
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
- [*batch_args, **batch_kwargs]
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`.