[Bug report] bug(catalog-hive): HiveCatalogCapability.caseSensitiveOnName() uses switch-on-enum which generates a synthetic $1 class, causing NoClassDefFoundError when called outside IsolatedClassLoader context
- Dominant language
- Java
- Stars
- 3.2k
- Forks
- 935
- Avg merge
- 1d 17h
- Merged PRs (30d)
- 286
Description
### Version
main branch
### Describe what's wrong
### Bug Description
`HiveCatalogCapability.caseSensitiveOnName()` uses a `switch` statement on
`Capability.Scope` (an enum). The Java compiler generates a synthetic helper
class `HiveCatalogCapability$1` for every `switch`-on-enum to maintain a
stable ordinal mapping table.
`caseSensitiveOnName()` is called by `SchemaNormalizeDispatcher` **outside**
the `IsolatedClassLoader.withClassLoader()` boundary:
```
SchemaNormalizeDispatcher.normalizeCaseSensitive()
→ getCapability() → c.capabilities()
→ withClassLoader { catalog.capability() } ← IsolatedClassLoader context; creates HiveCatalogCapability instance
← withClassLoader exits; thread context classloader restored to server classloader
→ applyCaseSensitive() → capabilities.caseSensitiveOnName(Scope.SCHEMA)
→ JVM attempts to load HiveCatalogCapability$1
```
When `HiveCatalogCapability` is correctly loaded by `IsolatedClassLoader`,
its defining classloader is `IsolatedClassLoader`, and `$1` is loaded from
`execJars` successfully. However, in certain classloader initialization timing
windows (e.g. concurrent requests during catalog cache rebuild), `$1` is
requested while the server classloader is involved in the loading chain.
**The JVM permanently caches this load failure**, and all subsequent calls
throw `NoClassDefFoundError` until the process is restarted.
### Root Cause
`SchemaNormalizeDispatcher` calls `Capability.caseSensitiveOnName()` — a
method on a catalog-side implementation — **after** returning from
`withClassLoader`. This is an architectural inconsistency: catalog
implementation code is invoked outside the catalog's isolated classloader
context.
The immediate trigger is the compiler-generated `$1` synthetic class, which
only exists because `switch`-on-enum is used. Replacing `switch` with
`if`-`else` (using `==` on enum constants) eliminates `$1` entirely and makes
`caseSensitiveOnName()` safe to call from any classloader context.
### Proposed Fix
Replace `switch` with `if`-`else` in `HiveCatalogCapability.caseSensitiveOnName()`:
```java
// Before
switch (scope) {
case SCHEMA: case TABLE: case COLUMN:
return CapabilityResult.unsupported("Hive is case insensitive.");
default:
return CapabilityResult.SUPPORTED;
}
// After
if (scope == Scope.SCHEMA || scope == Scope.TABLE || scope == Scope.COLUMN) {
return CapabilityResult.unsupported("Hive is case insensitive.");
}
return CapabilityResult.SUPPORTED;
```
This removes the `$1` synthetic class entirely. `==` on enum constants
compiles to `if_acmpeq` (reference comparison), which has no synthetic class
dependency and is safe across classloader boundaries.
### Additional Context
The same pattern should be reviewed in other `Capability` implementations
across catalogs, as any `switch`-on-enum in a catalog-side class called from
`SchemaNormalizeDispatcher` / `TableNormalizeDispatcher` (outside
`withClassLoader`) would be susceptible to the same issue.
### Error message and/or stacktrace
java.lang.NoClassDefFoundError: org/apache/gravitino/catalog/hive/HiveCatalogCapability$1
at org.apache.gravitino.catalog.hive.HiveCatalogCapability.caseSensitiveOnName(HiveCatalogCapability.java:45)
at org.apache.gravitino.catalog.CapabilityHelpers.applyCaseSensitiveOnName(CapabilityHelpers.java:476)
at org.apache.gravitino.catalog.CapabilityHelpers.applyCaseSensitive(CapabilityHelpers.java:123)
at org.apache.gravitino.catalog.SchemaNormalizeDispatcher.normalizeCaseSensitive(SchemaNormalizeDispatcher.java:94)
at org.apache.gravitino.catalog.SchemaNormalizeDispatcher.loadSchema(SchemaNormalizeDispatcher.java:71)
at org.apache.gravitino.listener.SchemaEventDispatcher.loadSchema(SchemaEventDispatcher.java:116)
...
at org.apache.gravitino.utils.PrincipalUtils.doAs(PrincipalUtils.java:44)
### How to reproduce
+ Which Gravitino version to use
main / any version with Hive catalog
+ Steps
1. Deploy Gravitino with a Hive catalog configured
2. Call any API that goes through SchemaNormalizeDispatcher
(e.g. loadSchema, schemaExists, loadTable on first access):
GET /api/metalakes/{metalake}/catalogs/{hive-catalog}/schemas/{schema}
3. Under specific JVM classloader initialization timing, observe:
NoClassDefFoundError: org/apache/gravitino/catalog/hive/HiveCatalogCapability$1
4. All subsequent requests to the same catalog on this process instance
fail with the same error until restart.
+ Root cause
SchemaNormalizeDispatcher calls capabilities.caseSensitiveOnName() AFTER
IsolatedClassLoader.withClassLoader() exits (i.e. outside the catalog's
isolated classloader context). The switch-on-enum in caseSensitiveOnName()
causes javac to generate HiveCatalogCapability$1. In certain classloader
initialization timing windows the server classloader is involved in the
loading chain and cannot find $1; the JVM permanently caches this failure.
+ Note
The bug is intermittent because it depends on classloader initialization
timing. Once triggered it is permanent within the process lifetime.
Restarting the process clears the cached failure.
### Additional context
_No response_
Contributor guide
Research direction
Start with HiveCatalogCapability.caseSensitiveOnName() and read the call chain through CapabilityHelpers.applyCaseSensitiveOnName(), SchemaNormalizeDispatcher.normalizeCaseSensitive(), and IsolatedClassLoader.withClassLoader(). Replace the enum switch as described, then exercise the Hive catalog API path for loadSchema or schemaExists and confirm the NoClassDefFoundError no longer occurs and the case-sensitivity result remains unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- databases
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 70/100