class GraphQL::Execution::Multiplex

Execute multiple queries under the same multiplex “umbrella”. They can share a batching context and reduce redundant database hits.

The flow is:

If one query raises an application error, all queries will be in undefined states.

Validation errors and {GraphQL::ExecutionError}s are handled in isolation: one of these errors in one query will not affect the other queries.

@see {Schema#multiplex} for public API @api private

Constants

NO_OPERATION

Used internally to signal that the query shouldn’t be executed @api private

Attributes

context[R]
dataloader[R]
max_complexity[R]
queries[R]
schema[R]

Public Class Methods

begin_query(results, idx, query, multiplex) click to toggle source

@param query [GraphQL::Query]

# File lib/graphql/execution/multiplex.rb, line 112
def begin_query(results, idx, query, multiplex)
  operation = query.selected_operation
  result = if operation.nil? || !query.valid? || query.context.errors.any?
    NO_OPERATION
  else
    begin
      query.schema.query_execution_strategy.begin_query(query, multiplex)
    rescue GraphQL::ExecutionError => err
      query.context.errors << err
      NO_OPERATION
    end
  end
  results[idx] = result
  nil
end
new(schema:, queries:, context:, max_complexity:) click to toggle source
# File lib/graphql/execution/multiplex.rb, line 33
def initialize(schema:, queries:, context:, max_complexity:)
  @schema = schema
  @queries = queries
  @queries.each { |q| q.multiplex = self }
  @context = context
  @dataloader = @context[:dataloader] ||= @schema.dataloader_class.new
  @tracers = schema.tracers + (context[:tracers] || [])
  # Support `context: {backtrace: true}`
  if context[:backtrace] && !@tracers.include?(GraphQL::Backtrace::Tracer)
    @tracers << GraphQL::Backtrace::Tracer
  end
  @max_complexity = max_complexity
end
run_all(schema, query_options, context: {}, max_complexity: schema.max_complexity) click to toggle source

@param schema [GraphQL::Schema] @param queries [Array<GraphQL::Query, Hash>] @param context [Hash] @param max_complexity [Integer, nil] @return [Array<Hash>] One result per query

# File lib/graphql/execution/multiplex.rb, line 53
def run_all(schema, query_options, context: {}, max_complexity: schema.max_complexity)
  queries = query_options.map do |opts|
    case opts
    when Hash
      GraphQL::Query.new(schema, nil, **opts)
    when GraphQL::Query
      opts
    else
      raise "Expected Hash or GraphQL::Query, not #{opts.class} (#{opts.inspect})"
    end
  end

  multiplex = self.new(schema: schema, queries: queries, context: context, max_complexity: max_complexity)
  multiplex.trace("execute_multiplex", { multiplex: multiplex }) do
    GraphQL::Execution::Instrumentation.apply_instrumenters(multiplex) do
      schema = multiplex.schema
      multiplex_analyzers = schema.multiplex_analyzers
      if multiplex.max_complexity
        multiplex_analyzers += [GraphQL::Analysis::AST::MaxQueryComplexity]
      end

      schema.analysis_engine.analyze_multiplex(multiplex, multiplex_analyzers)

      begin
        multiplex.schema.query_execution_strategy.begin_multiplex(multiplex)
        # Do as much eager evaluation of the query as possible
        results = []
        queries.each_with_index do |query, idx|
          multiplex.dataloader.append_job { begin_query(results, idx, query, multiplex) }
        end

        multiplex.dataloader.run

        # Then, work through lazy results in a breadth-first way
        multiplex.dataloader.append_job {
          multiplex.schema.query_execution_strategy.finish_multiplex(results, multiplex)
        }
        multiplex.dataloader.run

        # Then, find all errors and assign the result to the query object
        results.each_with_index do |data_result, idx|
          query = queries[idx]
          finish_query(data_result, query, multiplex)
          # Get the Query::Result, not the Hash
          results[idx] = query.result
        end

        results
      rescue Exception
        # TODO rescue at a higher level so it will catch errors in analysis, too
        # Assign values here so that the query's `@executed` becomes true
        queries.map { |q| q.result_values ||= {} }
        raise
      end
    end
  end
end

Private Class Methods

finish_query(data_result, query, multiplex) click to toggle source

@param data_result [Hash] The result for the “data” key, if any @param query [GraphQL::Query] The query which was run @return [Hash] final result of this query, including all values and errors

# File lib/graphql/execution/multiplex.rb, line 133
def finish_query(data_result, query, multiplex)
  # Assign the result so that it can be accessed in instrumentation
  query.result_values = if data_result.equal?(NO_OPERATION)
    if !query.valid? || query.context.errors.any?
      # A bit weird, but `Query#static_errors` _includes_ `query.context.errors`
      { "errors" => query.static_errors.map(&:to_h) }
    else
      data_result
    end
  else
    # Use `context.value` which was assigned during execution
    result = query.schema.query_execution_strategy.finish_query(query, multiplex)

    if query.context.errors.any?
      error_result = query.context.errors.map(&:to_h)
      result["errors"] = error_result
    end

    result
  end
  if query.context.namespace?(:__query_result_extensions__)
    query.result_values["extensions"] = query.context.namespace(:__query_result_extensions__)
  end
end