use super::*;
use crate::durable::{
durable_state, ms_to_system_time, system_time_to_ms, DurableRuntime, DurableTaskState,
};
use crate::executor::FixedExecutor;
use crate::runtime_loop::coordinator_loop;
use crate::timing::{now_seed, parse_schedule, ParsedSchedule};
#[derive(Clone)]
pub struct TaskHandle {
id: String,
cancelled: Arc<AtomicBool>,
scheduler: Weak<SchedulerInner>,
}
impl TaskHandle {
pub fn id(&self) -> &str {
&self.id
}
pub fn cancel(&self) {
self.cancelled.store(true, Ordering::Release);
if let Some(scheduler) = self.scheduler.upgrade() {
scheduler.wakeup.notify_all();
}
}
pub fn is_cancelled(&self) -> bool {
self.cancelled.load(Ordering::Acquire)
}
}
pub struct Scheduler {
inner: Arc<SchedulerInner>,
coordinator: Mutex<Option<JoinHandle<()>>>,
}
pub(super) struct SchedulerInner {
pub(super) config: SchedulerConfig,
pub(super) durable: Option<DurableRuntime>,
pub(super) state: Mutex<SchedulerState>,
pub(super) wakeup: Condvar,
pub(super) shutdown: Arc<AtomicBool>,
pub(super) active: AtomicUsize,
pub(super) inflight: AtomicUsize,
pub(super) sequence: AtomicU64,
pub(super) jitter_state: AtomicU64,
pub(super) state_errors: AtomicU64,
pub(super) dispatch_limit: usize,
pub(super) executor: FixedExecutor,
}
#[derive(Default)]
pub(super) struct SchedulerState {
pub(super) tasks: HashMap<String, TaskEntry>,
}
pub(super) struct TaskEntry {
pub(super) schedule: ParsedSchedule,
pub(super) retry: RetryPolicy,
pub(super) priority: u8,
pub(super) callback: TaskCallback,
pub(super) cancelled: Arc<AtomicBool>,
pub(super) next_run: SystemTime,
pub(super) dispatched: bool,
pub(super) running: bool,
pub(super) attempts: u32,
pub(super) order: u64,
pub(super) last_error: Option<String>,
pub(super) trace: Option<TraceContext>,
pub(super) durable: Option<DurableTaskState>,
}
impl Scheduler {
pub fn new(config: SchedulerConfig) -> Result<Self, SchedulerError> {
config.validate()?;
Self::start(config, None)
}
pub fn with_state_provider(
config: SchedulerConfig,
durable_config: DurableSchedulerConfigV1,
provider: Arc<dyn SchedulerStateProvider>,
) -> Result<Self, SchedulerError> {
config.validate()?;
let durable = DurableRuntime::new(
durable_config,
provider,
config.max_tasks,
config.poll_interval,
)?;
Self::start(config, Some(durable))
}
fn start(
config: SchedulerConfig,
durable: Option<DurableRuntime>,
) -> Result<Self, SchedulerError> {
let worker_count = config.max_concurrent_tasks.min(config.max_tasks);
let dispatch_limit = worker_count.saturating_mul(2).min(config.max_tasks).max(1);
let executor = FixedExecutor::new(worker_count, dispatch_limit)
.map_err(|_| SchedulerError::WorkerPanicked)?;
let inner = Arc::new(SchedulerInner {
config,
durable,
state: Mutex::new(SchedulerState::default()),
wakeup: Condvar::new(),
shutdown: Arc::new(AtomicBool::new(false)),
active: AtomicUsize::new(0),
inflight: AtomicUsize::new(0),
sequence: AtomicU64::new(1),
jitter_state: AtomicU64::new(now_seed()),
state_errors: AtomicU64::new(0),
dispatch_limit,
executor,
});
let coordinator_inner = Arc::clone(&inner);
let coordinator = thread::Builder::new()
.name("appcore-scheduler".to_string())
.spawn(move || coordinator_loop(coordinator_inner))
.map_err(|_| SchedulerError::WorkerPanicked)?;
Ok(Self {
inner,
coordinator: Mutex::new(Some(coordinator)),
})
}
pub fn schedule(
&self,
task: ScheduledTask,
callback: TaskCallback,
) -> Result<TaskHandle, SchedulerError> {
self.schedule_inner(task, callback, None)
}
pub fn schedule_durable(
&self,
task: ScheduledTask,
misfire_policy: DurableTaskMisfirePolicyV1,
callback: TaskCallback,
) -> Result<TaskHandle, SchedulerError> {
self.schedule_inner(task, callback, Some(misfire_policy))
}
fn schedule_inner(
&self,
task: ScheduledTask,
callback: TaskCallback,
misfire_policy: Option<DurableTaskMisfirePolicyV1>,
) -> Result<TaskHandle, SchedulerError> {
if self.inner.shutdown.load(Ordering::Acquire) {
return Err(SchedulerError::Shutdown);
}
task.validate()?;
self.ensure_task_capacity(&task.id)?;
let (schedule, initial_next_run) = parse_schedule(task.schedule.clone())?;
let now_ms = system_time_to_ms(SystemTime::now())?;
let (next_run, attempts, durable) = if let Some(misfire_policy) = misfire_policy {
let runtime = self
.inner
.durable
.as_ref()
.ok_or(SchedulerError::InvalidConfig(
"state provider is not configured",
))?;
let initial_next_run_ms = system_time_to_ms(initial_next_run)?;
let record = runtime.register(&task, initial_next_run_ms, misfire_policy)?;
if record.completed {
return Ok(self.inactive_handle(task.id));
}
(
ms_to_system_time(record.next_run_ms)?,
record.attempts,
Some(durable_state(&record, now_ms)),
)
} else {
(initial_next_run, 0, None)
};
let cancelled = Arc::new(AtomicBool::new(false));
let mut state = self.inner.state.lock();
if self.inner.shutdown.load(Ordering::Acquire) {
return Err(SchedulerError::Shutdown);
}
if state.tasks.contains_key(&task.id) {
return Err(SchedulerError::DuplicateTask(task.id));
}
if state.tasks.len() >= self.inner.config.max_tasks {
return Err(SchedulerError::CapacityExceeded {
max_tasks: self.inner.config.max_tasks,
});
}
let id = task.id;
state.tasks.insert(
id.clone(),
TaskEntry {
schedule,
retry: task.retry,
priority: task.priority,
callback,
cancelled: Arc::clone(&cancelled),
next_run,
dispatched: false,
running: false,
attempts,
order: self.inner.sequence.fetch_add(1, Ordering::Relaxed),
last_error: None,
trace: task.trace,
durable,
},
);
drop(state);
self.inner.wakeup.notify_all();
Ok(TaskHandle {
id,
cancelled,
scheduler: Arc::downgrade(&self.inner),
})
}
fn ensure_task_capacity(&self, task_id: &str) -> Result<(), SchedulerError> {
let state = self.inner.state.lock();
if state.tasks.contains_key(task_id) {
return Err(SchedulerError::DuplicateTask(task_id.to_string()));
}
if state.tasks.len() >= self.inner.config.max_tasks {
return Err(SchedulerError::CapacityExceeded {
max_tasks: self.inner.config.max_tasks,
});
}
Ok(())
}
fn inactive_handle(&self, id: String) -> TaskHandle {
TaskHandle {
id,
cancelled: Arc::new(AtomicBool::new(false)),
scheduler: Arc::downgrade(&self.inner),
}
}
pub fn cancel(&self, task_id: &str) -> bool {
let state = self.inner.state.lock();
let Some(task) = state.tasks.get(task_id) else {
return false;
};
task.cancelled.store(true, Ordering::Release);
drop(state);
self.inner.wakeup.notify_all();
true
}
pub fn snapshot(&self) -> SchedulerSnapshot {
let state = self.inner.state.lock();
let mut tasks = state
.tasks
.iter()
.map(|(id, task)| TaskSnapshot {
id: id.clone(),
priority: task.priority,
running: task.running,
attempts: task.attempts,
next_run: task.next_run,
last_error: task.last_error.clone(),
trace: task.trace.clone(),
})
.collect::<Vec<_>>();
tasks.sort_by(|left, right| left.id.cmp(&right.id));
SchedulerSnapshot {
shutdown: self.inner.shutdown.load(Ordering::Acquire),
active_tasks: self.inner.active.load(Ordering::Acquire),
tasks,
}
}
pub fn worker_thread_count(&self) -> usize {
self.inner.executor.worker_count()
}
pub fn queued_task_count(&self) -> usize {
self.inner.executor.queue_depth()
}
pub fn queue_saturation_count(&self) -> u64 {
self.inner.executor.saturation_count()
}
pub fn state_provider_error_count(&self) -> u64 {
self.inner.state_errors.load(Ordering::Acquire)
}
pub fn shutdown(&self) -> Result<(), SchedulerError> {
self.inner.shutdown.store(true, Ordering::Release);
{
let state = self.inner.state.lock();
for task in state.tasks.values() {
task.cancelled.store(true, Ordering::Release);
}
}
self.inner.wakeup.notify_all();
if let Some(coordinator) = self.coordinator.lock().take() {
coordinator
.join()
.map_err(|_| SchedulerError::WorkerPanicked)?;
}
Ok(())
}
}
impl Drop for Scheduler {
fn drop(&mut self) {
let _ = self.shutdown();
}
}