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]
queries[R]
schema[R]

Public Class Methods

new(schema:, queries:, context:) click to toggle source
# File lib/graphql/execution/multiplex.rb, line 33
def initialize(schema:, queries:, context:)
  @schema = schema
  @queries = queries
  @context = context
  # TODO remove support for global tracers
  @tracers = schema.tracers + GraphQL::Tracing.tracers + (context[:tracers] || [])
  # Support `context: {backtrace: true}`
  if context[:backtrace] && !@tracers.include?(GraphQL::Backtrace::Tracer)
    @tracers << GraphQL::Backtrace::Tracer
  end
end
run_all(schema, query_options, *args) click to toggle source
# File lib/graphql/execution/multiplex.rb, line 46
def run_all(schema, query_options, *args)
  queries = query_options.map { |opts| GraphQL::Query.new(schema, nil, opts) }
  run_queries(schema, queries, *args)
end
run_queries(schema, queries, context: {}, max_complexity: schema.max_complexity) click to toggle source

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

# File lib/graphql/execution/multiplex.rb, line 56
def run_queries(schema, queries, context: {}, max_complexity: schema.max_complexity)
  multiplex = self.new(schema: schema, queries: queries, context: context)
  multiplex.trace("execute_multiplex", { multiplex: multiplex }) do
    if has_custom_strategy?(schema)
      if queries.length != 1
        raise ArgumentError, "Multiplexing doesn't support custom execution strategies, run one query at a time instead"
      else
        instrument_and_analyze(multiplex, max_complexity: max_complexity) do
          [run_one_legacy(schema, queries.first)]
        end
      end
    else
      instrument_and_analyze(multiplex, max_complexity: max_complexity) do
        run_as_multiplex(multiplex)
      end
    end
  end
end

Private Class Methods

begin_query(query) click to toggle source

@param query [GraphQL::Query] @return [Hash] The initial result (may not be finished if there are lazy values)

# File lib/graphql/execution/multiplex.rb, line 102
def begin_query(query)
  operation = query.selected_operation
  if operation.nil? || !query.valid? || query.context.errors.any?
    NO_OPERATION
  else
    begin
      GraphQL::Execution::Execute::ExecutionFunctions.resolve_root_selection(query)
    rescue GraphQL::ExecutionError => err
      query.context.errors << err
      NO_OPERATION
    end
  end
end
finish_query(data_result, query) 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 119
def finish_query(data_result, query)
  # 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 = {
      "data" => Execution::Flatten.call(query.context)
    }

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

    result
  end
end
has_custom_strategy?(schema) click to toggle source
# File lib/graphql/execution/multiplex.rb, line 157
def has_custom_strategy?(schema)
  schema.query_execution_strategy != GraphQL::Execution::Execute ||
    schema.mutation_execution_strategy != GraphQL::Execution::Execute ||
    schema.subscription_execution_strategy != GraphQL::Execution::Execute
end
instrument_and_analyze(multiplex, max_complexity:) { || ... } click to toggle source

Apply multiplex & query instrumentation to `queries`.

It yields when the queries should be executed, then runs teardown.

# File lib/graphql/execution/multiplex.rb, line 166
def instrument_and_analyze(multiplex, max_complexity:)
  GraphQL::Execution::Instrumentation.apply_instrumenters(multiplex) do
    schema = multiplex.schema
    multiplex_analyzers = schema.multiplex_analyzers
    if max_complexity
      multiplex_analyzers += [GraphQL::Analysis::MaxQueryComplexity.new(max_complexity)]
    end

    GraphQL::Analysis.analyze_multiplex(multiplex, multiplex_analyzers)
    yield
  end
end
run_as_multiplex(multiplex) click to toggle source
# File lib/graphql/execution/multiplex.rb, line 77
def run_as_multiplex(multiplex)
  queries = multiplex.queries
  # Do as much eager evaluation of the query as possible
  results = queries.map do |query|
    begin_query(query)
  end

  # Then, work through lazy results in a breadth-first way
  GraphQL::Execution::Execute::ExecutionFunctions.lazy_resolve_root_selection(results, { multiplex: multiplex })

  # Then, find all errors and assign the result to the query object
  results.each_with_index.map do |data_result, idx|
    query = queries[idx]
    finish_query(data_result, query)
    # Get the Query::Result, not the Hash
    query.result
  end
rescue Exception
  # Assign values here so that the query's `@executed` becomes true
  queries.map { |q| q.result_values ||= {} }
  raise
end
run_one_legacy(schema, query) click to toggle source

use the old `query_execution_strategy` etc to run this query

# File lib/graphql/execution/multiplex.rb, line 144
def run_one_legacy(schema, query)
  query.result_values = if !query.valid?
    all_errors = query.validation_errors + query.analysis_errors + query.context.errors
    if all_errors.any?
      { "errors" => all_errors.map(&:to_h) }
    else
      nil
    end
  else
    GraphQL::Query::Executor.new(query).result
  end
end