// The gRPC client keepalive state machine (← gRPC `keepalive_time` / `keepalive_timeout`): keep a
// long-lived connection healthy by sending a PING when it has been idle, and declaring it dead if
// the PING's ACK does not come back in time. This is the pure decision core — it takes the current
// time and answers "ping now?" / "dead now?"; the Channel drives it, sending the actual HTTP/2 PING
// frame and closing the connection. Time is in the caller's unit (the same clock passed throughout).
///|
/// Keepalive state for one connection: the idle threshold that triggers a PING, the ACK deadline
/// that declares the connection dead, and the timers tracking the last activity and any in-flight
/// PING.
pub struct Keepalive {
keepalive_time : Int64
keepalive_timeout : Int64
mut last_activity : Int64
mut ping_outstanding : Bool
mut ping_sent_at : Int64
}
///|
/// A keepalive that pings after `keepalive_time` of idleness and gives a PING `keepalive_timeout`
/// to be acknowledged. `now` seeds the activity clock.
pub fn Keepalive::new(
keepalive_time~ : Int64,
keepalive_timeout~ : Int64,
now~ : Int64 = 0L,
) -> Keepalive {
{
keepalive_time,
keepalive_timeout,
last_activity: now,
ping_outstanding: false,
ping_sent_at: 0L,
}
}
///|
/// Record connection activity (a frame sent or received) at `now`, resetting the idle timer.
pub fn Keepalive::on_activity(self : Keepalive, now : Int64) -> Unit {
self.last_activity = now
}
///|
/// Whether a keepalive PING should be sent at `now`: the connection has been idle at least
/// `keepalive_time` and no PING is already awaiting its ACK.
pub fn Keepalive::should_ping(self : Keepalive, now : Int64) -> Bool {
!self.ping_outstanding && now - self.last_activity >= self.keepalive_time
}
///|
/// Record that a keepalive PING was sent at `now`.
pub fn Keepalive::on_ping_sent(self : Keepalive, now : Int64) -> Unit {
self.ping_outstanding = true
self.ping_sent_at = now
}
///|
/// Record that the PING's ACK arrived at `now` — the connection is alive, and this counts as
/// activity.
pub fn Keepalive::on_ping_ack(self : Keepalive, now : Int64) -> Unit {
self.ping_outstanding = false
self.last_activity = now
}
///|
/// Whether the connection is dead at `now`: a PING is outstanding and its ACK has not arrived within
/// `keepalive_timeout` of sending it.
pub fn Keepalive::is_timed_out(self : Keepalive, now : Int64) -> Bool {
self.ping_outstanding && now - self.ping_sent_at >= self.keepalive_timeout
}
///|
/// A server's keepalive policy (gRPC A8): `time` is how long an idle connection is left
/// alone before the server pings it, `timeout` how long the PING may go unanswered before
/// the connection is declared dead, `min_ping_interval` the shortest gap the server will
/// tolerate between a peer's PINGs, and `max_ping_strikes` how many of those too-soon PINGs
/// a peer gets before the connection is closed.
pub(all) struct ServerKeepalive {
time : Int64
timeout : Int64
min_ping_interval : Int64
permit_without_calls : Bool
max_ping_strikes : Int32
}
///|
/// The policy gRPC core uses when a server sets none: two hours of idleness before a PING,
/// twenty seconds to answer it, a PING no more often than every five minutes, and two
/// strikes before the peer is told to calm down (`ENHANCE_YOUR_CALM`).
pub let server_keepalive : ServerKeepalive = {
time: 7_200_000L,
timeout: 20_000L,
min_ping_interval: 300_000L,
permit_without_calls: false,
max_ping_strikes: 2,
}
///|
/// The guard a server keeps against a peer that pings it faster than the policy allows: a
/// PING inside `min_interval` is a strike, and `max_strikes` of them end the connection.
/// This is what stops a client from keeping a hundred thousand connections alive with a
/// PING every millisecond.
pub(all) struct PingGuard {
min_interval : Int64
max_strikes : Int32
mut last : Int64
mut strikes : Int32
}
///|
/// A guard that has seen no PING yet.
pub fn PingGuard::new(
min_interval~ : Int64,
max_strikes~ : Int32,
now~ : Int64 = 0L,
) -> PingGuard {
{ min_interval, max_strikes, last: now, strikes: 0, }
}
///|
/// Record a PING at `now` and say whether the peer has struck out. The first PING of a
/// connection is never a strike: `last` starts at the connection's own start.
pub fn PingGuard::observe(self : PingGuard, now : Int64) -> Bool {
// A well-spaced PING earns back nothing: the count is of strikes on this connection,
// not a rate, so a peer that hammers three times and then behaves is still out.
if now - self.last < self.min_interval {
self.strikes = self.strikes + 1
}
self.last = now
self.strikes > self.max_strikes
}
///|
/// The guard a policy implies, or `None` when the server takes no view on PING rate.
pub fn ServerKeepalive::ping_guard(
self : ServerKeepalive,
now~ : Int64 = 0L,
) -> PingGuard {
PingGuard::new(
min_interval=self.min_ping_interval,
max_strikes=self.max_ping_strikes,
now~,
)
}