Reputation: 15529
I am using the following code to get a JDBC connection, inside a blocking
block, and pass that connection to a fn: Connection => Future[_]
. After fn finishes I'd like to commit/rollback the transaction and close the connection.
def withTransactionAsync[T](fn: Connection => Future[T]): Future[T] =
Future {
blocking {
ds.getConnection
}
}.flatMap { conn =>
fn(conn)
.map { r => conn.commit(); conn.close(); r }
.recoverWith {
case e: Throwable =>
conn.rollback()
conn.close()
throw e
}
}
I am using a separate execution context based on a ForkJoinPool
.
With enough calls, this code goes into a deadlock. Intuitively, this makes sense. The first future, with the getConnection
call, gets blocked while waiting for available connections, while available connections are waiting for available threads in the ExecutionContext to run the commit(); close()
block to free the connection and free a thread in the execution context for getConnection
to run. I verified this is the case with a thread dump.
The only way I found around this problem is to run everything on the same Future {}
and therefore avoid switching the context:
def withTransactionAsync[T](fn: Connection => Future[T]): Future[T] =
Future {
blocking {
val conn = ds.getConnection
try {
conn.setAutoCommit(false)
val r = Await.result(fn(conn), Duration.Inf)
conn.commit()
r
} catch {
case e: Throwable =>
conn.rollback()
throw e
} finally
conn.close()
}
}
But this way I am blocking on Await.result
. I suppose this is not a big problem because I am blocking inside a blocking
block, but I am afraid this would have unforeseen consequences and is not necessarily what the caller of this API expects.
Is there a way around this deadlock without using Await
and just rely on Future composition?
I suppose a case could be made that this this function not be accepting Connection => Future[T]
but only a Connection => T
, but I'd like to keep that API.
If I increase the size of the ForkJoinPool
enough, it works, but that size is difficult to calculate/predict for all workloads and I don't want to have a ForkJoinPool
many times the size of my database pool.
Upvotes: 0
Views: 321
Reputation: 27356
As mentioned in the comments, fn
is blocking code. But it is not inside a blocking
clause, so it will tie up one of the main threads in the pool. If this happens enough times, the pool will run out of threads and the system will deadlock.
So the call to fn
and the code that follows needs to be inside a blocking
clause so that a separate thread is created for it and the main threads remain available for non-blocking code.
Given the amount of blocking code, it is probably worth looking at a Task
model with a thread per connection rather than a thread per pending operation, so that the number of threads is constrained. This is basically a work-around for the fact that getConnection
is synchronous, which is a problem with HikariCP.
Upvotes: 1