Skip to content

Instantly share code, notes, and snippets.

@bessey
Last active December 22, 2020 18:24
Show Gist options
  • Save bessey/62e1ddd30bc3d2467cf196ae9e44d412 to your computer and use it in GitHub Desktop.
Save bessey/62e1ddd30bc3d2467cf196ae9e44d412 to your computer and use it in GitHub Desktop.
Proposal for alternative Fiber based design for GraphQL Ruby batched data loading
# The general idea is that similarly to lazy evaluation, the interpreter executes each resolver from inside a Fiber.
# Once we have instantiated a Fiber for every resolver reachable in this pass, we loop over the Fibers, yielding till
# they are dead (the resolver has returned). Resolvers that are not using the Fiber based Loader API would simply
# return there value on the first pass.
# Complete pseudo-cpde, I don't know GraphQL Ruby internals well
def main_graphql_ruby_interpreter_loop(reachable_fields)
unresolved_fields = reachable_fields.reduce({}) do |acc, field|
# Creating one Fiber per field is probably too expensive, and we'd want to re-use them across loops or even across
# different fields in the same loop. A topic for another time.
fiber = Fiber.new { field.resolve }
# Associate the field to the fiber so we can later attach the value to the right field
acc[field] = fiber
end
while !unresolved_fields.empty? do
unresolved_fields.each { |field, fiber| field.value = fiber.resume }
unresolved_fields = unresolved_fields.select { |field, fiber| fiber.alive? }
end
# Now every field has returned a concrete value, we can return those values, and be called again with the previously
# unreachable leaf nodes. Rinse and repeat until there's no fields left to call
reachable_fields
end
class MyQueryType < GraphQL::BaseObject
field :user, UserType, null: true
# No data-loading example for reference
def user(id:)
User.find(id)
end
# Fibre based data-loading
def user(id:)
loader = context.loader # Query wide LoaderManager instance
user = loader.find(User, id)
# At this point we have a real object, not a Promise, or any other lazy proxy. The loading complexity is completely
# hidden from the resolver, unlike with Shopify/graphql-batch
if user.deactivated?
# And loaders can be chained unlike with exAspArk/batch-loader, although I think similarly to React hooks, if loader
# calls / call order is non deterministic, it will reduce the effectiveness of batching
loader.find(User, user.active_user_id)
else
user
end
end
end
class LoaderManager
def initialize
@store = LoaderStore.new
end
def find(scope, id)
loader = store.get(scope)
loader.enqueue(id)
# Where the magic happens, yield back to the interpreter so it may queue other IDs before we try and load data
Fiber.yield
loader.find(id)
end
end
class LoaderStore
def initialize
@cache = {}
end
def get(scope)
@cache[scope] ||= Loader.new(scope)
end
end
class Loader
def initialize(scope)
@scope = scope
@ids = []
@results = nil
end
def enqueue(id)
@ids << id
end
def find(id)
populate_results_if_missing
@results.find { |record| record.id == id }
end
private
def populate_results_if_missing
return if @results
@results = scope.where(id: ids)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment