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

Interpreter #1394

Merged
merged 119 commits into from
Oct 5, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
119 commits
Select commit Hold shift + click to select a range
7b0c6d8
Pass the default value to the InputValueDefinition constructor
tenderlove Mar 9, 2018
113e9dd
Change `attr_accessor` to `attr_reader`
tenderlove Mar 9, 2018
00fa20a
Merge branch 'master' into immutable-nodes
Mar 14, 2018
032df27
Add first-class method API to Visitor
rmosolgo Apr 2, 2018
396cec3
Use class-based visitor for validation
rmosolgo Apr 2, 2018
77e22e0
Use class-based visitor for static_validation
rmosolgo Apr 2, 2018
ed9e968
Add default visitor for ALL_RULES
rmosolgo Apr 2, 2018
2f59322
Migrate rewrite to use class-based visitor
rmosolgo Apr 2, 2018
8bf1e32
Code cleanup
rmosolgo Apr 2, 2018
296627c
Merge branch '1.9-dev' into immutable-nodes
Jul 9, 2018
255842c
Merge pull request #1338 from tenderlove/immutable-nodes
Jul 9, 2018
f9fbe53
Merge branch '1.9-dev' into class-based-visitor
Jul 10, 2018
ab60ccd
migrate ArgumentNamesAreUnique to class-based
rmosolgo Jul 10, 2018
230759d
Add a test for visit methods; fill in more visit_methods
rmosolgo Jul 10, 2018
b5f9df0
Update visitor methods for new nodes
rmosolgo Jul 10, 2018
68c5614
update no_definitions_are_present for extension methods
rmosolgo Jul 10, 2018
d9fbe10
Put TypeStack back for graphql-client
rmosolgo Jul 10, 2018
b9e210f
Fix lint errors
rmosolgo Jul 10, 2018
161dafa
Fix missing require; update typestack spec
rmosolgo Jul 10, 2018
82cbc7f
Merge pull request #1290 from rmosolgo/class-based-visitor
Jul 10, 2018
030416b
Start on basic AST manipulation
rmosolgo Aug 6, 2018
7fd8ccb
Add explicit tests for perisisted nodes
rmosolgo Aug 6, 2018
fce6b84
Support deleting nodes from AST
rmosolgo Aug 6, 2018
5b7d78b
Add check for maybe-necessary method
rmosolgo Aug 7, 2018
f190344
Add prototype add child methods
rmosolgo Aug 13, 2018
0504f91
Generate methods for AST node classes
rmosolgo Aug 13, 2018
a27a807
Use generated initialize_node
rmosolgo Aug 13, 2018
bc940ba
Also freeze children arrays
rmosolgo Aug 13, 2018
62cdab5
Give up on being restrictive with visitor
rmosolgo Aug 13, 2018
ae2690b
Add visitor guide
rmosolgo Aug 13, 2018
3df83dc
Merge pull request #1740 from rmosolgo/ast-maniputation
Aug 14, 2018
0fe0938
Use an AST visitor
rmosolgo Aug 16, 2018
c3ad8b5
Try running it on Jazz schema
rmosolgo Aug 17, 2018
d79e1bd
Try to hook up input objects
rmosolgo Aug 20, 2018
a4d983b
Fix modifying fragment spreads with visitor
rmosolgo Aug 20, 2018
63df47b
Merge branch 'field-filter-instances' into field-extensions-1.9
rmosolgo Aug 20, 2018
2a20b68
Merge branch 'fix-connections' into field-extensions-1.9
rmosolgo Aug 20, 2018
c9ff734
Merge pull request #1795 from rmosolgo/field-extensions-1.9
Aug 21, 2018
0505b3b
Fix lint error
rmosolgo Aug 21, 2018
e6b4695
Merge pull request #1793 from rmosolgo/fix-replace-inline-fragment
Aug 21, 2018
23427e7
Make all Jazz schema tests pass
rmosolgo Aug 24, 2018
f85df9f
update to run introspection query
rmosolgo Aug 24, 2018
8ede1f8
Fix empty lists
rmosolgo Aug 24, 2018
6c9cc62
Support lazies in Interpreter
rmosolgo Aug 27, 2018
3325caa
try to make the interpreter run authorization
rmosolgo Aug 28, 2018
8a80091
Merge branch '1.9-dev' into interpreter
rmosolgo Aug 28, 2018
fd62944
Split out interpreter files; better authorization support
rmosolgo Aug 28, 2018
552d48b
Fix some stuff after merging
rmosolgo Aug 29, 2018
7e36ac5
Use a hacky flag to support both interpreter and non-interpreter
rmosolgo Sep 5, 2018
8ab3f6a
Support mutations the old way
rmosolgo Sep 5, 2018
e81c28a
Implement null propagation
rmosolgo Sep 6, 2018
25103fb
Merge branch '1.9-dev' into interpreter
rmosolgo Sep 14, 2018
f30b34d
Support skip & include
rmosolgo Sep 14, 2018
b383774
Fix some stuff for dairy schema with interpreter
rmosolgo Sep 17, 2018
2844c72
Try avoiding double-resolution
rmosolgo Sep 20, 2018
33ae307
Intepret with a custom AST routine
rmosolgo Sep 20, 2018
fc549e3
Fix null propagation with later writes
rmosolgo Sep 21, 2018
827608a
Improve support for nil propagation and errors
rmosolgo Sep 21, 2018
1ea1f8c
resolve late-bound types
rmosolgo Sep 21, 2018
96fb4b8
Support Tracing
rmosolgo Sep 21, 2018
8b86f53
Start multiplexing support
rmosolgo Sep 21, 2018
358388b
Skip some rescue from tests
rmosolgo Sep 21, 2018
cd2e78c
Extract Interpreter installation into a plugin
rmosolgo Sep 21, 2018
c1ef9d2
Add a guide
rmosolgo Sep 21, 2018
f66654c
more doc
rmosolgo Sep 21, 2018
c8479d3
Move context flag out of user hash
rmosolgo Sep 21, 2018
c5414aa
Remove some needless changes
rmosolgo Sep 21, 2018
9152913
remove more needless changes
rmosolgo Sep 21, 2018
eeaff01
Less nesting in interpreter
rmosolgo Sep 21, 2018
1b306fa
Update-bench
rmosolgo Sep 21, 2018
128027d
Update a few tests
rmosolgo Sep 25, 2018
55a35d6
Fix typestack spec
rmosolgo Sep 25, 2018
f36e75b
Make the tests pass without the interpreter; add a single build with …
rmosolgo Sep 25, 2018
06cce11
Fix rubocop, fix for old rubies
rmosolgo Sep 25, 2018
04de1fc
Fix travis config; fix for old rubies
rmosolgo Sep 25, 2018
4134ea8
Add interpreter to more test schemas
rmosolgo Sep 25, 2018
fc799d8
update warden_spec for interpreter'
rmosolgo Sep 25, 2018
f511ebe
Migrate system test to class-based, add interpreter run on CI
rmosolgo Sep 25, 2018
cb3fc0c
Remove needless fields stack
rmosolgo Sep 28, 2018
aa5c5c0
Remove objects stack
rmosolgo Sep 28, 2018
f21e29d
Remove path stack
rmosolgo Sep 28, 2018
c466d02
remove types stack
rmosolgo Sep 28, 2018
4340573
update tracing
rmosolgo Sep 28, 2018
3286c91
Keep a reference to trace instead of passing it everywhere
rmosolgo Sep 28, 2018
79e49d7
Trace cleanup
rmosolgo Sep 28, 2018
1f6050b
Update some test schemas to class-based
rmosolgo Sep 28, 2018
51a0b95
Start on subscriptions implementation
rmosolgo Sep 28, 2018
b020e6f
Support subscription & mutation root fields; improve support for unau…
rmosolgo Sep 29, 2018
c5127ca
Make sure interpreter-introspected types have class definitions
rmosolgo Sep 29, 2018
232b7c8
Implement batching across multiplexes
rmosolgo Sep 29, 2018
09d4a56
Fix execute_query_lazy and running one query
rmosolgo Sep 29, 2018
3fcf1c9
implement prepare for arguments
rmosolgo Sep 29, 2018
ddb9baf
Fix system tests
rmosolgo Sep 29, 2018
53a38d4
Fix rails/relay tests
rmosolgo Sep 29, 2018
1401f05
Update .node and .nodes fields
rmosolgo Oct 1, 2018
af2167e
Ignore tests that don't apply to interpreter
rmosolgo Oct 1, 2018
4f13db8
Fix client_mutation_id for interpreter
rmosolgo Oct 1, 2018
e972235
Get tracing parity
rmosolgo Oct 1, 2018
608e369
Add workaroudn for list auth test
rmosolgo Oct 2, 2018
85d6b79
Make #write_into_result private; refactor @completely_nulled
rmosolgo Oct 5, 2018
8739e56
Fix lint error
rmosolgo Oct 5, 2018
8e623f0
Remove needless late-bound type check
rmosolgo Oct 5, 2018
74295cc
Refactor to not use throw/catch
rmosolgo Oct 5, 2018
233c77b
Document a method
rmosolgo Oct 5, 2018
cf2db9a
Clean up next_selections prep
rmosolgo Oct 5, 2018
4e33f2b
Extract subscription root field logic into a module
rmosolgo Oct 5, 2018
30b3e60
Reduce usage of trace in visitor
rmosolgo Oct 5, 2018
c3481bf
fix-lint-error
rmosolgo Oct 5, 2018
505bdcc
Extract response collection from runtime
rmosolgo Oct 5, 2018
47c3c6d
Improve dead path handling
rmosolgo Oct 5, 2018
3e52d58
Remove needless legacy adapter
rmosolgo Oct 5, 2018
26b10bc
Document the multiplex interactions a little bit
rmosolgo Oct 5, 2018
f2d8f6b
merge trace and visitor into runtime, since they have the same lifecy…
rmosolgo Oct 5, 2018
8b31eed
Clarify behavior differences
rmosolgo Oct 5, 2018
057fd4a
Update some done todos
rmosolgo Oct 5, 2018
eac1add
Explain difference in tracing spec
rmosolgo Oct 5, 2018
89b8d9f
Uncomment non-broken thing
rmosolgo Oct 5, 2018
224c4a9
Rename resolve_field_2 -> resolve
rmosolgo Oct 5, 2018
b6767d6
fix lint error
rmosolgo Oct 5, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,29 @@ matrix:
gemfile: spec/dummy/Gemfile
script:
- cd spec/dummy && bundle exec rails test:system
- env:
- DISPLAY=':99.0'
- TESTING_INTERPRETER=yes
rvm: 2.4.3
addons:
apt:
sources:
- google-chrome
packages:
- google-chrome-stable
before_install:
- export CHROMEDRIVER_VERSION=`curl -s http://chromedriver.storage.googleapis.com/LATEST_RELEASE`
- curl -L -O "http://chromedriver.storage.googleapis.com/${CHROMEDRIVER_VERSION}/chromedriver_linux64.zip"
- unzip chromedriver_linux64.zip && chmod +x chromedriver && sudo mv chromedriver /usr/local/bin
before_script:
- sh -e /etc/init.d/xvfb start
gemfile: spec/dummy/Gemfile
script:
- cd spec/dummy && bundle exec rails test:system
- env:
- TESTING_INTERPRETER=yes
rvm: 2.4.3
gemfile: gemfiles/rails_5.2.gemfile
- rvm: 2.2.8
gemfile: Gemfile
- rvm: 2.2.8
Expand Down
21 changes: 10 additions & 11 deletions benchmark/run.rb
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
# frozen_string_literal: true
require "dummy/schema"
TESTING_INTERPRETER = true
require "graphql"
require "jazz"
require "benchmark/ips"
require 'ruby-prof'
require 'memory_profiler'
require "ruby-prof"
require "memory_profiler"

