…sPlugin
Adds the SPI surface in analytics-framework that lets frontend plugins
(e.g., opensearch-sql) consume analytics-engine services without taking
a hard install-time dependency on analytics-engine. Adds the
producer-side wiring in AnalyticsPlugin: discover consumers via
ExtensiblePlugin#loadExtensions, push the executor + schemaProvider
bundle once Guice constructs DefaultPlanExecutor.
Mirrors the JobSchedulerExtension pattern from opensearch-job-scheduler.
analytics-framework (sandbox/libs/analytics-framework):
- AnalyticsFrontEndExtension — single-method SPI interface,
setAnalyticsServices(AnalyticsServices). Implementers declared
via standard Java SPI (META-INF/services).
- AnalyticsServices — record bundling QueryPlanExecutor and
SchemaProvider. Bundled rather than separate setters so future
services can be added without changing the SPI signature.
analytics-engine (sandbox/plugins/analytics-engine):
- AnalyticsPlugin#loadExtensions: collect AnalyticsFrontEndExtension
consumers alongside the existing AnalyticsSearchBackendPlugin
collection.
- AnalyticsPlugin#createGuiceModules: bind a Guice TypeListener that
fires when DefaultPlanExecutor is instantiated. The
InjectionListener wraps OpenSearchSchemaBuilder::buildSchema as a
SchemaProvider, packs both into AnalyticsServices, and pushes to
every registered consumer (per-consumer try/catch so one bad
consumer doesn't crash the others).
- The push is idempotent — guarded by a servicesPushed flag so Guice
re-instantiation (no Singleton scope today) doesn't re-fire the
callback.
Verified end-to-end against opensearch-sql:
- WITH analytics-engine: SQL routes parquet_* queries through the
pushed executor (confirmed via /_plugins/_ppl/_explain showing
Calcite logical plan).
- WITHOUT analytics-engine: SQL plugin loads fine; parquet_* queries
fall through to the legacy path with a clean IndexNotFoundException
— no NoClassDefFoundError, no startup crash.
Pairs with the SQL-side consumer wiring in opensearch-project/sql.
Signed-off-by: Kai Huang <ahkcs@amazon.com>
Summary
Adds an SPI in
analytics-frameworkthat lets frontend plugins (e.g.,opensearch-sql) consume analytics-engine services without taking a hard install-time dependency on analytics-engine. Adds the matching producer-side wiring inAnalyticsPlugin: discover consumers viaExtensiblePlugin#loadExtensions, push the executor + schemaProvider once Guice constructsDefaultPlanExecutor.Mirrors the
JobSchedulerExtensionpattern fromopensearch-job-scheduler.Why
Today
opensearch-sqldeclaresextendedPlugins = [..., 'analytics-engine']as a HARD dependency. SQL plugin install fails on stock OpenSearch distros that don't shipanalytics-engine(Missing plugin [analytics-engine]). Marking it;optional=trueis necessary but not sufficient —TransportPPLQueryActionGuice-injectsQueryPlanExecutor, and Guice cannot satisfy that binding when the providing plugin is absent.The SPI inverts the dependency. Frontend plugins implement
AnalyticsFrontEndExtensionto receive analytics-engine's services through a push lifecycle; analytics-engine never imports the frontend.What's in this PR
sandbox/libs/analytics-framework/src/main/java/org/opensearch/analytics/spi/:AnalyticsFrontEndExtension— single-method SPI:setAnalyticsServices(AnalyticsServices). Implementers declared via standard Java SPI (META-INF/services).AnalyticsServices— record bundlingQueryPlanExecutor<RelNode, Iterable<Object[]>>andSchemaProvider. Bundled (rather than separate setters) so future analytics-engine services can be added without changing the SPI signature.sandbox/plugins/analytics-engine/src/main/java/org/opensearch/analytics/AnalyticsPlugin.java:loadExtensions(loader)— collectsAnalyticsFrontEndExtensionconsumers alongside the existingAnalyticsSearchBackendPlugincollection.createGuiceModules— adds a GuiceTypeListenerthat fires whenDefaultPlanExecutoris instantiated. TheInjectionListenerwrapsOpenSearchSchemaBuilder::buildSchemaas aSchemaProvider, packs both intoAnalyticsServices, and pushes to every registered consumer.pushAnalyticsServices(...)— idempotent (guarded byservicesPushedflag) so Guice re-instantiation doesn't re-fire the callback. Per-consumertry/catchso one bad consumer doesn't crash the others.End-to-end verification
Built this branch + the matching consumer-side PR (opensearch-project/sql#5xxx), assembled an OpenSearch 3.7 distro, and ran
bin/opensearch-plugin installfollowed by real PPL queries against a single-node cluster.Scenario A — analytics-engine INSTALLED
Node started cleanly:
Lucene PPL query against a regular index:
Parquet-prefixed PPL query (routes through analytics-engine — confirms the SPI fired and the executor was received):
The
calcite.logicalblock is produced only by the analytics-engine planner — it confirms the routing went throughRestUnifiedQueryAction → AnalyticsExecutionEngine, which proves the SPI push lifecycle worked end-to-end (consumer received services, holder was populated, lazy handler was constructed, request was dispatched).Scenario B — analytics-engine NOT installed (the optional path)
Note the WARN (not ERROR) for the missing optional dep — that's the
;optional=truemechanism inPluginsService.checkBundleJarHellworking as intended.Node started cleanly with only two plugins:
Lucene PPL query (the legacy path) — works:
Parquet-prefixed PPL query — falls through to legacy (no SPI push ever fired, holder stays null,
analyticsHandler()returns null, request takes the legacy path):This is the critical assertion: a clean
IndexNotFoundExceptionfrom the legacy engine — NOTNoClassDefFoundError: org/opensearch/analytics/exec/QueryPlanExecutor(the failure mode this SPI is designed to prevent). The SQL plugin's bytecode never resolves an analytics-framework type when analytics-engine is absent.Reproducing locally
The full reproduction is in the PR body: build this branch + the SQL PR, then for each scenario:
Pairs with
opensearch-project/sql#5xxx — the SQL-side consumer wiring, must merge after this so it can vendor the rebuilt
analytics-framework-3.7.0-SNAPSHOT.jar.