-
Notifications
You must be signed in to change notification settings - Fork 105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow custom executors #68
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had considered passing in the executor into the Setup class initializers, but then noticed that we call start_batching here, not from an instance but from another class method: https://github.com/Shopify/graphql-batch/blob/master/lib/graphql/batch/setup.rb#L30
That method is deprecated, so we don't need to support a custom executor there
lib/graphql/batch.rb
Outdated
instrumentation = GraphQL::Batch::SetupMultiplex.new(schema) | ||
schema_defn.instrument(:multiplex, instrumentation) | ||
schema_defn.instrument(:field, instrumentation) | ||
else | ||
GraphQL::Batch::Setup.executor_class = executor_class | ||
instrumentation = GraphQL::Batch::Setup.new(schema) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Store the state on the instance rather than the class. It is only supposed to affect the schema it is called on.
lib/graphql/batch.rb
Outdated
@@ -12,20 +12,22 @@ class NestedError < StandardError; end | |||
def self.batch | |||
raise NestedError if GraphQL::Batch::Executor.current | |||
begin | |||
GraphQL::Batch::Executor.current = GraphQL::Batch::Executor.new | |||
GraphQL::Batch::Executor.current = GraphQL::Batch::Executor.new # TODO |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing this isn't important for our purposes, so you could just remove the # TODO
comment.
Thanks @dylanahsmith ! I've updated the PR in response to your suggestions. Any suggestions on how/if we should unit test this change? We don't have a SetupTest, The only testable changes appear to be in |
Integration style tests would be preferred for something like that, since it is mean to be used through graphql-ruby rather than directly. We test |
test/graphql_test.rb
Outdated
query_string = '{ product(id: "1") { id } }' | ||
schema.execute(query_string) | ||
|
||
refute custom_executor.loaders_executed.empty? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Possibly related to https://github.com/Shopify/shopify/pull/136940#issuecomment-342542248?
I noticed that this ended up containing two references to the same instance of RecordLoader.
Updated. I ended up adding a gem dependency (mocha) to make assertions on some trivial behaviour defined on a custom executor. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM but also wondering about the two loaders being executed
@swalkinshaw I'm looking into this now. It seems to be because we call the instrumented method twice via It does this regardless of whether an existing collection id is queried in the query mentioned here. |
graphql-batch.gemspec
Outdated
@@ -27,4 +27,5 @@ Gem::Specification.new do |spec| | |||
spec.add_development_dependency "bundler", "~> 1.10" | |||
spec.add_development_dependency "rake", "~> 10.0" | |||
spec.add_development_dependency "minitest" | |||
spec.add_development_dependency "mocha", "~> 1.3.0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would like to try to avoid including a stubbing library, since they can result in tests passing only because of the presence of the stub. Integration tests provide more confidence that the code actually works.
test/graphql_test.rb
Outdated
end | ||
|
||
custom_executor = MyCustomExecutor.new | ||
MyCustomExecutor.stubs(:new).returns(custom_executor) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer if you at least use a class variable for the loaders_executed array so that you don't need to mess with the implementation using stubbing.
test/graphql_test.rb
Outdated
end | ||
end | ||
|
||
def test_custom_executor_class |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should move to another test class, since this one gets re-used in GraphQL::ExecutionStrategyTest which tests the deprecated code that we don't support custom executors for.
test/graphql_test.rb
Outdated
end | ||
|
||
def around_promise_callbacks(loader) | ||
@loaders_executed << loader |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is called each time a promise is fulfilled, not for each loader being executed. E.g. if 4 keys are being loaded by a loader, then this will be called for all 4 keys rather than the one load.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is called each time a promise is fulfilled, not for each loader being executed.
Thanks for the clarification!
It looks like around_promise_callbacks
is also invoked via Loader#reject
. So that's twice per field, even if the field is fulfilled. reject
is called even when the loader finds a result, via check_for_broken_promises
. This explains the double logging in my current implementation 🤔
lib/graphql/batch/loader.rb:78:in `reject'
lib/graphql/batch/loader.rb:131:in `block in check_for_broken_promises'
lib/graphql/batch/loader.rb:130:in `each'
lib/graphql/batch/loader.rb:130:in `check_for_broken_promises'
lib/graphql/batch/loader.rb:48:in `resolve'
lib/graphql/batch/executor.rb:34:in `resolve'
lib/graphql/batch/loader.rb:56:in `wait'
lib/promise.rb:129:in `wait'
lib/promise.rb:129:in `wait'
lib/promise.rb:129:in `wait'
lib/promise.rb:80:in `sync'
lib/graphql/field.rb:309:in `public_send'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was able to around the double-logging by resetting @start_time
to nil in around_promise_callbacks
and returning early if it's not present on next call, like so:
module GraphModel
class TimedBatchExecutor < GraphQL::Batch::Executor
# etc.
def resolve(loader)
@start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
super
end
def around_promise_callbacks(loader)
return super unless @start_time
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
TIMINGS[loader.loader_key] ||= []
TIMINGS[loader.loader_key] << end_time - @start_time
@start_time = nil
super
end
end
end
This prevents the second call to around_promise_callbacks
(via Executor#resolve -> Loader#resolve, #check_for_broken_promises, #reject, #finish_resolve) from logging the time between the original @start_time
and the time it runs again.
433b142
to
4a2d094
Compare
Another follow-up change after some discussion with @swalkinshaw. I think we can get around modifying |
Updated and ready for another review |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
squash and
lib/graphql/batch/setup_multiplex.rb
Outdated
@@ -1,11 +1,12 @@ | |||
module GraphQL::Batch | |||
class SetupMultiplex | |||
def initialize(schema) | |||
def initialize(schema, executor_class: executor_class) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚨 hold up.
lib/graphql/batch/setup_multiplex.rb:3: warning: circular argument reference - executor_class
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👀
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- def initialize(schema, executor_class: executor_class)
+ def initialize(schema, executor_class:)
✅
Pass custom executor as instance variable Expose loader to around_promise_callbacks Add Mocha gem Test passing custom executor_class Move custom_executor test to separate file Reverting change to around_promise_callbacks signature Fix circular reference
f95884d
to
acf575c
Compare
custom_executor
GraphQL::Batch::Setup
andGraphQL::Batch::SetupMultiplex
updated to hold reference to the executor class (defaults to GraphQL::Batch::Executor) as instance variables.@dylanahsmith @eapache @swalkinshaw 👋
I'm looking to verify my approach here before continuing. See the linked Shopify/shopify PR for more context.Updated according PR review comments so far.