Hakon Grotte
05/15/2023, 8:33 AMHikariPool-1 - Start completed
HikariPool-2 - Start completed
...
HikariPool-1 - Shutdown completed
HikariPool-2 - Shutdown initiated
second test:
HikariPool-3 - Start completed
HikariPool-4 - Start completed
...
HikariPool-3 - Shutdown completed
HikariPool-4 - Shutdown initiated
etc, increasing linearly with the number of tests. I close the HikariCP during the ApplicationStopPreparing
event in ktor (more info in thread).
All the tests use the a custom testApplication
method for reusable setup with the same mssql testcontainer. Usually test 'X' fails because it starts using a connection from a HikariPool of one of the previous tests (which is then closed), causing java.sql.SQLException: HikariDataSource HikariDataSource (HikariPool-10) has been closed.
Is this expected behavior with the testApplication
?Database.connect(databaseFactory.hikariDataSource)
, and this approach for flyway migration:
val flyway = Flyway
.configure()
.dataSource(databaseFactory.flywayDataSource)
.load()
flyway.migrate()
Both DS are of type HikariDataSource
I close the connections with the following code:
environment.monitor.subscribe(ApplicationStopPreparing) {
databaseFactory.flywayDataSource.close()
databaseFactory.hikariDataSource.close()
}
spand
05/15/2023, 8:45 AMDatabase
object returned from Database.connect
:
fun Database.asCloseable() = Closeable {
TransactionManager.closeAndUnregister(this)
}
Hakon Grotte
05/15/2023, 9:48 AM@AfterEach
when investigating this error:
TransactionManager.defaultDatabase?.let { TransactionManager.closeAndUnregister(it) }
. I like the approach with the Closeable interface you suggest: I invoked the database.close() inside the ApplicationStopPreparing
lambda subscription.
Unfortunately, the problem still persists:
java.lang.RuntimeException: database org.jetbrains.exposed.sql.Database@5d7badc6 don't have any transaction manager
at org.jetbrains.exposed.sql.transactions.TransactionApiKt.getTransactionManager(TransactionApi.kt:157)
at org.jetbrains.exposed.sql.transactions.experimental.SuspendedKt.closeAsync(Suspended.kt:87)
at org.jetbrains.exposed.sql.transactions.experimental.SuspendedKt.access$closeAsync(Suspended.kt:1)
at org.jetbrains.exposed.sql.transactions.experimental.SuspendedKt$suspendedTransactionAsyncInternal$1.invokeSuspend(Suspended.kt:140)
at _COROUTINE._BOUNDARY._(CoroutineDebugging.kt:46)
at org.jetbrains.exposed.sql.transactions.experimental.SuspendedKt$newSuspendedTransaction$2.invokeSuspend(Suspended.kt:59)
This database object database org.jetbrains.exposed.sql.Database@5d7badc6
was created in a preceding test, so the problem is essentially the same as before.spand
05/15/2023, 10:21 AMHakon Grotte
05/16/2023, 10:10 AMspand
05/16/2023, 10:20 AMDatabase
first.Hakon Grotte
05/16/2023, 10:52 AMAleksei Tirman [JB]
05/18/2023, 10:09 AMDbConnectionWithKtorTest
class pass. How to reproduce the problem using your project?Hakon Grotte
05/19/2023, 9:26 AMAleksei Tirman [JB]
05/19/2023, 9:29 AMHakon Grotte
05/23/2023, 7:10 AMnewSuspendedTransaction
.
Unfortunately, I was unable to modify tests for increased probability of failure, but managed to reproduce locally with Correto JDK 11. @Aleksei Tirman [JB] sorry for taking your time.
For those interested: The Exposed code location close to "picking the old Database object" seems to be somewhere around the red line in the image (the rest are usually null with newSuspendedTransaction). This code path can naturally be avoided by passing the Database object (the blue line). Following the red code line TransactionManager.manager
brings me to `TransactionApi.kt`:
private val currentThreadManager = TransactionManagerThreadLocal()
val manager: TransactionManager
get() = currentThreadManager.get()
, where the former class uses Java ThreadLocal
to wrap the TransactionManager. Perhaps there is some interop issues between Java ThreadLocal and Kotlin Coroutines 🤷♂️spand
05/23/2023, 7:22 AMHakon Grotte
05/23/2023, 7:27 AM.connect()
and even has a defaultDatabase
variable:
internal val currentDefaultDatabase = AtomicReference<Database>()
var defaultDatabase: Database?
@Synchronized get() = currentDefaultDatabase.get() ?: databases.firstOrNull()
@Synchronized set(value) { currentDefaultDatabase.set(value) }
private val databases = ConcurrentLinkedDeque<Database>()
private val registeredDatabases = ConcurrentHashMap<Database, TransactionManager>()
, so that's how it's possiblespand
05/23/2023, 7:28 AM