Statement Timeouts
Long-running queries can hold a database connection for minutes and starve a pool. Saferis lets you cap the time the database spends on a statement using JDBC's Statement.setQueryTimeout, which, unlike ZIO.timeout, asks the driver to actually cancel the query server-side.
There are two composable layers for setting a timeout:
Saferis.queryTimeout(d)aspect: scopes a timeout to any Saferis fragments executed inside the decorated effect. Composes with other ZIO aspects.Transactor
defaultTimeout: applies to every statement run through that Transactor.
When both apply, the aspect wins (per-scope ad-hoc overrides beat the Transactor-wide default).
The `Saferis.queryTimeout` aspect
{
def slowReport(xa: Transactor) =
xa.run(sql"SELECT * FROM ${Table[User]}".query[User]) @@ Saferis.queryTimeout(5.seconds)
slowReport
}saferis.docs.StatementTimeouts$$$Lambda/0x0000000092343828@2b5ec5d3Because queryTimeout is a regular ZIOAspect, it composes with the rest of ZIO's aspect ecosystem (@@ ZIOAspect.loggedWith(...), etc.) and stacks naturally with >>=/flatMap.
Transactor-wide default
{
// Every statement run through this Transactor is bounded by 30 seconds, unless overridden by the aspect.
val xaLayer = Transactor.layer(defaultTimeout = Some(30.seconds))
xaLayer
}Suspend(zio.ZLayer$ZLayerProvideSomeOps$$$Lambda/0x00000000920c87a0@47567634)Resolution order
When a statement is about to execute, Saferis picks the timeout in this priority order:
The value installed by
Saferis.queryTimeout(d)for the current fiber, if any.The Transactor's
defaultTimeout, if set.No timeout (current default behavior).
Granularity
JDBC's setQueryTimeout accepts whole seconds. Saferis accepts a zio.Duration and rounds up to the nearest second, with a minimum of 1 second. This is intentional: setQueryTimeout(0) means no limit in JDBC, so a sub-second duration must not silently disable the cap.
Handling timeout errors
A timed-out statement surfaces as SaferisError.Timeout. This applies to both client-side timeouts triggered by setQueryTimeout and server-side cancellations (SQLState 57014):
{
def safeReport(xa: Transactor) =
xa.run(sql"SELECT * FROM ${Table[User]}".query[User])
.catchSome:
case _: SaferisError.Timeout =>
ZIO.logWarning("Report query timed out, returning empty result").as(Chunk.empty)
@@ Saferis.queryTimeout(5.seconds)
safeReport
}saferis.docs.StatementTimeouts$$$Lambda/0x000000009234af20@41e87627A timeout firing for real
Here is a timeout actually firing against the live database: a SELECT pg_sleep(3)
bounded by a 1-second timeout. The effect fails, and the real SaferisError.Timeout
is shown beneath the snippet:
(xa.run(sql"SELECT pg_sleep(3)".queryValue[Int]) @@ Saferis.queryTimeout(
1.second
)).unit: ZIO[Scope, SaferisError, Unit]saferis.SaferisError$Timeout: Timeout(org.postgresql.util.PSQLException: ERROR: canceling statement due to user request,Some(SELECT pg_sleep(3)))