module GraphQLBenchmark
QUERY_STRING = GraphQL::Introspection::INTROSPECTION_QUERY
DOCUMENT = GraphQL.parse(QUERY_STRING)
SCHEMA = Dummy::Schema
SCHEMA = Jazz::Schema

BENCHMARK_PATH = File.expand_path("../", __FILE__)
CARD_SCHEMA = GraphQL::Schema.from_definition(File.read(File.join(BENCHMARK_PATH, "schema.graphql")))
ABSTRACT_FRAGMENTS = GraphQL.parse(File.read(File.join(BENCHMARK_PATH, "abstract_fragments.graphql")))
ABSTRACT_FRAGMENTS_2 = GraphQL.parse(File.read(File.join(BENCHMARK_PATH, "abstract_fragments_2.graphql")))


BIG_SCHEMA = GraphQL::Schema.from_definition(File.join(BENCHMARK_PATH, "big_schema.graphql"))
BIG_QUERY = GraphQL.parse(File.read(File.join(BENCHMARK_PATH, "big_query.graphql")))

module_function
def self.run(task)

Benchmark.ips do |x|
case task
when "query"
Expand All @@ -40,15 +40,14 @@ def self.profile
# Warm up any caches:
SCHEMA.execute(document: DOCUMENT)
# CARD_SCHEMA.validate(ABSTRACT_FRAGMENTS)

