Skip to content

Commit fc3b87c

Browse files
authored
Merge pull request #55 from zspitzer/LDEV-6390-classloader-cache-regression
LDEV-6390 fix wiring-invalid regression from 5.6.15.18 CachingClassLoaderService
2 parents 9d8ee14 + ee58ea5 commit fc3b87c

11 files changed

Lines changed: 160 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 5.6.15.19
4+
5+
- [LDEV-6390](https://luceeserver.atlassian.net/browse/LDEV-6390) — Fix `bundle wiring for org.lucee.hibernate.extension is no longer valid` regression introduced by 5.6.15.18 when the extension is hot-deployed. `CachingClassLoaderService` no longer caches positive resolutions or pins the Hibernate-extension bundle classloader. Negative cache retained — full perf win preserved
6+
37
## 5.6.15.18
48

59
- [LDEV-6390](https://luceeserver.atlassian.net/browse/LDEV-6390) — Cache Hibernate `ClassLoaderService` to eliminate `ClassNotFoundException` storm: CFC entity names never resolve as Java classes, so `MetamodelImpl.getImplementors()` threw 3 CNFEs per Criteria/HQL load with no negative caching. Decorator caches positive + negative results, re-throwing the same exception instance. Criteria/HQL entity-load throughput +130-150%, eliminates `AggregatedClassLoader` lock contention

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ Install via Lucee Admin, or pin in your environment:
88

99
```bash
1010
# Lucee 7.0+ (Maven coordinates, auto-updates to latest snapshot)
11-
LUCEE_EXTENSIONS=org.lucee:hibernate-extension:5.6.15.18-SNAPSHOT
11+
LUCEE_EXTENSIONS=org.lucee:hibernate-extension:5.6.15.19-SNAPSHOT
1212

1313
# Lucee 6.2 (extension GUID, pinned version)
14-
LUCEE_EXTENSIONS=FAD1E8CB-4F45-4184-86359145767C29DE;version=5.6.15.18-SNAPSHOT
14+
LUCEE_EXTENSIONS=FAD1E8CB-4F45-4184-86359145767C29DE;version=5.6.15.19-SNAPSHOT
1515
```
1616

1717
## History

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<modelVersion>4.0.0</modelVersion>
55
<groupId>org.lucee</groupId>
66
<artifactId>hibernate-extension</artifactId>
7-
<version>5.6.15.18-SNAPSHOT</version>
7+
<version>5.6.15.19-SNAPSHOT</version>
88
<packaging>jar</packaging>
99
<name>Hibernate Extension</name>
1010

source/java/src/org/lucee/extension/orm/hibernate/util/CachingClassLoaderService.java

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import org.hibernate.boot.registry.classloading.spi.ClassLoadingException;
1313

1414
/**
15-
* Decorates a {@link ClassLoaderService} with a positive + negative cache around
15+
* Decorates a {@link ClassLoaderService} with a negative cache around
1616
* {@link #classForName(String)}.
1717
*
1818
* Hibernate's {@code MetamodelImpl.getImplementors(entityName)} calls
@@ -22,16 +22,24 @@
2222
* costing both the per-throw stacktrace construction and contention on the inner
2323
* {@code AggregatedClassLoader} monitor.
2424
*
25-
* The cache stores the resolved {@code Class} for hits and the original
26-
* {@code ClassLoadingException} for misses. On subsequent misses the cached exception
27-
* instance is re-thrown so the stacktrace is constructed once per name, not per call.
28-
* Hibernate's downstream behaviour is unchanged: same return value on hit, same
29-
* exception type on miss → same {@code return new String[]{ className }} fall-back.
25+
* The cache stores the original {@code ClassLoadingException} for misses, keyed by
26+
* class name. On subsequent calls for the same name the cached exception instance is
27+
* re-thrown so the stacktrace is constructed once per name, not per call.
28+
*
29+
* <p><b>Hits are deliberately NOT cached.</b> Hibernate already maintains positive
30+
* caches downstream ({@code MetamodelImpl.knownValidImports}, {@code implementorsCache});
31+
* caching resolved {@code Class<?>} objects here would pin them to whichever bundle
32+
* classloader resolved them. After a Felix bundle refresh (extension hot-swap), the
33+
* cached {@code Class} survives in memory but its defining loader is disposed,
34+
* causing downstream Hibernate operations that touch it to fail with
35+
* "bundle wiring is no longer valid".
36+
*
37+
* The cached exception is safe across bundle refresh — re-throwing a frozen exception
38+
* doesn't invoke methods on any class referenced by its stack trace.
3039
*/
3140
public class CachingClassLoaderService implements ClassLoaderService {
3241

3342
private final ClassLoaderService delegate;
34-
private final ConcurrentMap<String, Class<?>> hits = new ConcurrentHashMap<>();
3543
private final ConcurrentMap<String, ClassLoadingException> misses = new ConcurrentHashMap<>();
3644

3745
public CachingClassLoaderService(ClassLoaderService delegate) {
@@ -40,18 +48,12 @@ public CachingClassLoaderService(ClassLoaderService delegate) {
4048
}
4149

4250
@Override
43-
@SuppressWarnings("unchecked")
4451
public <T> Class<T> classForName(String className) {
45-
Class<?> hit = hits.get(className);
46-
if (hit != null) return (Class<T>) hit;
47-
4852
ClassLoadingException miss = misses.get(className);
4953
if (miss != null) throw miss;
5054

5155
try {
52-
Class<T> resolved = delegate.classForName(className);
53-
hits.put(className, resolved);
54-
return resolved;
56+
return delegate.classForName(className);
5557
}
5658
catch (ClassLoadingException e) {
5759
misses.putIfAbsent(className, e);
@@ -97,7 +99,6 @@ public <T> T workWithClassLoader(Work<T> work) {
9799

98100
@Override
99101
public void stop() {
100-
hits.clear();
101102
misses.clear();
102103
delegate.stop();
103104
}

source/java/src/org/lucee/extension/orm/hibernate/util/ConfigurationBuilder.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.hibernate.boot.registry.BootstrapServiceRegistry;
1313
import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder;
1414
import org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl;
15+
import org.hibernate.boot.registry.classloading.internal.TcclLookupPrecedence;
1516
import org.hibernate.cache.ehcache.internal.EhcacheRegionFactory;
1617
import org.hibernate.cfg.AvailableSettings;
1718
import org.hibernate.cfg.Configuration;
@@ -58,8 +59,15 @@ public class ConfigurationBuilder {
5859
* @throws PageException
5960
*/
6061
public Configuration build() throws SQLException, IOException, PageException {
62+
// LDEV-6390 follow-up: don't pin the Hibernate-extension bundle classloader.
63+
// new ClassLoaderServiceImpl() (no-args) calls ClassLoaderServiceImpl.class.getClassLoader()
64+
// which is the Felix BundleClassLoader. Pinning that loader means cached refs survive bundle
65+
// refresh but their wiring goes invalid → "bundle wiring no longer valid" on subsequent ops.
66+
// Match Hibernate's default instead (empty providedClassLoaders, TCCL fallback) so lookups
67+
// dynamically route through Lucee's EnvClassLoader → current OSGi wiring.
6168
BootstrapServiceRegistry bootstrapRegistry = new BootstrapServiceRegistryBuilder()
62-
.applyClassLoaderService(new CachingClassLoaderService(new ClassLoaderServiceImpl()))
69+
.applyClassLoaderService(new CachingClassLoaderService(
70+
new ClassLoaderServiceImpl(java.util.Collections.emptyList(), TcclLookupPrecedence.AFTER)))
6371
.applyIntegrator(this.eventListener).build();
6472
this.configuration = new Configuration(bootstrapRegistry);
6573

tests/tickets/LDEV6390.cfc

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
component extends="org.lucee.cfml.test.LuceeTestCase" labels="orm" {
2+
3+
function beforeAll() {
4+
variables.uri = createURI( "LDEV6390" );
5+
}
6+
7+
function run( testResults, testBox ) {
8+
describe( "LDEV-6390 ClassLoaderService cache must not break Criteria/Projection loads", function() {
9+
10+
it( "should load org.hibernate.criterion.* and SQL type descriptors via createObject", function() {
11+
local.result = _InternalRequest( template: "#uri#/loadCriterion.cfm" );
12+
expect( trim( result.filecontent ) ).toBe( "ok" );
13+
});
14+
15+
it( "probe: bundle CL vs TCCL identity and live ClassLoaderService instance", function() {
16+
local.result = _InternalRequest( template: "#uri#/classloaderProbe.cfm" );
17+
expect( trim( result.filecontent ) ).toBe( "ok" );
18+
});
19+
20+
it( "should run a Criteria with rowCount AggregateProjection", function() {
21+
local.result = _InternalRequest( template: "#uri#/rowCount.cfm" );
22+
expect( trim( result.filecontent ) ).toBe( "ok" );
23+
});
24+
25+
});
26+
}
27+
28+
private string function createURI( string calledName ) {
29+
var baseURI = getDirectoryFromPath( contractPath( getCurrentTemplatePath() ) );
30+
return baseURI & calledName;
31+
}
32+
33+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
component {
2+
this.name = "LDEV-6390";
3+
this.datasources[ "ldev6390" ] = server.getDatasource( "h2", "#getDirectoryFromPath( getCurrentTemplatePath() )#datasource/db" );
4+
this.ormEnabled = true;
5+
this.ormSettings = {
6+
dbcreate: "dropcreate",
7+
datasource: "ldev6390"
8+
};
9+
}

tests/tickets/LDEV6390/Entity.cfc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
component persistent="true" entityname="LDEV6390Entity" table="ldev6390_entity" {
2+
property name="id" fieldtype="id" generator="assigned";
3+
property name="name" type="string";
4+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<cfscript>
2+
// LDEV-6390 probe: what classloader does new ClassLoaderServiceImpl() pin,
3+
// and is it different from Lucee's TCCL?
4+
clsServiceImpl = createObject( "java", "org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl" );
5+
bundleCL = clsServiceImpl.getClass().getClassLoader();
6+
tccl = createObject( "java", "java.lang.Thread" ).currentThread().getContextClassLoader();
7+
8+
systemOutput( "", true );
9+
systemOutput( "[LDEV-6390 probe]", true );
10+
systemOutput( " bundle CL class : " & bundleCL.getClass().getName(), true );
11+
systemOutput( " bundle CL toStr : " & bundleCL.toString(), true );
12+
systemOutput( " TCCL class : " & tccl.getClass().getName(), true );
13+
systemOutput( " TCCL toStr : " & tccl.toString(), true );
14+
systemOutput( " same instance? : " & bundleCL.equals( tccl ), true );
15+
16+
// Bundle CL can see Hibernate's lazy inner classes; TCCL deliberately cannot
17+
// (EnvClassLoader only resolves exported packages, and Hibernate's internal
18+
// org.hibernate.type.descriptor.sql package is not exported). This asymmetry
19+
// is by design — pre-LDEV-6390 was TCCL-only and worked because such classes
20+
// are reached via JVM-direct lookup from the defining Hibernate class, never
21+
// via Hibernate's ClassLoaderService.
22+
loaded = bundleCL.loadClass( "org.hibernate.type.descriptor.sql.DoubleTypeDescriptor$2" );
23+
systemOutput( " inner via bundle: " & loaded.getName() & " defined by " & loaded.getClassLoader().toString(), true );
24+
25+
try {
26+
loadedTccl = tccl.loadClass( "org.hibernate.type.descriptor.sql.DoubleTypeDescriptor$2" );
27+
systemOutput( " inner via tccl : " & loadedTccl.getName() & " defined by " & loadedTccl.getClassLoader().toString(), true );
28+
} catch ( any e ) {
29+
systemOutput( " inner via tccl : NOT VISIBLE (expected) — " & left( e.message, 100 ), true );
30+
}
31+
32+
// Live SessionFactory's ClassLoaderService — what does IT delegate to?
33+
sf = ORMGetSessionFactory();
34+
serviceRegistry = sf.getServiceRegistry();
35+
cls = serviceRegistry.getService( bundleCL.loadClass( "org.hibernate.boot.registry.classloading.spi.ClassLoaderService" ) );
36+
systemOutput( " live svc class : " & cls.getClass().getName(), true );
37+
38+
// Trigger MetamodelImpl.getImplementors() with a real CFC entity name (miss path)
39+
// and with a real Hibernate class (hit path) — measure throws via diff timings.
40+
metamodel = sf.getMetamodel();
41+
startMiss = createObject( "java", "java.lang.System" ).nanoTime();
42+
for ( i = 1; i <= 1000; i++ ) {
43+
impls = metamodel.getImplementors( "LDEV6390Entity" );
44+
}
45+
missNs = createObject( "java", "java.lang.System" ).nanoTime() - startMiss;
46+
systemOutput( " 1000x CFC miss : " & ( missNs / 1000000 ) & " ms (impls=" & impls[ 1 ] & ")", true );
47+
48+
echo( "ok" );
49+
</cfscript>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<cfscript>
2+
// LDEV-6390 probe: load org.hibernate.criterion classes via createObject.
3+
// Reproduces myleslee's 'bundle wiring for org.lucee.hibernate.extension is no longer valid'
4+
// reported against 5.6.15.18-SNAPSHOT when CBORM constructs Criteria/Projections.
5+
aggregate = createObject( "java", "org.hibernate.criterion.AggregateProjection" );
6+
projections = createObject( "java", "org.hibernate.criterion.Projections" );
7+
descriptor = createObject( "java", "org.hibernate.type.descriptor.sql.DoubleTypeDescriptor" );
8+
echo( "ok" );
9+
</cfscript>

0 commit comments

Comments
 (0)