Skip to content

[Feature] Apache Felix OSGi Plugin Framework Integration #6760

@kiran536

Description

@kiran536

[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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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):

-Dplugin.framework=osgi    # Enable OSGi mode
-Dplugin.framework=legacy  # Default, existing behavior

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                     DualModePluginManager                       │
│              (feature flag: plugin.framework=legacy|osgi)       │
├────────────────────────────┬────────────────────────────────────┤
│       LEGACY MODE          │           OSGI MODE               │
│                            │                                    │
│  ClasspathPluginProvider   │  FelixPluginManager               │
│    └─ Reflections scan     │    └─ Embedded Felix Framework    │
│                            │       └─ OSGi Service Registry   │
│  ClasspathExtension        │                                    │
│  ClassProvider             │  OsgiPluginProvider               │
│    └─ Reflections scan     │    └─ Queries OSGi registry      │
│                            │                                    │
│                            │  OsgiExtensionClassProvider       │
│                            │    └─ Resolves extensions via OSGi│
│                            │                                    │
│                            │  SpringOsgiBridge                 │
│                            │    └─ BeanFactory as OSGi service │
│                            │                                    │
│                            │  PluginHotLoader                  │
│                            │    └─ Runtime install/uninstall   │
│                            │                                    │
│                            │  BundleHealthCheck                │
│                            │    └─ Status + isolation verify   │
├────────────────────────────┴────────────────────────────────────┤
│                    SHARED (both modes)                           │
│  DefaultPluginFactory → PluginCreator → ComponentPlugin         │
│                         ArgumentsContext                        │
│  PluginConfigurationConverter, VariableExpander                 │
│  PluginBeanFactoryProvider (isolated Spring contexts)           │
│  ExtensionLoader → ExtensionsApplier                           │
└─────────────────────────────────────────────────────────────────┘

Key Design Decisions

1. Controlled Classloader Delegation

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.

5. Dynamic Plugin Loading

PluginHotLoader supports runtime install(location) / uninstall(location)
with:

  • 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)
  • Gradle 8.x for the convention plugin
  • The existing directory structure from [RFC] Directory Structure for Data Prepper #305 is in place
  • Spring Framework 5.3.x remains the DI framework (no Spring-DM or Blueprint)

Backward Compatibility

This proposal is fully backward compatible:

  • Default mode is legacy — zero behavior change
  • No public API changes to data-prepper-api
  • @DataPrepperPlugin, @DataPrepperPluginConstructor,
    @DataPrepperExtensionPlugin annotations unchanged
  • PluginProvider interface unchanged (new addPluginProvider() method on
    PluginProviderLoader is additive)
  • ExtensionClassProvider interface unchanged (new
    addExtensionPluginClasses() method on ClasspathExtensionClassProvider is
    additive)
  • Verified: OsiDataPrepperInternalPlugins and OsiDataPrepperOtherAmazonPlugins
    (34+ plugins) build and test with zero changes

Alternatives Considered

  • JPMS (Java Modules): Built into the JDK but lacks dynamic loading and a service registry.
  • Custom classloaders (Elasticsearch model): Simpler but no standardized lifecycle or service discovery.
  • Fat/shadow JARs: Build-time isolation only; no dynamic loading and produces larger artifacts.

OSGi was chosen for its combination of classloader isolation, service registry, dynamic lifecycle management, and standardized module metadata.

OSGi Glossary

  • Bundle: A JAR with OSGi manifest headers; the unit of deployment and isolation.
  • Service Registry: Framework-managed registry where bundles publish and discover services.
  • BundleActivator: Callback interface (start/stop) invoked when a bundle is activated or stopped.
  • Boot Delegation: Packages delegated directly to the parent classloader (e.g., sun.*, javax.*).
  • System Bundle: Bundle 0; represents the framework itself and exports shared packages to all bundles.

Risks and Mitigations

Risk Mitigation
Felix adds startup latency Lazy bundle activation; benchmark before/after
Classloader conflicts with shared deps Explicit system package exports for API packages
Reflections library fails inside OSGi classloader LegacyPluginBundleActivator runs Reflections within bundle classloader scope
Spring DI incompatible with bundle isolation SpringOsgiBridge exposes BeanFactory as OSGi service
Operational complexity Feature flag defaults to legacy; OSGi is opt-in

Related Issues

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type
No fields configured for issues without a type.

Projects

Status

Unplanned

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions