-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Description
Is your feature request related to a problem? Please describe.
I want to parallelize batched lazy-loading using Concurrent::Promises while maintaining (most) compatibility with graphql-batch. To achieve this, I was attempting to create a Promise chain:
def field
Concurrent::Promises.delay do
# This runs when graphql-ruby calls the defined `lazy_resolve()` on it
Concurrent::Promises.future do
# This runs in a background thread to do the actual loading and resolves when that's done
# More realistically, this would kick off a shared promise (for the batch) and `.then()` on that
end
end
endThe hope being that the inner Concurrent::Promises.future block would run in parallel with sibling fields, releasing GVL while the database works.
In reality, however, resolution is done depth-first, so graphql-ruby waits for the inner block to resolve before resolving sibling fields.
Describe the solution you'd like
I would like to propose a change from depth-first to breadth-first resolution of lazy values. That is, resolve siblings before you resolve a nested lazy value. This would enable this use case, as well as improve batching in cases where you have a second layer of resolvers. For example, you might have multiple fields like this:
Loader.for().load().then do |results|
OtherLoader.for().load(results)
endCurrently, the OtherLoader won't batch across fields, but with a breadth-first approach it would.
Describe alternatives you've considered
One alternative might be to create a resolution queue. As fields return lazy values, push them onto the queue, and resolve them in a first-in-first-out manner similar to the JS event loop until there's nothing left.
Example
Assuming lazy_resolve(::Concurrent::Promises::Future, :value!), and given the following type:
class Types::Query < GraphQL::Schema::Object
field :field_a, String, null: false
def field_a
Concurrent::Promises.delay do
puts 'A HELLO'
Concurrent::Promises.delay do
sleep 5
puts 'A WORLD'
'Hello'
end
end
end
field :field_b, String, null: false
def field_b
Concurrent::Promises.delay do
puts 'B HELLO'
Concurrent::Promises.delay do
sleep 5
puts 'B WORLD'
'World'
end
end
end
endFor the query { fieldA fieldB } what we want to see (across 5 seconds) is:
A HELLO
B HELLO
A WORLD
B WORLD
But what we see instead (across 10 seconds) is
A HELLO
A WORLD
B HELLO
B WORLD
This is obviously a bit contrived, but this same parallelism would be beneficial to real applications as well, since it would interleave database queries