weft/poll
Bounded polling in the caller’s own process.
Some waits cannot be handed to another process: a launcher that needs a lock before it can do anything else, a caller reaching into a tree that may be mid-restart, a shell that must know whether a server came up before it prints the address. The waiting process is the one that needs the answer, the thing it is waiting on is a synchronous probe rather than a message, and the only honest bound is the wall clock. Every such site ends up hand-rolling the same loop — try, check the deadline, sleep, recurse — and every copy re-decides the same three corners: whether the first attempt sleeps first, whether an attempt is made at the deadline itself, and how a probe that failed for good is told apart from one that merely has not succeeded yet.
until is that loop, decided once. The first attempt is immediate. An
attempt that says Retry is followed by a sleep of every
milliseconds, clipped to what remains of the budget, and then another
attempt; when the budget is spent a last attempt is still made, so a
probe that would have succeeded exactly at the deadline is not reported
as expired. Fail ends the wait at once with the caller’s own error,
and Expired is a distinct outcome rather than an error the caller has
to invent — “did not happen in time” is a fact about the clock, not
about the probe.
This module deliberately owns no process: it sleeps in the caller,
which is the whole point. A wait that could be a message should be a
weft/state_machine state with a timeout instead.
Example
import weft/poll
pub fn await_lock(path: String) -> Result(Lock, String) {
case
poll.until(within: 5000, every: 25, attempt: fn() {
case try_lock(path) {
Ok(lock) -> poll.Done(lock)
Error("busy") -> poll.Retry
Error(reason) -> poll.Fail(reason)
}
})
{
poll.Answered(lock) -> Ok(lock)
poll.Failed(reason) -> Error("acquire lock: " <> reason)
poll.Expired -> Error("timed out waiting for the lock")
}
}
Types
What one attempt of the probe reports.
pub type Attempt(a, e) {
Done(value: a)
Retry
Fail(error: e)
}
Constructors
-
Done(value: a)The probe succeeded; the wait is over.
Arguments
- value
-
The value the probe produced.
-
RetryNot yet. Sleep and try again, budget permitting.
-
Fail(error: e)The probe failed in a way that more time will not fix. The wait ends at once, carrying the caller’s own error.
Arguments
- error
-
Why the probe cannot succeed.
How a bounded wait ended.
pub type Outcome(a, e) {
Answered(value: a)
Failed(error: e)
Expired
}
Constructors
-
Answered(value: a)An attempt reported
Done.Arguments
- value
-
The value that attempt produced.
-
Failed(error: e)An attempt reported
Fail.Arguments
- error
-
The error that attempt carried.
-
ExpiredEvery attempt inside the budget, including the last one made at the deadline, reported
Retry.
Values
pub fn until(
within within: Int,
every every: Int,
attempt attempt: fn() -> Attempt(a, e),
) -> Outcome(a, e)
Poll attempt until it answers, fails, or within milliseconds have
passed, sleeping every milliseconds between attempts.
The first attempt is immediate, a final attempt is made when the budget
runs out, and the sleep never overshoots the deadline. A within of
zero therefore still makes exactly one attempt. Time is measured on the
monotonic clock, so a wall-clock step cannot lengthen or shorten the
wait.
Examples
poll.until(within: 1000, every: 50, attempt: fn() {
case probe() {
Ok(value) -> poll.Done(value)
Error(Nil) -> poll.Retry
}
})
// -> poll.Answered(value), or poll.Expired after about a second