diff --git a/test/tickets/LDEV6129.cfc b/test/tickets/LDEV6129.cfc
new file mode 100644
index 00000000000..2872b8201b6
--- /dev/null
+++ b/test/tickets/LDEV6129.cfc
@@ -0,0 +1,160 @@
+component extends="org.lucee.cfml.test.LuceeTestCase" labels="orm" {
+
+ function run( testResults, testBox ) {
+ runSuite( testResults, testBox, basicConfig() );
+ runSuite( testResults, testBox, customConfig() );
+ runCustomSuite( testResults, testBox, customConfig() );
+ }
+
+ private void function runSuite( testResults, testBox, required struct cfg ) {
+
+ describe( "LDEV-6129 [#cfg.label#] - ORM connection not released when flushAll() throws at request end", function() {
+
+ /*
+ * Bug: PageContextImpl.releaseORM():
+ *
+ * try {
+ * ormSession.flushAll(pc); // throws constraint violation
+ * ormSession.closeAll(pc); // skipped — same try block
+ * manager.releaseORM();
+ * } finally {
+ * ormSession = null; // ref dropped, dc never returned to pool
+ * }
+ *
+ * Pool is maxTotal=1. If the connection leaks after the flush error,
+ * the subsequent simple.cfm requests will fail to get a connection.
+ */
+ it( title="connection returned to pool even when auto-flush throws a constraint violation", body=function( currentSpec ) {
+
+ _InternalRequest( template: "#cfg.uri#/setup.cfm", url: cfg.params );
+
+ // Trigger the potential leak: unique constraint violation at request end
+ try {
+ _InternalRequest( template: "#cfg.uri#/flush_leak.cfm", url: cfg.params );
+ } catch ( any e ) {
+ // _InternalRequest may propagate template exceptions — that's fine,
+ // the important thing is what happens to the connection afterwards
+ systemOutput( "flush_leak threw: #e.stacktrace#", true );
+ }
+
+ var metrics = getSystemMetrics();
+ var active = getPoolActive( metrics, "LDEV6129h2" );
+ var idle = getPoolIdle( metrics, "LDEV6129h2" );
+ systemOutput( "[#cfg.label#] after flush error: active=#active#, idle=#idle#", true );
+
+ expect( active ).toBe( 0,
+ "Connection leaked after flush error — active=#active# (pool maxTotal=1)"
+ );
+
+ // Now verify the connection is actually usable: make N simple requests.
+ // If the connection was leaked (active but not in pool), these will fail.
+ var N = 5;
+ for ( var i = 1; i <= N; i++ ) {
+ var result = _InternalRequest( template: "#cfg.uri#/simple.cfm", url: cfg.params );
+ systemOutput( "[#cfg.label#] simple request #i#: status=#result.status#, content=#trim( result.filecontent )#", true );
+ expect( result.status ).toBe( 200,
+ "simple request #i# failed — connection not available (pool exhausted?)"
+ );
+ expect( left( trim( result.filecontent ), 2 ) ).toBe( "ok",
+ "simple request #i# returned unexpected content: #trim( result.filecontent )#"
+ );
+ }
+
+ } );
+
+ } );
+
+ describe( "LDEV-6129 [#cfg.label#] - dead reconnect code throws when session.isConnected() returns false", function() {
+
+ /*
+ * Bug: HibernateORMSession.getSessionAndConn() has a dead reconnect block:
+ *
+ * if ( !s.isOpen() || !s.isConnected() || isClosed( s ) ) {
+ * sac.connect( pc ); // acquires dc from pool
+ * s.reconnect( sac.getConnection( pc ) ); // ALWAYS throws IllegalStateException
+ * } // dc is leaked
+ *
+ * Session.reconnect(Connection) is not supported for factory-opened sessions
+ * in Hibernate 5.6 — it unconditionally throws IllegalStateException.
+ *
+ * Fix: remove the reconnect block. ConnectionProvider handles the lifecycle.
+ */
+ it( title="entityLoad succeeds when session isConnected() is forced false via reflection", body=function( currentSpec ) {
+
+ _InternalRequest( template: "#cfg.uri#/setup.cfm", url: cfg.params );
+
+ var result = _InternalRequest( template: "#cfg.uri#/reconnect_leak.cfm", url: cfg.params );
+ systemOutput( "[#cfg.label#] reconnect_leak result: status=#result.status#, content=#trim( result.filecontent )#", true );
+
+ expect( result.status ).toBe( 200 );
+ expect( left( trim( result.filecontent ), 2 ) ).toBe( "ok",
+ "entityLoad failed after isConnected()=false — reconnect dead code is broken: #trim( result.filecontent )#"
+ );
+
+ } );
+
+ } );
+
+ }
+
+ private void function runCustomSuite( testResults, testBox, required struct cfg ) {
+
+ describe( "LDEV-6129 [#cfg.label#] - dead reconnect code triggered naturally by after_transaction release mode", function() {
+
+ /*
+ * With connection.release_mode=after_transaction, Hibernate calls afterTransaction()
+ * after every ormFlush(), setting physicalConnection=null → isConnected()=false.
+ * The next ORM call then enters the dead reconnect block in getSessionAndConn(),
+ * which calls s.reconnect() — always throws ResourceClosedException in Hibernate 5.6.
+ */
+ it( title="entityLoad succeeds after ormFlush() with after_transaction release mode", body=function( currentSpec ) {
+
+ _InternalRequest( template: "#cfg.uri#/setup.cfm", url: { flushAtRequestEnd: false } );
+
+ var result = _InternalRequest( template: "#cfg.uri#/multi_transaction.cfm", url: { flushAtRequestEnd: false } );
+ systemOutput( "[#cfg.label#] multi_transaction result: status=#result.status#, content=#trim( result.filecontent )#", true );
+
+ expect( result.status ).toBe( 200 );
+ expect( left( trim( result.filecontent ), 2 ) ).toBe( "ok",
+ "entityLoad failed after ormFlush() with after_transaction — dead reconnect code triggered: #trim( result.filecontent )#"
+ );
+
+ } );
+
+ } );
+
+ }
+
+ private struct function basicConfig() {
+ return { label: "basic", uri: createURI( "LDEV6129/basic" ), params: {} };
+ }
+
+ private struct function customConfig() {
+ return { label: "custom (after_transaction)", uri: createURI( "LDEV6129/custom" ), params: { flushAtRequestEnd: true } };
+ }
+
+ private numeric function getPoolActive( required struct metrics, required string dsName ) {
+ return getPoolStat( arguments.metrics, arguments.dsName, "activeDatasourceConnections" );
+ }
+
+ private numeric function getPoolIdle( required struct metrics, required string dsName ) {
+ return getPoolStat( arguments.metrics, arguments.dsName, "idleDatasourceConnections" );
+ }
+
+ private numeric function getPoolStat( required struct metrics, required string dsName, required string stat ) {
+ if ( !structKeyExists( arguments.metrics, "datasourceConnections" ) ) return 0;
+ for ( var key in arguments.metrics.datasourceConnections ) {
+ var pool = arguments.metrics.datasourceConnections[ key ];
+ if ( structKeyExists( pool, "name" ) && pool.name == arguments.dsName ) {
+ return val( pool[ arguments.stat ] ?: 0 );
+ }
+ }
+ return 0;
+ }
+
+ private string function createURI( string calledName ) {
+ var baseURI = "/test/#listLast( getDirectoryFromPath( getCurrenttemplatepath() ), "\/" )#/";
+ return baseURI & calledName;
+ }
+
+}
diff --git a/test/tickets/LDEV6129/basic/Application.cfc b/test/tickets/LDEV6129/basic/Application.cfc
new file mode 100644
index 00000000000..387ae455efb
--- /dev/null
+++ b/test/tickets/LDEV6129/basic/Application.cfc
@@ -0,0 +1,20 @@
+component {
+
+ this.name = "LDEV-6129";
+ this.datasources["LDEV6129h2"] = server.getDatasource( "h2", server._getTempDir( "LDEV6129basic" ) );
+ this.datasources["LDEV6129h2"]["connectionLimit"] = 1;
+ this.datasources["LDEV6129h2"]["maxTotal"] = 1;
+ this.ormEnabled = true;
+ this.datasource = "LDEV6129h2";
+ this.ormSettings = {
+ dbcreate: "dropcreate",
+ dialect: "h2",
+ flushAtRequestEnd: true,
+ autoManageSession: true
+ };
+
+ public function onRequestStart() {
+ setting requesttimeout = 10;
+ }
+
+}
diff --git a/test/tickets/LDEV6129/basic/LDEV6129Person.cfc b/test/tickets/LDEV6129/basic/LDEV6129Person.cfc
new file mode 100644
index 00000000000..40f0e5b5d81
--- /dev/null
+++ b/test/tickets/LDEV6129/basic/LDEV6129Person.cfc
@@ -0,0 +1,6 @@
+component persistent="true" entityname="LDEV6129Person" {
+
+ property name="id" fieldtype="id" type="numeric" ormtype="long" generator="increment";
+ property name="name" type="string" unique="true";
+
+}
diff --git a/test/tickets/LDEV6129/basic/flush_leak.cfm b/test/tickets/LDEV6129/basic/flush_leak.cfm
new file mode 100644
index 00000000000..d0a73592042
--- /dev/null
+++ b/test/tickets/LDEV6129/basic/flush_leak.cfm
@@ -0,0 +1,25 @@
+
+ /*
+ * LDEV-6129: trigger a connection leak by causing flushAll() to throw at request end.
+ *
+ * 1. entitySave(p1) + ormFlush() — commits "test" to DB
+ * 2. entitySave(p2) with same unique name — no error yet (Hibernate doesn't query DB)
+ * 3. Request ends: flushAtRequestEnd=true → releaseORM() → flushAll() throws unique violation
+ *
+ * BUG: flushAll() and closeAll() are in the same try block in PageContextImpl.releaseORM().
+ * When flushAll() throws, closeAll() is skipped → DatasourceConnection dc is never returned.
+ */
+ uniqueName = createUUID();
+
+ p1 = entityNew( "LDEV6129Person" );
+ p1.setName( uniqueName );
+ entitySave( p1 );
+ ormFlush(); // commit p1 to DB — now uniqueName exists with a unique constraint
+
+ p2 = entityNew( "LDEV6129Person" );
+ p2.setName( uniqueName ); // same name — will collide at flush time
+ entitySave( p2 );
+ systemOutput( "flush_leak.cfm: entitySave(p2) done, request ending now — expect flush error", true );
+ // request ends here: auto-flush tries to INSERT p2, throws unique constraint violation
+ // closeAll() is skipped → dc leaked
+
diff --git a/test/tickets/LDEV6129/basic/reconnect_leak.cfm b/test/tickets/LDEV6129/basic/reconnect_leak.cfm
new file mode 100644
index 00000000000..1b26ca6e36b
--- /dev/null
+++ b/test/tickets/LDEV6129/basic/reconnect_leak.cfm
@@ -0,0 +1,69 @@
+
+ /*
+ * LDEV-6129: reproduce the dead reconnect code bug in HibernateORMSession.getSessionAndConn().
+ *
+ * The condition `!s.isConnected()` can fire under load (e.g. MySQL 9.5, pool pressure).
+ * When it does, the current code calls:
+ * sac.connect(pc) -- acquires DatasourceConnection dc from pool
+ * s.reconnect(conn) -- ALWAYS throws IllegalStateException on Hibernate 5.6
+ * factory-opened sessions; dc is leaked
+ *
+ * We force isConnected() = false via reflection on the Hibernate internals, then
+ * call entityLoad() to trigger the path.
+ *
+ * Expected output before fix: ERROR: ... Cannot manually reconnect ...
+ * Expected output after fix: ok
+ */
+
+ // 1. Load any entity to ensure the session + connection are open and dc is in place
+ entityLoad( "LDEV6129Person" );
+
+ // 2. Get the raw Hibernate SessionImpl
+ hibSession = ormGetSession();
+
+ // 3. Walk the class hierarchy to find the private jdbcCoordinator field
+ // (declared on AbstractSharedSessionContract, not SessionImpl itself)
+ coordField = javaCast( "null", "" );
+ klass = hibSession.getClass();
+ while ( !isNull( klass ) ) {
+ try {
+ coordField = klass.getDeclaredField( "jdbcCoordinator" );
+ break;
+ }
+ catch ( any e ) {
+ klass = klass.getSuperclass();
+ }
+ }
+
+ if ( isNull( coordField ) ) {
+ writeOutput( "SKIP: jdbcCoordinator field not found — Hibernate internals changed" );
+ return;
+ }
+
+ coordField.setAccessible( true );
+ jdbcCoord = coordField.get( hibSession );
+
+ // 4. Get logicalConnection from JdbcCoordinatorImpl
+ logConnField = jdbcCoord.getClass().getDeclaredField( "logicalConnection" );
+ logConnField.setAccessible( true );
+ logConn = logConnField.get( jdbcCoord );
+
+ // 5. Force closed = true → isConnected() now returns false
+ closedField = logConn.getClass().getDeclaredField( "closed" );
+ closedField.setAccessible( true );
+ closedField.set( logConn, javaCast( "boolean", true ) );
+
+ systemOutput( "isConnected() after force-close: #hibSession.isConnected()#", true );
+
+ // 6. Trigger getSessionAndConn() — enters reconnect path because isConnected() == false
+ // Before fix: throws IllegalStateException (s.reconnect() always throws on managed sessions)
+ // After fix: reconnect block removed, entityLoad succeeds normally
+ try {
+ entityLoad( "LDEV6129Person" );
+ writeOutput( "ok" );
+ }
+ catch ( any e ) {
+ writeOutput( "ERROR: #e.type# - #e.message#" );
+ systemOutput( "reconnect_leak: caught exception: #e.type# - #e.message#", true );
+ }
+
diff --git a/test/tickets/LDEV6129/basic/setup.cfm b/test/tickets/LDEV6129/basic/setup.cfm
new file mode 100644
index 00000000000..49af802e8a7
--- /dev/null
+++ b/test/tickets/LDEV6129/basic/setup.cfm
@@ -0,0 +1,4 @@
+
+ // Drop and recreate all ORM tables — ensures no stale data from previous runs
+ ormReload();
+
diff --git a/test/tickets/LDEV6129/basic/simple.cfm b/test/tickets/LDEV6129/basic/simple.cfm
new file mode 100644
index 00000000000..67dde45379b
--- /dev/null
+++ b/test/tickets/LDEV6129/basic/simple.cfm
@@ -0,0 +1,6 @@
+
+ // Simple ORM load — just proves we can get a connection from the pool.
+ // If a previous request leaked the only connection (maxTotal=1), this will throw.
+ result = entityLoad( "LDEV6129Person" );
+ writeOutput( "ok:#arrayLen( result )#" );
+
diff --git a/test/tickets/LDEV6129/custom/Application.cfc b/test/tickets/LDEV6129/custom/Application.cfc
new file mode 100644
index 00000000000..6555a96635b
--- /dev/null
+++ b/test/tickets/LDEV6129/custom/Application.cfc
@@ -0,0 +1,26 @@
+component {
+
+ this.name = "LDEV-6129-custom";
+ this.datasources["LDEV6129h2"] = server.getDatasource( "h2", server._getTempDir( "LDEV6129custom" ) );
+ this.datasources["LDEV6129h2"]["connectionLimit"] = 1;
+ this.datasources["LDEV6129h2"]["maxTotal"] = 1;
+ this.ormEnabled = true;
+ this.datasource = "LDEV6129h2";
+ this.ormSettings = {
+ dbcreate: "dropcreate",
+ dialect: "h2",
+ flushAtRequestEnd: url.flushAtRequestEnd,
+ autoManageSession: true,
+ hibernateConfig: {
+ "connection.release_mode": "after_transaction",
+ "hibernate.connection.provider_class": extensionExists( "D062D72F-F8A2-46F0-8CBC91325B2F067B" )
+ ? "ortus.extension.orm.jdbc.ConnectionProviderImpl"
+ : "org.lucee.extension.orm.hibernate.jdbc.ConnectionProviderImpl"
+ }
+ };
+
+ public function onRequestStart() {
+ setting requesttimeout = 10;
+ }
+
+}
diff --git a/test/tickets/LDEV6129/custom/LDEV6129Person.cfc b/test/tickets/LDEV6129/custom/LDEV6129Person.cfc
new file mode 100644
index 00000000000..40f0e5b5d81
--- /dev/null
+++ b/test/tickets/LDEV6129/custom/LDEV6129Person.cfc
@@ -0,0 +1,6 @@
+component persistent="true" entityname="LDEV6129Person" {
+
+ property name="id" fieldtype="id" type="numeric" ormtype="long" generator="increment";
+ property name="name" type="string" unique="true";
+
+}
diff --git a/test/tickets/LDEV6129/custom/flush_leak.cfm b/test/tickets/LDEV6129/custom/flush_leak.cfm
new file mode 100644
index 00000000000..d0a73592042
--- /dev/null
+++ b/test/tickets/LDEV6129/custom/flush_leak.cfm
@@ -0,0 +1,25 @@
+
+ /*
+ * LDEV-6129: trigger a connection leak by causing flushAll() to throw at request end.
+ *
+ * 1. entitySave(p1) + ormFlush() — commits "test" to DB
+ * 2. entitySave(p2) with same unique name — no error yet (Hibernate doesn't query DB)
+ * 3. Request ends: flushAtRequestEnd=true → releaseORM() → flushAll() throws unique violation
+ *
+ * BUG: flushAll() and closeAll() are in the same try block in PageContextImpl.releaseORM().
+ * When flushAll() throws, closeAll() is skipped → DatasourceConnection dc is never returned.
+ */
+ uniqueName = createUUID();
+
+ p1 = entityNew( "LDEV6129Person" );
+ p1.setName( uniqueName );
+ entitySave( p1 );
+ ormFlush(); // commit p1 to DB — now uniqueName exists with a unique constraint
+
+ p2 = entityNew( "LDEV6129Person" );
+ p2.setName( uniqueName ); // same name — will collide at flush time
+ entitySave( p2 );
+ systemOutput( "flush_leak.cfm: entitySave(p2) done, request ending now — expect flush error", true );
+ // request ends here: auto-flush tries to INSERT p2, throws unique constraint violation
+ // closeAll() is skipped → dc leaked
+
diff --git a/test/tickets/LDEV6129/custom/multi_transaction.cfm b/test/tickets/LDEV6129/custom/multi_transaction.cfm
new file mode 100644
index 00000000000..1561c88ee8f
--- /dev/null
+++ b/test/tickets/LDEV6129/custom/multi_transaction.cfm
@@ -0,0 +1,35 @@
+
+ /*
+ * LDEV-6129: reproduce the dead reconnect code bug naturally via after_transaction release mode.
+ *
+ * With connection.release_mode=after_transaction, Hibernate releases the physical connection
+ * after every ormFlush() (afterTransaction callback sets physicalConnection=null).
+ *
+ * On the next ORM call, isConnected()=false triggers the dead reconnect block in
+ * HibernateORMSession.getSessionAndConn() which calls s.reconnect() — always throws
+ * ResourceClosedException in Hibernate 5.6 — and leaks the dc acquired by sac.connect().
+ *
+ * No reflection required: ormFlush() commits the transaction → after_transaction fires.
+ *
+ * Expected before fix: ERROR: org.hibernate.ResourceClosedException
+ * Expected after fix: ok
+ */
+
+ // transaction 1: save and flush
+ p1 = entityNew( "LDEV6129Person" );
+ p1.setName( createUUID() );
+ entitySave( p1 );
+ ormFlush(); // commits → afterTransaction() → physicalConnection=null → isConnected()=false
+
+ systemOutput( "multi_transaction: after first ormFlush, isConnected=#ormGetSession().isConnected()#", true );
+
+ // transaction 2: next ORM call should trigger getSessionAndConn() with isConnected()=false
+ try {
+ people = entityLoad( "LDEV6129Person" );
+ systemOutput( "multi_transaction: entityLoad after ormFlush returned #arrayLen( people )# rows", true );
+ writeOutput( "ok" );
+ } catch ( any e ) {
+ writeOutput( "ERROR: #e.type# - #e.message#" );
+ systemOutput( "multi_transaction: caught exception: #e.stacktrace#", true );
+ }
+
diff --git a/test/tickets/LDEV6129/custom/reconnect_leak.cfm b/test/tickets/LDEV6129/custom/reconnect_leak.cfm
new file mode 100644
index 00000000000..1b26ca6e36b
--- /dev/null
+++ b/test/tickets/LDEV6129/custom/reconnect_leak.cfm
@@ -0,0 +1,69 @@
+
+ /*
+ * LDEV-6129: reproduce the dead reconnect code bug in HibernateORMSession.getSessionAndConn().
+ *
+ * The condition `!s.isConnected()` can fire under load (e.g. MySQL 9.5, pool pressure).
+ * When it does, the current code calls:
+ * sac.connect(pc) -- acquires DatasourceConnection dc from pool
+ * s.reconnect(conn) -- ALWAYS throws IllegalStateException on Hibernate 5.6
+ * factory-opened sessions; dc is leaked
+ *
+ * We force isConnected() = false via reflection on the Hibernate internals, then
+ * call entityLoad() to trigger the path.
+ *
+ * Expected output before fix: ERROR: ... Cannot manually reconnect ...
+ * Expected output after fix: ok
+ */
+
+ // 1. Load any entity to ensure the session + connection are open and dc is in place
+ entityLoad( "LDEV6129Person" );
+
+ // 2. Get the raw Hibernate SessionImpl
+ hibSession = ormGetSession();
+
+ // 3. Walk the class hierarchy to find the private jdbcCoordinator field
+ // (declared on AbstractSharedSessionContract, not SessionImpl itself)
+ coordField = javaCast( "null", "" );
+ klass = hibSession.getClass();
+ while ( !isNull( klass ) ) {
+ try {
+ coordField = klass.getDeclaredField( "jdbcCoordinator" );
+ break;
+ }
+ catch ( any e ) {
+ klass = klass.getSuperclass();
+ }
+ }
+
+ if ( isNull( coordField ) ) {
+ writeOutput( "SKIP: jdbcCoordinator field not found — Hibernate internals changed" );
+ return;
+ }
+
+ coordField.setAccessible( true );
+ jdbcCoord = coordField.get( hibSession );
+
+ // 4. Get logicalConnection from JdbcCoordinatorImpl
+ logConnField = jdbcCoord.getClass().getDeclaredField( "logicalConnection" );
+ logConnField.setAccessible( true );
+ logConn = logConnField.get( jdbcCoord );
+
+ // 5. Force closed = true → isConnected() now returns false
+ closedField = logConn.getClass().getDeclaredField( "closed" );
+ closedField.setAccessible( true );
+ closedField.set( logConn, javaCast( "boolean", true ) );
+
+ systemOutput( "isConnected() after force-close: #hibSession.isConnected()#", true );
+
+ // 6. Trigger getSessionAndConn() — enters reconnect path because isConnected() == false
+ // Before fix: throws IllegalStateException (s.reconnect() always throws on managed sessions)
+ // After fix: reconnect block removed, entityLoad succeeds normally
+ try {
+ entityLoad( "LDEV6129Person" );
+ writeOutput( "ok" );
+ }
+ catch ( any e ) {
+ writeOutput( "ERROR: #e.type# - #e.message#" );
+ systemOutput( "reconnect_leak: caught exception: #e.type# - #e.message#", true );
+ }
+
diff --git a/test/tickets/LDEV6129/custom/setup.cfm b/test/tickets/LDEV6129/custom/setup.cfm
new file mode 100644
index 00000000000..49af802e8a7
--- /dev/null
+++ b/test/tickets/LDEV6129/custom/setup.cfm
@@ -0,0 +1,4 @@
+
+ // Drop and recreate all ORM tables — ensures no stale data from previous runs
+ ormReload();
+
diff --git a/test/tickets/LDEV6129/custom/simple.cfm b/test/tickets/LDEV6129/custom/simple.cfm
new file mode 100644
index 00000000000..67dde45379b
--- /dev/null
+++ b/test/tickets/LDEV6129/custom/simple.cfm
@@ -0,0 +1,6 @@
+
+ // Simple ORM load — just proves we can get a connection from the pool.
+ // If a previous request leaked the only connection (maxTotal=1), this will throw.
+ result = entityLoad( "LDEV6129Person" );
+ writeOutput( "ok:#arrayLen( result )#" );
+