Skip to content

Instantly share code, notes, and snippets.

@rmosolgo
Last active November 20, 2023 17:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rmosolgo/a51486fdf5691e6ddcc5b44736fdabad to your computer and use it in GitHub Desktop.
Save rmosolgo/a51486fdf5691e6ddcc5b44736fdabad to your computer and use it in GitHub Desktop.
Using GraphQL-Ruby 1.12 with @defer and { backtrace: true }
require "bundler/inline"
gemfile do
gem "graphql", "1.12.18"
gem "graphql-pro", "1.24.13"
gem "graphql-batch"
end
# Monkey-patch this to avoid deleting keys from multiplex.context
# in the ensure block below.
# (Fixed on new GraphQL-Ruby versions in https://github.com/rmosolgo/graphql-ruby/pull/4708)
module GraphQL
class Backtrace
module Tracer
module_function
def trace(key, metadata)
case key
when "lex", "parse"
# No context here, don't have a query yet
nil
when "execute_multiplex", "analyze_multiplex"
# No query context yet
nil
when "validate", "analyze_query", "execute_query", "execute_query_lazy"
push_key = []
if (query = metadata[:query]) || ((queries = metadata[:queries]) && (query = queries.first))
push_data = query
multiplex = query.multiplex
elsif (multiplex = metadata[:multiplex])
push_data = multiplex.queries.first
end
when "execute_field", "execute_field_lazy"
query = metadata[:query] || raise(ArgumentError, "Add `legacy: true` to use GraphQL::Backtrace without the interpreter runtime.")
multiplex = query.multiplex
push_key = metadata[:path]
parent_frame = multiplex.context[:graphql_backtrace_contexts][push_key[0..-2]]
if parent_frame.is_a?(GraphQL::Query)
parent_frame = parent_frame.context
end
push_data = Frame.new(
query: query,
path: push_key,
ast_node: metadata[:ast_node],
field: metadata[:field],
object: metadata[:object],
arguments: metadata[:arguments],
parent_frame: parent_frame,
)
else
# Custom key, no backtrace data for this
nil
end
if push_data && multiplex
push_storage = multiplex.context[:graphql_backtrace_contexts] ||= {}
push_storage[push_key] = push_data
multiplex.context[:last_graphql_backtrace_context] = push_data
end
if key == "execute_multiplex"
multiplex_context = metadata[:multiplex].context
begin
yield
rescue StandardError => err
# This is an unhandled error from execution,
# Re-raise it with a GraphQL trace.
potential_context = multiplex_context[:last_graphql_backtrace_context]
if potential_context.is_a?(GraphQL::Query::Context) ||
potential_context.is_a?(GraphQL::Query::Context::FieldResolutionContext) ||
potential_context.is_a?(Backtrace::Frame)
raise TracedError.new(err, potential_context)
else
raise
end
# Don't clean these up -- leave them for @defer to use
# ensure
# multiplex_context.delete(:graphql_backtrace_contexts)
# multiplex_context.delete(:last_graphql_backtrace_context)
end
else
yield
end
end
end
end
end
# Use the example from https://graphql-ruby.org/defer/setup.html#with-graphql-batch
module Directives
# Modify the library's `@defer` implementation to work with GraphQL-Batch
class Defer < GraphQL::Pro::Defer
def self.resolve(obj, arguments, context, &block)
# While the query is running, store the batch executor to re-use later
context[:graphql_batch_executor] ||= GraphQL::Batch::Executor.current
super
end
class Deferral < GraphQL::Pro::Defer::Deferral
def resolve
# Before calling the deferred execution,
# set GraphQL-Batch back up:
prev_executor = GraphQL::Batch::Executor.current
GraphQL::Batch::Executor.current ||= @context[:graphql_batch_executor]
super
ensure
# Clean up afterward:
GraphQL::Batch::Executor.current = prev_executor
end
end
end
end
require "ostruct"
class Schema < GraphQL::Schema
class UserLoader < GraphQL::Batch::Loader
def perform(ids)
p "Fetching IDs: #{ids}"
ids.each { |id| fulfill(id, OpenStruct.new({ name: "User ##{id}" })) }
end
end
class User < GraphQL::Schema::Object
field :name, String, null: false
def name
user.then(&:name)
end
private
def user
UserLoader.load(object)
end
end
class UserCollection < GraphQL::Schema::Object
field :collection, [User], null: false
def collection
object # [1, 2, 3]
end
end
class Query < GraphQL::Schema::Object
field :users, UserCollection, null: false
def users
[1, 2, 3]
end
end
query(Query)
use Directives::Defer
use GraphQL::Batch
end
query_str = "{ users { collection { name @defer } } }"
res = Schema.execute(query_str, context: { backtrace: false })
pp res.to_h
res.context[:defer].each do |deferral|
pp [:deferred, deferral.to_h]
end
# {"data"=>{"users"=>{"collection"=>[{}, {}, {}]}}}
# [:deferred, {:hasNext=>true, :data=>{"users"=>{"collection"=>[{}, {}, {}]}}}]
# "Fetching IDs: [1]"
# [:deferred, {:path=>["users", "collection", 0, "name"], :hasNext=>true, :data=>"User #1"}]
# "Fetching IDs: [2]"
# [:deferred, {:path=>["users", "collection", 1, "name"], :hasNext=>true, :data=>"User #2"}]
# "Fetching IDs: [3]"
# [:deferred, {:path=>["users", "collection", 2, "name"], :hasNext=>false, :data=>"User #3"}]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment