lib/graphql/schema/timeout.rb in graphql-1.11.4 vs lib/graphql/schema/timeout.rb in graphql-1.11.5

- old
+ new

@@ -5,11 +5,11 @@ # 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 the `handle_timeout` method + # 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 @@ -31,12 +31,10 @@ # class MySchema < GraphQL::Schema # use MyTimeout, max_seconds: 2 # end # class Timeout - attr_reader :max_seconds - def self.use(schema, **options) tracer = new(**options) schema.tracer(tracer) end @@ -46,44 +44,60 @@ end def trace(key, data) case key when 'execute_multiplex' - timeout_state = { - timeout_at: Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) + max_seconds * 1000, - timed_out: false - } - 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 = data[:context] ? data.fetch(:context).query : data.fetch(:query) - timeout_state = query.context.namespace(self.class).fetch(:state) - if Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) > timeout_state.fetch(:timeout_at) + 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] - context = data.fetch(:context) - GraphQL::Schema::Timeout::TimeoutError.new(context.parent_type, context.field) + 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 - unless timeout_state[:timed_out] + if !timeout_state[:timed_out] timeout_state[:timed_out] = true - handle_timeout(error, query) + handle_timeout(error, query_context.query) end error else yield end else yield end + end + + # Called at the start of each query. + # The default implementation returns the `max_seconds:` value from installing this plugin. + # + # @param query [GraphQL::Query] The query that's about to run + # @return [Integer, false] The number of seconds after which to interrupt query execution and call {#handle_error}, or `false` to bypass the timeout. + def max_seconds(query) + @max_seconds end # Invoked when a query times out. # @param error [GraphQL::Schema::Timeout::TimeoutError] # @param query [GraphQL::Error]