// Deadlines of one exchange. Each phase has its own limit from the
// ClientConfig; the attempt deadline (`SendOptions.timeout_ms`) caps every
// phase it covers with the time left. Whichever ends first is the one a
// `Timeout(ms)` reports.
///|
/// The per-phase limits in milliseconds; `None` sets none.
priv struct Timeouts {
pool : Int32?
connect : Int32?
write : Int32?
read : Int32?
}
///|
/// The limit of `phase`: Head and Body share the read limit.
fn Timeouts::of(self : Timeouts, phase : @mttp_core.Phase) -> Int32? {
match phase {
Pool => self.pool
Dial => self.connect
Write => self.write
_ => self.read
}
}
///|
/// How long a phase may run and the limit to report when it runs out: the
/// phase's own `limit`, or the `remaining` time of an attempt limited to
/// `total` when that ends first (a tie reports the phase's own). `None`
/// when neither applies. A nonpositive budget has already run out.
fn phase_budget(
limit : Int32?,
total~ : Int32,
remaining~ : Int64?,
) -> (Int32, Int32)? {
match (limit, remaining) {
(Some(limit), Some(left)) if left < limit.to_int64() =>
Some((left.max(0L).to_int(), total))
(Some(limit), _) => Some((limit, limit))
(None, Some(left)) => Some((left.max(0L).min(2147483647L).to_int(), total))
(None, None) => None
}
}
///|
/// Runs the phases of one exchange against its limits on `clock`.
priv struct PhaseTimer {
timer : &@rt.Timer
clock : &@rt.Clock
limits : Timeouts
/// The attempt deadline's length, reported when it runs out.
total : Int32
/// When the attempt must be done, or `None` when no deadline covers the
/// phases this timer runs.
deadline : Int64?
}
///|
/// A timer for phases of an attempt starting now and due within `total`
/// milliseconds, or without a deadline when `total` is `None`.
fn PhaseTimer::new(
clock : &@rt.Clock,
timer : &@rt.Timer,
limits : Timeouts,
total : Int32?,
) -> PhaseTimer {
let deadline = total.map(ms => {
let now = clock.now_unix_ms()
let duration = ms.max(0).to_int64()
if now > 9223372036854775807L - duration {
9223372036854775807L
} else {
now + duration
}
})
{ timer, clock, limits, total: total.unwrap_or(0), deadline, }
}
///|
/// Milliseconds left before the attempt deadline, or `None` without one.
fn PhaseTimer::remaining(self : PhaseTimer) -> Int64? {
self.deadline.map(at => {
let now = self.clock.now_unix_ms()
if at <= now {
0L
} else {
let left = at - now
if left < 0L {
9223372036854775807L
} else {
left
}
}
})
}
///|
/// Runs `f` as `phase`. A limit that runs out cancels `f` and raises
/// `Timeout` in `phase` with the limit that expired; one already spent
/// raises without starting `f`. Failures of `f` become a TransportError of
/// `phase` through `failure`. Cancellation from outside is not an `Error`,
/// so it propagates untouched.
async fn[X] PhaseTimer::run(
self : PhaseTimer,
phase : @mttp_core.Phase,
f : async () -> X,
) -> X raise @mttp_core.TransportError {
let remaining = self.remaining()
guard phase_budget(self.limits.of(phase), total=self.total, remaining~)
is Some((budget, limit)) else {
return f() catch { error => raise failure(phase, error) }
}
let expired = @mttp_core.TransportError(
kind=Timeout(ms=limit),
phase~,
detail="the limit of \{limit} ms ran out",
)
if budget <= 0 {
raise expired
}
let mut outcome : Result[X, @mttp_core.TransportError]? = None
let completed = self.timer.within(budget, () => {
outcome = Some(Ok(f()) catch { error => Err(failure(phase, error)) })
})
match outcome {
Some(Ok(value)) if completed => value
Some(Err(error)) if completed => raise error
_ => raise expired
}
}