// Connection backoff (gRPC `doc/connection-backoff.md`): after a backend fails, the next
// dial to it waits longer and longer, with jitter, so a hundred clients reconnecting to a
// restarted server do not arrive in the same millisecond. Pure decisions here; the channel
// dials when this says the wait is over.

///|
/// A backoff schedule: the first wait, the factor each failure multiplies it by, the cap,
/// and how much of the wait is jitter.
pub(all) struct Backoff {
  initial : @moondate.Span
  max : @moondate.Span
  factor : Double
  jitter : Double
}

///|
/// The schedule the gRPC document specifies: one second, times 1.6 each time, capped at two
/// minutes, with the wait drawn from ±20% of it.
pub let backoff : Backoff = {
  initial: @moondate.Span::new(millis=1000L),
  max: @moondate.Span::new(millis=120_000L),
  factor: 1.6,
  jitter: 0.2,
}

///|
/// The shortest time a connection attempt may be given before it counts as failed, however
/// short the backoff has become. The document's `minConnectTimeout`.
pub let min_connect_timeout : @moondate.Span = @moondate.Span::new(
  millis=20_000L,
)

///|
/// A schedule by its four numbers.
pub fn Backoff::new(
  initial~ : @moondate.Span = backoff.initial,
  max~ : @moondate.Span = backoff.max,
  factor~ : Double = backoff.factor,
  jitter~ : Double = backoff.jitter,
) -> Backoff {
  { initial, max, factor, jitter, }
}

///|
/// How long to wait before the `attempt`-th dial (the first being 1): the initial wait
/// times the factor once per earlier failure, capped, then spread by the jitter — `roll`
/// draws from `[0, 1)` and 0.5 is no spread at all.
pub fn Backoff::delay(
  self : Backoff,
  attempt : Int32,
  roll~ : Double = 0.5,
) -> @moondate.Span {
  let cap = self.max.nanos.to_double()
  let mut wait = self.initial.nanos.to_double()
  for _i = 1; _i < attempt; _i = _i + 1 {
    wait = wait * self.factor
    if wait > cap {
      wait = cap
      break
    }
  }
  // The jitter multiplies the wait by something in [1 - jitter, 1 + jitter).
  let spread = 1.0 + (roll * 2.0 - 1.0) * self.jitter
  @moondate.Span::new(nanos=(wait * spread).to_int64())
}

///|
/// One backend's reconnect clock: how many dials have failed in a row, and when the next one
/// may happen.
pub(all) struct Reconnect {
  backoff : Backoff
  mut failures : Int32
  mut next_at : Int64
}

///|
/// A reconnect clock that dials at once.
pub fn Reconnect::new(
  backoff~ : Backoff = backoff,
  now~ : Int64 = 0L,
) -> Reconnect {
  { backoff, failures: 0, next_at: now, }
}

///|
/// Record a failed dial at `now` and schedule the next one. `roll` draws the jitter.
pub fn Reconnect::failed(
  self : Reconnect,
  now : Int64,
  roll~ : Double = 0.5,
) -> Unit {
  self.failures = self.failures + 1
  let wait = self.backoff.delay(self.failures, roll~)
  self.next_at = now + wait.nanos / 1_000_000L
}

///|
/// Record a dial that worked: the next failure starts the schedule over.
pub fn Reconnect::succeeded(self : Reconnect) -> Unit {
  self.failures = 0
  self.next_at = 0L
}

///|
/// Whether a dial may be made now, or how long is left before it may (`0` when it may).
pub fn Reconnect::wait_millis(self : Reconnect, now : Int64) -> Int64 {
  let left = self.next_at - now
  if left < 0L {
    0L
  } else {
    left
  }
}

///|
/// Whether a dial may be made now.
pub fn Reconnect::due(self : Reconnect, now : Int64) -> Bool {
  self.wait_millis(now) == 0L
}