Skip to content

Commit 6ef7919

Browse files
authored
Apply indexer extension modules when the Indexer boots (#1300)
## Why With runtime metadata storage (#1298) and the registration API (#1299) in place, `elasticgraph-indexer` needs to actually apply registered extensions--introducing the indexer extension concept, mirroring how `elasticgraph-graphql` applies GraphQL extension modules from both settings YAML and runtime metadata. ## What - `Indexer#initialize` extends configured extension modules onto the instance: first from the new `indexer.extension_modules` setting (mirroring `graphql.extension_modules`), then from the runtime metadata's `indexer_extension_modules` - Regenerated config schema artifacts for the new setting - Nothing registers an indexer extension yet, so this is a no-op for all existing schemas ## Verification - `script/lint`, `script/type_check` - `script/run_gem_specs elasticgraph-indexer` (100% coverage) - `script/run_gem_specs elasticgraph-local` ## Stack Current PR is marked with `->`. - [#1298 Add indexer_extension_modules to runtime metadata](#1298) - [#1299 Add register_indexer_extension schema definition API](#1299) - -> [#1300 Apply indexer extension modules when the Indexer boots](#1300) - [#1301 Extract an ingestion adapter seam inside elasticgraph-indexer](#1301) - [#1302 Move JSON ingestion into elasticgraph-json_ingestion via an indexer extension](#1302)
1 parent 0b42a07 commit 6ef7919

15 files changed

Lines changed: 262 additions & 97 deletions

File tree

elasticgraph-graphql/lib/elastic_graph/graphql/config.rb

Lines changed: 2 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
# frozen_string_literal: true
88

99
require "elastic_graph/support/config"
10-
require "elastic_graph/errors"
1110
require "elastic_graph/graphql/client"
1211
require "elastic_graph/schema_artifacts/runtime_metadata/extension_loader"
1312

@@ -79,38 +78,7 @@ class Config < Support::Config.define(
7978
}
8079
]
8180
},
82-
extension_modules: {
83-
description: "Array of modules that will be extended onto the `GraphQL` instance to support extension libraries.",
84-
type: "array",
85-
items: {
86-
type: "object",
87-
properties: {
88-
name: {
89-
type: "string",
90-
minLength: 1,
91-
description: "The name of the extension module class to load.",
92-
examples: ["MyExtensionModule", "ElasticGraph::MyExtension"]
93-
},
94-
require_path: {
95-
type: "string",
96-
minLength: 1,
97-
description: "The path to require to load the extension module.",
98-
examples: ["./my_extension_module", "elastic_graph/my_extension"]
99-
}
100-
},
101-
required: ["name", "require_path"]
102-
},
103-
default: [], # : untyped
104-
examples: [
105-
[], # : untyped
106-
[
107-
{
108-
"name" => "MyExtensionModule",
109-
"require_path" => "./my_extension_module"
110-
}
111-
]
112-
]
113-
}
81+
extension_modules: Support::Config::EXTENSION_MODULE_SCHEMA
11482
}
11583

11684
# The standard ElasticGraph root config setting keys; anything else is assumed to be extension settings.
@@ -128,7 +96,7 @@ def self.from_parsed_yaml(parsed_yaml)
12896

12997
def convert_values(client_resolver:, extension_modules:, **values)
13098
client_resolver = load_client_resolver(client_resolver)
131-
extension_modules = load_extension_modules(extension_modules)
99+
extension_modules = SchemaArtifacts::RuntimeMetadata::ExtensionLoader.load_component_extensions(extension_modules)
132100

