///|
// Health accounting. Each backend carries a consecutive-failure count and a
// health bit; the functions here are the only place those fields change, so the
// policy lives in one testable spot. Functions take the pool's backend array
// and an index (MoonBit arrays have interior mutability) rather than a mutable
// receiver. Active probing schedules real dial attempts in the native runtime
// and reports through `record_success` / `record_failure`.
///|
/// The thresholds that move a backend between healthy and unhealthy. A backend
/// goes unhealthy after `unhealthy_threshold` consecutive failures and returns
/// healthy only after `healthy_threshold` consecutive successes while it was
/// down — a single lucky probe should not flap a server back into rotation.
pub struct HealthPolicy {
unhealthy_threshold : Int
healthy_threshold : Int
}
///|
/// Build a policy; thresholds below 1 are treated as 1.
pub fn HealthPolicy::new(
unhealthy_threshold? : Int = 2,
healthy_threshold? : Int = 2,
) -> HealthPolicy {
{
unhealthy_threshold: if unhealthy_threshold < 1 {
1
} else {
unhealthy_threshold
},
healthy_threshold: if healthy_threshold < 1 {
1
} else {
healthy_threshold
},
}
}
///|
/// Count one successful exchange: clear the failure streak and, if the backend
/// was being held out, require a full run of successes before re-enabling it.
pub fn record_success(
backends : Array[Backend],
idx : Int,
policy : HealthPolicy,
) -> Unit {
let b = backends[idx]
if b.healthy {
b.failures = 0
} else {
b.failures = b.failures + 1
if b.failures >= policy.healthy_threshold {
b.healthy = true
b.failures = 0
}
}
backends[idx] = b
}
///|
/// Count one failed exchange: reset progress toward recovery and, once the
/// failure streak reaches the threshold, pull the backend out of rotation.
pub fn record_failure(
backends : Array[Backend],
idx : Int,
policy : HealthPolicy,
) -> Unit {
let b = backends[idx]
if b.healthy {
b.failures = b.failures + 1
if b.failures >= policy.unhealthy_threshold {
b.healthy = false
b.failures = 0
}
} else {
b.failures = 0
}
backends[idx] = b
}
///|
/// Mark a backend directly down (used when a connection cannot even be
/// established — an unambiguous failure worth a hard removal).
pub fn mark_down(backends : Array[Backend], idx : Int) -> Unit {
let b = backends[idx]
b.healthy = false
b.failures = 0
backends[idx] = b
}
///|
/// Mark a backend directly up after a successful active probe.
pub fn mark_up(backends : Array[Backend], idx : Int) -> Unit {
let b = backends[idx]
b.healthy = true
b.failures = 0
backends[idx] = b
}
///|
/// Whether this backend may receive traffic.
pub fn Backend::is_healthy(self : Backend) -> Bool {
self.healthy
}
///|
/// Record that a request has started using backend `idx`, for least-connections
/// balancing.
pub fn acquire(backends : Array[Backend], idx : Int) -> Unit {
let b = backends[idx]
b.active_conns = b.active_conns + 1
backends[idx] = b
}
///|
/// Record that a request has finished using backend `idx`; the count never goes
/// negative.
pub fn release(backends : Array[Backend], idx : Int) -> Unit {
let b = backends[idx]
if b.active_conns > 0 {
b.active_conns = b.active_conns - 1
}
backends[idx] = b
}
///|
/// The indices of every healthy backend, in array order. Returning indices
/// (rather than copies) lets the runtime select and then mutate the entry.
pub fn healthy_indices(backends : Array[Backend]) -> Array[Int] {
let out : Array[Int] = []
let mut i = 0
while i < backends.length() {
if backends[i].healthy {
out.push(i)
}
i = i + 1
}
out
}