///|
/// Classification used by reliability counters and incident reports.
pub(all) enum ReliabilityFailure {
TimeoutFailure
ChecksumFailure
ProtocolFailure
TransportFailure
DeviceException
ValidationFailure
} derive(Debug, Eq)
///|
/// State of a circuit guard protecting a device or route.
pub(all) enum CircuitGuardState {
ClosedCircuit
OpenCircuit
HalfOpenCircuit
} derive(Debug, Eq)
///|
/// One outcome recorded by a transport or polling worker.
pub(all) struct ReliabilitySample {
timestamp : Int
latency : Int
success : Bool
failure : ReliabilityFailure?
operation : String
}
///|
pub fn ReliabilitySample::new(
timestamp : Int,
latency : Int,
success : Bool,
operation : String,
failure? : ReliabilityFailure = TimeoutFailure,
) -> Result[ReliabilitySample, ModbusError] {
if timestamp < 0 || latency < 0 || operation == "" {
return Err(InvalidData)
}
let selected_failure = if success { None } else { Some(failure) }
Ok({ timestamp, latency, success, failure: selected_failure, operation })
}
///|
pub fn ReliabilitySample::timestamp(self : ReliabilitySample) -> Int {
self.timestamp
}
///|
pub fn ReliabilitySample::latency(self : ReliabilitySample) -> Int {
self.latency
}
///|
pub fn ReliabilitySample::success(self : ReliabilitySample) -> Bool {
self.success
}
///|
pub fn ReliabilitySample::failure(
self : ReliabilitySample,
) -> ReliabilityFailure? {
self.failure
}
///|
pub fn ReliabilitySample::operation(self : ReliabilitySample) -> String {
self.operation
}
///|
pub fn reliability_failure_name(failure : ReliabilityFailure) -> String {
match failure {
TimeoutFailure => "timeout"
ChecksumFailure => "checksum"
ProtocolFailure => "protocol"
TransportFailure => "transport"
DeviceException => "device-exception"
ValidationFailure => "validation"
}
}
///|
/// A bounded sliding window of request outcomes.
pub(all) struct ReliabilityWindow {
width : Int
max_samples : Int
samples : Array[ReliabilitySample]
mut evicted : Int
}
///|
pub fn ReliabilityWindow::new(
width : Int,
max_samples? : Int = 4096,
) -> Result[ReliabilityWindow, ModbusError] {
if width < 1 || max_samples < 1 {
Err(InvalidData)
} else {
Ok({ width, max_samples, samples: [], evicted: 0 })
}
}
///|
pub fn ReliabilityWindow::width(self : ReliabilityWindow) -> Int {
self.width
}
///|
pub fn ReliabilityWindow::length(self : ReliabilityWindow) -> Int {
self.samples.length()
}
///|
pub fn ReliabilityWindow::evicted(self : ReliabilityWindow) -> Int {
self.evicted
}
///|
pub fn ReliabilityWindow::append(
self : ReliabilityWindow,
sample : ReliabilitySample,
) -> Result[Unit, ModbusError] {
if self.samples.length() > 0 &&
sample.timestamp() < self.samples[self.samples.length() - 1].timestamp() {
return Err(InvalidData)
}
self.samples.push(sample)
ignore(self.prune(sample.timestamp()))
if self.samples.length() > self.max_samples {
ignore(self.samples.remove(0))
self.evicted += 1
}
Ok(())
}
///|
pub fn ReliabilityWindow::prune(
self : ReliabilityWindow,
now : Int,
) -> Result[Int, ModbusError] {
if now < 0 {
return Err(InvalidData)
}
let mut removed = 0
while self.samples.length() > 0 &&
now - self.samples[0].timestamp() > self.width {
ignore(self.samples.remove(0))
removed += 1
}
Ok(removed)
}
///|
pub fn ReliabilityWindow::successes(self : ReliabilityWindow) -> Int {
let mut count = 0
for sample in self.samples {
if sample.success() {
count += 1
}
}
count
}
///|
pub fn ReliabilityWindow::failures(self : ReliabilityWindow) -> Int {
self.samples.length() - self.successes()
}
///|
pub fn ReliabilityWindow::success_rate(self : ReliabilityWindow) -> Int {
if self.samples.length() == 0 {
0
} else {
self.successes() * 100 / self.samples.length()
}
}
///|
pub fn ReliabilityWindow::average_latency(self : ReliabilityWindow) -> Int {
if self.samples.length() == 0 {
return 0
}
let mut sum = 0
for sample in self.samples {
sum += sample.latency()
}
sum / self.samples.length()
}
///|
pub fn ReliabilityWindow::failure_count(
self : ReliabilityWindow,
failure : ReliabilityFailure,
) -> Int {
let mut count = 0
for sample in self.samples {
match sample.failure() {
Some(value) => if value == failure { count += 1 }
None => ()
}
}
count
}
///|
pub fn ReliabilityWindow::latest(
self : ReliabilityWindow,
) -> ReliabilitySample? {
if self.samples.length() == 0 {
None
} else {
Some(self.samples[self.samples.length() - 1])
}
}
///|
pub fn ReliabilityWindow::clear(self : ReliabilityWindow) -> Unit {
self.samples.clear()
}
///|
/// A circuit breaker with failure threshold and recovery probes.
pub(all) struct CircuitGuard {
name : String
failure_threshold : Int
recovery_after : Int
probe_limit : Int
mut state : CircuitGuardState
mut consecutive_failures : Int
mut opened_at : Int?
mut probes : Int
}
///|
pub fn CircuitGuard::new(
name : String,
failure_threshold : Int,
recovery_after : Int,
probe_limit? : Int = 1,
) -> Result[CircuitGuard, ModbusError] {
if name == "" ||
failure_threshold < 1 ||
recovery_after < 1 ||
probe_limit < 1 {
return Err(InvalidData)
}
Ok({
name,
failure_threshold,
recovery_after,
probe_limit,
state: ClosedCircuit,
consecutive_failures: 0,
opened_at: None,
probes: 0,
})
}
///|
pub fn CircuitGuard::name(self : CircuitGuard) -> String {
self.name
}
///|
pub fn CircuitGuard::state(self : CircuitGuard) -> CircuitGuardState {
self.state
}
///|
pub fn CircuitGuard::failure_threshold(self : CircuitGuard) -> Int {
self.failure_threshold
}
///|
pub fn CircuitGuard::recovery_after(self : CircuitGuard) -> Int {
self.recovery_after
}
///|
pub fn CircuitGuard::consecutive_failures(self : CircuitGuard) -> Int {
self.consecutive_failures
}
///|
pub fn CircuitGuard::opened_at(self : CircuitGuard) -> Int? {
self.opened_at
}
///|
pub fn CircuitGuard::allow(
self : CircuitGuard,
now : Int,
) -> Result[Bool, ModbusError] {
if now < 0 {
return Err(InvalidData)
}
match self.state {
ClosedCircuit => Ok(true)
HalfOpenCircuit =>
if self.probes < self.probe_limit {
self.probes += 1
Ok(true)
} else {
Ok(false)
}
OpenCircuit =>
match self.opened_at {
Some(opened) =>
if now - opened >= self.recovery_after {
self.state = HalfOpenCircuit
self.probes = 1
Ok(true)
} else {
Ok(false)
}
None => Err(InvalidData)
}
}
}
///|
pub fn CircuitGuard::record_success(self : CircuitGuard) -> Unit {
self.consecutive_failures = 0
self.probes = 0
self.opened_at = None
self.state = ClosedCircuit
}
///|
pub fn CircuitGuard::record_failure(
self : CircuitGuard,
now : Int,
) -> Result[Unit, ModbusError] {
if now < 0 {
return Err(InvalidData)
}
self.consecutive_failures += 1
self.probes = 0
if self.consecutive_failures >= self.failure_threshold {
self.state = OpenCircuit
self.opened_at = Some(now)
}
Ok(())
}
///|
pub fn CircuitGuard::force_open(
self : CircuitGuard,
now : Int,
) -> Result[Unit, ModbusError] {
if now < 0 {
Err(InvalidData)
} else {
self.state = OpenCircuit
self.opened_at = Some(now)
Ok(())
}
}
///|
pub fn CircuitGuard::force_close(self : CircuitGuard) -> Unit {
self.record_success()
}
///|
/// Current availability and latency indicators for a device.
pub(all) struct DeviceReliability {
device_id : String
window : ReliabilityWindow
circuit : CircuitGuard
mut last_seen : Int?
mut total_requests : Int
mut total_successes : Int
}
///|
pub fn DeviceReliability::new(
device_id : String,
window_width : Int,
failure_threshold : Int,
recovery_after : Int,
) -> Result[DeviceReliability, ModbusError] {
if device_id == "" {
return Err(InvalidData)
}
let window = match ReliabilityWindow::new(window_width) {
Ok(value) => value
Err(error) => return Err(error)
}
let circuit = match
CircuitGuard::new(device_id, failure_threshold, recovery_after) {
Ok(value) => value
Err(error) => return Err(error)
}
Ok({
device_id,
window,
circuit,
last_seen: None,
total_requests: 0,
total_successes: 0,
})
}
///|
pub fn DeviceReliability::device_id(self : DeviceReliability) -> String {
self.device_id
}
///|
pub fn DeviceReliability::last_seen(self : DeviceReliability) -> Int? {
self.last_seen
}
///|
pub fn DeviceReliability::total_requests(self : DeviceReliability) -> Int {
self.total_requests
}
///|
pub fn DeviceReliability::total_successes(self : DeviceReliability) -> Int {
self.total_successes
}
///|
pub fn DeviceReliability::state(self : DeviceReliability) -> CircuitGuardState {
self.circuit.state()
}
///|
pub fn DeviceReliability::success_rate(self : DeviceReliability) -> Int {
self.window.success_rate()
}
///|
pub fn DeviceReliability::average_latency(self : DeviceReliability) -> Int {
self.window.average_latency()
}
///|
pub fn DeviceReliability::allow_request(
self : DeviceReliability,
now : Int,
) -> Result[Bool, ModbusError] {
self.circuit.allow(now)
}
///|
/// Record one outcome and update breaker state atomically from the caller's view.
pub fn DeviceReliability::record(
self : DeviceReliability,
sample : ReliabilitySample,
) -> Result[Unit, ModbusError] {
self.total_requests += 1
if sample.success() {
self.total_successes += 1
self.circuit.record_success()
self.last_seen = Some(sample.timestamp())
} else {
match self.circuit.record_failure(sample.timestamp()) {
Err(error) => return Err(error)
Ok(_) => ()
}
}
self.window.append(sample)
}
///|
/// Return a health score from 0 (offline) to 100 (healthy).
pub fn DeviceReliability::health_score(
self : DeviceReliability,
now : Int,
) -> Result[Int, ModbusError] {
if now < 0 {
return Err(InvalidData)
}
let base = self.success_rate()
let latency_penalty = if self.average_latency() > 1000 {
30
} else {
self.average_latency() / 50
}
let stale_penalty = match self.last_seen {
Some(timestamp) =>
if now - timestamp > self.window.width() {
40
} else {
0
}
None => 60
}
let state_penalty = match self.circuit.state() {
ClosedCircuit => 0
HalfOpenCircuit => 20
OpenCircuit => 60
}
let score = base - latency_penalty - stale_penalty - state_penalty
Ok(if score < 0 { 0 } else if score > 100 { 100 } else { score })
}
///|
pub fn DeviceReliability::failure_breakdown(
self : DeviceReliability,
) -> Array[Int] {
[
self.window.failure_count(TimeoutFailure),
self.window.failure_count(ChecksumFailure),
self.window.failure_count(ProtocolFailure),
self.window.failure_count(TransportFailure),
self.window.failure_count(DeviceException),
self.window.failure_count(ValidationFailure),
]
}
///|
/// A fleet-level reliability snapshot for dashboards and alert rules.
pub(all) struct ReliabilityFleetSummary {
devices : Int
healthy : Int
degraded : Int
open_circuits : Int
average_success_rate : Int
average_latency : Int
}
///|
pub fn reliability_fleet_summary(
devices : Array[DeviceReliability],
) -> ReliabilityFleetSummary {
let mut healthy = 0
let mut degraded = 0
let mut open = 0
let mut rate = 0
let mut latency = 0
for device in devices {
if device.state() == ClosedCircuit && device.success_rate() >= 95 {
healthy += 1
} else {
degraded += 1
}
if device.state() == OpenCircuit {
open += 1
}
rate += device.success_rate()
latency += device.average_latency()
}
let count = devices.length()
{
devices: count,
healthy,
degraded,
open_circuits: open,
average_success_rate: if count == 0 {
0
} else {
rate / count
},
average_latency: if count == 0 {
0
} else {
latency / count
},
}
}
///|
pub fn ReliabilityFleetSummary::devices(self : ReliabilityFleetSummary) -> Int {
self.devices
}
///|
pub fn ReliabilityFleetSummary::healthy(self : ReliabilityFleetSummary) -> Int {
self.healthy
}
///|
pub fn ReliabilityFleetSummary::degraded(self : ReliabilityFleetSummary) -> Int {
self.degraded
}
///|
pub fn ReliabilityFleetSummary::open_circuits(
self : ReliabilityFleetSummary,
) -> Int {
self.open_circuits
}
///|
pub fn ReliabilityFleetSummary::average_success_rate(
self : ReliabilityFleetSummary,
) -> Int {
self.average_success_rate
}
///|
pub fn ReliabilityFleetSummary::average_latency(
self : ReliabilityFleetSummary,
) -> Int {
self.average_latency
}