133101
values.merge({
134102
client_resolver: client_resolver,
@@ -150,18 +118,6 @@ def load_client_resolver(config)
150118

151119
__skip__ = extension_class.new(extension.config)
152120
end
153-
154-
def load_extension_modules(extension_module_hashes)
155-
extension_loader = SchemaArtifacts::RuntimeMetadata::ExtensionLoader.new(::Module.new)
156-
157-
extension_module_hashes.map do |mod_hash|
158-
extension_loader.load(mod_hash.fetch("name"), from: mod_hash.fetch("require_path"), config: {}).extension_class.tap do |mod|
159-
unless mod.instance_of?(::Module)
160-
raise Errors::ConfigError, "`#{mod_hash.fetch("name")}` is not a module, but all application extension modules must be modules."
161-
end
162-
end
163-
end
164-
end
165121
end
166122
end
167123
end

elasticgraph-graphql/sig/elastic_graph/graphql/config.rbs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ module ElasticGraph
4343
) -> ::Hash[::Symbol, untyped]
4444

4545
def load_client_resolver: (::Hash[::String, untyped]) -> Client::_Resolver
46-
def load_extension_modules: (::Array[::Hash[::String, untyped]]) -> ::Array[::Module]
4746
end
4847
end
4948
end

elasticgraph-graphql/spec/unit/elastic_graph/graphql/config_spec.rb

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -150,16 +150,6 @@ module EgExtensionModule2
150150
expect(extension_modules).to eq([::EgExtensionModule1, ::EgExtensionModule2])
151151
end
152152

153-
it "raises a clear error if the extension can't be loaded" do
154-
expect {
155-
extension_modules_from(<<~YAML)
156-
extension_modules:
157-
- require_path: ./not_real
158-
name: NotReal
159-
YAML
160-
}.to raise_error LoadError, a_string_including("not_real")
161-
end
162-
163153
it "raises a clear error if the config is malformed" do
164154
expect {
165155
extension_modules_from(<<~YAML)
@@ -183,33 +173,6 @@ module EgExtensionModule1
183173
}.to raise_error a_string_including("name")
184174
end
185175

186-
it "raises a clear error if the named extension is not a module" do
187-
File.write("eg_extension_class1.rb", <<~EOS)
188-
class EgExtensionClass1
189-
end
190-
EOS
191-
192-
expect {
193-
extension_modules_from(<<~YAML)
194-
extension_modules:
195-
- require_path: ./eg_extension_class1
196-
name: EgExtensionClass1
197-
YAML
198-
}.to raise_error a_string_including("not a module")
199-
200-
File.write("eg_extension_object1.rb", <<~EOS)
201-
EgExtensionObject1 = Object.new
202-
EOS
203-
204-
expect {
205-
extension_modules_from(<<~YAML)
206-
extension_modules:
207-
- require_path: ./eg_extension_object1
208-
name: EgExtensionObject1
209-
YAML
210-
}.to raise_error a_string_including("not a class or module")
211-
end
212-
213176
def load_config_from_yaml(yaml)
214177
yaml = <<~EOS
215178
default_page_size: 27

elasticgraph-indexer/lib/elastic_graph/indexer.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ def initialize(
4040
@schema_artifacts = @datastore_core.schema_artifacts
4141
@monotonic_clock = monotonic_clock
4242
@clock = clock || ::Time
43+
44+
# Apply any extension modules that have been configured.
45+
config.extension_modules.each { |mod| extend mod }
46+
@schema_artifacts.runtime_metadata.indexer_extension_modules.each { |ext_mod| extend ext_mod.load_extension.extension_class }
4347
end
4448

4549
def datastore_router

elasticgraph-indexer/lib/elastic_graph/indexer/config.rb

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
#
77
# frozen_string_literal: true
88

9+
require "elastic_graph/schema_artifacts/runtime_metadata/extension_loader"
910
require "elastic_graph/support/config"
10-
require "elastic_graph/errors"
1111

1212
module ElasticGraph
1313
class Indexer
14-
class Config < Support::Config.define(:latency_slo_thresholds_by_timestamp_in_ms, :skip_derived_indexing_type_updates)
14+
class Config < Support::Config.define(:latency_slo_thresholds_by_timestamp_in_ms, :skip_derived_indexing_type_updates, :extension_modules)
1515
json_schema at: "indexer",
1616
optional: false,
1717
description: "Configuration for indexing operations and metrics used by `elasticgraph-indexer`.",
@@ -42,15 +42,17 @@ class Config < Support::Config.define(:latency_slo_thresholds_by_timestamp_in_ms
4242
{}, # : untyped
4343
{"WidgetWorkspace" => ["ABC12345678"]}
4444
]
45-
}
45+
},
46+
extension_modules: Support::Config::EXTENSION_MODULE_SCHEMA
4647
}
4748

4849
private
4950

