Class: GraphQL::Schema::Timeout

Inherits:
Object
  • Object
show all
Defined in:
lib/graphql/schema/timeout.rb

Overview

This plugin will stop resolving new fields after max_seconds have elapsed. After the time has passed, any remaining fields will be nil, with errors added to the errors key. Any already-resolved fields will be in the data key, so you'll get a partial response.

You can subclass GraphQL::Schema::Timeout and override max_seconds and/or handle_timeout to provide custom logic when a timeout error occurs.

Note that this will stop a query in between field resolutions, but it doesn't interrupt long-running resolve functions. Be sure to use timeout options for external connections. For more info, see www.mikeperham.com/2015/05/08/timeout-rubys-most-dangerous-api/

Examples:

Stop resolving fields after 2 seconds

class MySchema < GraphQL::Schema
  use GraphQL::Schema::Timeout, max_seconds: 2
end

Notifying Bugsnag and logging a timeout

class MyTimeout < GraphQL::Schema::Timeout
  def handle_timeout(error, query)
     Rails.logger.warn("GraphQL Timeout: #{error.message}: #{query.query_string}")
     Bugsnag.notify(error, {query_string: query.query_string})
  end
end

class MySchema < GraphQL::Schema
  use MyTimeout, max_seconds: 2
end

Defined Under Namespace

Classes: TimeoutError

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(max_seconds:) ⇒ Timeout

Returns a new instance of Timeout.

Parameters:

  • max_seconds (Numeric)

    how many seconds the query should be allowed to resolve new fields


42
43
44
# File 'lib/graphql/schema/timeout.rb', line 42

def initialize(max_seconds:)
  @max_seconds = max_seconds
end

Class Method Details

.use(schema, **options) ⇒ Object


36
37
38
39
# File 'lib/graphql/schema/timeout.rb', line 36

def self.use(schema, **options)
  tracer = new(**options)
  schema.tracer(tracer)
end

Instance Method Details

#handle_timeout(error, query) ⇒ Object

Invoked when a query times out.


105
106
107
# File 'lib/graphql/schema/timeout.rb', line 105

def handle_timeout(error, query)
  # override to do something interesting
end

#max_seconds(query) ⇒ Integer, false

Called at the start of each query. The default implementation returns the max_seconds: value from installing this plugin.

Parameters:

Returns:

  • (Integer, false)

    The number of seconds after which to interrupt query execution and call #handle_error, or false to bypass the timeout.


98
99
100
# File 'lib/graphql/schema/timeout.rb', line 98

def max_seconds(query)
  @max_seconds
end

#trace(key, data) ⇒ Object


46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/graphql/schema/timeout.rb', line 46

def trace(key, data)
  case key
  when 'execute_multiplex'
    data.fetch(:multiplex).queries.each do |query|
      timeout_duration_s = max_seconds(query)
      timeout_state = if timeout_duration_s == false
        # if the method returns `false`, don't apply a timeout
        false
      else
        now = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
        timeout_at = now + (max_seconds(query) * 1000)
        {
          timeout_at: timeout_at,
          timed_out: false
        }
      end
      query.context.namespace(self.class)[:state] = timeout_state
    end

    yield
  when 'execute_field', 'execute_field_lazy'
    query_context = data[:context] || data[:query].context
    timeout_state = query_context.namespace(self.class).fetch(:state)
    # If the `:state` is `false`, then `max_seconds(query)` opted out of timeout for this query.
    if timeout_state != false && Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) > timeout_state.fetch(:timeout_at)
      error = if data[:context]
        GraphQL::Schema::Timeout::TimeoutError.new(query_context.parent_type, query_context.field)
      else
        field = data.fetch(:field)
        GraphQL::Schema::Timeout::TimeoutError.new(field.owner, field)
      end

      # Only invoke the timeout callback for the first timeout
      if !timeout_state[:timed_out]
        timeout_state[:timed_out] = true
        handle_timeout(error, query_context.query)
      end

      error
    else
      yield
    end
  else
    yield
  end
end