res = nil
result = RubyProf.profile do
# CARD_SCHEMA.validate(ABSTRACT_FRAGMENTS)
SCHEMA.execute(document: DOCUMENT)
res = SCHEMA.execute(document: DOCUMENT)
end

printer = RubyProf::FlatPrinter.new(result)
# printer = RubyProf::FlatPrinter.new(result)
# printer = RubyProf::GraphHtmlPrinter.new(result)
# printer = RubyProf::FlatPrinterWithLineNumbers.new(result)
printer = RubyProf::FlatPrinterWithLineNumbers.new(result)

printer.print(STDOUT, {})
end
Expand Down
100 changes: 100 additions & 0 deletions guides/queries/interpreter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
---
title: Interpreter
layout: guide
doc_stub: false
search: true
section: Queries
desc: A New Runtime for GraphQL-Ruby
experimental: true
index: 11
---

GraphQL-Ruby 1.9.0 includes a new runtime module which you may use for your schema. Eventually, it will become the default.

It's called `GraphQL::Execute::Interpreter` and you can hook it up with `use ...` in your schema class:

```ruby
class MySchema < GraphQL::Schema
use GraphQL::Execute::Interpreter
end
```

Read on to learn more!

## Rationale

The new runtime was added to address a few specific concerns:

- __Validation Performance__: The previous runtime depended on a preparation step (`GraphQL::InternalRepresentation::Rewrite`) which could be very slow in some cases. In many cases, the overhead of that step provided no value.
- __Runtime Performance__: For very large results, the previous runtime was slow because it allocated a new `ctx` object for every field, even very simple fields that didn't need any special tracking.
- __Extensibility__: Although the GraphQL specification supports custom directives, GraphQL-Ruby didn't have a good way to build them.

