use super::*;
pub type TaskResult = Result<(), String>;
pub type TaskCallback = Arc<dyn Fn(TaskContext) -> TaskResult + Send + Sync + 'static>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SchedulerError {
InvalidConfig(&'static str),
InvalidTaskId,
InvalidSchedule(&'static str),
InvalidCron(String),
DuplicateTask(String),
CapacityExceeded {
max_tasks: usize,
},
Shutdown,
WorkerPanicked,
}
impl fmt::Display for SchedulerError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{self:?}")
}
}
impl std::error::Error for SchedulerError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SchedulerConfig {
pub max_tasks: usize,
pub max_concurrent_tasks: usize,
pub poll_interval: Duration,
}
impl Default for SchedulerConfig {
fn default() -> Self {
Self {
max_tasks: 1_024,
max_concurrent_tasks: 4,
poll_interval: Duration::from_millis(25),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TaskSchedule {
Once {
run_at: SystemTime,
},
Interval {
every: Duration,
start_at: Option<SystemTime>,
},
Cron {
expression: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RetryPolicy {
pub max_attempts: u32,
pub initial_backoff: Duration,
pub max_backoff: Duration,
pub multiplier: u32,
pub jitter: Duration,
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_attempts: 1,
initial_backoff: Duration::from_millis(100),
max_backoff: Duration::from_secs(30),
multiplier: 2,
jitter: Duration::ZERO,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScheduledTask {
pub id: String,
pub schedule: TaskSchedule,
pub retry: RetryPolicy,
pub priority: u8,
pub trace: Option<TraceContext>,
}
impl ScheduledTask {
pub fn validate(&self) -> Result<(), SchedulerError> {
validate_task_id(&self.id)?;
validate_retry(&self.retry)?;
validate_schedule(&self.schedule)
}
}
fn validate_schedule(schedule: &TaskSchedule) -> Result<(), SchedulerError> {
match schedule {
TaskSchedule::Once { .. } => Ok(()),
TaskSchedule::Interval { every, .. } if every.is_zero() => {
Err(SchedulerError::InvalidSchedule("zero interval"))
}
TaskSchedule::Interval { .. } => Ok(()),
TaskSchedule::Cron { expression } => Schedule::from_str(expression)
.map(|_| ())
.map_err(|error| SchedulerError::InvalidCron(error.to_string())),
}
}
fn validate_retry(policy: &RetryPolicy) -> Result<(), SchedulerError> {
if policy.max_attempts == 0 {
return Err(SchedulerError::InvalidSchedule(
"max_attempts must be positive",
));
}
if policy.multiplier == 0 {
return Err(SchedulerError::InvalidSchedule(
"retry multiplier must be positive",
));
}
if policy.initial_backoff > policy.max_backoff {
return Err(SchedulerError::InvalidSchedule(
"initial_backoff exceeds max_backoff",
));
}
Ok(())
}
fn validate_task_id(task_id: &str) -> Result<(), SchedulerError> {
if task_id.is_empty()
|| task_id.len() > MAX_TASK_ID_BYTES
|| !task_id
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
{
return Err(SchedulerError::InvalidTaskId);
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TaskSnapshot {
pub id: String,
pub priority: u8,
pub running: bool,
pub attempts: u32,
pub next_run: SystemTime,
pub last_error: Option<String>,
pub trace: Option<TraceContext>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SchedulerSnapshot {
pub shutdown: bool,
pub active_tasks: usize,
pub tasks: Vec<TaskSnapshot>,
}
pub struct TaskContext {
task_id: String,
attempt: u32,
cancelled: Arc<AtomicBool>,
shutdown: Arc<AtomicBool>,
trace: Option<TraceContext>,
}
impl TaskContext {
pub(crate) fn new(
task_id: String,
attempt: u32,
cancelled: Arc<AtomicBool>,
shutdown: Arc<AtomicBool>,
trace: Option<TraceContext>,
) -> Self {
Self {
task_id,
attempt,
cancelled,
shutdown,
trace,
}
}
pub fn task_id(&self) -> &str {
&self.task_id
}
pub fn attempt(&self) -> u32 {
self.attempt
}
pub fn is_cancelled(&self) -> bool {
self.cancelled.load(Ordering::Acquire) || self.shutdown.load(Ordering::Acquire)
}
pub fn trace(&self) -> Option<&TraceContext> {
self.trace.as_ref()
}
}