From 3bc37b14aa8c5245e057c08e296b43d9a45d01a5 Mon Sep 17 00:00:00 2001 From: myronmarston-toast Date: Wed, 28 Jan 2026 18:32:31 -0800 Subject: [PATCH] Make graphql-c_parser optional with warning when unavailable graphql-c_parser requires native C extensions which don't work on all platforms (e.g. JRuby). By making it optional, ElasticGraph can run on more platforms while still recommending the faster parser. GraphQLGemLoader loads both `graphql` and `graphql/c_parser` together. When c_parser is unavailable, prints a yellow warning to stderr (once) directing users to add the gem. Warning is suppressed on JRuby since C extensions can't work there anyway. New projects still include graphql-c_parser in their generated Gemfile since it's recommended for better performance. Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) --- CODEBASE_OVERVIEW.md | 12 ---- Gemfile | 2 + Gemfile.lock | 4 +- elasticgraph-graphql/README.md | 4 -- .../elasticgraph-graphql.gemspec | 1 - .../lib/elastic_graph/graphql.rb | 13 +--- elasticgraph-query_registry/README.md | 4 -- .../elasticgraph-query_registry.gemspec | 1 - .../query_registry/query_validator.rb | 5 +- elasticgraph-schema_definition/README.md | 4 -- .../elasticgraph-schema_definition.gemspec | 1 - .../schema_artifact_manager.rb | 5 +- .../support/graphql_gem_loader.rb | 49 +++++++++++++ .../support/graphql_gem_loader.rbs | 14 ++++ .../support/graphql_gem_loader_spec.rb | 70 +++++++++++++++++++ .../elastic_graph/project_template/Gemfile.tt | 3 + 16 files changed, 148 insertions(+), 44 deletions(-) create mode 100644 elasticgraph-support/lib/elastic_graph/support/graphql_gem_loader.rb create mode 100644 elasticgraph-support/sig/elastic_graph/support/graphql_gem_loader.rbs create mode 100644 elasticgraph-support/spec/unit/elastic_graph/support/graphql_gem_loader_spec.rb diff --git a/CODEBASE_OVERVIEW.md b/CODEBASE_OVERVIEW.md index b9d3d6fc4..eada5e0b3 100644 --- a/CODEBASE_OVERVIEW.md +++ b/CODEBASE_OVERVIEW.md @@ -35,7 +35,6 @@ graph LR; elasticgraph-graphql["eg-graphql"]; base64["base64"]; graphql["graphql"]; - graphql-c_parser["graphql-c_parser"]; hashdiff["hashdiff"]; rack["rack"]; logger["logger"]; @@ -52,7 +51,6 @@ graph LR; elasticgraph-graphql --> elasticgraph-datastore_core; elasticgraph-graphql --> elasticgraph-schema_artifacts; elasticgraph-graphql --> graphql; - elasticgraph-graphql --> graphql-c_parser; elasticgraph-indexer --> elasticgraph-datastore_core; elasticgraph-indexer --> elasticgraph-schema_artifacts; elasticgraph-indexer --> elasticgraph-support; @@ -73,7 +71,6 @@ graph LR; class elasticgraph-graphql targetGemStyle; class base64 externalGemCatStyle; class graphql externalGemCatStyle; - class graphql-c_parser externalGemCatStyle; class hashdiff externalGemCatStyle; class rack externalGemCatStyle; class logger externalGemCatStyle; @@ -81,7 +78,6 @@ graph LR; click rake href "https://rubygems.org/gems/rake" "Open on RubyGems.org" _blank; click base64 href "https://rubygems.org/gems/base64" "Open on RubyGems.org" _blank; click graphql href "https://rubygems.org/gems/graphql" "Open on RubyGems.org" _blank; - click graphql-c_parser href "https://rubygems.org/gems/graphql-c_parser" "Open on RubyGems.org" _blank; click hashdiff href "https://rubygems.org/gems/hashdiff" "Open on RubyGems.org" _blank; click rack href "https://rubygems.org/gems/rack" "Open on RubyGems.org" _blank; click logger href "https://rubygems.org/gems/logger" "Open on RubyGems.org" _blank; @@ -117,7 +113,6 @@ graph LR; webrick["webrick"]; elasticgraph-schema_artifacts["eg-schema_artifacts"]; graphql["graphql"]; - graphql-c_parser["graphql-c_parser"]; elasticgraph --> elasticgraph-support; elasticgraph --> thor; elasticgraph-local --> elasticgraph-admin; @@ -133,7 +128,6 @@ graph LR; elasticgraph-schema_definition --> elasticgraph-schema_artifacts; elasticgraph-schema_definition --> elasticgraph-support; elasticgraph-schema_definition --> graphql; - elasticgraph-schema_definition --> graphql-c_parser; elasticgraph-schema_definition --> rake; class elasticgraph targetGemStyle; class elasticgraph-support otherEgGemStyle; @@ -149,13 +143,11 @@ graph LR; class webrick externalGemCatStyle; class elasticgraph-schema_artifacts otherEgGemStyle; class graphql externalGemCatStyle; - class graphql-c_parser externalGemCatStyle; click thor href "https://rubygems.org/gems/thor" "Open on RubyGems.org" _blank; click rackup href "https://rubygems.org/gems/rackup" "Open on RubyGems.org" _blank; click rake href "https://rubygems.org/gems/rake" "Open on RubyGems.org" _blank; click webrick href "https://rubygems.org/gems/webrick" "Open on RubyGems.org" _blank; click graphql href "https://rubygems.org/gems/graphql" "Open on RubyGems.org" _blank; - click graphql-c_parser href "https://rubygems.org/gems/graphql-c_parser" "Open on RubyGems.org" _blank; ``` ### Datastore Adapters (2 gems) @@ -227,7 +219,6 @@ graph LR; elasticgraph-query_interceptor["eg-query_interceptor"]; elasticgraph-schema_artifacts["eg-schema_artifacts"]; elasticgraph-query_registry["eg-query_registry"]; - graphql-c_parser["graphql-c_parser"]; rake["rake"]; elasticgraph-warehouse["eg-warehouse"]; elasticgraph-apollo --> elasticgraph-graphql; @@ -242,7 +233,6 @@ graph LR; elasticgraph-query_registry --> elasticgraph-graphql; elasticgraph-query_registry --> elasticgraph-support; elasticgraph-query_registry --> graphql; - elasticgraph-query_registry --> graphql-c_parser; elasticgraph-query_registry --> rake; elasticgraph-warehouse --> elasticgraph-support; class elasticgraph-apollo targetGemStyle; @@ -255,12 +245,10 @@ graph LR; class elasticgraph-query_interceptor targetGemStyle; class elasticgraph-schema_artifacts otherEgGemStyle; class elasticgraph-query_registry targetGemStyle; - class graphql-c_parser externalGemCatStyle; class rake externalGemCatStyle; class elasticgraph-warehouse targetGemStyle; click graphql href "https://rubygems.org/gems/graphql" "Open on RubyGems.org" _blank; click apollo-federation href "https://rubygems.org/gems/apollo-federation" "Open on RubyGems.org" _blank; - click graphql-c_parser href "https://rubygems.org/gems/graphql-c_parser" "Open on RubyGems.org" _blank; click rake href "https://rubygems.org/gems/rake" "Open on RubyGems.org" _blank; ``` diff --git a/Gemfile b/Gemfile index 6f73d3334..abf08b00a 100644 --- a/Gemfile +++ b/Gemfile @@ -11,6 +11,8 @@ source "https://rubygems.org" # Gems needed by the test suite and other CI checks. group :development do gem "aws_lambda_ric", "~> 3.1", ">= 3.1.3" + # graphql-c_parser is no longer a hard dependency, but we include it here for faster CI tests + gem "graphql-c_parser", "~> 1.1", ">= 1.1.3" gem "benchmark-ips", "~> 2.14" gem "coderay", "~> 1.1", ">= 1.1.3" gem "factory_bot", "~> 6.5", ">= 6.5.6" diff --git a/Gemfile.lock b/Gemfile.lock index f056434cc..88586597c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -92,7 +92,6 @@ PATH elasticgraph-datastore_core (= 1.1.1.pre) elasticgraph-schema_artifacts (= 1.1.1.pre) graphql (~> 2.5.18) - graphql-c_parser (~> 1.1, >= 1.1.3) PATH remote: elasticgraph-health_check @@ -174,7 +173,6 @@ PATH elasticgraph-graphql (= 1.1.1.pre) elasticgraph-support (= 1.1.1.pre) graphql (~> 2.5.18) - graphql-c_parser (~> 1.1, >= 1.1.3) rake (~> 13.3, >= 13.3.1) PATH @@ -199,7 +197,6 @@ PATH elasticgraph-schema_artifacts (= 1.1.1.pre) elasticgraph-support (= 1.1.1.pre) graphql (~> 2.5.18) - graphql-c_parser (~> 1.1, >= 1.1.3) rake (~> 13.3, >= 13.3.1) PATH @@ -689,6 +686,7 @@ DEPENDENCIES faker (~> 3.6) filewatcher (~> 2.1)! flatware-rspec (~> 2.3, >= 2.3.4)! + graphql-c_parser (~> 1.1, >= 1.1.3) html-proofer (~> 5.2) httpx (~> 1.7, >= 1.7.2) irb (~> 1.16) diff --git a/elasticgraph-graphql/README.md b/elasticgraph-graphql/README.md index 7d1910e3b..75f161269 100644 --- a/elasticgraph-graphql/README.md +++ b/elasticgraph-graphql/README.md @@ -23,9 +23,6 @@ graph LR; graphql["graphql"]; elasticgraph-graphql --> graphql; class graphql externalGemStyle; - graphql-c_parser["graphql-c_parser"]; - elasticgraph-graphql --> graphql-c_parser; - class graphql-c_parser externalGemStyle; elasticgraph-apollo["elasticgraph-apollo"]; elasticgraph-apollo --> elasticgraph-graphql; class elasticgraph-apollo otherEgGemStyle; @@ -52,7 +49,6 @@ graph LR; class elasticgraph-schema_definition otherEgGemStyle; click base64 href "https://rubygems.org/gems/base64" "Open on RubyGems.org" _blank; click graphql href "https://rubygems.org/gems/graphql" "Open on RubyGems.org" _blank; - click graphql-c_parser href "https://rubygems.org/gems/graphql-c_parser" "Open on RubyGems.org" _blank; ``` ## Usage diff --git a/elasticgraph-graphql/elasticgraph-graphql.gemspec b/elasticgraph-graphql/elasticgraph-graphql.gemspec index 8bd162697..09bf687c6 100644 --- a/elasticgraph-graphql/elasticgraph-graphql.gemspec +++ b/elasticgraph-graphql/elasticgraph-graphql.gemspec @@ -45,7 +45,6 @@ Gem::Specification.new do |spec| spec.add_dependency "elasticgraph-datastore_core", ElasticGraph::VERSION spec.add_dependency "elasticgraph-schema_artifacts", ElasticGraph::VERSION spec.add_dependency "graphql", "~> 2.5.18" - spec.add_dependency "graphql-c_parser", "~> 1.1", ">= 1.1.3" spec.add_development_dependency "elasticgraph-admin", ElasticGraph::VERSION spec.add_development_dependency "elasticgraph-elasticsearch", ElasticGraph::VERSION diff --git a/elasticgraph-graphql/lib/elastic_graph/graphql.rb b/elasticgraph-graphql/lib/elastic_graph/graphql.rb index 406ae00dd..2f21ab2ec 100644 --- a/elasticgraph-graphql/lib/elastic_graph/graphql.rb +++ b/elasticgraph-graphql/lib/elastic_graph/graphql.rb @@ -10,6 +10,7 @@ require "elastic_graph/graphql/config" require "elastic_graph/constants" require "elastic_graph/support/from_yaml_file" +require "elastic_graph/support/graphql_gem_loader" module ElasticGraph # The main entry point for ElasticGraph GraphQL handling. Instantiate this to get access to the @@ -132,13 +133,7 @@ def datastore_query_builder # @private def graphql_gem_plugins @graphql_gem_plugins ||= begin - require "graphql" - # As per https://graphql-ruby.org/language_tools/c_parser.html, loading the - # C parser causes the faster parser to be assigned as the `::GraphQL.default_parser`, - # providing greater efficiency. - # - # We load it here since this is where we load the GraphQL gem. - require "graphql/c_parser" + Support::GraphQLGemLoader.load { # We depend on this to avoid N+1 calls to the datastore. @@ -268,9 +263,7 @@ def monotonic_clock # at boot time instead of deferring dependency loading until we handle the first query. In other environments (such as tests), # it's nice to load dependencies when needed. def load_dependencies_eagerly - require "graphql" - require "graphql/c_parser" - + Support::GraphQLGemLoader.load ::GraphQL.eager_load! # run a simple GraphQL query to force load any dependencies needed to handle GraphQL queries diff --git a/elasticgraph-query_registry/README.md b/elasticgraph-query_registry/README.md index 008e28802..d2a7b5aa6 100644 --- a/elasticgraph-query_registry/README.md +++ b/elasticgraph-query_registry/README.md @@ -47,14 +47,10 @@ graph LR; graphql["graphql"]; elasticgraph-query_registry --> graphql; class graphql externalGemStyle; - graphql-c_parser["graphql-c_parser"]; - elasticgraph-query_registry --> graphql-c_parser; - class graphql-c_parser externalGemStyle; rake["rake"]; elasticgraph-query_registry --> rake; class rake externalGemStyle; click graphql href "https://rubygems.org/gems/graphql" "Open on RubyGems.org" _blank; - click graphql-c_parser href "https://rubygems.org/gems/graphql-c_parser" "Open on RubyGems.org" _blank; click rake href "https://rubygems.org/gems/rake" "Open on RubyGems.org" _blank; ``` diff --git a/elasticgraph-query_registry/elasticgraph-query_registry.gemspec b/elasticgraph-query_registry/elasticgraph-query_registry.gemspec index 87750f786..fa01ec032 100644 --- a/elasticgraph-query_registry/elasticgraph-query_registry.gemspec +++ b/elasticgraph-query_registry/elasticgraph-query_registry.gemspec @@ -44,7 +44,6 @@ Gem::Specification.new do |spec| spec.add_dependency "elasticgraph-graphql", ElasticGraph::VERSION spec.add_dependency "elasticgraph-support", ElasticGraph::VERSION spec.add_dependency "graphql", "~> 2.5.18" - spec.add_dependency "graphql-c_parser", "~> 1.1", ">= 1.1.3" spec.add_dependency "rake", "~> 13.3", ">= 13.3.1" spec.add_development_dependency "elasticgraph-elasticsearch", ElasticGraph::VERSION diff --git a/elasticgraph-query_registry/lib/elastic_graph/query_registry/query_validator.rb b/elasticgraph-query_registry/lib/elastic_graph/query_registry/query_validator.rb index 2fb44161a..24538b15f 100644 --- a/elasticgraph-query_registry/lib/elastic_graph/query_registry/query_validator.rb +++ b/elasticgraph-query_registry/lib/elastic_graph/query_registry/query_validator.rb @@ -8,8 +8,9 @@ require "elastic_graph/query_registry/variable_backward_incompatibility_detector" require "elastic_graph/query_registry/variable_dumper" -require "graphql" -require "graphql/c_parser" +require "elastic_graph/support/graphql_gem_loader" + +ElasticGraph::Support::GraphQLGemLoader.load module ElasticGraph module QueryRegistry diff --git a/elasticgraph-schema_definition/README.md b/elasticgraph-schema_definition/README.md index abb2fcdea..b85f79b9e 100644 --- a/elasticgraph-schema_definition/README.md +++ b/elasticgraph-schema_definition/README.md @@ -30,9 +30,6 @@ graph LR; graphql["graphql"]; elasticgraph-schema_definition --> graphql; class graphql externalGemStyle; - graphql-c_parser["graphql-c_parser"]; - elasticgraph-schema_definition --> graphql-c_parser; - class graphql-c_parser externalGemStyle; rake["rake"]; elasticgraph-schema_definition --> rake; class rake externalGemStyle; @@ -40,7 +37,6 @@ graph LR; elasticgraph-local --> elasticgraph-schema_definition; class elasticgraph-local otherEgGemStyle; click graphql href "https://rubygems.org/gems/graphql" "Open on RubyGems.org" _blank; - click graphql-c_parser href "https://rubygems.org/gems/graphql-c_parser" "Open on RubyGems.org" _blank; click rake href "https://rubygems.org/gems/rake" "Open on RubyGems.org" _blank; ``` diff --git a/elasticgraph-schema_definition/elasticgraph-schema_definition.gemspec b/elasticgraph-schema_definition/elasticgraph-schema_definition.gemspec index 3208c4015..01a958a2e 100644 --- a/elasticgraph-schema_definition/elasticgraph-schema_definition.gemspec +++ b/elasticgraph-schema_definition/elasticgraph-schema_definition.gemspec @@ -46,7 +46,6 @@ Gem::Specification.new do |spec| spec.add_dependency "elasticgraph-schema_artifacts", ElasticGraph::VERSION spec.add_dependency "elasticgraph-support", ElasticGraph::VERSION spec.add_dependency "graphql", "~> 2.5.18" - spec.add_dependency "graphql-c_parser", "~> 1.1", ">= 1.1.3" spec.add_dependency "rake", "~> 13.3", ">= 13.3.1" spec.add_development_dependency "elasticgraph-admin", ElasticGraph::VERSION diff --git a/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_artifact_manager.rb b/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_artifact_manager.rb index d24d9dd31..56d288fae 100644 --- a/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_artifact_manager.rb +++ b/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_artifact_manager.rb @@ -9,13 +9,14 @@ require "did_you_mean" require "elastic_graph/constants" require "elastic_graph/schema_definition/json_schema_pruner" +require "elastic_graph/support/graphql_gem_loader" require "elastic_graph/support/memoizable_data" require "fileutils" -require "graphql" -require "graphql/c_parser" require "tempfile" require "yaml" +ElasticGraph::Support::GraphQLGemLoader.load + module ElasticGraph module SchemaDefinition # Manages schema artifacts. Note: not tested directly. Instead, the `RakeTasks` tests drive this class. diff --git a/elasticgraph-support/lib/elastic_graph/support/graphql_gem_loader.rb b/elasticgraph-support/lib/elastic_graph/support/graphql_gem_loader.rb new file mode 100644 index 000000000..cab9dcb31 --- /dev/null +++ b/elasticgraph-support/lib/elastic_graph/support/graphql_gem_loader.rb @@ -0,0 +1,49 @@ +# Copyright 2024 - 2026 Block, Inc. +# +# Use of this source code is governed by an MIT-style +# license that can be found in the LICENSE file or at +# https://opensource.org/licenses/MIT. +# +# frozen_string_literal: true + +module ElasticGraph + module Support + # Helper module to load the graphql gem and optionally graphql-c_parser for better performance. + # Prints a yellow warning to stderr when c_parser is unavailable (except on JRuby where C extensions don't work). + # + # @private + module GraphQLGemLoader + # ANSI escape code for yellow text + YELLOW = "\e[33m" + RESET = "\e[0m" + + @warning_printed = false # : bool + + # Loads the graphql gem and attempts to load graphql/c_parser. + # If c_parser is unavailable, prints a warning once (unless on JRuby). + def self.load + require "graphql" + + begin + require "graphql/c_parser" + rescue LoadError + print_warning_once + end + end + + def self.print_warning_once + return if @warning_printed || RUBY_ENGINE == "jruby" + + @warning_printed = true + warn "#{YELLOW}[ElasticGraph] For better performance, add `graphql-c_parser` to your Gemfile. See: https://graphql-ruby.org/language_tools/c_parser.html#{RESET}" + end + + # Resets warning state; only intended for use in tests. + def self.reset_warning_state! + @warning_printed = false + end + + private_class_method :print_warning_once, :reset_warning_state! + end + end +end diff --git a/elasticgraph-support/sig/elastic_graph/support/graphql_gem_loader.rbs b/elasticgraph-support/sig/elastic_graph/support/graphql_gem_loader.rbs new file mode 100644 index 000000000..f0160b776 --- /dev/null +++ b/elasticgraph-support/sig/elastic_graph/support/graphql_gem_loader.rbs @@ -0,0 +1,14 @@ +module ElasticGraph + module Support + module GraphQLGemLoader + YELLOW: ::String + RESET: ::String + + self.@warning_printed: bool + + def self.load: () -> void + def self.print_warning_once: () -> void + def self.reset_warning_state!: () -> void + end + end +end diff --git a/elasticgraph-support/spec/unit/elastic_graph/support/graphql_gem_loader_spec.rb b/elasticgraph-support/spec/unit/elastic_graph/support/graphql_gem_loader_spec.rb new file mode 100644 index 000000000..b278a4573 --- /dev/null +++ b/elasticgraph-support/spec/unit/elastic_graph/support/graphql_gem_loader_spec.rb @@ -0,0 +1,70 @@ +# Copyright 2024 - 2026 Block, Inc. +# +# Use of this source code is governed by an MIT-style +# license that can be found in the LICENSE file or at +# https://opensource.org/licenses/MIT. +# +# frozen_string_literal: true + +require "elastic_graph/support/graphql_gem_loader" + +module ElasticGraph + module Support + RSpec.describe GraphQLGemLoader do + before do + GraphQLGemLoader.send(:reset_warning_state!) + end + + describe ".load" do + it "prints a yellow warning to stderr when graphql-c_parser is unavailable" do + simulate_missing_gems "graphql/c_parser" + + expect { + GraphQLGemLoader.load + }.to output(/\[ElasticGraph\] For better performance, add `graphql-c_parser` to your Gemfile/).to_stderr + end + + it "prints the warning only once across multiple calls" do + simulate_missing_gems "graphql/c_parser" + + stderr_output = StringIO.new + original_stderr = $stderr + $stderr = stderr_output + + begin + GraphQLGemLoader.load + GraphQLGemLoader.load + GraphQLGemLoader.load + ensure + $stderr = original_stderr + end + + expect(stderr_output.string.scan("graphql-c_parser").size).to eq(1) + end + + it "still raises if the graphql gem is unavailable" do + simulate_missing_gems "graphql", "graphql/c_parser" + + expect { + GraphQLGemLoader.load + }.to raise_error(LoadError) + end + + it "does not print a warning when graphql-c_parser loads successfully" do + # The gem is installed in development, so require will succeed without stubbing + expect { + GraphQLGemLoader.load + }.not_to output.to_stderr + end + + def simulate_missing_gems(*gems) + allow(GraphQLGemLoader).to receive(:require).and_call_original + + gems.each do |gem_name| + allow(GraphQLGemLoader).to receive(:require).with(gem_name).and_raise(LoadError) + end + end + end + end + end +end diff --git a/elasticgraph/lib/elastic_graph/project_template/Gemfile.tt b/elasticgraph/lib/elastic_graph/project_template/Gemfile.tt index f0f907e93..db8476b02 100644 --- a/elasticgraph/lib/elastic_graph/project_template/Gemfile.tt +++ b/elasticgraph/lib/elastic_graph/project_template/Gemfile.tt @@ -1,5 +1,8 @@ source "https://rubygems.org" +# Recommended for better GraphQL parsing performance. +gem "graphql-c_parser", "~> 1.1", platforms: :ruby + # Gem details for the elasticgraph gems. elasticgraph_details = <%= ElasticGraph.setup_env.gemfile_elasticgraph_details_code_snippet %>