Skip to content
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

Merged
merged 1 commit into from
Nov 9, 2017
Merged

Allow custom executors #68

merged 1 commit into from
Nov 9, 2017

Conversation

chrisbutcher
Copy link
Contributor

@chrisbutcher chrisbutcher commented Nov 6, 2017

  • Changed: GraphQL::Batch.use can take optional custom_executor
    • GraphQL::Batch::Setup and GraphQL::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.

Copy link
Contributor

@dylanahsmith dylanahsmith left a 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

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)
Copy link
Contributor

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.

@@ -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
Copy link
Contributor

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.

@chrisbutcher chrisbutcher changed the title WIP Allow custom executors Allow custom executors Nov 7, 2017
@chrisbutcher
Copy link
Contributor Author

chrisbutcher commented Nov 7, 2017

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, nor coverage on GraphQL::Batch.use.

The only testable changes appear to be in GraphQL::Batch.use's forwarding of executor_class, and the expectation in start_batching that the passed-in Executor class instantiated.

@dylanahsmith
Copy link
Contributor

Any suggestions on how/if we should unit test this change? We don't have a SetupTest, nor coverage on GraphQL::Batch.use.

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 GraphQL::Batch.use through use GraphQL::Batch right now

query_string = '{ product(id: "1") { id } }'
schema.execute(query_string)

refute custom_executor.loaders_executed.empty?
Copy link
Contributor Author

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.

@chrisbutcher
Copy link
Contributor Author

Updated. I ended up adding a gem dependency (mocha) to make assertions on some trivial behaviour defined on a custom executor.

Copy link
Contributor

@swalkinshaw swalkinshaw left a 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

@chrisbutcher
Copy link
Contributor Author

@swalkinshaw I'm looking into this now. It seems to be because we call the instrumented method twice via finish_resolve https://github.com/Shopify/graphql-batch/blob/master/lib/graphql/batch/loader.rb#L101, because we call finish_resolve in both fulfill and reject: https://github.com/Shopify/graphql-batch/blob/master/lib/graphql/batch/loader.rb#L69-L79

It does this regardless of whether an existing collection id is queried in the query mentioned here.

@@ -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"
Copy link
Contributor

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.

end

custom_executor = MyCustomExecutor.new
MyCustomExecutor.stubs(:new).returns(custom_executor)
Copy link
Contributor

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.

end
end

def test_custom_executor_class
Copy link
Contributor

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.

end

def around_promise_callbacks(loader)
@loaders_executed << loader
Copy link
Contributor

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.

Copy link
Contributor Author

@chrisbutcher chrisbutcher Nov 7, 2017

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'

Copy link
Contributor Author

@chrisbutcher chrisbutcher Nov 8, 2017

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.

@chrisbutcher chrisbutcher force-pushed the allow-custom-executor branch from 433b142 to 4a2d094 Compare November 7, 2017 21:35
@chrisbutcher
Copy link
Contributor Author

chrisbutcher commented Nov 8, 2017

Another follow-up change after some discussion with @swalkinshaw.

I think we can get around modifying around_promise_callbacks's signature to take a loader by simply keeping a reference to the loader we already receive in Executor#resolve. Will push up commit to https://github.com/Shopify/shopify/pull/136940 shortly.

@chrisbutcher
Copy link
Contributor Author

Updated and ready for another review

Copy link
Contributor

@swalkinshaw swalkinshaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

squash and :shipit:

@@ -1,11 +1,12 @@
module GraphQL::Batch
class SetupMultiplex
def initialize(schema)
def initialize(schema, executor_class: executor_class)
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👀

Copy link
Contributor Author

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants