TransactionSystemException in Grails @Transactional: async cancellation
0 reputation · 05 Feb 2023, 00:37 UTC
0 reputation · 05 Feb 2023, 00:37 UTC
Determine how to cancel asynchronous work when a Grails transaction exceeds its configured timeout.
Grails applies Spring’s @Transactional(timeout=N) or grails.transaction.defaultTimeout to database operations. When the timeout is hit, Spring throws a org.springframework.transaction.TransactionSystemException, which Grails wraps into a runtime exception and propagates to the controller. However, Grails async/Promise-based controller actions do not inherit this timeout; the underlying CompletableFuture or other async construct continues to run, potentially leaking threads or holding resources.
It is unclear which mechanisms, if any, exist in Grails to propagate the transaction timeout to async tasks and how to cleanly cancel those tasks without manual boilerplate.
TransactionSystemException to async operations started in a controller?@Transactional(timeout=…) be combined with CompletableFuture.orTimeout or other cancellation hooks in a Grails service?32775 reputation · 05 Feb 2023, 01:01 UTC
Grails does not automatically cancel asynchronous work when a @Transactional(timeout=…) limit is exceeded; the timeout only aborts the database transaction and throws a TransactionSystemException. Any CompletableFuture, Promise or GPars task started inside the controller continues to run.
TransactionSystemException on that thread.Because the timeout is enforced by Spring’s transaction manager, not by the async framework, there is no built‑in hook that propagates the exception to the async work. You must explicitly link the two.
In your service method, wrap the database call in a CompletableFuture and apply @Transactional to the call:
@Transactional(timeout = 2)
public CompletableFuture<Result> doWork() {
return CompletableFuture.supplyAsync(() -> {
// slow DB query or update
return repository.findSomething();
});
}
In the controller, obtain the future and attach a timeout that matches or is shorter than the transaction timeout:
def resultFuture = service.doWork()
resultFuture.orTimeout(1, TimeUnit.SECONDS) // will complete exceptionally with TimeoutException after 1s
.whenComplete((res, ex) -> {
if (ex != null) {
// propagate cancellation or log
response.status = 500
render ex.message
} else {
render res as JSON
}
});
If you need to interrupt the async thread, keep a reference to the CompletableFuture and call future.cancel(true) in the timeout handler:
resultFuture.orTimeout(1, TimeUnit.SECONDS)
.whenComplete((res, ex) -> {
if (ex instanceof TimeoutException) {
resultFuture.cancel(true); // attempts to interrupt the running task
render status: 408, text: "Request timed out"
}
});
Note: If you are using Grails Promise or GPars instead of raw CompletableFuture, the same principle applies: catch TransactionSystemException in the service and call promise.cancel() or complete the promise exceptionally. Let me know which async abstraction you are using so I can tailor the code.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.