Skip to content

Commit

Permalink
Merge pull request #5179 from rmosolgo/autoload-docs
Browse files Browse the repository at this point in the history
Call eager_load! on loaded dependents, too
  • Loading branch information
rmosolgo authored Dec 2, 2024
2 parents cd6d1f9 + d670173 commit 797d1e1
Show file tree
Hide file tree
Showing 10 changed files with 151 additions and 64 deletions.
22 changes: 0 additions & 22 deletions guides/code_loading.md

This file was deleted.

12 changes: 0 additions & 12 deletions guides/parser_cache.md

This file was deleted.

50 changes: 26 additions & 24 deletions guides/schema/definition.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,8 @@ For defining GraphQL types, see the guides for those types: {% internal_link "ob

## Types in the Schema

{{ "Schema.query" | api_doc }}, {{ "Schema.mutation" | api_doc }}, and {{ "Schema.subscription" | api_doc}} declare the [entry-point types](https://graphql.org/learn/schema/#the-query-and-mutation-types) of the schema.

{{ "Schema.orphan_types" | api_doc }} declares object types which implement {% internal_link "Interfaces", "/type_definitions/interfaces" %} but aren't used as field return types in the schema. For more about this specific scenario, see {% internal_link "Orphan Types", "/type_definitions/interfaces#orphan-types" %}
- {{ "Schema.query" | api_doc }}, {{ "Schema.mutation" | api_doc }}, and {{ "Schema.subscription" | api_doc}} declare the [entry-point types](https://graphql.org/learn/schema/#the-query-and-mutation-types) of the schema.
- {{ "Schema.orphan_types" | api_doc }} declares object types which implement {% internal_link "Interfaces", "/type_definitions/interfaces" %} but aren't used as field return types in the schema. For more about this specific scenario, see {% internal_link "Orphan Types", "/type_definitions/interfaces#orphan-types" %}

### Lazy-loading types

Expand Down Expand Up @@ -88,42 +87,45 @@ Additionally, {{ "Schema.resolve_type" | api_doc }} is called by GraphQL-Ruby to

## Error Handling

{{ "Schema.type_error" | api_doc }} handles type errors at runtime, read more in the {% internal_link "Type errors guide", "/errors/type_errors" %}.

{{ "Schema.rescue_from" | api_doc }} defines error handlers for application errors. See the {% internal_link "error handling guide", "/errors/error_handling" %} for more.

{{ "Schema.parse_error" | api_doc }} and {{ "Schema.query_stack_error" | api_doc }} provide hooks for reporting errors to your bug tracker.
- {{ "Schema.type_error" | api_doc }} handles type errors at runtime, read more in the {% internal_link "Type errors guide", "/errors/type_errors" %}.
- {{ "Schema.rescue_from" | api_doc }} defines error handlers for application errors. See the {% internal_link "error handling guide", "/errors/error_handling" %} for more.
- {{ "Schema.parse_error" | api_doc }} and {{ "Schema.query_stack_error" | api_doc }} provide hooks for reporting errors to your bug tracker.

## Default Limits

{{ "Schema.max_depth" | api_doc }} and {{ "Schema.max_complexity" | api_doc }} apply some limits to incoming queries. See {% internal_link "Complexity and Depth", "/queries/complexity_and_depth" %} for more.

{{ "Schema.default_max_page_size" | api_doc }} applies limits to {% internal_link "connection fields", "/pagination/overview" %}.

{{ "Schema.validate_timeout" | api_doc }}, {{ "Schema.validate_max_errors" | api_doc }} and {{ "Schema.max_query_string_tokens" | api_doc }} all apply limits to query execution. See {% internal_link "Timeout", "/queries/timeout" %} for more.
- {{ "Schema.max_depth" | api_doc }} and {{ "Schema.max_complexity" | api_doc }} apply some limits to incoming queries. See {% internal_link "Complexity and Depth", "/queries/complexity_and_depth" %} for more.
- {{ "Schema.default_max_page_size" | api_doc }} applies limits to {% internal_link "connection fields", "/pagination/overview" %}.
- {{ "Schema.validate_timeout" | api_doc }}, {{ "Schema.validate_max_errors" | api_doc }} and {{ "Schema.max_query_string_tokens" | api_doc }} all apply limits to query execution. See {% internal_link "Timeout", "/queries/timeout" %} for more.

## Introspection

{{ "Schema.extra_types" | api_doc }} declares types which should be printed in the SDL and returned in introspection queries, but aren't otherwise used in the schema.

{{ "Schema.introspection" | api_doc }} can attach a {% internal_link "custom introspection system", "/schema/introspection" %} to the schema.
- {{ "Schema.extra_types" | api_doc }} declares types which should be printed in the SDL and returned in introspection queries, but aren't otherwise used in the schema.
- {{ "Schema.introspection" | api_doc }} can attach a {% internal_link "custom introspection system", "/schema/introspection" %} to the schema.

## Authorization

{{ "Schema.unauthorized_object" | api_doc }} and {{ "Schema.unauthorized_field" | api_doc }} are called when {% internal_link "authorization hooks", "/authorization/authorization" %} return `false` during query execution.
- {{ "Schema.unauthorized_object" | api_doc }} and {{ "Schema.unauthorized_field" | api_doc }} are called when {% internal_link "authorization hooks", "/authorization/authorization" %} return `false` during query execution.

## Execution Configuration

{{ "Schema.trace_with" | api_doc }} attaches tracer modules. See {% internal_link "Tracing", "/queries/tracing" %} for more.
- {{ "Schema.trace_with" | api_doc }} attaches tracer modules. See {% internal_link "Tracing", "/queries/tracing" %} for more.
- {{ "Schema.query_analyzer" | api_doc }} and {{ "Schema.multiplex_analyzer" }} accept processors for ahead-of-time query analysis, see {% internal_link "Analysis", "/queries/ast_analysis" %} for more.
- {{ "Schema.default_logger" | api_doc }} configures a logger for runtime. See {% internal_link "Logging", "/queries/logging" %}.
- {{ "Schema.context_class" | api_doc }} and {{ "Schema.query_class" | api_doc }} attach custom subclasses to your schema to use during execution.
- {{ "Schema.lazy_resolve" | api_doc }} registers classes with {% internal_link "lazy execution", "/schema/lazy_execution" %}.

{{ "Schema.query_analyzer" | api_doc }} and {{ "Schema.multiplex_analyzer" }} accept processors for ahead-of-time query analysis, see {% internal_link "Analysis", "/queries/ast_analysis" %} for more.
## Plugins

{{ "Schema.default_logger" | api_doc }} configures a logger for runtime. See {% internal_link "Logging", "/queries/logging" %}.
- {{ "Schema.use" | api_doc }} adds plugins to your schema. For example, {{ "GraphQL::Dataloader" | api_doc }} and {{ "GraphQL::Schema::Visibility" | api_doc }} are installed this way.

{{ "Schema.context_class" | api_doc }} and {{ "Schema.query_class" | api_doc }} attach custom subclasses to your schema to use during execution.
## Production Considerations

{{ "Schema.lazy_resolve" | api_doc }} registers classes with {% internal_link "lazy execution", "/schema/lazy_execution" %}.
- __Parser caching__: if your application parses GraphQL _files_ (queries or schema definition), it may benefit from enabling {{ "GraphQL::Parser::Cache" | api_doc }}.
- __Eager loading the library__: by default, GraphQL-Ruby autoloads its constants as-needed. In production, they should be autoloaded instead, using `GraphQL.eager_load!`.

## Plugins
- Rails: enabled automatically. (ActiveSupport calls `.eager_load!`.)
- Sinatra: add `configure(:production) { GraphQL.eager_load! }` to your application file.
- Hanami: add `environment(:production) { GraphQL.eager_load! }` to your application file.
- Other frameworks: call `GraphQL.eager_load!` when your application is booting in production mode.

{{ "Schema.use" | api_doc }} adds plugins to your schema. For example, {{ "GraphQL::Dataloader" | api_doc }} and {{ "GraphQL::Schema::Visibility" | api_doc }} are installed this way.
See {{"GraphQL::Autoload#eager_load!" | api_doc }} for more details.
34 changes: 28 additions & 6 deletions lib/graphql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@
module GraphQL
extend Autoload

# Load all `autoload`-configured classes, and also eager-load dependents who have autoloads of their own.
def self.eager_load!
super
Query.eager_load!
Types.eager_load!
end

class Error < StandardError
end

Expand Down Expand Up @@ -74,24 +81,39 @@ module EmptyObjects
class << self
# If true, the parser should raise when an integer or float is followed immediately by an identifier (instead of a space or punctuation)
attr_accessor :reject_numbers_followed_by_names
end

self.reject_numbers_followed_by_names = false

class << self
# If `production?` is detected but `eager_load!` wasn't called, emit a warning.
# @return [void]
def ensure_eager_load!
if production? && !eager_loading?
warn "GraphQL should be eager loaded in production environments!"
warn <<~WARNING
GraphQL-Ruby thinks this is a production deployment but didn't eager-load its constants. Address this by:
- Calling `GraphQL.eager_load!` in a production-only initializer or setup hook
- Assign `GraphQL.env = "..."` to something _other_ than `"production"` (for example, `GraphQL.env = "development"`)
More details: https://graphql-ruby.org/schema/definition#production-considerations
WARNING
end
end

attr_accessor :env

private

# Detect whether this is a production deployment or not
def production?
(env = ENV["RACK_ENV"] || ENV["RAILS_ENV"] || ENV["HANAMI_ENV"] || ENV["APP_ENV"]) && env.to_s.downcase == "production"
if env
# Manually assigned to production?
env == "production"
else
(detected_env = ENV["RACK_ENV"] || ENV["RAILS_ENV"] || ENV["HANAMI_ENV"] || ENV["APP_ENV"]) && detected_env.to_s.downcase == "production"
end
end
end

self.reject_numbers_followed_by_names = false

autoload :ExecutionError, "graphql/execution_error"
autoload :RuntimeTypeError, "graphql/runtime_type_error"
autoload :UnresolvedTypeError, "graphql/unresolved_type_error"
Expand Down
9 changes: 9 additions & 0 deletions lib/graphql/autoload.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,34 @@

module GraphQL
module Autoload
# Register a constant named `const_name` to be loaded from `path`.
# This is like `Kernel#autoload` but it tracks the constants so they can be eager-loaded with {#eager_load!}
# @param const_name [Symbol]
# @param path [String]
# @return [void]
def autoload(const_name, path)
@_eagerloaded_constants ||= []
@_eagerloaded_constants << const_name

super const_name, path
end

# Call this to load this constant's `autoload` dependents and continue calling recursively
# @return [void]
def eager_load!
@_eager_loading = true
if @_eagerloaded_constants
@_eagerloaded_constants.each { |const_name| const_get(const_name) }
@_eagerloaded_constants = nil
end
nil
ensure
@_eager_loading = false
end

private

# @return [Boolean] `true` if GraphQL-Ruby is currently eager-loading its constants
def eager_loading?
@_eager_loading ||= false
end
Expand Down
13 changes: 13 additions & 0 deletions lib/graphql/language/cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,25 @@

module GraphQL
module Language
# This cache is used by {GraphQL::Language::Parser.parse_file} when it's enabled.
#
# With Rails, parser caching may enabled by setting `config.graphql.parser_cache = true` in your Rails application.
#
# The cache may be manually built by assigning `GraphQL::Language::Parser.cache = GraphQL::Language::Cache.new("some_dir")`.
# This will create a directory (`tmp/cache/graphql` by default) that stores a cache of parsed files.
#
# Much like [bootsnap](https://github.com/Shopify/bootsnap), the parser cache needs to be cleaned up manually.
# You will need to clear the cache directory for each new deployment of your application.
# Also note that the parser cache will grow as your schema is loaded, so the cache directory must be writable.
#
# @see GraphQL::Railtie for simple Rails integration
class Cache
def initialize(path)
@path = path
end

DIGEST = Digest::SHA256.new << GraphQL::VERSION

def fetch(filename)
hash = DIGEST.dup << filename
begin
Expand Down
6 changes: 6 additions & 0 deletions lib/graphql/railtie.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# frozen_string_literal: true

module GraphQL
# Support {GraphQL::Parser::Cache}
#
# @example Enable the parser cache with default directory
#
# config.graphql.parser_cache = true
#
class Railtie < Rails::Railtie
config.graphql = ActiveSupport::OrderedOptions.new
config.graphql.parser_cache = false
Expand Down
7 changes: 7 additions & 0 deletions spec/fixtures/eager_module/nested_eager_module.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true
module EagerModule
module NestedEagerModule
extend GraphQL::Autoload
autoload(:NestedEagerClass, "fixtures/eager_module/nested_eager_module/nested_eager_class")
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true
module EagerModule
module NestedEagerModule
class NestedEagerClass
end
end
end
55 changes: 55 additions & 0 deletions spec/graphql/autoload_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,18 @@ module EagerModule
extend GraphQL::Autoload
autoload(:EagerClass, "fixtures/eager_module/eager_class")
autoload(:OtherEagerClass, "fixtures/eager_module/other_eager_class")
autoload(:NestedEagerModule, "fixtures/eager_module/nested_eager_module")

def self.eager_load!
super

NestedEagerModule.eager_load!
end
end

describe "#autoload" do
it "sets autoload" do
assert LazyModule.const_defined?(:LazyClass)
assert_equal("fixtures/lazy_module/lazy_class", LazyModule.autoload?(:LazyClass))
LazyModule::LazyClass
assert_nil(LazyModule.autoload?(:LazyClass))
Expand All @@ -24,10 +32,57 @@ module EagerModule

describe "#eager_load!" do
it "eagerly loads autoload entries" do
assert EagerModule.autoload?(:EagerClass)
assert EagerModule.autoload?(:OtherEagerClass)
assert EagerModule.autoload?(:NestedEagerModule)

EagerModule.eager_load!

assert_nil(EagerModule.autoload?(:EagerClass))
assert_nil(EagerModule.autoload?(:OtherEagerClass))
assert_nil(EagerModule.autoload?(:NestedEagerModule))
assert_nil(EagerModule::NestedEagerModule.autoload?(:NestedEagerClass))
assert EagerModule::NestedEagerModule::NestedEagerClass
end
end


describe "warning in production" do
before do
@prev_env = ENV.to_hash
ENV.update("HANAMI_ENV" => "production")
end

after do
ENV.update(@prev_env)
end

it "emits a warning when not eager-loading" do
stdout, stderr = capture_io do
GraphQL.ensure_eager_load!
end

assert_equal "", stdout
expected_warning = "GraphQL-Ruby thinks this is a production deployment but didn't eager-load its constants. Address this by:
- Calling `GraphQL.eager_load!` in a production-only initializer or setup hook
- Assign `GraphQL.env = \"...\"` to something _other_ than `\"production\"` (for example, `GraphQL.env = \"development\"`)
More details: https://graphql-ruby.org/schema/definition#production-considerations
"
assert_equal expected_warning, stderr
end

it "silences the warning when GraphQL.env is assigned" do
prev_env = GraphQL.env
GraphQL.env = "staging"
stdout, stderr = capture_io do
GraphQL.ensure_eager_load!
end
assert_equal "", stdout
assert_equal "", stderr
ensure
GraphQL.env = prev_env
end
end
end

0 comments on commit 797d1e1

Please sign in to comment.