## Installation

You can opt in to the interpreter in your schema class:

```ruby
class MySchema < GraphQL::Schema
use GraphQL::Execution::Interpreter
end
```

If you have a subscription root type, it will also need an update. Extend this new module:

```ruby
class Types::Subscription < Types::BaseObject
# Extend this module to support subscription root fields with Interpreter
extend GraphQL::Subscriptions::SubscriptionRoot
end
```

Some Relay configurations must be updated too. For example:

```diff
- field :node, field: GraphQL::Relay::Node.field
+ add_field(GraphQL::Types::Relay::NodeField)
```

(Alternatively, consider implementing `Query.node` in your own app, using `NodeField` as inspiration.)

## Compatibility

The new runtime works with class-based schemas only. Several features are no longer supported:

- Proc-dependent field features:

- Field Instrumentation
- Middleware
- Resolve procs

All these depend on the memory- and time-hungry per-field `ctx` object. To improve performance, only method-based resolves are supported. If need something from `ctx`, you can get it with the `extras: [...]` configuration option. To wrap resolve behaviors, try {% internal_link "Field Extensions", "/type_definitions/field_extensions" %}.

- Query analyzers and `irep_node`s

These depend on the now-removed `Rewrite` step, which wasted a lot of time making often-unneeded preparation. Most of the attributes you might need from an `irep_node` are available with `extras: [...]`. Query analyzers can be refactored to be static checks (custom validation rules) or dynamic checks, made at runtime. The built-in analyzers have been refactored to run as validators.

