Statement Timeouts
Long-running queries can hold a database connection for minutes and starve a pool. Saferis caps the time the database spends on a statement. The JDBC driver calls Statement.setQueryTimeout, which, unlike ZIO.timeout, asks the driver to cancel the query server-side.
There are three places to set a timeout, highest priority first:
sql"...".withTimeout(d): this statement only.Saferis.queryTimeout(d)aspect: any Saferis fragment executed on the current fiber. It composes with other ZIO aspects.JdbcSessionConfig.defaultTimeout: every statement on that session when neither of the above is set.
A set fragment timeout wins over the aspect. The aspect wins over the session default.
The `Saferis.queryTimeout` aspect
{
def slowReport(session: SqlSession) =
(sql"SELECT * FROM ${Table[User]}".query[User]) @@ Saferis.queryTimeout(5.seconds)
slowReport
}saferis.docs.StatementTimeouts$$$Lambda/0x00000000881a1370@78d43feaBecause queryTimeout is a regular ZIOAspect, it composes with the rest of ZIO's aspect ecosystem (@@ ZIOAspect.loggedWith(...), etc.) and stacks naturally with >>=/flatMap.
Session-wide default
{
// Every statement on this session is bounded by 30 seconds, unless a fragment or the aspect sets one.
val session = PostgresJdbc.layer(JdbcSessionConfig(defaultTimeout = Some(30.seconds)))
session
}Suspend(zio.ZLayer$$$Lambda/0x00000000880afa40@596fc8f1)Resolution order
When a statement is about to execute, Saferis picks the timeout in this priority order:
withTimeouton the fragment, if set.The value installed by
Saferis.queryTimeout(d)for the current fiber, if any.JdbcSessionConfig.defaultTimeout, if set.No timeout.
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(session: SqlSession) =
(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/0x00000000881ac698@6bc56bcaA 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:
((sql"SELECT pg_sleep(3)".queryValue[Int]) @@ Saferis.queryTimeout(
1.second
)).unit
.provideLayer(DocsTransactor.layer)saferis.SaferisError$Timeout: Timeout(ServerDetail(ERROR: canceling statement due to user request,Some(SELECT pg_sleep(3)),Some(57014),Some(0),None))
at saferis.jdbc.JdbcConnection.queryAtMostOne(JdbcSession.scala:95)