50-
def convert_values(skip_derived_indexing_type_updates:, latency_slo_thresholds_by_timestamp_in_ms:)
51+
def convert_values(skip_derived_indexing_type_updates:, latency_slo_thresholds_by_timestamp_in_ms:, extension_modules:)
5152
{
5253
skip_derived_indexing_type_updates: skip_derived_indexing_type_updates.transform_values(&:to_set),
53-
latency_slo_thresholds_by_timestamp_in_ms: latency_slo_thresholds_by_timestamp_in_ms
54+
latency_slo_thresholds_by_timestamp_in_ms: latency_slo_thresholds_by_timestamp_in_ms,
55+
extension_modules: SchemaArtifacts::RuntimeMetadata::ExtensionLoader.load_component_extensions(extension_modules)
5456
}
5557
end
5658
end

elasticgraph-indexer/sig/elastic_graph/indexer/config.rbs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@ module ElasticGraph
55

66
attr_reader latency_slo_thresholds_by_timestamp_in_ms: ::Hash[::String, ::Integer]
77
attr_reader skip_derived_indexing_type_updates: ::Hash[::String, ::Set[::String]]
8+
attr_reader extension_modules: ::Array[::Module]
89

910
def initialize: (
1011
?latency_slo_thresholds_by_timestamp_in_ms: ::Hash[::String, ::Integer],
11-
?skip_derived_indexing_type_updates: ::Hash[::String, ::Set[::String]]) -> void
12+
?skip_derived_indexing_type_updates: ::Hash[::String, ::Set[::String]],
13+
?extension_modules: ::Array[::Module]) -> void
1214

1315
def with: (
1416
?latency_slo_thresholds_by_timestamp_in_ms: ::Hash[::String, ::Integer],
15-
?skip_derived_indexing_type_updates: ::Hash[::String, ::Set[::String]]) -> Config
17+
?skip_derived_indexing_type_updates: ::Hash[::String, ::Set[::String]],
18+
?extension_modules: ::Array[::Module]) -> Config
1619

1720
def self.members: () -> ::Array[::Symbol]
1821
end
@@ -22,8 +25,10 @@ module ElasticGraph
2225

2326
def convert_values: (
2427
latency_slo_thresholds_by_timestamp_in_ms: untyped,
25-
skip_derived_indexing_type_updates: untyped
28+
skip_derived_indexing_type_updates: untyped,
29+
extension_modules: untyped
2630
) -> ::Hash[::Symbol, untyped]
31+
2732
end
2833
end
2934
end

elasticgraph-indexer/spec/unit/elastic_graph/indexer/config_spec.rb

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,57 @@ class Indexer
3131

3232
expect(config.skip_derived_indexing_type_updates).to eq("WidgetCurrency" => ["USD"].to_set)
3333
end
34+
35+
describe "#extension_modules", :in_temp_dir do
36+
it "loads the extension modules from disk" do
37+
File.write("eg_extension_module1.rb", <<~EOS)
38+
module EgExtensionModule1
39+
end
40+
EOS
41+
42+
File.write("eg_extension_module2.rb", <<~EOS)
43+
module EgExtensionModule2
44+
end
45+
EOS
46+
47+
extension_modules = extension_modules_from(<<~YAML)
48+
extension_modules:
49+
- require_path: ./eg_extension_module1
50+
name: EgExtensionModule1
51+
- require_path: ./eg_extension_module2
52+
name: EgExtensionModule2
53+
YAML
54+
55+
expect(extension_modules).to eq([::EgExtensionModule1, ::EgExtensionModule2])
56+
end
57+
58+
it "raises a clear error if the config is malformed" do
59+
expect {
60+
extension_modules_from(<<~YAML)
61+
extension_modules:
62+
- require: ./not_real
63+
name: NotReal
64+
YAML
65+
}.to raise_error a_string_including("require_path")
66+
67+
File.write("eg_extension_module1.rb", <<~EOS)
68+
module EgExtensionModule1
69+
end
70+
EOS
71+
72+
expect {
73+
extension_modules_from(<<~YAML)
74+
extension_modules:
75+
- require_path: ./eg_extension_module1
76+
extension: EgExtensionModule1
77+
YAML
78+
}.to raise_error a_string_including("name")
79+
end
80+
81+
def extension_modules_from(yaml)
82+
Config.from_parsed_yaml("indexer" => ::YAML.safe_load(yaml)).extension_modules
83+
end
84+
end
3485
end
3586
end
3687
end