`irep_node`-based lookahead is not supported. Stay tuned for a replacement.

- `rescue_from`

This was built on middleware, which is not supported anymore. Stay tuned for a replacement.

- `.graphql_definition` and `def to_graphql`

The interpreter uses class-based schema definitions only, and never converts them to legacy GraphQL definition objects. Any custom definitions to GraphQL objects should be re-implemented on custom base classes.

Maybe this section should have been called _incompatibility_ 🤔.

## Extending the Runtime

🚧 👷🚧

The internals aren't clean enough to build on yet. Stay tuned.

## Implementation Notes

Instead of a tree of `irep_nodes`, the interpreter consumes the AST directly. This removes a complicated concept from GraphQL-Ruby (`irep_node`s) and simplifies the query lifecycle. The main difference relates to how fragment spreads are resolved. In the previous runtime, the possible combinations of fields for a given object were calculated ahead of time, then some of those combinations were used during runtime, but many of them may not have been. In the new runtime, no precalculation is made; instead each object is checked against each fragment at runtime.

Instead of creating a `GraphQL::Query::Context::FieldResolutionContext` for _every_ field in the response, the interpreter uses long-lived, mutable objects for execution bookkeeping. This is more complicated to manage, since the changes to those objects can be hard to predict, but it's worth it for the performance gain. When needed, those bookkeeping objects can be "forked", so that two parts of an operation can be resolved independently.

Instead of calling `.to_graphql` internally to convert class-based definitions to `.define`-based definitions, the interpreter operates on class-based definitions directly. This simplifies the workflow for creating custom configurations and using them at runtime.
2 changes: 1 addition & 1 deletion lib/graphql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ def self.scan_with_ragel(graphql_string)
require "graphql/language"
require "graphql/analysis"
require "graphql/tracing"
require "graphql/execution"
require "graphql/schema"
require "graphql/execution"
require "graphql/types"
require "graphql/relay"
require "graphql/boolean_type"
Expand Down
5 changes: 5 additions & 0 deletions lib/graphql/argument.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ def expose_as
@expose_as ||= (@as || @name).to_s
end

# Backport this to support legacy-style directives
def keyword
@keyword ||= GraphQL::Schema::Member::BuildType.underscore(expose_as).to_sym
end

# @param value [Object] The incoming value from variables or query string literal
# @param ctx [GraphQL::Query::Context]
# @return [Object] The prepared `value` for this argument or `value` itself if no `prepare` function exists.
Expand Down
1 change: 1 addition & 0 deletions lib/graphql/execution.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require "graphql/execution/execute"
require "graphql/execution/flatten"
require "graphql/execution/instrumentation"
require "graphql/execution/interpreter"
require "graphql/execution/lazy"
require "graphql/execution/multiplex"
require "graphql/execution/typecast"
19 changes: 18 additions & 1 deletion lib/graphql/execution/execute.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,23 @@ def execute(ast_operation, root_type, query)
GraphQL::Execution::Flatten.call(query.context)
end

def self.begin_multiplex(_multiplex)
end

def self.begin_query(query, _multiplex)
ExecutionFunctions.resolve_root_selection(query)
end

