Affects: Any spring versions having

Hello,

As I did not find anything about it, I ask it here.

Problem

Why there isn't any way to cancel a query easily using the JdbcOperations, NamedParameterJdbcOperations or JdbcClient ?

For sure, there is queryForStream that provides a cancelable Stream, that only cancels after the query execution, and is not using the PreparedStatement.cancel() to interrupt the maybe long running query.

Solution idea (kotlin)

We can do it using a PreparedStatementCallback within (NamedParameter)JdbcOperations.execute(...) to plug the PreparedStatement.cancel() within the continuation.invokeOnCancellation triggered when the continuation is cancelled.

The following is able to forward the coroutine cancel signal. But there is still another issue: we are not able to cancel getting the connection (sadly) — maybe play with the connection timeout depending on the needs. But, by being able to cancel at least after obtained the connection, it prevents to overload the database with useless queries.

suspend fun <T> NamedParameterJdbcOperations.coExecute(sql: String, params: Map<String, *>, context: CoroutineContext = Dispatchers.IO, rowMapper: (ResultSet) -> T): List<T> = withContext(context) { // Force to switch of context to easily not block the caller thread
    suspendCancellableCoroutine { continuation ->
        continuation.resumeWith(runCatching {
            this@coExecute.execute(sql, params) { statement ->
                continuation.invokeOnCancellation { error ->
                    // When the coroutine is cancelled or had an error, do cancel the statement
                    // An potential issue here is that the statement cancel() is a blocking method, that is not the preferred way by kotlin. It should instead be non-blocking and fast. Why not wrapping the cancellation in a non blocking `launch { cancel() }`
                    if (error != null) {
                        statement.cancel()
                    }
                }
                if (continuation.isCancelled)
                // When getting inside this prepared statement callback, we acquired the connection, and it's possible that the continuation has been cancelled in between.
                // Then we don't execute the query itself for nothing by just returning an empty list (whatever else, it won't be used at the end).
                    emptyList()
                else
                    RowMapperResultSetExtractor { row, _ -> rowMapper(row) }.extractData(statement.executeQuery())
            }!!
        })
    }
}

TL;DR:

It would be great to have an easiest way of cancelling queries, with by example having a JdbcOperations.asyncQuery giving a Future<List<T>> (or any other interface to propose cancellation). That way, it's much more simple to cancel queries while keeping a comprehensible user interface.

I can make a PR, but I prefer talking a little before to see if it could be acceptable and discuss about other potential issues.

Comment From: sdeleuze

If we provide something here, that should probably be not Kotlin specific. Also when Spring provides a Coroutines based-feature, it is to translate a Reactive concept so more tied to R2DBC than JDBC. So I would maybe suggest to discuss this feature from a R2DBC angle with @simonbasle and/or @mp911de.

For JDBC, not sure how we would expose that.

Comment From: mp911de

In R2DBC we always operate on a stream whose subscription can be cancelled at any point. Focusing on the driver, Postgres and SQL Server drivers stop consuming the cursor upon cancellation.

There is also a way to cancel the server-side query but taking pipelining into account, a cancellation signal can always target a later query depending on when the cancellation actually happens.

Comment From: jhoeller

I don't see much we can do about this with JDBC since Statement.cancel() is barely supported among JDBC drivers and even throws an exception when not supported, incurring a cost. In queryForStream, we can only return the Stream once the initial query execution has completed, with no chance for cancellation to have an effect.

Pretty much the only case where cancellation could make sense is async queries as suggested. However, even that would be rather involved with JDBC, having to prepare a special Future that tries to call cancel() on a tracked Statement in the execution thread... with no reliable semantics across database drivers.

All in all, we are not going to introduce this at the JdbcTemplate level, and it does not seem to be common enough to justify a dedicated AsyncJdbcTemplate. FWIW we used to have an AsyncRestTemplate next to RestTemplate which we eventually deprecated and removed. Such asynchronous pipelines really suggest reactive processing instead, e.g. with WebClient or R2DBC.

Comment From: Chuckame

@mp911de Do you know where I can find such information about the risk of cancelling a future query ? We are using the drivers org.postgresql:postgresql:42.7.1 and mysql:mysql-connector-java:8.0.33.

We have a service with thousands of calls per second, where we have only 75 connections on 5 instances, each http call is making multiple sql queries.

Comment From: Chuckame

As far as we understood, pgsql and mysql jdbc drivers are cancelling the current executing query of the connection's thread. But if the cancel happens and is a little late (let's say because of high cpu usage), then the cancelled query is maybe finished and another query is running on the same connection, so the same db server-thread, so the another query will be cancelled.