ol.clave.lease
Cooperative cancellation and deadline propagation for concurrent operations.
A lease represents a bounded lifetime for work.
It carries an optional deadline, tracks cancellation state, and provides a
completion signal that callers can poll or block on.
Motivation
Individual timeout parameters scattered across function calls lead to
inconsistent budgets and subtle hangs.
A lease provides one coherent boundary: the caller defines the maximum time
for an entire operation, and all nested work inherits that constraint.
The lease is intentionally minimal and does not prescribe any particulary
concurrency model. Virtual threads, core.async go-loops, Java
StructuredTaskScopes, etc all work equally well with leases.
This design enables leases to serve as common ground between libraries with different runtime preferences. A library using one concurrency model can accept the same lease type as a library using another, allowing callers to compose them without adapter layers or runtime coupling.
Cooperative Cancellation
Cancellation are entirely advisory.
A lease does not forcibly terminate anything. It merely records that
cancellation has been requested and notifies interested parties.
The lease transitions from :active to :cancelled or :timed-out, but
running code continues unless it explicitly checks the lease and decides to
stop.
This cooperative model requires discipline from functions that receive a lease: they must periodically consult the lease and honor cancellation promptly. The tradeoff is predictability. Code always controls its own teardown, resources are released in an orderly fashion, and there are no surprising interruptions mid-operation.
Concepts
Lease: A value representing cancellation state (:active, :cancelled, or
:timed-out), an optional deadline (monotonic nanoTime), and parent-child
relationships for propagation.
Deadline vs timeout: A timeout is a duration ("at most 2 seconds"); a
deadline is an absolute monotonic bound ("stop at nanoTime T").
This library accepts either via with-timeout and with-deadline, but
internally tracks a single deadline per lease.
Deadlines use System/nanoTime for monotonic timing, immune to wall-clock
changes (NTP adjustments, manual clock changes, VM suspend/resume).
Honoring a Lease
When your function receives a lease as an argument, you are accepting responsibility to respect it. The caller trusts that if they cancel the lease, your function will notice and stop work promptly and correctly.
At a minimum, check the lease before starting expensive operations.
Call active?! at the top of your function and at natural checkpoints: the
start of each loop iteration, before issuing a network request, or after
returning from a potentially slow sub-call.
If the lease has been cancelled, active?! throws an exception containing
the cancellation cause, which unwinds the stack cleanly.
For non-throwing checks, use active? and return early or break out of
loops when it returns false.
The choice between throwing and returning depends on your error-handling
style, but the principle is the same: stop doing new work once the lease is
no longer active.
When calling other lease-aware functions, pass the lease through so they
inherit the same cancellation boundary.
If you need to spawn concurrent work, derive child leases with with-cancel
or with-timeout and cancel them in a finally block to ensure cleanup.
The goal is prompt, graceful termination without leaking resources. A well-behaved function notices cancellation within a reasonable window (tens to hundreds of milliseconds for most operations) and exits without leaving resources dangling or work half-done.
Parent-Child Relationships
Derived leases form a tree. When a parent cancels, all descendants cancel automatically. Children never cancel parents. Child deadlines adopt the earliest of the parent deadline and any explicitly provided deadline.
Thread Safety
Leases are safe for concurrent use. Multiple threads may read state, register listeners, or attempt cancellation. Only the first cancellation wins; subsequent calls are no-ops.
Usage
(require ' [ol.clave.lease :as l])
;; Create a root lease and derive a child with timeout
(let [[lease cancel] (l/with-timeout (l/background) 5000 )]
(try
(do-work lease)
(finally
(cancel))))
;; Check cancellation state
(when (l/active? lease)
(continue-work))
;; Block until cancelled
(deref (l/done-signal lease))
See also: with-cancel, with-timeout, with-deadline, active?!
ILease
Protocol for cooperative cancellation and deadline tracking.
All methods are non-blocking and safe for concurrent use from multiple
threads.
The done-signal method returns a derefable; blocking occurs only when
dereferencing that signal.
protocol
deadline
(deadline lease)
Returns the deadline as a monotonic nanoTime (Long), or nil if no deadline.
The value is from `System/nanoTime` and is only meaningful for comparison with other nanoTime values. Use <<remaining,`remaining`>> to get a human-readable `Duration` until expiry.
done-signal
(done-signal lease)
Returns a read-only derefable that yields true when lease ends.
Use `deref` with a timeout to wait for cancellation, or `clojure.core/realized?` for a non-blocking check.
[source,clojure]
;; Block with timeout (deref (done-signal lease) 1000 :still-active) ;; Non-blocking check (realized? (done-signal lease))
background
(background)
Creates a root lease with no deadline or parent.
The background lease is never cancelled on its own; it serves as the ancestor
for all derived leases in an operation tree.
Use with-cancel, with-timeout, or with-deadline to derive child
leases with cancellation or deadline constraints.
(let [root (background)
[child cancel] (with-timeout root 5000 )]
(try
(do-work child)
(finally
(cancel))))
with-cancel
(with-cancel parent)
Derives a cancellable child lease from parent.
Returns [child cancel-fn] where:
- child is the derived lease, inheriting parent’s deadline
- `cancel-fn cancels child and all its descendants
The cancel function accepts an optional cause argument.
Without arguments, it uses a generic :lease/cancelled exception.
Calling cancel multiple times is safe; only the first call takes effect.
When parent cancels, child cancels automatically with the same cause.
(let [[lease cancel] (with-cancel parent)]
(try
(do-work lease)
(finally
(cancel))))
;; Cancel with custom cause
(cancel (ex-info "user abort" {:reason :user-request }))
See also: with-timeout, with-deadline, background
with-deadline
(with-deadline parent dl)
Derives a child lease from parent with an absolute monotonic deadline.
Returns [child cancel-fn] where child will automatically cancel with
:lease/deadline-exceeded when the deadline passes.
dl is a monotonic nanoTime value from System/nanoTime.
The effective deadline is the earlier of parent’s deadline and `dl.
If dl has already passed, child is cancelled immediately.
Most callers should prefer with-timeout which accepts human-friendly
Duration or milliseconds.
;; 30 second deadline using nanoTime
(let [dl (+ (System/nanoTime) (* 30 1000000000 ))
[lease cancel] (l/with-deadline parent dl)]
(try
(do-work lease)
(finally
(cancel))))
See also: with-timeout, with-cancel, deadline
with-timeout
(with-timeout parent timeout)
Derives a child lease from parent with a relative timeout.
Returns [child cancel-fn] where child will automatically cancel with
:lease/deadline-exceeded after timeout elapses.
timeout may be:
- java.time.Duration for precise control
- Long/integer for milliseconds
The effective deadline is computed as (now + timeout) and combined with
the parent deadline using earliest-wins semantics.
A zero or negative timeout cancels the child immediately.
;; 5 second timeout
(let [[lease cancel] (with-timeout parent 5000 )]
(try
(do-work lease)
(finally
(cancel))))
;; Using Duration
(with-timeout parent (Duration/ofSeconds 30 ))
See also: with-deadline, with-cancel
active?!
(active?! lease)
Returns lease if active, otherwise throws the cancellation cause.
Use this for explicit cancellation checks that should fail fast.
The thrown exception is the same value returned by cause, containing
:type of either :lease/cancelled or :lease/deadline-exceeded.
;; Check and continue
(active?! lease)
(do-next-step)
;; In a loop
(loop []
(active?! lease)
(when (more-work?)
(process-item)
(recur)))
remaining
(remaining lease)
Returns the time remaining until lease expires as a java.time.Duration.
Returns nil if the lease has no deadline.
Returns Duration/ZERO if the deadline has already passed.
(when-let [dur (l/remaining lease)]
(println "Time left:" (.toMillis dur) "ms" ))