///|
/// Priority used by the application-level request scheduler.
pub(all) enum JobPriority {
Background
NormalPriority
HighPriority
Emergency
} derive(Debug, Eq)
///|
/// Lifecycle state of a scheduled request.
pub(all) enum JobState {
Ready
InFlight
Waiting
Completed
Failed
Cancelled
} derive(Debug, Eq)
///|
/// Policy controlling retry count and backoff growth.
pub(all) struct RetryPolicy {
max_attempts : Int
initial_backoff : Int
max_backoff : Int
multiplier : Int
}
///|
pub fn RetryPolicy::new(
max_attempts : Int,
initial_backoff : Int,
max_backoff : Int,
multiplier? : Int = 2,
) -> Result[RetryPolicy, ModbusError] {
if max_attempts < 1 ||
initial_backoff < 0 ||
max_backoff < initial_backoff ||
multiplier < 1 {
Err(InvalidData)
} else {
Ok({ max_attempts, initial_backoff, max_backoff, multiplier })
}
}
///|
pub fn default_retry_policy() -> RetryPolicy {
{ max_attempts: 3, initial_backoff: 100, max_backoff: 5000, multiplier: 2 }
}
///|
pub fn RetryPolicy::max_attempts(self : RetryPolicy) -> Int {
self.max_attempts
}
///|
pub fn RetryPolicy::initial_backoff(self : RetryPolicy) -> Int {
self.initial_backoff
}
///|
pub fn RetryPolicy::max_backoff(self : RetryPolicy) -> Int {
self.max_backoff
}
///|
pub fn RetryPolicy::multiplier(self : RetryPolicy) -> Int {
self.multiplier
}
///|
pub fn RetryPolicy::backoff(
self : RetryPolicy,
attempt : Int,
) -> Result[Int, ModbusError] {
if attempt < 1 || attempt > self.max_attempts {
return Err(InvalidQuantity)
}
let mut result = self.initial_backoff
for _ in 1.. self.max_backoff / self.multiplier {
result = self.max_backoff
} else {
result *= self.multiplier
}
}
Ok(if result > self.max_backoff { self.max_backoff } else { result })
}
///|
pub fn RetryPolicy::can_retry(self : RetryPolicy, attempt : Int) -> Bool {
attempt < self.max_attempts
}
///|
pub fn RetryPolicy::summary(self : RetryPolicy) -> String {
"attempts=" +
self.max_attempts.to_string() +
" backoff=" +
self.initial_backoff.to_string() +
".." +
self.max_backoff.to_string() +
" multiplier=" +
self.multiplier.to_string()
}
///|
/// A request retained by the scheduler until it is dispatched or expires.
pub(all) struct ScheduledJob {
id : String
request : Frame
priority : JobPriority
period : Int
deadline : Int
policy : RetryPolicy
mut due_at : Int
mut attempts : Int
mut state : JobState
mut last_error : ModbusError?
mut last_started : Int?
mut last_finished : Int?
tag : String
}
///|
pub fn ScheduledJob::new(
id : String,
request : Frame,
due_at : Int,
period? : Int = 0,
priority? : JobPriority = NormalPriority,
deadline? : Int = 0,
policy? : RetryPolicy = default_retry_policy(),
tag? : String = "",
) -> Result[ScheduledJob, ModbusError] {
if id == "" || due_at < 0 || period < 0 || deadline < 0 {
return Err(InvalidData)
}
match validate_frame(request, false) {
Err(error) => return Err(error)
Ok(_) => ()
}
Ok({
id,
request,
priority,
period,
deadline,
policy,
due_at,
attempts: 0,
state: Ready,
last_error: None,
last_started: None,
last_finished: None,
tag,
})
}
///|
pub fn ScheduledJob::id(self : ScheduledJob) -> String {
self.id
}
///|
pub fn ScheduledJob::request(self : ScheduledJob) -> Frame {
self.request
}
///|
pub fn ScheduledJob::priority(self : ScheduledJob) -> JobPriority {
self.priority
}
///|
pub fn ScheduledJob::period(self : ScheduledJob) -> Int {
self.period
}
///|
pub fn ScheduledJob::deadline(self : ScheduledJob) -> Int {
self.deadline
}
///|
pub fn ScheduledJob::due_at(self : ScheduledJob) -> Int {
self.due_at
}
///|
pub fn ScheduledJob::attempts(self : ScheduledJob) -> Int {
self.attempts
}
///|
pub fn ScheduledJob::state(self : ScheduledJob) -> JobState {
self.state
}
///|
pub fn ScheduledJob::tag(self : ScheduledJob) -> String {
self.tag
}
///|
pub fn ScheduledJob::last_error(self : ScheduledJob) -> ModbusError? {
self.last_error
}
///|
pub fn ScheduledJob::is_periodic(self : ScheduledJob) -> Bool {
self.period > 0
}
///|
pub fn ScheduledJob::is_due(self : ScheduledJob, now : Int) -> Bool {
self.state == Ready && now >= self.due_at
}
///|
pub fn ScheduledJob::is_expired(self : ScheduledJob, now : Int) -> Bool {
self.deadline > 0 && now > self.due_at + self.deadline
}
///|
pub fn ScheduledJob::age(self : ScheduledJob, now : Int) -> Int {
if now < self.due_at {
0
} else {
now - self.due_at
}
}
///|
pub fn ScheduledJob::start(
self : ScheduledJob,
now : Int,
) -> Result[Unit, ModbusError] {
if self.state != Ready || now < self.due_at {
return Err(Busy)
}
self.state = InFlight
self.attempts += 1
self.last_started = Some(now)
Ok(())
}
///|
pub fn ScheduledJob::complete(
self : ScheduledJob,
now : Int,
) -> Result[Unit, ModbusError] {
if self.state != InFlight {
return Err(InvalidData)
}
self.last_finished = Some(now)
self.last_error = None
if self.period > 0 {
self.state = Waiting
self.due_at = now + self.period
self.attempts = 0
} else {
self.state = Completed
}
Ok(())
}
///|
pub fn ScheduledJob::fail(
self : ScheduledJob,
error : ModbusError,
now : Int,
) -> Result[Bool, ModbusError] {
if self.state != InFlight {
return Err(InvalidData)
}
self.last_error = Some(error)
self.last_finished = Some(now)
if self.policy.can_retry(self.attempts) {
let delay = self.policy.backoff(self.attempts).unwrap()
self.state = Waiting
self.due_at = now + delay
Ok(true)
} else {
self.state = Failed
Ok(false)
}
}
///|
pub fn ScheduledJob::cancel(self : ScheduledJob) -> Result[Unit, ModbusError] {
if self.state == Completed || self.state == Failed {
Err(InvalidData)
} else {
self.state = Cancelled
Ok(())
}
}
///|
pub fn ScheduledJob::reset(
self : ScheduledJob,
due_at : Int,
) -> Result[Unit, ModbusError] {
if due_at < 0 {
return Err(InvalidData)
}
self.due_at = due_at
self.attempts = 0
self.state = Ready
self.last_error = None
self.last_started = None
self.last_finished = None
Ok(())
}
///|
pub fn ScheduledJob::latency(self : ScheduledJob) -> Int? {
match (self.last_started, self.last_finished) {
(Some(start), Some(finish)) =>
Some(if finish < start { 0 } else { finish - start })
_ => None
}
}
///|
pub fn job_priority_rank(priority : JobPriority) -> Int {
match priority {
Background => 0
NormalPriority => 1
HighPriority => 2
Emergency => 3
}
}
///|
pub fn job_state_name(state : JobState) -> String {
match state {
Ready => "ready"
InFlight => "in-flight"
Waiting => "waiting"
Completed => "completed"
Failed => "failed"
Cancelled => "cancelled"
}
}
///|
/// A bounded scheduler with priority-aware due-job selection.
pub(all) struct JobScheduler {
jobs : Array[ScheduledJob]
max_jobs : Int
mut now : Int
mut dispatched : Int
mut completed : Int
mut failed : Int
mut cancelled : Int
}
///|
pub fn JobScheduler::new(
max_jobs? : Int = 256,
) -> Result[JobScheduler, ModbusError] {
if max_jobs < 1 {
Err(CapacityExceeded)
} else {
Ok({
jobs: [],
max_jobs,
now: 0,
dispatched: 0,
completed: 0,
failed: 0,
cancelled: 0,
})
}
}
///|
pub fn JobScheduler::length(self : JobScheduler) -> Int {
self.jobs.length()
}
///|
pub fn JobScheduler::now(self : JobScheduler) -> Int {
self.now
}
///|
pub fn JobScheduler::add(
self : JobScheduler,
job : ScheduledJob,
) -> Result[Unit, ModbusError] {
if self.jobs.length() >= self.max_jobs {
return Err(CapacityExceeded)
}
for current in self.jobs {
if current.id() == job.id() {
return Err(InvalidData)
}
}
self.jobs.push(job)
Ok(())
}
///|
pub fn JobScheduler::find(self : JobScheduler, id : String) -> ScheduledJob? {
for job in self.jobs {
if job.id() == id {
return Some(job)
}
}
None
}
///|
pub fn JobScheduler::get(
self : JobScheduler,
index : Int,
) -> Result[ScheduledJob, ModbusError] {
if index < 0 || index >= self.jobs.length() {
Err(InvalidAddress)
} else {
Ok(self.jobs[index])
}
}
///|
pub fn JobScheduler::snapshot(self : JobScheduler) -> Array[ScheduledJob] {
let result : Array[ScheduledJob] = []
for job in self.jobs {
result.push(job)
}
result
}
///|
pub fn JobScheduler::remove(
self : JobScheduler,
id : String,
) -> Result[ScheduledJob, ModbusError] {
for index, job in self.jobs {
if job.id() == id {
ignore(self.jobs.remove(index))
if job.state() == Cancelled {
self.cancelled += 1
}
return Ok(job)
}
}
Err(InvalidData)
}
///|
fn scheduler_better(left : ScheduledJob, right : ScheduledJob) -> Bool {
let left_rank = job_priority_rank(left.priority())
let right_rank = job_priority_rank(right.priority())
left_rank > right_rank ||
(left_rank == right_rank && left.due_at() < right.due_at())
}
///|
pub fn JobScheduler::next_due(self : JobScheduler, now : Int) -> ScheduledJob? {
let mut selected : ScheduledJob? = None
for job in self.jobs {
if job.is_due(now) {
match selected {
None => selected = Some(job)
Some(current) =>
if scheduler_better(job, current) {
selected = Some(job)
}
}
}
}
selected
}
///|
pub fn JobScheduler::dispatch(
self : JobScheduler,
now : Int,
) -> Result[ScheduledJob, ModbusError] {
self.now = if now < self.now { self.now } else { now }
let job = match self.next_due(self.now) {
Some(value) => value
None => return Err(Incomplete)
}
for index, current in self.jobs {
if current.id() == job.id() {
match current.start(self.now) {
Err(error) => return Err(error)
Ok(_) => {
self.jobs[index] = current
self.dispatched += 1
return Ok(current)
}
}
}
}
Err(InvalidData)
}
///|
pub fn JobScheduler::complete(
self : JobScheduler,
id : String,
now : Int,
) -> Result[Unit, ModbusError] {
for index, job in self.jobs {
if job.id() == id {
match job.complete(now) {
Err(error) => return Err(error)
Ok(_) => {
self.jobs[index] = job
self.completed += 1
return Ok(())
}
}
}
}
Err(InvalidData)
}
///|
pub fn JobScheduler::fail(
self : JobScheduler,
id : String,
error : ModbusError,
now : Int,
) -> Result[Bool, ModbusError] {
for index, job in self.jobs {
if job.id() == id {
let retry = match job.fail(error, now) {
Ok(value) => value
Err(error) => return Err(error)
}
self.jobs[index] = job
if !retry {
self.failed += 1
}
return Ok(retry)
}
}
Err(InvalidData)
}
///|
pub fn JobScheduler::cancel(
self : JobScheduler,
id : String,
) -> Result[Unit, ModbusError] {
for index, job in self.jobs {
if job.id() == id {
match job.cancel() {
Err(error) => return Err(error)
Ok(_) => {
self.jobs[index] = job
self.cancelled += 1
return Ok(())
}
}
}
}
Err(InvalidData)
}
///|
pub fn JobScheduler::reschedule(
self : JobScheduler,
id : String,
due_at : Int,
) -> Result[Unit, ModbusError] {
for index, job in self.jobs {
if job.id() == id {
match job.reset(due_at) {
Err(error) => return Err(error)
Ok(_) => {
self.jobs[index] = job
return Ok(())
}
}
}
}
Err(InvalidData)
}
///|
pub fn JobScheduler::due_count(self : JobScheduler, now : Int) -> Int {
let mut count = 0
for job in self.jobs {
if job.is_due(now) {
count += 1
}
}
count
}
///|
pub fn JobScheduler::state_count(self : JobScheduler, state : JobState) -> Int {
let mut count = 0
for job in self.jobs {
if job.state() == state {
count += 1
}
}
count
}
///|
pub fn JobScheduler::dispatched(self : JobScheduler) -> Int {
self.dispatched
}
///|
pub fn JobScheduler::completed(self : JobScheduler) -> Int {
self.completed
}
///|
pub fn JobScheduler::failed(self : JobScheduler) -> Int {
self.failed
}
///|
pub fn JobScheduler::cancelled(self : JobScheduler) -> Int {
self.cancelled
}
///|
pub fn JobScheduler::reset_metrics(self : JobScheduler) -> Unit {
self.dispatched = 0
self.completed = 0
self.failed = 0
self.cancelled = 0
}
///|
pub fn JobScheduler::summary(self : JobScheduler) -> String {
"jobs=" +
self.length().to_string() +
" due=" +
self.due_count(self.now).to_string() +
" dispatched=" +
self.dispatched.to_string() +
" completed=" +
self.completed.to_string() +
" failed=" +
self.failed.to_string()
}
///|
/// A compact schedule snapshot for metrics export.
pub(all) struct SchedulerSnapshot {
total : Int
ready : Int
in_flight : Int
waiting : Int
completed : Int
failed : Int
cancelled : Int
}
///|
pub fn JobScheduler::snapshot_metrics(self : JobScheduler) -> SchedulerSnapshot {
{
total: self.jobs.length(),
ready: self.state_count(Ready),
in_flight: self.state_count(InFlight),
waiting: self.state_count(Waiting),
completed: self.state_count(Completed),
failed: self.state_count(Failed),
cancelled: self.state_count(Cancelled),
}
}
///|
pub fn SchedulerSnapshot::healthy(self : SchedulerSnapshot) -> Bool {
self.failed == 0
}
///|
pub fn SchedulerSnapshot::summary(self : SchedulerSnapshot) -> String {
"total=" +
self.total.to_string() +
" ready=" +
self.ready.to_string() +
" in-flight=" +
self.in_flight.to_string() +
" waiting=" +
self.waiting.to_string() +
" completed=" +
self.completed.to_string() +
" failed=" +
self.failed.to_string()
}