Skip to main content

Timeout

← all patterns

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 deadlineoutcome() returns Outcome.Success; call() returns the value.
  • Deadline passes firstoutcome() returns Outcome.TimedOut; call() throws ResilientTimeoutException. Outcome.TimedOut.fold() also converts to a ResilientTimeoutException for callers using the functional style.
  • The operation itself throws before the deadline — outcome() returns Outcome.Failure; call() rethrows the original RuntimeException unchanged, or wraps a checked exception in a ResilientException. An Error thrown by the operation is never wrapped — it is stored on the worker and rethrown as-is on the caller's thread once join() 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

propertyrequireddescription
nameyesIdentifier used in every event, set via of(name, timeout) — no wither, not runtime-changeable
timeoutyesMaximum time the caller waits before the operation is considered timed out, set via of(name, timeout). Must be positive; no default
withTimeout()noChange the timeout on an existing instance, producing a new one. Must be positive
cancelOnTimeoutnoInterrupt the worker thread when the deadline passes. Default: true
withListener()noSubscribe to timeout events (e.g. Succeeded, TimedOut, Failed)
withClock()noCustom 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 TimedOut eventually succeeded anyway. Observability only; the caller already received Outcome.TimedOut. Carries: timestamp, name.
  • AbandonedWorkerFailed — a worker abandoned after TimedOut eventually 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.