Timeout
pattern
Runs an operation on a virtual thread and bounds how long the caller waits for it — enforcing the deadline with real thread interruption, not polling.
01
Cancellation mechanism
Each call starts a dedicated virtual thread that runs the operation. The caller blocks on
worker.join(timeout), so the deadline is enforced against real elapsed time — not against the
configurable Clock, which only timestamps events. If the worker finishes before the join times
out, its result is returned. If the join times out first, the caller is unblocked immediately and,
when cancelOnTimeout is true (the default), the worker thread is interrupted — a genuine
cancellation signal delivered to the operation, not a polling loop checking elapsed time.
Interruption only stops the operation if it actually responds to it — blocking calls that check
Thread.interrupted() or throw InterruptedException will stop; tight CPU loops or code that
swallows the interrupt will keep running in the background even though the caller has already moved
on. Either way, the caller's own result is decided the moment the deadline passes; it never waits
for the worker to actually stop.
02
Behavior
Every worker virtual thread is given a unique name (resiliencia-timeout-N, an internal
incrementing counter) purely so thread dumps under load can be correlated back to the Timeout
invocation that started them — it is not a configurable property.
Three outcomes are possible once the worker finishes or the deadline passes:
- Success within the deadline —
outcome()returnsOutcome.Success;call()returns the value. - Deadline passes first —
outcome()returnsOutcome.TimedOut;call()throwsResilientTimeoutException.Outcome.TimedOut.fold()also converts to aResilientTimeoutExceptionfor callers using the functional style. - The operation itself throws before the deadline —
outcome()returnsOutcome.Failure;call()rethrows the originalRuntimeExceptionunchanged, or wraps a checked exception in aResilientException. AnErrorthrown by the operation is never wrapped — it is stored on the worker and rethrown as-is on the caller's thread oncejoin()confirms the worker has finished.
When cancelOnTimeout is false, the worker is left to run to completion in the background instead
of being interrupted — the caller still gets Outcome.TimedOut / ResilientTimeoutException
immediately, only the worker's fate changes. In both cases, a worker that is still running when the
deadline passes is "abandoned": its eventual result can no longer reach the caller (who already
received TimedOut), so it is only reported best-effort via AbandonedWorkerSucceeded /
AbandonedWorkerFailed events, never as a second Outcome.
A separate edge case: if the calling thread itself is interrupted while blocked in join() — not
the deadline elapsing — the worker is interrupted as a best-effort cleanup, the caller's interrupt
status is restored, and outcome() returns Outcome.Failure wrapping a ResilientException
("Interrupted while waiting for operation to complete"). No TimeoutEvent is emitted for this path,
since it never reached a normal completion or a deadline verdict.
03
Configuration
| property | required | description |
|---|---|---|
| name | yes | Identifier used in every event, set via of(name, timeout) — no wither, not runtime-changeable |
| timeout | yes | Maximum time the caller waits before the operation is considered timed out, set via of(name, timeout). Must be positive; no default |
| withTimeout() | no | Change the timeout on an existing instance, producing a new one. Must be positive |
| cancelOnTimeout | no | Interrupt the worker thread when the deadline passes. Default: true |
| withListener() | no | Subscribe to timeout events (e.g. Succeeded, TimedOut, Failed) |
| withClock() | no | Custom Clock for event timestamps only — does not affect the deadline, which is enforced against real elapsed time. Default: system clock |
04
Events & failure
- Succeeded — the operation completed within the deadline. Carries: timestamp, name, elapsed time.
- Failed — the operation threw before the deadline elapsed. Carries: timestamp, name, the thrown error.
- TimedOut — the deadline elapsed before the operation finished. Carries: timestamp, name, the configured timeout.
- AbandonedWorkerSucceeded — a worker abandoned after
TimedOuteventually succeeded anyway. Observability only; the caller already receivedOutcome.TimedOut. Carries: timestamp, name. - AbandonedWorkerFailed — a worker abandoned after
TimedOuteventually threw. Observability only. Carries: timestamp, name, the thrown cause.
Throws ResilientTimeoutException when the deadline passes. Field: the configured
timeout that was exceeded. The operation's thread has already been interrupted (when
cancelOnTimeout is true) by the time this is thrown; whether it actually stopped
depends on it responding to interruption.