class GraphQL::Dataloader
This plugin supports Fiber-based concurrency, along with GraphQL::Dataloader::Source.
Examples
Example: Installing Dataloader
class MySchema < GraphQL::Schema use GraphQL::Dataloader end
Example: Waiting for batch-loaded data in a GraphQL field
field :team, Types::Team, null: true def team dataloader.with(Sources::Record, Team).load(object.team_id) end
Attributes
Returns
-
Integer, nil
Public Class Methods
Source
# File lib/graphql/dataloader.rb, line 66 def initialize(nonblocking: self.class.default_nonblocking, fiber_limit: self.class.default_fiber_limit) @source_cache = Hash.new { |h, k| h[k] = {} }.compare_by_identity @pending_source_set = Set.new.compare_by_identity @pending_sources = [] @pending_jobs = [] if !nonblocking.nil? @nonblocking = nonblocking end @fiber_limit = fiber_limit @lazies_at_depth = Hash.new { |h, k| h[k] = [] } end
# File lib/graphql/dataloader.rb, line 38 def self.use(schema, nonblocking: nil, fiber_limit: nil) dataloader_class = if nonblocking warn("`nonblocking: true` is deprecated from `GraphQL::Dataloader`, please use `GraphQL::Dataloader::AsyncDataloader` instead. Docs: https://graphql-ruby.org/dataloader/async_dataloader.") Class.new(self) { self.default_nonblocking = true } else self end if fiber_limit dataloader_class = Class.new(dataloader_class) dataloader_class.default_fiber_limit = fiber_limit end schema.dataloader_class = dataloader_class end
Source
# File lib/graphql/dataloader.rb, line 56 def self.with_dataloading(&block) dataloader = self.new result = nil #: untyped dataloader.append_job { result = block.call(dataloader) } dataloader.run result end
Call the block with a Dataloader instance, then run all enqueued jobs and return the result of the block.
Public Instance Methods
Source
# File lib/graphql/dataloader.rb, line 125 def cleanup_fiber end
This method is called when Dataloader is finished using a fiber. Use it to perform any cleanup, such as releasing database connections (if required manually)
Source
# File lib/graphql/dataloader.rb, line 198 def clear_cache @source_cache.each do |_source_class, batched_sources| batched_sources.each_value(&:clear_cache) end nil end
# File lib/graphql/dataloader.rb, line 96 def get_fiber_variables fiber_vars = {} Thread.current.keys.each do |fiber_var_key| fiber_vars[fiber_var_key] = Thread.current[fiber_var_key] end fiber_vars end
This is called before the fiber is spawned, from the parent context (i.e. from the thread or fiber that it is scheduled from).
Returns
-
Hash<Symbol, Object>—Currentfiber-local variables
# File lib/graphql/dataloader.rb, line 330 def merge_records(records, index_by: :id) records_by_class = Hash.new { |h, k| h[k] = {} } records.each do |r| records_by_class[r.class][r.public_send(index_by)] = r end records_by_class.each do |r_class, records| with(ActiveRecordSource, r_class).merge(records) end end
Pre-warm the Dataloader cache with ActiveRecord objects which were loaded elsewhere. These will be used by Dataloader::ActiveRecordSource, Dataloader::ActiveRecordAssociationSource and their helper methods, dataload_record and dataload_association.
Parameters
-
records(Array<ActiveRecord::Base>) — Already-loaded records to warm the cache with -
index_by(Symbol) — The attribute to use as the cache key. (Should matchfind_by:when usingActiveRecordSource)
Returns
-
void
# File lib/graphql/dataloader.rb, line 250 def run(trace_query_lazy: nil) trace = Fiber[:__graphql_current_multiplex]&.current_trace jobs_fiber_limit, total_fiber_limit = calculate_fiber_limit job_fibers = [] next_job_fibers = [] source_fibers = [] next_source_fibers = [] first_pass = true #: bool manager = spawn_fiber do trace&.begin_dataloader(self) while first_pass || !job_fibers.empty? first_pass = false run_pending_steps(trace, job_fibers, next_job_fibers, jobs_fiber_limit, source_fibers, next_source_fibers, total_fiber_limit) if !@lazies_at_depth.empty? with_trace_query_lazy(trace_query_lazy) do if enqueue_next_pending_lazies(@lazies_at_depth) job_fibers.unshift(spawn_job_fiber(trace)) run_pending_steps(trace, job_fibers, next_job_fibers, jobs_fiber_limit, source_fibers, next_source_fibers, total_fiber_limit) end end end end trace&.end_dataloader(self) end run_fiber(manager) if manager.alive? raise "Invariant: Manager fiber didn't terminate properly." end if !job_fibers.empty? raise "Invariant: job fibers should have exited but #{job_fibers.size} remained" end if !source_fibers.empty? raise "Invariant: source fibers should have exited but #{source_fibers.size} remained" end rescue UncaughtThrowError => e throw e.tag, e.value end
Parameters
-
trace_query_lazy(nil, Execution::Multiplex)
Source
# File lib/graphql/dataloader.rb, line 206 def run_isolated prev_queue = @pending_jobs prev_pending_keys = {} prev_lazies_at_depth = @lazies_at_depth @lazies_at_depth = @lazies_at_depth.dup.clear # Clear pending loads but keep already-cached records # in case they are useful to the given block. @source_cache.each do |source_class, batched_sources| batched_sources.each do |batch_args, batched_source_instance| if batched_source_instance.pending? prev_pending_keys[batched_source_instance] = batched_source_instance.pending.dup batched_source_instance.pending.clear end end end @pending_jobs = [] res = nil #: untyped # Make sure the block is inside a Fiber, so it can `Fiber.yield` append_job { res = yield } run res ensure @pending_jobs = prev_queue @lazies_at_depth = prev_lazies_at_depth prev_pending_keys #: as !nil .each do |source_instance, pending| pending.each do |key, value| next if source_instance.results.key?(key) queue_pending_source(source_instance) if source_instance.pending.empty? source_instance.pending[key] = value end end end
Use a self-contained queue for the work in the block.
# File lib/graphql/dataloader.rb, line 118 def set_fiber_variables(vars) vars.each { |k, v| Thread.current[k] = v } nil end
Set up the fiber variables in a new fiber.
This is called within the fiber, right after it is spawned.
Parameters
-
vars(Hash<Symbol, Object>) — Fiber-local variables fromget_fiber_variables
Returns
-
void
Source
# File lib/graphql/dataloader.rb, line 303 def spawn_fiber fiber_vars = get_fiber_variables Fiber.new(blocking: !@nonblocking) do # Fibers inherit the spawning fiber's storage; give each one its own execution state. Fiber[:__graphql_runtime_info] = nil set_fiber_variables(fiber_vars) yield ensure cleanup_fiber end end
# File lib/graphql/dataloader.rb, line 149 def with(source_class, *batch_args, **batch_kwargs) batch_key = source_class.batch_key_for(*batch_args, **batch_kwargs) @source_cache[source_class][batch_key] ||= begin source = source_class.new(*batch_args, **batch_kwargs) source.setup(self) source end end
Source
# File lib/graphql/dataloader.rb, line 168 def yield(source = Fiber[:__graphql_current_dataloader_source]) trace = Fiber[:__graphql_current_multiplex]&.current_trace trace&.dataloader_fiber_yield(source) Fiber.yield trace&.dataloader_fiber_resume(source) nil end
Tell the dataloader that this fiber is waiting for data.
Dataloader will resume the fiber after the requested data has been loaded (by another Fiber).
Returns
-
void