def self.finish_multiplex(results, multiplex)
ExecutionFunctions.lazy_resolve_root_selection(results, multiplex: multiplex)
end

def self.finish_query(query, _multiplex)
{
"data" => Execution::Flatten.call(query.context)
}
end

# @api private
module ExecutionFunctions
module_function
Expand Down Expand Up @@ -179,7 +196,7 @@ def continue_resolve_field(raw_value, field_type, field_ctx)
if list_errors.any?
list_errors.each do |error, index|
error.ast_node = field_ctx.ast_node
error.path = field_ctx.path + [index]
error.path = field_ctx.path + (field_ctx.type.list? ? [index] : [])
query.context.errors.push(error)
end
end
Expand Down
89 changes: 89 additions & 0 deletions lib/graphql/execution/interpreter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# frozen_string_literal: true
require "graphql/execution/interpreter/execution_errors"
require "graphql/execution/interpreter/hash_response"
require "graphql/execution/interpreter/runtime"

module GraphQL
module Execution
class Interpreter
def initialize
# A buffer shared by all queries running in this interpreter
@lazies = []
end

# Support `Executor` :S
def execute(_operation, _root_type, query)
runtime = evaluate(query)
sync_lazies(query: query)
runtime.final_value
end

def self.use(schema_defn)
schema_defn.query_execution_strategy(GraphQL::Execution::Interpreter)
schema_defn.mutation_execution_strategy(GraphQL::Execution::Interpreter)
schema_defn.subscription_execution_strategy(GraphQL::Execution::Interpreter)
end

def self.begin_multiplex(multiplex)
# Since this is basically the batching context,
# share it for a whole multiplex
multiplex.context[:interpreter_instance] ||= self.new
end

def self.begin_query(query, multiplex)
# The batching context is shared by the multiplex,
# so fetch it out and use that instance.
interpreter =
query.context.namespace(:interpreter)[:interpreter_instance] =
multiplex.context[:interpreter_instance]
interpreter.evaluate(query)
query
end

def self.finish_multiplex(_results, multiplex)
interpreter = multiplex.context[:interpreter_instance]
interpreter.sync_lazies(multiplex: multiplex)
end

def self.finish_query(query, _multiplex)
{
"data" => query.context.namespace(:interpreter)[:runtime].final_value
}
end

def evaluate(query)
query.context.interpreter = true
# Although queries in a multiplex _share_ an Interpreter instance,
# they also have another item of state, which is private to that query
# in particular, assign it here:
runtime = Runtime.new(
query: query,
lazies: @lazies,
response: HashResponse.new,
)
query.context.namespace(:interpreter)[:runtime] = runtime

query.trace("execute_query", {query: query}) do
runtime.run_eager
end

runtime
end

def sync_lazies(query: nil, multiplex: nil)
tracer = query || multiplex
if query.nil? && multiplex.queries.length == 1
query = multiplex.queries[0]
end
tracer.trace("execute_query_lazy", {multiplex: multiplex, query: query}) do
while @lazies.any?
next_wave = @lazies.dup
@lazies.clear
# This will cause a side-effect with `.write(...)`
next_wave.each(&:value)
end
end
end
end
end
end
29 changes: 29 additions & 0 deletions lib/graphql/execution/interpreter/execution_errors.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# frozen_string_literal: true

module GraphQL
module Execution
class Interpreter
class ExecutionErrors
def initialize(ctx, ast_node, path)
@context = ctx
@ast_node = ast_node
@path = path
end

def add(err_or_msg)
err = case err_or_msg
when String
GraphQL::ExecutionError.new(err_or_msg)
when GraphQL::ExecutionError
err_or_msg
else
raise ArgumentError, "expected String or GraphQL::ExecutionError, not #{err_or_msg.class} (#{err_or_msg.inspect})"
end
err.ast_node ||= @ast_node
err.path ||= @path
@context.add_error(err)
end
end
end
end
end
Loading