You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[RFC] Apache Felix OSGi Plugin Framework Integration
Description
This RFC proposes integrating Apache Felix as the OSGi runtime for Data Prepper's
plugin system. It builds on the goals outlined in #321 (Plugin Redesign)
and #1543 (Run plugins in their own classloader),
delivering classloader isolation, dynamic plugin loading, and a modular
architecture while maintaining full backward compatibility with existing plugins.
What is the problem?
Data Prepper's current plugin architecture has several limitations:
No classloader isolation. All plugins share the application classpath. If
two plugins depend on different versions of the same library (e.g., Jackson
2.14 vs 2.17), one will shadow the other, causing runtime failures.
No dynamic loading. Plugins must be present at startup. There is no way
to install, update, or remove a plugin without restarting the entire Data
Prepper process.
Monolithic distribution. As noted in [RFC] Plugin Redesign #321, all plugins are embedded in a
single distribution. Users running in air-gapped environments must build
custom distributions to include only the plugins they need.
No lifecycle management. Plugins are instantiated and garbage-collected
but have no formal start/stop lifecycle beyond what the pipeline provides.
There is no framework-level health monitoring of plugin state.
Dependency conflicts at scale. With 100+ plugin modules, the flat
classpath makes it increasingly difficult to manage transitive dependency
versions without conflicts.
What are you proposing?
Introduce an embedded Apache Felix OSGi framework as an opt-in plugin
loading mode alongside the existing classpath-based loading. The two modes are
controlled by a single Java system property (set via -D JVM flag):
Felix is configured with FRAMEWORK_BUNDLE_PARENT = FRAMEWORK so each plugin
bundle gets its own classloader. Shared packages are explicitly exported from
the system bundle:
org.opensearch.dataprepper.model.* — Plugin API (14 sub-packages)
org.opensearch.dataprepper.metrics — Metrics API
org.opensearch.dataprepper.plugin — Plugin SPI
org.springframework.beans.factory + org.springframework.context — Spring DI
org.slf4j + org.slf4j.event — Logging
These are explicit sub-package entries (not wildcards); adding a new API package requires updating the export list.
Boot delegation covers JDK internals (sun.*, com.sun.*, javax.*, org.xml.*, org.w3c.*, jdk.*). This
means plugins can bundle their own versions of libraries like Jackson or Guava
without conflicting with other plugins.
2. Legacy Plugin Bridge
Existing plugins require zero code changes. LegacyPluginBundleActivator reads META-INF/data-prepper.plugins.properties, scans for @DataPrepperPlugin
annotated classes using Reflections, and registers them as OSGi services. LegacyExtensionBundleActivator does the same for ExtensionPlugin subclasses.
3. OSGi Service Convention
Plugin descriptors are registered as String services with properties pluginName, pluginType, and pluginClass. Plugin classes are resolved via Bundle.loadClass() to ensure correct classloader delegation.
4. Spring DI Integration
SpringOsgiBridge registers the Spring BeanFactory as an OSGi service. Plugin
bundles can look up this service to resolve Spring-managed beans. The existing PluginBeanFactoryProvider isolated-context hierarchy (core → shared → plugin)
is preserved.
Rollback on start failure (broken bundles are automatically uninstalled)
BundleListener for event tracking
uninstallAll() for bulk cleanup during shutdown
Idempotent operations (re-installing an active bundle is a no-op)
6. Resilient Lifecycle
FelixPluginManager.stop() uses waitForStop(30s) with timeout detection
DualModePluginManager.shutdown() tears down in reverse order, catching
errors per component to ensure all components get a chance to clean up
All providers guard against framework-not-running state and return empty
results rather than throwing
7. Health Monitoring
BundleHealthCheck provides:
getBundleStatuses() — id, symbolicName, version, state for each bundle
verifyClassloaderIsolation() — confirms each bundle has its own classloader
isHealthy() — detects bundles stuck in INSTALLED (unresolved) state
OSGi Bundle Convention Plugin
A Gradle convention plugin (data-prepper.osgi-bundle) generates OSGi manifest
headers for any plugin module:
plugins {
id 'data-prepper.osgi-bundle'
}
This adds Bundle-ManifestVersion, Bundle-SymbolicName, Bundle-Version, Export-Package, and Import-Package headers to the JAR manifest.
What are your assumptions or prerequisites?
Apache Felix Framework 7.0.5+ (Java 11+ compatible)
OSGi R7 (Core 7.0) specification (Felix 7.x implements R7, not R8; the compile-time dependency is osgi.core:8.0.0 for API completeness but only the R7 subset is used at runtime)
[RFC] Apache Felix OSGi Plugin Framework Integration
Description
This RFC proposes integrating Apache Felix as the OSGi runtime for Data Prepper's
plugin system. It builds on the goals outlined in
#321 (Plugin Redesign)
and #1543 (Run plugins in their own classloader),
delivering classloader isolation, dynamic plugin loading, and a modular
architecture while maintaining full backward compatibility with existing plugins.
What is the problem?
Data Prepper's current plugin architecture has several limitations:
No classloader isolation. All plugins share the application classpath. If
two plugins depend on different versions of the same library (e.g., Jackson
2.14 vs 2.17), one will shadow the other, causing runtime failures.
No dynamic loading. Plugins must be present at startup. There is no way
to install, update, or remove a plugin without restarting the entire Data
Prepper process.
Monolithic distribution. As noted in [RFC] Plugin Redesign #321, all plugins are embedded in a
single distribution. Users running in air-gapped environments must build
custom distributions to include only the plugins they need.
No lifecycle management. Plugins are instantiated and garbage-collected
but have no formal start/stop lifecycle beyond what the pipeline provides.
There is no framework-level health monitoring of plugin state.
Dependency conflicts at scale. With 100+ plugin modules, the flat
classpath makes it increasingly difficult to manage transitive dependency
versions without conflicts.
What are you proposing?
Introduce an embedded Apache Felix OSGi framework as an opt-in plugin
loading mode alongside the existing classpath-based loading. The two modes are
controlled by a single Java system property (set via
-DJVM flag):Architecture
Key Design Decisions
1. Controlled Classloader Delegation
Felix is configured with
FRAMEWORK_BUNDLE_PARENT = FRAMEWORKso each pluginbundle gets its own classloader. Shared packages are explicitly exported from
the system bundle:
org.opensearch.dataprepper.model.*— Plugin API (14 sub-packages)org.opensearch.dataprepper.metrics— Metrics APIorg.opensearch.dataprepper.plugin— Plugin SPIorg.springframework.beans.factory+org.springframework.context— Spring DIorg.slf4j+org.slf4j.event— LoggingThese are explicit sub-package entries (not wildcards); adding a new API package requires updating the export list.
Boot delegation covers JDK internals (
sun.*,com.sun.*,javax.*,org.xml.*,org.w3c.*,jdk.*). Thismeans plugins can bundle their own versions of libraries like Jackson or Guava
without conflicting with other plugins.
2. Legacy Plugin Bridge
Existing plugins require zero code changes.
LegacyPluginBundleActivatorreadsMETA-INF/data-prepper.plugins.properties, scans for@DataPrepperPluginannotated classes using Reflections, and registers them as OSGi services.
LegacyExtensionBundleActivatordoes the same forExtensionPluginsubclasses.3. OSGi Service Convention
Plugin descriptors are registered as
Stringservices with propertiespluginName,pluginType, andpluginClass. Plugin classes are resolved viaBundle.loadClass()to ensure correct classloader delegation.4. Spring DI Integration
SpringOsgiBridgeregisters the SpringBeanFactoryas an OSGi service. Pluginbundles can look up this service to resolve Spring-managed beans. The existing
PluginBeanFactoryProviderisolated-context hierarchy (core → shared → plugin)is preserved.
5. Dynamic Plugin Loading
PluginHotLoadersupports runtimeinstall(location)/uninstall(location)with:
BundleListenerfor event trackinguninstallAll()for bulk cleanup during shutdown6. Resilient Lifecycle
FelixPluginManager.stop()useswaitForStop(30s)with timeout detectionDualModePluginManager.shutdown()tears down in reverse order, catchingerrors per component to ensure all components get a chance to clean up
results rather than throwing
7. Health Monitoring
BundleHealthCheckprovides:getBundleStatuses()— id, symbolicName, version, state for each bundleverifyClassloaderIsolation()— confirms each bundle has its own classloaderisHealthy()— detects bundles stuck in INSTALLED (unresolved) stateOSGi Bundle Convention Plugin
A Gradle convention plugin (
data-prepper.osgi-bundle) generates OSGi manifestheaders for any plugin module:
plugins { id 'data-prepper.osgi-bundle' }This adds
Bundle-ManifestVersion,Bundle-SymbolicName,Bundle-Version,Export-Package, andImport-Packageheaders to the JAR manifest.What are your assumptions or prerequisites?
osgi.core:8.0.0for API completeness but only the R7 subset is used at runtime)Backward Compatibility
This proposal is fully backward compatible:
legacy— zero behavior changedata-prepper-api@DataPrepperPlugin,@DataPrepperPluginConstructor,@DataPrepperExtensionPluginannotations unchangedPluginProviderinterface unchanged (newaddPluginProvider()method onPluginProviderLoaderis additive)ExtensionClassProviderinterface unchanged (newaddExtensionPluginClasses()method onClasspathExtensionClassProviderisadditive)
(34+ plugins) build and test with zero changes
Alternatives Considered
OSGi was chosen for its combination of classloader isolation, service registry, dynamic lifecycle management, and standardized module metadata.
OSGi Glossary
start/stop) invoked when a bundle is activated or stopped.sun.*,javax.*).Risks and Mitigations
LegacyPluginBundleActivatorruns Reflections within bundle classloader scopeSpringOsgiBridgeexposes BeanFactory as OSGi servicelegacy; OSGi is opt-inRelated Issues