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

[View source]

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

[View source]

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.

[View source]

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.

[View source]

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

def max_seconds(query)
  @max_seconds
end

#trace(key, data) ⇒ Object

[View source]

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