gortiz commented on PR #18459:
URL: https://github.com/apache/pinot/pull/18459#issuecomment-4429183628
# Review: PR #18459 — Replace global shading with classloader-realm
isolation for plugins
**Repo:** apache/pinot
**Branch:** pinot-no-shading → master
**Author:** gortiz
**Date:** 2026-05-11
**Repo context loaded:**
- [x] CLAUDE.md read (present, comprehensive)
- [x] CODEOWNERS read (not present)
- [x] Module-level docs for touched paths read (N/A — no per-module
CLAUDE.md in touched paths)
---
## Stage 1: Triage & Evidence Collection
### PR description
- [x] Description present
- [x] States what problem is being solved (four problems enumerated: plugin
code baked into main jar, startup script loading plugins onto system classpath,
shading prefix collisions, fragmented extension-point discovery)
- [x] States why this approach (classloader-realm isolation is the right
semantic for plugin isolation; shading is kept only for driver libs and
external-process connectors)
- [x] States risky areas (backward compatibility section, `@MetricsFactory`
annotation change noted, external plugin authors migration path described)
**Findings:**
The description is thorough and accurate. One gap: the description does not
mention that `pinot-kafka-4.0` is intentionally **not** converted (no
`pinot-plugin.properties`, no plugin-zip entry in distribution). This omission
is a potential source of confusion for reviewers and operators. Severity:
observation. Confidence: high. Evidence:
`pinot-plugins/pinot-stream-ingestion/pinot-kafka-4.0/` — no
`src/main/resources` directory exists on the branch; not listed in
`pinot-distribution/pom.xml` plugin deps.
### Change summary
- [x] Full diff read (GitHub API reports 100 files; actual branch diff
against master is 116 files — 16 files absent from the GitHub PR files list
including the most critical ones)
- [x] Summary produced
- [x] Files listed by module
**Summary:**
This is a large architectural refactor with three coordinated phases: (1)
replace `ServiceLoader.load(Iface.class)` call-sites with
`PluginManager.get().loadServices(Iface.class)` across 9+ modules; (2) add
`pinot-plugin.properties` to every plugin module under `pinot-plugins/` and
`pinot-connectors/` so they can be discovered as classloader realms; (3)
replace shaded fat-jar plugin layout in the distribution with plugin-zip layout
(classes + dep jars side-by-side), removing global relocation from the root
`pom.xml`. New tooling: `pinot-plugin-verifier` standalone CLI. New library
modules: `pinot-json-base`, `pinot-parquet-base`.
**Files by module:**
- `pinot-spi/`: `PluginManager.java` (core new logic: `loadServices`,
`loadClassFromAnyPlugin`, jar-resident `pinot-plugin.properties` discovery),
`PinotMetricUtils.java`, `ExecutorServiceUtils.java`,
`ResponseStoreService.java`, `RecordEnricherRegistry.java`,
`PluginManagerTest.java` (+ new test SPI fixtures)
- `pinot-segment-spi/`: `IndexService.java`
- `pinot-query-runtime/`: `OpChainConverterDispatcher.java`
- `pinot-broker/`: `AccessControlFactory.java`, new
`AccessControlFactoryLoaderTest.java`
- `pinot-common/`: `pom.xml` (remove shade profile),
`LogicalTableConfigSerDeProvider.java`, `FakeMetricsFactory.java`
- `pinot-core/`: `pom.xml` (remove shade profile),
`TimeBoundaryStrategyService.java`
- `pinot-distribution/`: `pom.xml` (plugin-zip deps + shade execution
override), `pinot-assembly.xml` (complete rewrite from file-list to
dependencySet unpack)
- `pinot-plugins/`: ~30 `pom.xml` files (`shade.phase.prop` removal,
`plugin.type` removal), ~25 new `pinot-plugin.properties` files; new
`assembly-descriptor/` with `pinot-plugin.xml` assembly descriptor; new
`pinot-json-base/`, new `pinot-parquet-base/`; compound-metrics
`CompoundPinotMetricsFactory.java`
- `pinot-plugin-verifier/`: entirely new module (`PluginVerifier.java`,
check classes, smoke test)
- `pinot-minion/`: `SegmentConversionUtils.java` (moved from
`pinot-minion-builtin-tasks` to `pinot-minion`)
- `pinot-tools/`: `LaunchBackfillIngestionJobCommand.java` (import update
for moved class)
- `pinot-clients/`: both client `pom.xml` files (explicit shade relocations
added)
- Root `pom.xml`: remove global shade relocations, add
`pinot-plugin-verifier` module, add `pinot-json-base` and `pinot-parquet-base`
to `<dependencyManagement>`
**NOTE:** The GitHub PR files API omits 16 files from the diff, including
`PluginManager.java`, `PinotMetricUtils.java`, `IndexService.java`,
`ResponseStoreService.java`, `ExecutorServiceUtils.java`,
`OpChainConverterDispatcher.java`, `RecordEnricherRegistry.java`, root
`pom.xml`, and several test files. These were retrieved by fetching the branch
directly. Reviewers relying only on the GitHub diff UI will miss the most
critical code changes in this PR.
### Risk classification
- [x] Classified as **high**
- [x] Reason stated
**Classification:** HIGH
**Reason:** This PR modifies the plugin loading subsystem (PluginManager —
the central classloader registry for all Pinot extension points), changes how
every `ServiceLoader`-based extension point is discovered at startup across
broker/server/controller/minion, rewrites the distribution packaging layout for
all plugins, removes global shading relocations from the root POM (affecting
every module that inherits the shade plugin configuration), and introduces a
new module ordering dependency. A failure in any of these areas breaks every
Pinot service role at startup. The blast radius is the entire cluster.
### Test gaps
- [x] Changed behaviors/contracts checked for corresponding tests
- [x] Test coverage of new/changed behavior verified
**Gaps:**
1. **`loadServices` deduplication across multiple classloaders not tested.**
The code comments say cross-classloader FQCN deduplication is intentional but
"out of scope for this unit test." However this is exactly the scenario that
matters in production: a class reachable via the DEFAULT classloader AND a
plugin realm. Without a test, the correctness of `seenFqcns` de-duplication
under the real multi-classloader layout is unverified. Severity: concern.
Confidence: medium. Evidence:
`PluginManagerTest.testLoadServicesIsIdempotentAcrossInvocations` comment
explicitly says the cross-classloader dedup test "is out of scope."
2. **`readPluginConfigurationFromJar` startup cost untested.** The
jar-scanning fallback opens every JAR in a plugin directory to look for
`pinot-plugin.properties`. Kafka-3.0 ships 150+ JAR files in its zip. No
benchmark or test validates acceptable startup time under the new layout.
Severity: concern. Confidence: medium. Evidence:
`PluginManager.readPluginConfiguration()` iterates
`FileUtils.listFiles(directory, new String[]{"jar"}, true)` opening each with
`new JarFile(jarFile)`.
3. **No integration test for the complete plugin-zip → realm load → class
instantiation path in CI.** The `PluginVerifierSmokeTest` explicitly
acknowledges it cannot test the real production layout; the end-to-end test
(`quick-start-batch.sh`) is manual in the test plan. Severity: concern.
Confidence: high. Evidence: `PluginVerifierSmokeTest` class Javadoc: "We don't
have a built distribution at unit-test time, so these only confirm that the
binary is wired together correctly."
4. **`AccessControlFactory` test does not verify plugin-realm-resident class
loading.** The test only covers system-classloader-resident classes. Severity:
observation. Confidence: high. Evidence: `AccessControlFactoryLoaderTest` — all
tested FQCNs are in-tree classes visible on the test classpath.
---
## Stage 2: Deep Review
### Does the implementation address the stated problem?
- [x] Problem statement identified from the PR description
- [x] Implementation traced through the code to check whether it appears to
address the stated problem
- [x] No obvious gaps between what the PR claims to fix and what the code
actually does
**Findings:**
All four stated problems have corresponding implementation changes:
1. Plugin code baked into main service jar — addressed: global shade
relocations removed from root `pom.xml`, plugins now produce zip artifacts
instead of shaded fat-jars.
2. Startup script putting plugin jars on the JVM classpath — the assembly
descriptor changes remove the `<file>` entries for shaded plugin jars, so the
distribution no longer places them where the startup script can find them.
However, the startup script (`appAssemblerScriptTemplate`) itself is not
modified in this PR; whether it still walks `plugins/**/*.jar` onto `CLASSPATH`
is not verified in the diff.
3. Shading prefix collisions — addressed by removing relocations from the
root `pom.xml`.
4. Fragmented extension-point discovery — addressed by the `loadServices`
helper replacing every `ServiceLoader.load(X.class)` call-site across the
codebase.
Severity: observation. Confidence: medium. Evidence: startup script not in
changed files; `appAssemblerScriptTemplate` behavior needs manual verification.
### High-risk files
- [x] Files ranked by logical risk
- [x] Risk reasons stated per file
| Rank | File | Risk reason |
|---|---|---|
| 1 | `pinot-spi/.../PluginManager.java` | New `loadServices`,
`loadClassFromAnyPlugin`, jar-scan fallback affect every plugin load at startup
|
| 2 | `pom.xml` (root) | Removing global shade relocations changes every
module's shading behavior |
| 3 | `pinot-distribution/pinot-assembly.xml` | Complete packaging rewrite;
assembly failure = broken distribution |
| 4 | `pinot-distribution/pom.xml` | New plugin-zip dependency declarations
+ shade execution override |
| 5 | `pinot-spi/.../PinotMetricUtils.java` | Metrics factory discovery
change affects startup of every service role |
| 6 | `pinot-plugins/assembly-descriptor/` | New shared assembly descriptor
drives plugin zip production; bugs here break all plugin zips |
**Findings:** The highest-risk file (`PluginManager.java`) is absent from
the GitHub PR files API list, making the GitHub diff UI an incomplete review
surface for this PR. Severity: concern. Confidence: high. Evidence: `gh pr view
18459 --repo apache/pinot --json files` returns 100 files; `git diff
origin/master...personal/pinot-no-shading` produces 116 file diffs.
### Center of gravity
- [x] 1-3 key files or decisions identified
**Files:**
1. `pinot-spi/src/main/java/org/apache/pinot/spi/plugin/PluginManager.java`
2.
`pinot-plugins/assembly-descriptor/src/main/resources/assemblies/pinot-plugin.xml`
3. `pom.xml` (root) — shade relocation removal
**Why these are central:**
- `PluginManager.java` is the runtime mechanism: the `loadServices` method
is called at startup from metrics, index, executor, record-enricher,
response-store, time-boundary, op-chain, and access-control subsystems.
Correctness here determines whether every plugin-based feature works.
- `pinot-plugin.xml` assembly descriptor determines the zip layout that
`PluginManager.readPluginConfiguration` and the distribution assembly depend
on. If the layout is wrong, no plugin is ever loaded as a realm.
- Root `pom.xml` shade relocation removal is the one change that could
silently break existing deployments: any module that previously worked because
it consumed relocated Jackson/Guava from `org.apache.pinot.shaded.*` would fail
at runtime if it now gets the unshaded version (or nothing).
### Invariants — targeted
- [x] Authorization — boundaries preserved; `AccessControlFactory` change
uses `PluginManager.loadClass` which delegates to system classloader first,
preserving current behavior for in-tree factories.
- [x] Idempotency — `loadServices` is not idempotent (each call does a fresh
walk), documented as intentional.
- [x] Backward compatibility — APIs: no wire protocol changes.
- [x] Backward compatibility — Schemas: not applicable; no schema changes.
- [x] Backward compatibility — SPI: `PluginManager.loadClass`,
`createInstance`, `loadServices` are new public APIs; existing
`loadClass(pluginName, className)` behavior is changed (now does realm walk for
DEFAULT_PLUGIN_NAME). This is additive but changes the resolution order for
bare-FQCN lookups.
- [x] Backward compatibility — Configuration: `pinot-plugin.properties`
files added to existing plugins. The root `pom.xml` shade relocation removal
changes the shading behavior for any module that inherited it.
- [x] Ordering guarantees — `loadServices` iteration order is documented
(PluginManager classloader first, then ClassWorld realms in registration order,
then legacy PluginClassLoaders). This is deterministic but depends on plugin
load order, which depends on filesystem iteration (directory listing order).
- [x] Null/empty handling — `loadClassFromAnyPlugin` throws
`ClassNotFoundException` cleanly when no classloader finds the class.
`loadServices` returns an empty list when no implementation is found. Both are
correct.
- [x] Retry safety — not applicable; no retry logic changed.
- [x] Observability — `LOGGER.warn` added for FQCN conflicts across
classloaders and malformed service entries. `LOGGER.info` logs jar-resident
property file discovery.
- [x] Resource cleanup — `readPluginConfigurationFromJar` uses
try-with-resources on `JarFile`. No resource leaks found.
- [x] Migration reversibility — removing global shade relocations is NOT
reversible without a coordinated release because any consumer that already
depends on `org.apache.pinot.shaded.*` class names (e.g. in configuration)
would break if shading is re-added.
- [x] Performance in hot paths — `loadServices` is called at startup, not on
hot paths. However, `readPluginConfiguration` opens all JARs in a plugin
directory recursively on startup to scan for `pinot-plugin.properties` — this
adds meaningful startup I/O for plugins with many dependencies (see Test gaps
#2).
- [x] API design — `loadServices` return type is `List<T>` (not
`Iterable<T>`), which is a reasonable choice. The `loadClassFromAnyPlugin`
first-hit-wins semantics is clearly documented. The `synchronized (this)`
snapshot approach is sound.
**Findings:**
BLOCKER 1 — `verify-plugins.sh` is referenced in `pinot-assembly.xml` but
does not exist in the repository. The distribution build will fail at assembly
time when Maven tries to include
`${pinot.root}/pinot-plugin-verifier/src/main/resources/bin/verify-plugins.sh`
because the file does not exist. Severity: blocker. Confidence: high. Evidence:
`pinot-distribution/pinot-assembly.xml` line includes
`<source>${pinot.root}/pinot-plugin-verifier/src/main/resources/bin/verify-plugins.sh</source>`;
`git ls-tree
personal/pinot-no-shading:pinot-plugin-verifier/src/main/resources/` returns no
`bin/` directory.
BLOCKER 2 — `pinot-plugin-verifier` pom.xml produces a plain `.jar` (no
`maven-assembly-plugin` config, no manifest main class, no
`jar-with-dependencies` classifier), but the PR test plan instructs users to
run `java -jar
pinot-plugin-verifier/target/pinot-plugin-verifier-*-jar-with-dependencies.jar`
and the assembly.xml includes the plain `.jar`. The artifact referenced in the
test plan does not exist and the plain `.jar` lacks a runnable manifest.
Severity: blocker. Confidence: high. Evidence: `pinot-plugin-verifier/pom.xml`
— no `maven-assembly-plugin` or `maven-jar-plugin` `<mainClass>` config; PR
test plan line: `java -jar
pinot-plugin-verifier/target/pinot-plugin-verifier-*-jar-with-dependencies.jar`.
CONCERN 1 — `pinot-kafka-4.0` has no `pinot-plugin.properties` and is not
included in the distribution's plugin-zip list. If Kafka 4.x is a supported
stream ingestion plugin, it will silently not load as a classloader realm under
the new layout. Any deployment that uses `pinot-kafka-4.0` would break.
Severity: concern. Confidence: medium (need to verify whether kafka-4.0 is in
active use). Evidence: `git ls-tree
personal/pinot-no-shading:pinot-plugins/pinot-stream-ingestion/pinot-kafka-4.0/src/main/`
shows only `java/`; `pinot-distribution/pom.xml` has no `pinot-kafka-4.0`
dependency.
CONCERN 2 — Backward-compatibility break for `@MetricsFactory` annotation.
The PR description states: "The `@MetricsFactory` annotation is preserved but
is no longer load-bearing; plugins must add
`@AutoService(PinotMetricsFactory.class)` (or hand-write a service file) to be
discovered." This is a **breaking change for third-party plugins** that rely on
`@MetricsFactory` for discovery. The PR provides no migration path or
grace-period warning in `PinotMetricUtils`. Severity: concern. Confidence:
high. Evidence: `PinotMetricUtils.initializePinotMetricsFactory` now calls
`PluginManager.get().loadServices(PinotMetricsFactory.class)` which requires a
`META-INF/services` entry; `@MetricsFactory` scanning via
`PinotReflectionUtils.getClassesThroughReflection` is removed entirely.
CONCERN 3 — The assembly `dependencySet` unpacks all plugin zips into a
single flat `plugins/` directory. Each plugin zip's `pinot-plugin.xml`
descriptor puts its contents under `${project.artifactId}/` inside the zip
(e.g., `pinot-kafka-3.0/classes/`, `pinot-kafka-3.0/*.jar`). After unpacking,
the plugins directory will contain subdirectories named after artifact IDs
(e.g., `plugins/pinot-kafka-3.0/`). The `PluginManager` scans
`<pluginsDir>/<subdirName>/` using `<subdirName>` as the plugin name. Verifying
that this directory structure matches what `PluginManager.load(pluginName,
directory)` expects requires an end-to-end build; it is not verifiable from the
diff alone. Severity: concern. Confidence: medium. Evidence: `pinot-plugin.xml`
assembly descriptor + `pinot-assembly.xml` dependencySet
`<outputDirectory>plugins</outputDirectory>` + `PluginManager.load(String
pluginName, File directory)`.
CONCERN 4 — Recursive JAR scan on startup. `readPluginConfiguration()` calls
`FileUtils.listFiles(directory, new String[]{"jar"}, true)` which recursively
scans all JAR files in the plugin directory, then opens each one with `new
JarFile(jarFile)` until a `pinot-plugin.properties` is found. A plugin with
100+ dependency JARs (e.g., `pinot-hdfs`) will cause 100 `JarFile` open
operations per plugin on every startup. The optimization (check the primary
plugin JAR first) helps but does not eliminate the worst case. Severity:
concern. Confidence: high. Evidence: `PluginManager.readPluginConfiguration`
iterates over all JARs recursively; `pinot-plugin-verifier/README.md` mentions
"100+ jars per plugin."
### Invariants — full sweep
- [x] Every invariant above checked regardless of whether the touched paths
trigger it
**Findings:**
ADDITIONAL OBSERVATION — `loadServices` is not thread-safe in its iteration
phase. The snapshot of `classLoaders` is taken under `synchronized (this)`, but
the iteration over `classLoaders` (and the `ServiceLoader` calls inside
`loadServicesInto`) happens outside the lock. This is documented as intentional
("point-in-time snapshot"). The scenario where a plugin is registered AFTER the
snapshot but BEFORE the iteration would miss that plugin's services. This is
the correct trade-off (avoid holding the lock during potentially slow I/O), but
it means callers in early-init code could miss services if another thread
registers plugins concurrently. Severity: observation. Confidence: medium.
Evidence: `PluginManager.loadServices` — `synchronized` block ends before the
`for (ClassLoader cl : classLoaders)` loop.
ADDITIONAL OBSERVATION — `loadClassFromAnyPlugin` calls `Class.forName(name,
true, cl)` with `initialize=true`. If a class triggers its static initializer
upon loading and that initializer has side effects (common in plugin code),
those side effects execute during the walk even if the class is ultimately not
the "winning" hit. This is the same behavior as before for the DEFAULT
classloader path but is now triggered across all classloaders. Severity:
observation. Confidence: medium. Evidence: `loadClassFromAnyPlugin` line:
`Class<?> c = Class.forName(name, true, cl)`.
### Test adequacy
- [x] Tests prove intended behavior (not just that code runs)
- [x] Failure modes covered
- [x] Boundary conditions covered
- [x] Not over-coupled to implementation details
**Findings:**
- `PluginManagerTest.testRealmWalkContinuesPastNoClassDefFoundError`
correctly tests the NCDFE bug fix by injecting a bad classloader via
reflection. This is good.
- `PluginManagerTest.testLoadServicesFindsImplementationsViaServiceLoader`
validates the system-classloader path but does not test the realm path (no
realm is populated in the test).
-
`AccessControlFactoryLoaderTest.loadFactory_withPackagePrivateConstructor_succeeds`
is an excellent regression test for the historical constructor-visibility
contract.
- The smoke test uses `System.setOut/setErr` without restoring on exception
in all paths — the finally block restores them, so this is fine.
- Test plan checklist items for `quick-start-batch.sh` and `PluginVerifier`
21/21 PASS are listed as unchecked boxes, meaning they have not been verified
as part of this PR submission.
Severity: concern. Confidence: high. Evidence: Test plan in PR body shows
unchecked boxes for the most important end-to-end validations.
### Collateral damage
- [x] No copy-paste duplication introduced
- [x] No partial renames (all references updated — `SegmentConversionUtils`
moved and all callers updated)
- [x] No dead code left behind
- [x] Docs and config match code changes — **EXCEPTION:
`pinot-plugin-verifier/README.md` references `java -jar
...-jar-with-dependencies.jar` but that artifact is not produced**
- [x] No one-off exceptions added to shared abstractions
**Findings:** The README/test-plan mismatch about the verifier jar artifact
name is a collateral damage issue. Severity: blocker (see Blocker 2 above).
### Data flow
- [x] Input paths traced from entry point to storage/output
- [x] Unvalidated input paths flagged
- [x] Sensitive data exposure checked
**Findings:**
The `readPluginConfigurationFromJar` method reads untrusted file content
(properties loaded from a JAR file on the filesystem). Properties are parsed
with `java.util.Properties.load()` which is safe (no code execution). The
resulting `PinotPluginConfiguration` is used to configure classloader parent
relationships — a malicious or corrupted `pinot-plugin.properties` could
configure an unexpected `parent.realmId`, but this is bounded by the realm
names already registered in `ClassWorld`. No sensitive data exposure found.
The `loadClassFromAnyPlugin` first-hit-wins semantics means a plugin that
ships a class with the same FQCN as a core class will have that class loaded
from the plugin if the plugin's realm is checked before the system classloader
(in the current ordering: system CL comes first via DEFAULT PluginClassLoader,
so core classes are safe). Severity: observation. Confidence: high.
### Duplicated logic
- [x] PR checked for logic that already exists elsewhere in the codebase
**Findings:**
`ExecutorServiceUtils` previously had a custom `forEachExecutorThatLoads`
that handled `ServiceConfigurationError` recovery. The new
`PluginManager.loadServicesInto` implements the same pattern generically. The
old custom method is deleted, so this is correct consolidation with no
duplication. No other duplicated logic found.
### Path-triggered review
- [x] Touched paths listed
- [x] Failure mode inferred per path
- [x] Reviewed for inferred risks
- [x] Boundary areas checked first
**Touched paths and inferred risks:**
| Path | Inferred failure mode | What was checked | Findings |
|---|---|---|---|
| `pinot-spi/...PluginManager.java` | Blast radius: class loading for all
plugins/services | `loadServices`, `loadClassFromAnyPlugin` logic,
synchronization, thread safety | See Concern 4, Observations on
`Class.forName(name, true, cl)` |
| `pinot-spi/...PinotMetricUtils.java` | Startup failure: metrics factory
not found → no metrics → service won't start | Discovery path: `loadServices` →
`matched.ifPresent` → `init` | Concern 2: `@MetricsFactory` no longer
load-bearing — breaking for external plugins |
| `pinot-distribution/pinot-assembly.xml` | Build failure: assembly
references missing file | `verify-plugins.sh` source path | BLOCKER 1: file
does not exist |
| `pinot-distribution/pom.xml` | Build failure: plugin zips not produced,
shade phase override | Shade execution override + plugin-zip deps | Correct —
shade execution hardcoded to `package` phase |
| `pinot-plugins/assembly-descriptor/` | All plugin zips wrong layout |
`pinot-plugin.xml` descriptor — zip structure | Appears correct; structure
matches `PluginManager.load` expectations |
| `pom.xml` (root) | Runtime failure: classes previously accessed as
`org.apache.pinot.shaded.*` | Global relocation removal | Concern — third-party
plugins that hardcode shaded package names would break |
| `pinot-plugins/pinot-stream-ingestion/pinot-kafka-4.0/` | Silent:
kafka-4.0 not loaded as realm | No `pinot-plugin.properties`, not in
distribution | Concern 1 |
| `pinot-minion/.../SegmentConversionUtils.java` | Package-move: callers
must update imports | All callers updated
(`BaseMultipleSegmentsConversionExecutor`,
`BaseSingleSegmentConversionExecutor`, `SegmentConversionUtilsTest`,
`LaunchBackfillIngestionJobCommand`) | Clean |
| `pinot-clients/pinot-java-client/pom.xml`, `pinot-jdbc-client/pom.xml` |
Shade regression: client jars must still relocate deps | Explicit relocation
config added | Correct; relocations moved from inherited global to module-local
|
**Findings:** Summary of path-triggered findings is incorporated into the
main Blockers/Concerns sections.
### Evidence
- [x] Required evidence checked
| Evidence type | Present? | Notes |
|---|---|---|
| Before/after benchmarks | No | No startup-time benchmarks for jar-scan
fallback |
| Failing test that now passes | Partial |
`testRealmWalkContinuesPastNoClassDefFoundError` is a new regression test, not
one converted from failing |
| Rollout plan with stages | No | This is a build-time change; no phased
rollout described |
| Migration notes and rollback steps | No | No rollback steps described;
removing shade relocations is not trivially reversible |
| Example input/output pairs | No | |
| Screenshot, trace, or log snippet | No | "21/21 passes" claimed but not
shown |
| Proof of backward compatibility | No | External plugin compat promised but
not demonstrated |
| Canary or feature flag plan | No | Merge is all-or-nothing |
**Missing:** The PR description claims 21/21 passes on the built
distribution but no evidence is provided. External plugin backward
compatibility is asserted but not demonstrated. No startup-time benchmark.
Severity: concern. Confidence: high. Evidence: PR test plan checkboxes are
unchecked.
---
## Verdict
**Risk:** high
**Verdict:** changes-requested
**Verdict confidence:** high
### Confidence reasoning
The review benefited from full access to the branch (fetched
`personal/pinot-no-shading` locally) which revealed 16 files not in the GitHub
PR diff API, including `PluginManager.java` — the most critical file. The two
blockers are concrete and reproducible from the diff alone. The concerns are
supported by code-level evidence. The main uncertainty is Concern 1 (kafka-4.0
intentional omission vs. oversight) and whether the startup script was
separately updated outside this PR.
### Blockers
**BLOCKER 1 — Missing `verify-plugins.sh` script breaks the distribution
build.**
Severity: blocker. Confidence: high. Evidence:
`pinot-distribution/pinot-assembly.xml` references
`${pinot.root}/pinot-plugin-verifier/src/main/resources/bin/verify-plugins.sh`;
`git ls-tree
personal/pinot-no-shading:pinot-plugin-verifier/src/main/resources/` confirms
the file does not exist. The Maven assembly plugin will fail with a "file not
found" error when building the distribution tarball under `-Pbin-dist`.
**BLOCKER 2 — `pinot-plugin-verifier` jar artifact mismatch makes the tool
non-functional.**
Severity: blocker. Confidence: high. Evidence:
`pinot-plugin-verifier/pom.xml` has no `maven-assembly-plugin` config to
produce a `jar-with-dependencies` artifact and no `maven-jar-plugin`
`<mainClass>` entry; the PR test plan instructs `java -jar
pinot-plugin-verifier/target/pinot-plugin-verifier-*-jar-with-dependencies.jar`;
`pinot-distribution/pinot-assembly.xml` includes the plain `.jar`. Neither the
test-plan artifact nor a runnable jar is produced by the pom as written.
### Concerns
**CONCERN 1 — `pinot-kafka-4.0` is silently not converted to the realm
layout.**
Severity: concern. Confidence: medium. The plugin has no
`pinot-plugin.properties` and is absent from `pinot-distribution/pom.xml`. If
Kafka 4.x is in active use, deployments that configure it will silently fall
back to legacy `PluginClassLoader` behavior — which may or may not work
depending on whether isolation matters for Kafka 4.x. If it is intentionally
excluded, this must be documented. Evidence:
`pinot-plugins/pinot-stream-ingestion/pinot-kafka-4.0/src/main/` has only
`java/`; no entry in `pinot-distribution/pom.xml`.
**CONCERN 2 — `@MetricsFactory` discovery removed without deprecation grace
period.**
Severity: concern. Confidence: high. Third-party plugins that rely on
`@MetricsFactory` annotation for discovery (without `@AutoService` or a
hand-written `META-INF/services` file) will silently stop being discovered
after this change. `PinotMetricUtils.getPinotMetricsFactoryClasses()` now
returns only plugins discoverable via `ServiceLoader`, which requires the
`META-INF/services` file. Evidence: `PinotMetricUtils.java` diff removes
`PinotReflectionUtils.getClassesThroughReflection(METRICS_PACKAGE_REGEX_PATTERN,
MetricsFactory.class)`.
**CONCERN 3 — Plugin-zip layout correctness not verifiable from diff alone.**
Severity: concern. Confidence: medium. The `pinot-plugin.xml` descriptor
produces a zip with `<artifactId>/classes/` and `<artifactId>/*.jar` layout.
This is unpacked into `plugins/` with
`<outputDirectory>plugins</outputDirectory>`. The
`PluginManager.load(pluginName, directory)` is called with `directory` being
one subdirectory under `plugins/`. Whether the resulting layout matches what
`PluginManager.readPluginConfiguration` expects (specifically the
`pluginPropertiesPath =
directory.toPath().resolve(PINOUT_PLUGIN_PROPERTIES_FILE_NAME)` path) requires
a build to verify. Evidence: `pinot-plugin.xml` puts `pinot-plugin.properties`
at `${project.artifactId}/` inside the zip, which after unpack becomes
`plugins/<artifactId>/pinot-plugin.properties` — this should match
`directory.toPath().resolve("pinot-plugin.properties")` if `directory =
plugins/<artifactId>/`. Appears correct but unverified.
**CONCERN 4 — Startup I/O cost from recursive JAR scanning for
`pinot-plugin.properties`.**
Severity: concern. Confidence: high. On each startup, for every plugin
directory that does NOT have a top-level `pinot-plugin.properties` file,
`readPluginConfiguration` opens every JAR file recursively until it finds the
properties. The optimization (primary JAR checked first by name prefix) helps
in the new zip layout but is insufficient if the primary JAR name doesn't match
the directory name (e.g., versioned or classifier suffixes). For 30+ plugins
each with 100+ deps, this can add seconds to startup. Evidence:
`PluginManager.readPluginConfiguration` — `FileUtils.listFiles(directory, new
String[]{"jar"}, true)` with `new JarFile(jarFile)` per file.
**CONCERN 5 — 16 files missing from GitHub PR diff (metadata gap).**
Severity: concern. Confidence: high. The GitHub PR files API returns 100
files; the actual branch contains 116 changed files. Reviewers using the GitHub
web UI will miss `PluginManager.java`, `PinotMetricUtils.java`,
`IndexService.java`, `ExecutorServiceUtils.java`,
`OpChainConverterDispatcher.java`, `RecordEnricherRegistry.java`,
`ResponseStoreService.java`, root `pom.xml`, and test fixtures — the most
important changes in the PR. This is likely caused by exceeding the GitHub API
files limit (100 files). The PR should be reviewed only against the full branch
diff, not the GitHub UI. Evidence: `gh pr view 18459 --json files | .files |
length` returns 100; `git diff origin/master...personal/pinot-no-shading | grep
"^diff --git" | wc -l` returns 116.
### Observations
1. The `PinotMetricUtils.initializePinotMetricsFactory` now instantiates the
factory via `loadServices` (which calls the no-arg constructor via
`ServiceLoader`) and then calls `instance.init(...)`. Previously, the factory
was discovered as a `Class<?>` and instantiated via
`getDeclaredConstructor().newInstance()`. The new path is cleaner (avoids
double construction) and correct. The `annotation == null` guard added here was
missing in the original.
2. The `AccessControlFactory` migration preserves the historical
`getDeclaredConstructor()` contract (non-public constructors work), with a
detailed comment explaining why. This is correct and the test covers it.
3. `SegmentConversionUtils` package move from `pinot-minion-builtin-tasks`
to `pinot-minion.utils` is logically correct (the class has no plugin-specific
content and belongs in the core minion module). All callers are updated.
4. The `assembly-descriptor` module has an integration test
(`maven-invoker-plugin` ITs) which is a good validation of the descriptor
itself.
5. Dropping the `plugin.type` property from parent POMs and the
`intermediate directory level` from the distribution is a net simplification.
The old layout (`plugins/pinot-stream-ingestion/pinot-kafka-3.0/`) is replaced
by flat `plugins/pinot-kafka-3.0/`. Existing deployments with custom scripts
that relied on the two-level layout will break, but this is a documented
trade-off.
6. `CompoundPinotMetricsFactory.Algorithm.SERVICE_LOADER` now uses
`PluginManager.get().loadServices()` which internally calls `ServiceLoader` on
each classloader. This means `CompoundPinotMetricsFactory` will discover Yammer
and Dropwizard factories if they are in plugin realms — which is the intended
behavior.
7. The PR removes the `pinot-batch-ingestion-spark-3` and
`pinot-batch-ingestion-hadoop` `pinot-plugin.properties` as described, but
`pinot-batch-ingestion-spark-3/pom.xml` adds an explicit shade execution
`<phase>package</phase>` to re-enable shading for that module. This is correct
per the PR description.
### Areas recommended for human focus
1. **Verify the startup script** (`appAssemblerScriptTemplate` or
`pinot-admin.sh`) to confirm it no longer adds `plugins/**/*.jar` to the JVM
classpath. If it still does, realm isolation is a no-op at runtime.
2. **`pinot-kafka-4.0` omission** — confirm whether this is intentional or
an oversight; if intentional, document it prominently.
3. **Third-party plugin compatibility** — review the `@MetricsFactory`-only
plugins in the StarTree distribution (if any) before merging; this change
requires them to add `@AutoService` or a service file.
4. **Plugin-zip layout end-to-end** — do a manual `./mvnw install
-DskipTests -Pbin-dist` build after fixing the blockers and verify
`build/plugins/<name>/pinot-plugin.properties` exists for every converted
plugin.
5. **Startup time regression** — time `PluginManager.init()` before and
after with `pinot-fastdev` disabled and a realistic set of plugins.
6. **The 16 missing files in the GitHub diff** — this PR must be reviewed
against `git diff origin/master...personal/pinot-no-shading`, not the GitHub
web UI. Committers should fetch the branch before approving.
### Status
- [x] All applicable Stage 1 boxes checked
- [x] All applicable Stage 2 boxes checked (high risk tier)
- [x] Verdict committed
- [x] Verdict confidence assigned with reasoning
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]