elasticgraph-indexer/spec/unit/elastic_graph/indexer_spec.rb

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,50 @@ module ElasticGraph
2929
expect(indexer).to be_a(Indexer)
3030
end
3131
end
32+
33+
context "when `config.extension_modules` or runtime metadata indexer extension modules are configured" do
34+
it "applies the extensions when the Indexer instance is instantiated without impacting any other instances" do
35+
config_extension_module = Module.new do
36+
def datastore_router
37+
:config_router
38+
end
39+
end
40+
stub_const("ConfigExtensionModule", config_extension_module)
41+
42+
runtime_metadata_extension_module = Module.new do
43+
def processor
44+
:runtime_metadata_processor
45+
end
46+
end
47+
stub_const("RuntimeMetadataExtensionModule", runtime_metadata_extension_module)
48+
49+
extended_indexer = build_indexer(
50+
extension_modules: [config_extension_module],
51+
schema_definition: lambda do |schema|
52+
# `defined_at` just needs a valid require path, but needs to be outside ElasticGraph
53+
# to not mess with our code coverage measurement.
54+
schema.register_indexer_extension runtime_metadata_extension_module, defined_at: "time"
55+
define_schema_elements(schema)
56+
end
57+
)
58+
59+
normal_indexer = build_indexer(
60+
schema_definition: lambda { |schema| define_schema_elements(schema) }
61+
)
62+
63+
expect(extended_indexer.datastore_router).to eq :config_router
64+
expect(extended_indexer.processor).to eq :runtime_metadata_processor
65+
66+
expect(normal_indexer.datastore_router).to be_a(Indexer::DatastoreIndexingRouter)
67+
expect(normal_indexer.processor).to be_a(Indexer::Processor)
68+
end
69+
70+
def define_schema_elements(schema)
71+
schema.object_type "Widget" do |t|
72+
t.field "id", "ID!"
73+
t.index "widgets"
74+
end
75+
end
76+
end
3277
end
3378
end

elasticgraph-local/lib/elastic_graph/local/spec_support/config_schema.yaml

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,7 @@ properties:
441441
header_name: X-Client-Name
442442
additionalProperties: false
443443
extension_modules:
444-
description: Array of modules that will be extended onto the `GraphQL` instance
444+
description: Array of modules that will be extended onto the component instance
445445
to support extension libraries.
446446
type: array
447447
items:
@@ -508,6 +508,36 @@ properties:
508508
- {}
509509
- WidgetWorkspace:
510510
- ABC12345678
511+
extension_modules:
512+
description: Array of modules that will be extended onto the component instance
513+
to support extension libraries.
514+
type: array
515+
items:
516+
type: object
517+
properties:
518+
name:
519+
type: string
520+
minLength: 1
521+
description: The name of the extension module class to load.
522+
examples:
523+
- MyExtensionModule
524+
- ElasticGraph::MyExtension
525+
require_path:
526+
type: string
527+
minLength: 1
528+
description: The path to require to load the extension module.
529+
examples:
530+
- "./my_extension_module"
531+
- elastic_graph/my_extension
532+
required:
533+
- name
534+
- require_path
535+
additionalProperties: false
536+
default: []
537+
examples:
538+
- []
539+
- - name: MyExtensionModule
540+
require_path: "./my_extension_module"
511541
additionalProperties: false
512542
logger:
513543
description: Configuration for logging used by all parts of ElasticGraph.

elasticgraph-schema_artifacts/lib/elastic_graph/schema_artifacts/runtime_metadata/extension_loader.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,20 @@ module RuntimeMetadata
2525
#
2626
# @private
2727
class ExtensionLoader
28+
# Loads the component extension modules described by the given config hashes (as configured
29+
# via an `extension_modules` config setting), verifying that each is a module.
30+
def self.load_component_extensions(extension_module_hashes)
31+
extension_loader = new(::Module.new)
32+
33+
extension_module_hashes.map do |mod_hash|
34+
extension_loader.load(mod_hash.fetch("name"), from: mod_hash.fetch("require_path"), config: {}).extension_class.tap do |mod|
35+
unless mod.instance_of?(::Module)
36+
raise Errors::ConfigError, "`#{mod_hash.fetch("name")}` is not a module, but all application extension modules must be modules."
37+
end
38+
end
39+
end
40+
end
41+
2842
def initialize(interface_def)
2943
@interface_def = interface_def
3044
@loaded_by_name = {}

0 commit comments

Comments
 (0)