# fe-extension-loader Developer Integration Guide This guide is for business module developers. It focuses on: 1. What Loader solves. 2. When to use it. 3. What each core class is responsible for. 4. How to integrate it in business modules. 5. How to diagnose common failures. ## 1. Module Positioning `fe-extension-loader` is the reusable plugin runtime foundation for Doris FE. It unifies repeated loading logic across modules, including: 1. Scanning `pluginRoots`. 2. Resolving jars under plugin directories. 3. Building child-first classloaders. 4. Discovering typed factories via `ServiceLoader`. 5. Aggregating load successes and failures. ## 2. Applicable Scenarios ### 2.1 Good Fit For Loader Use Loader if your module needs: 1. External plugin loading from directories. 2. Shared loading framework across multiple business modules. 3. Standardized failure semantics (`scan`, `resolve`, `discover`, etc.). 4. Less duplicated runtime loading code. ### 2.2 Not A Good Fit For Loader Loader may not be a direct fit if: 1. Plugin source is not directory-based and you already have a custom loading pipeline. 2. You only need SPI contracts and no runtime loading. In those cases, use `fe-extension-spi` only. ## 3. Core Class Guide ### 3.1 `DirectoryPluginRuntimeManager` This is the runtime entry class and unified facade. Primary methods: 1. `loadAll(...)` 2. `get(pluginName)` 3. `list()` Business modules should depend on this class directly. ### 3.2 Core Flow Inside `DirectoryPluginRuntimeManager` The manager performs: 1. Scan plugin subdirectories under each root. 2. Collect jars from `pluginDir/*.jar` and `pluginDir/lib/*.jar`. 3. Create classloader. 4. Discover and validate factory (exactly one per directory). 5. Handle duplicate names and record failures. 6. Return `LoadReport`. ### 3.3 `PluginLoader` Low-level utility class. It does not scan directories. It only handles: 1. Classloader creation. 2. Typed factory discovery. Use it when you already own classloader lifecycle externally. ### 3.4 `ChildFirstClassLoader` Classloading behavior: 1. Child-first by default. 2. Parent-first for allowlisted package prefixes. Purpose: 1. Reduce dependency conflict with FE process classpath. 2. Keep SPI interface class source consistent and avoid type-isolation `ClassCastException`. ### 3.5 `ClassLoadingPolicy` Used to configure parent-first prefixes: 1. Mandatory prefixes (always included by default). 2. Business prefixes (append per module). Common business examples: 1. `org.apache.doris.authentication.` 2. `org.apache.doris.authorization.` ### 3.6 `PluginHandle` Represents one successfully loaded plugin, including: 1. `pluginName` 2. `pluginDir` 3. `resolvedJars` 4. `classLoader` 5. `factory` 6. `loadedAt` Business modules typically consume `pluginName + factory` for registration. ### 3.7 `LoadFailure` Represents one failed plugin directory load, including: 1. `pluginDir` 2. `stage` 3. `message` 4. `cause` Failure stages: 1. `scan` 2. `resolve` 3. `createClassLoader` 4. `discover` 5. `instantiate` 6. `conflict` ### 3.8 `LoadReport` Represents the full result of one `loadAll` call, including: 1. Success list: `successes` 2. Failure list: `failures` 3. Statistics: `rootsScanned`, `dirsScanned` ### 3.9 Runtime Handle Storage (Built-In) `DirectoryPluginRuntimeManager` stores loaded handles in an internal concurrent map. No separate `PluginRuntimeRegistry` abstraction is exposed in current implementation. ## 4. Standard Integration Steps ### 4.1 Prepare Typed Factory Your business factory interface should extend `PluginFactory`. ### 4.2 Initialize Runtime Manager ```java DirectoryPluginRuntimeManager runtime = new DirectoryPluginRuntimeManager<>(); ``` ### 4.3 Configure Classloading Policy ```java ClassLoadingPolicy policy = new ClassLoadingPolicy( Collections.singletonList("org.apache.doris.mybiz.")); ``` ### 4.4 Call `loadAll` ```java LoadReport report = runtime.loadAll( pluginRoots, Thread.currentThread().getContextClassLoader(), MyPluginFactory.class, policy); ``` ### 4.5 Process Load Result ```java for (LoadFailure failure : report.getFailures()) { LOG.warn("plugin load failure: dir={}, stage={}, message={}", failure.getPluginDir(), failure.getStage(), failure.getMessage(), failure.getCause()); } for (PluginHandle handle : report.getSuccesses()) { factoryMap.putIfAbsent(handle.getPluginName(), handle.getFactory()); } ``` ## 5. Directory Convention Recommended layout: ```text / / pluginA.jar lib/ dep1.jar dep2.jar / pluginB.jar ``` Rules: 1. Only direct subdirectories under `pluginRoot` are scanned. 2. Each plugin directory must contain at least one jar. 3. Each plugin directory must discover exactly one factory. ## 6. Conflict And Failure Handling ### 6.1 Duplicate Plugin Name Current default strategy: 1. Keep the first successfully loaded plugin. 2. Record later duplicates as `conflict`. 3. Continue loading other directories. Business recommendations: 1. Treat `conflict` as warning/alert. 2. Avoid duplicate plugin names in production plugin roots. ### 6.2 Should All Failures Throw Loader returns `LoadReport` and does not force exception. Business modules choose policy by semantics: 1. Tolerant mode: log failures and continue startup. 2. Strict mode: fail-fast when no successful plugin exists. ### 6.3 Detailed `LoadReport` Handling Strategy `LoadReport` should be startup decision input, not just logs. Recommended goals: 1. All successful plugins are registered. 2. Every failure is traceable by stage and directory. 3. Startup behavior is deterministic (strict or tolerant). 4. Conflict/failure paths do not leak resources. Recommended processing order: 1. Log summary metrics: `rootsScanned`, `dirsScanned`, `successes.size`, `failures.size`. 2. Log each failure with `pluginDir + stage + message + cause`. 3. Register successful plugins to business map (`pluginName -> factory`). 4. Apply startup decision logic (strict/tolerant). Suggested stage severity grouping: 1. Environment/config issues: `scan`, `resolve`. 2. SPI/implementation issues: `discover`, `instantiate`. 3. Runtime construction issues: `createClassLoader`. 4. Naming conflict: `conflict` (usually warning, not immediate stop). Recommended decision rules: 1. `dirsScanned == 0`: often means empty roots or no external setup. 2. `dirsScanned > 0 && successes.isEmpty()` in strict mode: fail-fast. 3. `dirsScanned > 0 && successes.isEmpty()` in tolerant mode: warn and continue only if business allows no external plugin. 4. If `requiredPluginNames` exists, enforce presence even when partial loads succeeded. ### 6.4 Copy-And-Adapt Template ```java public static void processLoadReport( LoadReport report, Map factoryMap, boolean strictMode, Set requiredPluginNames) { Objects.requireNonNull(report, "report"); Objects.requireNonNull(factoryMap, "factoryMap"); Objects.requireNonNull(requiredPluginNames, "requiredPluginNames"); // Step 1: summary metrics LOG.info("plugin load summary: rootsScanned={}, dirsScanned={}, successCount={}, failureCount={}", report.getRootsScanned(), report.getDirsScanned(), report.getSuccesses().size(), report.getFailures().size()); // Step 2: failure details LoadFailure firstNonConflictFailure = null; for (LoadFailure failure : report.getFailures()) { LOG.warn("plugin load failure: dir={}, stage={}, message={}", failure.getPluginDir(), failure.getStage(), failure.getMessage(), failure.getCause()); if (!LoadFailure.STAGE_CONFLICT.equals(failure.getStage()) && firstNonConflictFailure == null) { firstNonConflictFailure = failure; } } // Step 3: register successful plugins int registered = 0; for (PluginHandle handle : report.getSuccesses()) { F existing = factoryMap.putIfAbsent(handle.getPluginName(), handle.getFactory()); if (existing != null) { // If business map already contains the name, close discarded external classloader. closeClassLoaderQuietly(handle.getClassLoader()); LOG.warn("skip duplicated plugin name in business map: {}", handle.getPluginName()); continue; } registered++; } // Step 4: startup decision (strict/tolerant) if (strictMode && report.getDirsScanned() > 0 && registered == 0 && firstNonConflictFailure != null) { throw new IllegalStateException( "No plugin loaded in strict mode: stage=" + firstNonConflictFailure.getStage() + ", dir=" + firstNonConflictFailure.getPluginDir() + ", message=" + firstNonConflictFailure.getMessage(), firstNonConflictFailure.getCause()); } // Step 5: required plugin checks for (String required : requiredPluginNames) { if (!factoryMap.containsKey(required)) { throw new IllegalStateException("Required plugin is missing: " + required); } } } ``` The `closeClassLoaderQuietly` implementation pattern can be referenced from: `../fe-authentication/fe-authentication-handler/src/main/java/org/apache/doris/authentication/handler/AuthenticationPluginManager.java` ### 6.5 Authentication Module Mapping Current authentication module handling is: 1. Iterate `report.getFailures()` and log warnings. 2. Iterate `report.getSuccesses()` and register factories (close duplicated external classloader if needed). 3. Throw `AuthenticationException` when directories were scanned but no external plugin was loaded. Reference implementation: `../fe-authentication/fe-authentication-handler/src/main/java/org/apache/doris/authentication/handler/AuthenticationPluginManager.java` ## 7. Current Runtime Scope (V1) Supported: 1. `loadAll` 2. `get` 3. `list` Not supported: 1. `reload` 2. `unload` Do not depend on runtime hot-reload semantics in V1. ## 8. Frequently Asked Questions ### 8.1 Factory Not Found Check: 1. Plugin jar includes `META-INF/services/`. 2. Service file class name is correct. 3. Provider class is included in final jar. ### 8.2 Multiple Factories Found Check: 1. Multiple jars may declare the same factory type. 2. Parent classpath may pollute service resources. Note: `DirectoryPluginRuntimeManager` includes parent service-resource filtering and prefers plugin-directory-local discovery. ### 8.3 Loaded Plugin Not Released Check: 1. Business layer may keep stale handle references. 2. Conflict/failure paths may skip classloader close. 3. Plugin instance `close()` may not release resources. ## 9. Integration With Authentication Module Authentication integration sample: `../fe-authentication/fe-authentication-handler/src/main/java/org/apache/doris/authentication/handler/AuthenticationPluginManager.java` Key integration points: 1. Use `DirectoryPluginRuntimeManager`. 2. Append authentication parent-first prefix. 3. Register successful factories into authentication factory map. 4. Close classloader for discarded conflicting handles. ## Related Docs 1. Chinese developer guide: `README_CN.md` 2. Unified runtime design (CN): `../fe-authentication/EXTENSION_LOADER_UNIFIED_DESIGN_CN.md` 3. SPI contracts: `../fe-extension-spi/README.md`