1use async_trait::async_trait;
7use itertools::Itertools;
8use std::{
9 collections::{BTreeMap, HashMap},
10 fmt::Debug,
11 sync::{Arc, Mutex, RwLock},
12};
13
14use chrono::{DateTime, Utc};
15use croner::Cron;
16use std::str::FromStr;
17
18use crate::prelude::*;
19use cloudillo_types::{lock, meta_adapter};
20
21pub type TaskId = u64;
22
23pub enum TaskType {
24 Periodic,
25 Once,
26}
27
28#[derive(Debug, Clone)]
31pub struct CronSchedule {
32 expr: Box<str>,
34 cron: Cron,
36}
37
38impl CronSchedule {
39 pub fn parse(expr: &str) -> ClResult<Self> {
41 let cron = Cron::from_str(expr)
42 .map_err(|e| Error::ValidationError(format!("invalid cron expression: {}", e)))?;
43 Ok(Self { expr: expr.into(), cron })
44 }
45
46 pub fn next_execution(&self, after: Timestamp) -> ClResult<Timestamp> {
51 let dt = DateTime::<Utc>::from_timestamp(after.0, 0).unwrap_or_else(Utc::now);
52
53 self.cron
54 .find_next_occurrence(&dt, false)
55 .map(|next| Timestamp(next.timestamp()))
56 .map_err(|e| {
57 tracing::error!("Failed to find next cron occurrence for '{}': {}", self.expr, e);
58 Error::ValidationError(format!("cron next_execution failed: {}", e))
59 })
60 }
61
62 pub fn to_cron_string(&self) -> String {
64 self.expr.to_string()
65 }
66}
67
68impl PartialEq for CronSchedule {
69 fn eq(&self, other: &Self) -> bool {
70 self.expr == other.expr
71 }
72}
73
74impl Eq for CronSchedule {}
75
76#[async_trait]
77pub trait Task<S: Clone>: Send + Sync + Debug {
78 fn kind() -> &'static str
79 where
80 Self: Sized;
81 fn build(id: TaskId, context: &str) -> ClResult<Arc<dyn Task<S>>>
82 where
83 Self: Sized;
84 fn serialize(&self) -> String;
85 async fn run(&self, state: &S) -> ClResult<()>;
86
87 fn kind_of(&self) -> &'static str;
88
89 async fn on_failed(&self, _state: &S, _attempts: u16, _last_error: &str) {}
95
96 async fn on_attempt_failed(&self, _state: &S, _attempt: u16, _last_error: &str) {}
103}
104
105#[derive(Debug)]
106pub enum TaskStatus {
107 Pending,
108 Completed,
109 Failed,
110}
111
112pub struct TaskData {
113 id: TaskId,
114 kind: Box<str>,
115 status: TaskStatus,
116 input: Box<str>,
117 deps: Box<[TaskId]>,
118 retry_data: Option<Box<str>>,
119 cron_data: Option<Box<str>>,
120 next_at: Option<Timestamp>,
121}
122
123#[async_trait]
124pub trait TaskStore<S: Clone>: Send + Sync {
125 async fn add(&self, task: &TaskMeta<S>, key: Option<&str>) -> ClResult<TaskId>;
126 async fn finished(&self, id: TaskId, output: &str) -> ClResult<()>;
127 async fn load(&self) -> ClResult<Vec<TaskData>>;
128 async fn update_task_error(
129 &self,
130 task_id: TaskId,
131 output: &str,
132 next_at: Option<Timestamp>,
133 ) -> ClResult<()>;
134 async fn find_by_key(&self, key: &str) -> ClResult<Option<(TaskId, TaskData)>>;
135 async fn update_task(&self, id: TaskId, task: &TaskMeta<S>) -> ClResult<()>;
136 async fn find_completed_deps(&self, deps: &[TaskId]) -> ClResult<Vec<TaskId>>;
137}
138
139pub struct InMemoryTaskStore {
142 last_id: Mutex<TaskId>,
143}
144
145impl InMemoryTaskStore {
146 pub fn new() -> Arc<Self> {
147 Arc::new(Self { last_id: Mutex::new(0) })
148 }
149}
150
151#[async_trait]
152impl<S: Clone> TaskStore<S> for InMemoryTaskStore {
153 async fn add(&self, _task: &TaskMeta<S>, _key: Option<&str>) -> ClResult<TaskId> {
154 let mut last_id = lock!(self.last_id)?;
155 *last_id += 1;
156 Ok(*last_id)
157 }
158
159 async fn finished(&self, _id: TaskId, _output: &str) -> ClResult<()> {
160 Ok(())
161 }
162
163 async fn load(&self) -> ClResult<Vec<TaskData>> {
164 Ok(vec![])
165 }
166
167 async fn update_task_error(
168 &self,
169 _task_id: TaskId,
170 _output: &str,
171 _next_at: Option<Timestamp>,
172 ) -> ClResult<()> {
173 Ok(())
174 }
175
176 async fn find_by_key(&self, _key: &str) -> ClResult<Option<(TaskId, TaskData)>> {
177 Ok(None)
179 }
180
181 async fn update_task(&self, _id: TaskId, _task: &TaskMeta<S>) -> ClResult<()> {
182 Ok(())
184 }
185
186 async fn find_completed_deps(&self, _deps: &[TaskId]) -> ClResult<Vec<TaskId>> {
187 Ok(vec![])
188 }
189}
190
191pub struct MetaAdapterTaskStore {
194 meta_adapter: Arc<dyn meta_adapter::MetaAdapter>,
195}
196
197impl MetaAdapterTaskStore {
198 pub fn new(meta_adapter: Arc<dyn meta_adapter::MetaAdapter>) -> Arc<Self> {
199 Arc::new(Self { meta_adapter })
200 }
201}
202
203#[async_trait]
204impl<S: Clone> TaskStore<S> for MetaAdapterTaskStore {
205 async fn add(&self, task: &TaskMeta<S>, key: Option<&str>) -> ClResult<TaskId> {
206 let id = self
207 .meta_adapter
208 .create_task(task.task.kind_of(), key, &task.task.serialize(), &task.deps)
209 .await?;
210
211 if let Some(cron) = &task.cron {
213 self.meta_adapter
214 .update_task(
215 id,
216 &meta_adapter::TaskPatch {
217 cron: Patch::Value(cron.to_cron_string()),
218 ..Default::default()
219 },
220 )
221 .await?;
222 }
223
224 Ok(id)
225 }
226
227 async fn finished(&self, id: TaskId, output: &str) -> ClResult<()> {
228 self.meta_adapter.update_task_finished(id, output).await
229 }
230
231 async fn load(&self) -> ClResult<Vec<TaskData>> {
232 let tasks = self.meta_adapter.list_tasks(meta_adapter::ListTaskOptions::default()).await?;
233 let tasks = tasks
234 .into_iter()
235 .map(|t| TaskData {
236 id: t.task_id,
237 kind: t.kind,
238 status: match t.status {
239 'P' => TaskStatus::Pending,
240 'F' => TaskStatus::Completed,
241 _ => TaskStatus::Failed,
243 },
244 input: t.input,
245 deps: t.deps,
246 retry_data: t.retry,
247 cron_data: t.cron,
248 next_at: t.next_at,
249 })
250 .collect();
251 Ok(tasks)
252 }
253
254 async fn update_task_error(
255 &self,
256 task_id: TaskId,
257 output: &str,
258 next_at: Option<Timestamp>,
259 ) -> ClResult<()> {
260 self.meta_adapter.update_task_error(task_id, output, next_at).await
261 }
262
263 async fn find_by_key(&self, key: &str) -> ClResult<Option<(TaskId, TaskData)>> {
264 let task_opt = self.meta_adapter.find_task_by_key(key).await?;
265
266 match task_opt {
267 Some(t) => Ok(Some((
268 t.task_id,
269 TaskData {
270 id: t.task_id,
271 kind: t.kind,
272 status: match t.status {
273 'P' => TaskStatus::Pending,
274 'F' => TaskStatus::Completed,
275 _ => TaskStatus::Failed,
277 },
278 input: t.input,
279 deps: t.deps,
280 retry_data: t.retry,
281 cron_data: t.cron,
282 next_at: t.next_at,
283 },
284 ))),
285 None => Ok(None),
286 }
287 }
288
289 async fn update_task(&self, id: TaskId, task: &TaskMeta<S>) -> ClResult<()> {
290 use cloudillo_types::types::Patch;
291
292 let mut patch = meta_adapter::TaskPatch {
294 input: Patch::Value(task.task.serialize()),
295 next_at: match task.next_at {
296 Some(ts) => Patch::Value(ts),
297 None => Patch::Null,
298 },
299 ..Default::default()
300 };
301
302 if !task.deps.is_empty() {
304 patch.deps = Patch::Value(task.deps.clone());
305 }
306
307 if let Some(ref retry) = task.retry {
309 let retry_str = format!(
310 "{},{},{},{}",
311 task.retry_count, retry.wait_min_max.0, retry.wait_min_max.1, retry.times
312 );
313 patch.retry = Patch::Value(retry_str);
314 }
315
316 if let Some(ref cron) = task.cron {
318 patch.cron = Patch::Value(cron.to_cron_string());
319 }
320
321 self.meta_adapter.update_task(id, &patch).await
322 }
323
324 async fn find_completed_deps(&self, deps: &[TaskId]) -> ClResult<Vec<TaskId>> {
325 self.meta_adapter.find_completed_deps(deps).await
326 }
327}
328
329type TaskBuilder<S> = dyn Fn(TaskId, &str) -> ClResult<Arc<dyn Task<S>>> + Send + Sync;
331
332#[derive(Debug, Clone)]
333pub struct RetryPolicy {
334 wait_min_max: (u64, u64),
335 times: u16,
336}
337
338impl Default for RetryPolicy {
339 fn default() -> Self {
340 Self { wait_min_max: (60, 3600), times: 10 }
341 }
342}
343
344impl RetryPolicy {
345 pub fn new(wait_min_max: (u64, u64), times: u16) -> Self {
347 Self { wait_min_max, times }
348 }
349
350 pub fn calculate_backoff(&self, attempt_count: u16) -> u64 {
356 let (min, max) = self.wait_min_max;
357 let backoff = 1u64
358 .checked_shl(u32::from(attempt_count))
359 .map_or(u64::MAX, |factor| min.saturating_mul(factor));
360 backoff.min(max)
361 }
362
363 pub fn should_retry(&self, attempt_count: u16) -> bool {
365 attempt_count < self.times
366 }
367}
368
369pub struct TaskSchedulerBuilder<'a, S: Clone> {
372 scheduler: &'a Scheduler<S>,
373 task: Arc<dyn Task<S>>,
374 key: Option<String>,
375 next_at: Option<Timestamp>,
376 deps: Vec<TaskId>,
377 retry: Option<RetryPolicy>,
378 cron: Option<CronSchedule>,
379 run_on_startup: bool,
380}
381
382impl<'a, S: Clone + Send + Sync + 'static> TaskSchedulerBuilder<'a, S> {
383 fn new(scheduler: &'a Scheduler<S>, task: Arc<dyn Task<S>>) -> Self {
385 Self {
386 scheduler,
387 task,
388 key: None,
389 next_at: None,
390 deps: Vec::new(),
391 retry: None,
392 cron: None,
393 run_on_startup: false,
394 }
395 }
396
397 pub fn key(mut self, key: impl Into<String>) -> Self {
399 self.key = Some(key.into());
400 self
401 }
402
403 pub fn schedule_at(mut self, timestamp: Timestamp) -> Self {
405 self.next_at = Some(timestamp);
406 self
407 }
408
409 pub fn schedule_after(mut self, seconds: i64) -> Self {
411 self.next_at = Some(Timestamp::from_now(seconds));
412 self
413 }
414
415 pub fn depend_on(mut self, deps: Vec<TaskId>) -> Self {
417 self.deps = deps;
418 self
419 }
420
421 pub fn depends_on(mut self, dep: TaskId) -> Self {
423 self.deps.push(dep);
424 self
425 }
426
427 pub fn with_retry(mut self, policy: RetryPolicy) -> Self {
429 self.retry = Some(policy);
430 self
431 }
432
433 pub fn cron(mut self, expr: impl Into<String>) -> Self {
446 let expr = expr.into();
447 match CronSchedule::parse(&expr) {
448 Ok(cron_schedule) => {
449 self.next_at = cron_schedule.next_execution(Timestamp::now()).ok();
450 self.cron = Some(cron_schedule);
451 }
452 Err(e) => error!(
453 "scheduler: task '{}' has an unusable cron expression {:?} ({}); it will run \
454 once instead of recurring",
455 self.task.kind_of(),
456 expr,
457 e
458 ),
459 }
460 self
461 }
462
463 pub fn daily_at(mut self, hour: u8, minute: u8) -> Self {
466 if hour <= 23 && minute <= 59 {
467 let expr = format!("{} {} * * *", minute, hour);
468 if let Ok(cron_schedule) = CronSchedule::parse(&expr) {
469 self.next_at = cron_schedule.next_execution(Timestamp::now()).ok();
472 self.cron = Some(cron_schedule);
473 }
474 }
475 self
476 }
477
478 pub fn weekly_at(mut self, weekday: u8, hour: u8, minute: u8) -> Self {
482 if weekday <= 6 && hour <= 23 && minute <= 59 {
483 let expr = format!("{} {} * * {}", minute, hour, weekday);
484 if let Ok(cron_schedule) = CronSchedule::parse(&expr) {
485 self.next_at = cron_schedule.next_execution(Timestamp::now()).ok();
488 self.cron = Some(cron_schedule);
489 }
490 }
491 self
492 }
493
494 pub fn run_on_startup(mut self) -> Self {
499 self.run_on_startup = true;
500 self
501 }
502
503 pub async fn now(self) -> ClResult<TaskId> {
505 self.schedule().await
506 }
507
508 pub async fn at(mut self, ts: Timestamp) -> ClResult<TaskId> {
510 self.next_at = Some(ts);
511 self.schedule().await
512 }
513
514 pub async fn after(mut self, seconds: i64) -> ClResult<TaskId> {
516 self.next_at = Some(Timestamp::from_now(seconds));
517 self.schedule().await
518 }
519
520 pub async fn after_task(mut self, dep: TaskId) -> ClResult<TaskId> {
522 self.deps.push(dep);
523 self.schedule().await
524 }
525
526 pub async fn with_automatic_retry(mut self) -> ClResult<TaskId> {
528 self.retry = Some(RetryPolicy::default());
529 self.schedule().await
530 }
531
532 pub async fn schedule(self) -> ClResult<TaskId> {
534 self.scheduler
535 .schedule_task_impl(
536 self.task,
537 self.key.as_deref(),
538 self.next_at,
539 if self.deps.is_empty() { None } else { Some(self.deps) },
540 self.retry,
541 self.cron,
542 self.run_on_startup,
543 )
544 .await
545 }
546}
547
548#[derive(Debug, Clone)]
549pub struct TaskMeta<S: Clone> {
550 pub task: Arc<dyn Task<S>>,
551 pub next_at: Option<Timestamp>,
552 pub deps: Vec<TaskId>,
553 retry_count: u16,
554 pub retry: Option<RetryPolicy>,
555 pub cron: Option<CronSchedule>,
556 rerun_requested: bool,
570}
571
572type TaskBuilderRegistry<S> = HashMap<&'static str, Box<TaskBuilder<S>>>;
573type ScheduledTaskMap<S> = BTreeMap<(Timestamp, TaskId), TaskMeta<S>>;
574
575#[derive(Clone)]
577pub struct Scheduler<S: Clone> {
578 task_builders: Arc<RwLock<TaskBuilderRegistry<S>>>,
579 store: Arc<dyn TaskStore<S>>,
580 tasks_running: Arc<Mutex<HashMap<TaskId, TaskMeta<S>>>>,
581 tasks_waiting: Arc<Mutex<HashMap<TaskId, TaskMeta<S>>>>,
582 task_dependents: Arc<Mutex<HashMap<TaskId, Vec<TaskId>>>>,
583 tasks_scheduled: Arc<Mutex<ScheduledTaskMap<S>>>,
584 tx_finish: flume::Sender<TaskId>,
585 rx_finish: flume::Receiver<TaskId>,
586 notify_schedule: Arc<tokio::sync::Notify>,
587}
588
589impl<S: Clone + Send + Sync + 'static> Scheduler<S> {
590 pub fn new(store: Arc<dyn TaskStore<S>>) -> Arc<Self> {
591 let (tx_finish, rx_finish) = flume::unbounded();
592
593 let scheduler = Self {
594 task_builders: Arc::new(RwLock::new(HashMap::new())),
595 store,
596 tasks_running: Arc::new(Mutex::new(HashMap::new())),
597 tasks_waiting: Arc::new(Mutex::new(HashMap::new())),
598 task_dependents: Arc::new(Mutex::new(HashMap::new())),
599 tasks_scheduled: Arc::new(Mutex::new(BTreeMap::new())),
600 tx_finish,
601 rx_finish,
602 notify_schedule: Arc::new(tokio::sync::Notify::new()),
603 };
604
605 Arc::new(scheduler)
608 }
609
610 pub fn start(&self, state: S) {
611 let schedule = self.clone();
613 let stat = state.clone();
614 let rx_finish = self.rx_finish.clone();
615
616 tokio::spawn(async move {
617 while let Ok(id) = rx_finish.recv_async().await {
618 debug!("Completed task {} (notified)", id);
619
620 let Some(task_meta) = schedule.take_running(id) else {
629 warn!("Completed task {} not found in running queue", id);
630 continue;
631 };
632
633 if task_meta.rerun_requested {
637 info!("Task {} was re-requested while running; running again", id);
642 let mut rerun_meta = task_meta;
643 rerun_meta.next_at = None;
644 rerun_meta.retry_count = 0;
645 rerun_meta.rerun_requested = false;
646 if let Err(e) = schedule.add_queue(id, rerun_meta).await {
647 error!(
648 "Failed to re-queue task {} after in-flight update: {} - task lost!",
649 id, e
650 );
651 }
652 } else if let Some(cron) = &task_meta.cron {
653 match cron.next_execution(Timestamp::now()) {
654 Ok(next_at) => {
655 info!(
656 "Recurring task {} completed, scheduling next execution at {}",
657 id, next_at
658 );
659 let mut updated_meta = task_meta.clone();
660 updated_meta.next_at = Some(next_at);
661 if let Err(e) = schedule.store.update_task(id, &updated_meta).await {
663 error!("Failed to update recurring task {} next_at: {}", id, e);
664 }
665 if let Err(e) = schedule.add_queue(id, updated_meta).await {
666 error!(
667 "Failed to reschedule recurring task {}: {} - task lost!",
668 id, e
669 );
670 }
671 }
672 Err(e) => {
673 error!(
674 "Failed to calculate next execution for recurring task {}: {} - task will not reschedule",
675 id, e
676 );
677 if let Err(e) = schedule.store.finished(id, "").await {
681 error!("Failed to mark task {} as finished: {}", id, e);
682 }
683 }
684 }
685 } else if let Err(e) = schedule.store.finished(id, "").await {
686 error!("Failed to mark task {} as finished: {}", id, e);
698 }
699
700 match schedule.release_dependents(id) {
702 Ok(ready_to_spawn) => {
703 for (dep_id, dep_task_meta) in ready_to_spawn {
704 match schedule.tasks_running.lock() {
706 Ok(mut tasks_running) => {
707 tasks_running.insert(dep_id, dep_task_meta.clone());
708 }
709 Err(poisoned) => {
710 error!("Mutex poisoned: tasks_running (recovering)");
711 poisoned.into_inner().insert(dep_id, dep_task_meta.clone());
712 }
713 }
714 schedule.spawn_task(
715 stat.clone(),
716 dep_task_meta.task.clone(),
717 dep_id,
718 dep_task_meta,
719 );
720 }
721 }
722 Err(e) => {
723 error!("Failed to release dependents of task {}: {}", id, e);
724 }
725 }
726 }
727 });
728
729 let schedule = self.clone();
731 tokio::spawn(async move {
732 loop {
733 let is_empty = match schedule.tasks_scheduled.lock() {
734 Ok(guard) => guard.is_empty(),
735 Err(poisoned) => {
736 error!("Mutex poisoned: tasks_scheduled (recovering)");
737 poisoned.into_inner().is_empty()
738 }
739 };
740 if is_empty {
741 schedule.notify_schedule.notified().await;
742 }
743 let time = Timestamp::now();
744 if let Some((timestamp, _id)) = loop {
745 let mut tasks_scheduled = match schedule.tasks_scheduled.lock() {
746 Ok(guard) => guard,
747 Err(poisoned) => {
748 error!("Mutex poisoned: tasks_scheduled (recovering)");
749 poisoned.into_inner()
750 }
751 };
752 if let Some((&(timestamp, id), _)) = tasks_scheduled.first_key_value() {
753 let (timestamp, id) = (timestamp, id);
754 if timestamp <= Timestamp::now() {
755 debug!("Spawning task id {} (from schedule)", id);
756 if let Some(task) = tasks_scheduled.remove(&(timestamp, id)) {
757 let mut tasks_running = match schedule.tasks_running.lock() {
758 Ok(guard) => guard,
759 Err(poisoned) => {
760 error!("Mutex poisoned: tasks_running (recovering)");
761 poisoned.into_inner()
762 }
763 };
764 tasks_running.insert(id, task.clone());
765 schedule.spawn_task(state.clone(), task.task.clone(), id, task);
766 } else {
767 error!("Task disappeared while being removed from schedule");
768 break None;
769 }
770 } else {
771 break Some((timestamp, id));
772 }
773 } else {
774 break None;
775 }
776 } {
777 let diff = timestamp.0 - time.0;
778 let wait =
779 tokio::time::Duration::from_secs(u64::try_from(diff).unwrap_or_default());
780 tokio::select! {
781 () = tokio::time::sleep(wait) => (), () = schedule.notify_schedule.notified() => ()
782 };
783 }
784 }
785 });
786
787 let schedule = self.clone();
788 tokio::spawn(async move {
789 if let Err(e) = schedule.load().await {
793 error!("scheduler: failed to load persisted tasks: {}", e);
794 }
795 });
796 }
797
798 fn register_builder(
799 &self,
800 name: &'static str,
801 builder: &'static TaskBuilder<S>,
802 ) -> ClResult<&Self> {
803 let mut task_builders = self
804 .task_builders
805 .write()
806 .map_err(|_| Error::Internal("task_builders RwLock poisoned".into()))?;
807 task_builders.insert(name, Box::new(builder));
808 Ok(self)
809 }
810
811 pub fn register<T: Task<S>>(&self) -> ClResult<&Self> {
812 info!("Registering task type {}", T::kind());
813 self.register_builder(T::kind(), &|id: TaskId, params: &str| T::build(id, params))?;
814 Ok(self)
815 }
816
817 pub fn task(&self, task: Arc<dyn Task<S>>) -> TaskSchedulerBuilder<'_, S> {
819 TaskSchedulerBuilder::new(self, task)
820 }
821
822 #[allow(clippy::too_many_arguments)]
825 async fn schedule_task_impl(
826 &self,
827 task: Arc<dyn Task<S>>,
828 key: Option<&str>,
829 next_at: Option<Timestamp>,
830 deps: Option<Vec<TaskId>>,
831 retry: Option<RetryPolicy>,
832 cron: Option<CronSchedule>,
833 run_on_startup: bool,
834 ) -> ClResult<TaskId> {
835 let existing = if let Some(k) = key { self.store.find_by_key(k).await? } else { None };
838
839 let effective_next_at = if run_on_startup && cron.is_some() {
841 match &existing {
842 Some((_existing_id, existing_data)) => {
843 match existing_data.next_at {
848 Some(persisted) if persisted > Timestamp::now() => next_at,
849 _ => Some(Timestamp::now()),
850 }
851 }
852 None => Some(Timestamp::now()), }
854 } else {
855 next_at
856 };
857
858 let task_meta = TaskMeta {
859 task: task.clone(),
860 next_at: effective_next_at,
861 deps: deps.clone().unwrap_or_default(),
862 retry_count: 0,
863 retry,
864 cron,
865 rerun_requested: false,
866 };
867
868 if let Some(key) = key
870 && let Some((existing_id, existing_data)) = existing
871 {
872 let new_serialized = task.serialize();
873 let existing_serialized = existing_data.input.as_ref();
874 let params_changed = new_serialized != existing_serialized;
875
876 if params_changed {
877 info!(
878 "Updating recurring task '{}' (id={}) - parameters changed",
879 key, existing_id
880 );
881 debug!(" Old params: {}", existing_serialized);
882 debug!(" New params: {}", new_serialized);
883 } else {
884 info!(
885 "Recurring task '{}' already exists with identical parameters (id={})",
886 key, existing_id
887 );
888 }
889
890 let was_running = {
912 let mut running = lock!(self.tasks_running, "tasks_running")?;
913 match running.get_mut(&existing_id) {
914 Some(existing_meta) => {
915 debug!("Task {} is running; updating metadata in place", existing_id);
916 let rerun = existing_meta.rerun_requested || task_meta.cron.is_none();
917 *existing_meta = task_meta.clone();
918 existing_meta.rerun_requested = rerun;
919 true
920 }
921 None => false,
922 }
923 };
924 if was_running {
925 self.store.update_task(existing_id, &task_meta).await?;
926 return Ok(existing_id);
927 }
928
929 if params_changed {
930 self.remove_from_queues(existing_id)?;
931 }
932
933 self.store.update_task(existing_id, &task_meta).await?;
936
937 self.add_queue(existing_id, task_meta).await?;
940
941 return Ok(existing_id);
942 }
943
944 let id = self.store.add(&task_meta, key).await?;
946 self.add_queue(id, task_meta).await
947 }
948
949 pub async fn add(&self, task: Arc<dyn Task<S>>) -> ClResult<TaskId> {
950 self.task(task).now().await
951 }
952
953 pub async fn add_queue(&self, id: TaskId, task_meta: TaskMeta<S>) -> ClResult<TaskId> {
954 debug_assert!(
955 !task_meta.rerun_requested,
956 "a queued task must not carry a pending rerun request"
957 );
958 {
961 let mut running = lock!(self.tasks_running, "tasks_running")?;
962 if let Some(existing_meta) = running.get_mut(&id) {
963 debug!(
964 "Task {} is already running, updating metadata (will reschedule on completion)",
965 id
966 );
967 let rerun = existing_meta.rerun_requested;
971 *existing_meta = task_meta;
972 existing_meta.rerun_requested = rerun;
973 return Ok(id);
974 }
975 }
976
977 {
979 let mut scheduled = lock!(self.tasks_scheduled, "tasks_scheduled")?;
980 if let Some(key) = scheduled
981 .iter()
982 .find(|((_, tid), _)| *tid == id)
983 .map(|((ts, tid), _)| (*ts, *tid))
984 {
985 scheduled.remove(&key);
986 debug!("Removed existing scheduled entry for task {} before re-queueing", id);
987 }
988 }
989 {
990 let mut waiting = lock!(self.tasks_waiting, "tasks_waiting")?;
991 if waiting.remove(&id).is_some() {
992 debug!("Removed existing waiting entry for task {} before re-queueing", id);
993 }
994 }
995
996 let deps = task_meta.deps.clone();
997
998 if !deps.is_empty() && task_meta.next_at.is_some() {
1000 warn!(
1001 "Task {} has both dependencies and scheduled time - ignoring next_at, placing in waiting queue",
1002 id
1003 );
1004 lock!(self.tasks_waiting, "tasks_waiting")?.insert(id, task_meta);
1006 debug!("Task {} is waiting for {:?}", id, &deps);
1007 for dep in &deps {
1008 lock!(self.task_dependents, "task_dependents")?
1009 .entry(*dep)
1010 .or_default()
1011 .push(id);
1012 }
1013
1014 self.check_and_resolve_completed_deps(id, &deps).await?;
1015 return Ok(id);
1016 }
1017
1018 if deps.is_empty() && task_meta.next_at.unwrap_or(Timestamp(0)) < Timestamp::now() {
1019 debug!("Spawning task {}", id);
1020 lock!(self.tasks_scheduled, "tasks_scheduled")?.insert((Timestamp(0), id), task_meta);
1021 self.notify_schedule.notify_one();
1022 } else if let Some(next_at) = task_meta.next_at {
1023 debug!("Scheduling task {} for {}", id, next_at);
1024 lock!(self.tasks_scheduled, "tasks_scheduled")?.insert((next_at, id), task_meta);
1025 self.notify_schedule.notify_one();
1026 } else {
1027 lock!(self.tasks_waiting, "tasks_waiting")?.insert(id, task_meta);
1028 debug!("Task {} is waiting for {:?}", id, &deps);
1029 for dep in &deps {
1030 lock!(self.task_dependents, "task_dependents")?
1031 .entry(*dep)
1032 .or_default()
1033 .push(id);
1034 }
1035
1036 self.check_and_resolve_completed_deps(id, &deps).await?;
1037 }
1038 Ok(id)
1039 }
1040
1041 async fn check_and_resolve_completed_deps(&self, id: TaskId, deps: &[TaskId]) -> ClResult<()> {
1044 let completed_deps = self.store.find_completed_deps(deps).await?;
1045 if completed_deps.is_empty() {
1046 return Ok(());
1047 }
1048 let mut waiting = lock!(self.tasks_waiting, "tasks_waiting")?;
1049 if let Some(task_meta) = waiting.get_mut(&id) {
1050 for dep in &completed_deps {
1051 task_meta.deps.retain(|d| *d != *dep);
1052 }
1053 if task_meta.deps.is_empty()
1054 && let Some(ready_task) = waiting.remove(&id)
1055 {
1056 drop(waiting);
1057 let mut dependents = lock!(self.task_dependents, "task_dependents")?;
1058 for dep in deps {
1059 if let Some(dep_list) = dependents.get_mut(dep) {
1060 dep_list.retain(|d| *d != id);
1061 if dep_list.is_empty() {
1062 dependents.remove(dep);
1063 }
1064 }
1065 }
1066 drop(dependents);
1067 debug!("Task {} deps already completed, scheduling immediately", id);
1068 lock!(self.tasks_scheduled, "tasks_scheduled")?
1069 .insert((Timestamp(0), id), ready_task);
1070 self.notify_schedule.notify_one();
1071 }
1072 }
1073 Ok(())
1074 }
1075
1076 fn remove_from_queues(&self, task_id: TaskId) -> ClResult<Option<TaskMeta<S>>> {
1079 if let Some(task_meta) = lock!(self.tasks_waiting, "tasks_waiting")?.remove(&task_id) {
1081 debug!("Removed task {} from waiting queue for update", task_id);
1082 return Ok(Some(task_meta));
1083 }
1084
1085 {
1087 let mut scheduled = lock!(self.tasks_scheduled, "tasks_scheduled")?;
1088 if let Some(key) = scheduled
1089 .iter()
1090 .find(|((_, id), _)| *id == task_id)
1091 .map(|((ts, id), _)| (*ts, *id))
1092 && let Some(task_meta) = scheduled.remove(&key)
1093 {
1094 debug!("Removed task {} from scheduled queue for update", task_id);
1095 return Ok(Some(task_meta));
1096 }
1097 }
1098
1099 if let Some(task_meta) = lock!(self.tasks_running, "tasks_running")?.remove(&task_id) {
1101 warn!("Removed task {} from running queue during update", task_id);
1102 return Ok(Some(task_meta));
1103 }
1104
1105 Ok(None)
1106 }
1107
1108 fn release_dependents(
1111 &self,
1112 completed_task_id: TaskId,
1113 ) -> ClResult<Vec<(TaskId, TaskMeta<S>)>> {
1114 let dependents = {
1116 let mut deps_map = lock!(self.task_dependents, "task_dependents")?;
1117 deps_map.remove(&completed_task_id).unwrap_or_default()
1118 };
1119
1120 if dependents.is_empty() {
1121 return Ok(Vec::new()); }
1123
1124 debug!("Releasing {} dependents of completed task {}", dependents.len(), completed_task_id);
1125
1126 let mut ready_to_spawn = Vec::new();
1127
1128 for dependent_id in dependents {
1130 {
1132 let mut waiting = lock!(self.tasks_waiting, "tasks_waiting")?;
1133 if let Some(task_meta) = waiting.get_mut(&dependent_id) {
1134 task_meta.deps.retain(|x| *x != completed_task_id);
1136
1137 if task_meta.deps.is_empty() {
1139 if let Some(task_to_spawn) = waiting.remove(&dependent_id) {
1140 debug!(
1141 "Dependent task {} ready to spawn (all dependencies cleared)",
1142 dependent_id
1143 );
1144 ready_to_spawn.push((dependent_id, task_to_spawn));
1145 }
1146 } else {
1147 debug!(
1148 "Task {} still has {} remaining dependencies",
1149 dependent_id,
1150 task_meta.deps.len()
1151 );
1152 }
1153 continue;
1154 }
1155 }
1156
1157 {
1159 let mut scheduled = lock!(self.tasks_scheduled, "tasks_scheduled")?;
1160 if let Some(scheduled_key) = scheduled
1161 .iter()
1162 .find(|((_, id), _)| *id == dependent_id)
1163 .map(|((ts, id), _)| (*ts, *id))
1164 {
1165 if let Some(task_meta) = scheduled.get_mut(&scheduled_key) {
1166 task_meta.deps.retain(|x| *x != completed_task_id);
1167 let remaining = task_meta.deps.len();
1168 if remaining == 0 {
1169 debug!(
1170 "Task {} in scheduled queue has no remaining dependencies",
1171 dependent_id
1172 );
1173 } else {
1174 debug!(
1175 "Task {} in scheduled queue has {} remaining dependencies",
1176 dependent_id, remaining
1177 );
1178 }
1179 }
1180 continue;
1181 }
1182 }
1183
1184 warn!(
1186 "Dependent task {} of completed task {} not found in any queue",
1187 dependent_id, completed_task_id
1188 );
1189 }
1190
1191 Ok(ready_to_spawn)
1192 }
1193
1194 async fn load(&self) -> ClResult<()> {
1206 let tasks = self.store.load().await?;
1207 debug!("Loaded {} tasks from store", tasks.len());
1208 let (mut seen, mut queued, mut skipped) = (0usize, 0usize, 0usize);
1209 for t in tasks {
1210 if !matches!(t.status, TaskStatus::Pending) {
1211 continue;
1212 }
1213 seen += 1;
1214 let (id, kind) = (t.id, t.kind.clone());
1215 match self.load_one(t).await {
1216 Ok(()) => queued += 1,
1217 Err(e) => {
1218 skipped += 1;
1219 error!("scheduler: skipping persisted task {} ({}): {}", id, kind, e);
1220 }
1221 }
1222 }
1223 info!("scheduler: loaded {} pending tasks, {} queued, {} skipped", seen, queued, skipped);
1224 Ok(())
1225 }
1226
1227 async fn load_one(&self, t: TaskData) -> ClResult<()> {
1229 debug!("Loading task {} {}", t.id, t.kind);
1230 let task = {
1231 let builder_map = self
1232 .task_builders
1233 .read()
1234 .map_err(|_| Error::Internal("task_builders RwLock poisoned".into()))?;
1235 let builder = builder_map
1236 .get(t.kind.as_ref())
1237 .ok_or(Error::Internal(format!("task builder not registered: {}", t.kind)))?;
1238 builder(t.id, &t.input)?
1239 };
1240 let (retry_count, retry) = match t.retry_data {
1241 Some(retry_str) => {
1242 let (retry_count, retry_min, retry_max, retry_times) = retry_str
1243 .split(',')
1244 .collect_tuple()
1245 .ok_or(Error::Internal("invalid retry policy format".into()))?;
1246 let retry_count: u16 = retry_count
1247 .parse()
1248 .map_err(|_| Error::Internal("retry count must be u16".into()))?;
1249 let retry = RetryPolicy {
1250 wait_min_max: (
1251 retry_min
1252 .parse()
1253 .map_err(|_| Error::Internal("retry_min must be u64".into()))?,
1254 retry_max
1255 .parse()
1256 .map_err(|_| Error::Internal("retry_max must be u64".into()))?,
1257 ),
1258 times: retry_times
1259 .parse()
1260 .map_err(|_| Error::Internal("retry times must be u64".into()))?,
1261 };
1262 debug!("Loaded retry policy: {:?}", retry);
1263 (retry_count, Some(retry))
1264 }
1265 _ => (0, None),
1266 };
1267 let cron = match t.cron_data.as_deref() {
1271 Some(cron_str) => match CronSchedule::parse(cron_str) {
1272 Ok(cron) => Some(cron),
1273 Err(e) => {
1274 error!(
1275 "scheduler: persisted task {} ({}) has an unusable cron expression \
1276 {:?} ({}); it will not recur",
1277 t.id, t.kind, cron_str, e
1278 );
1279 None
1280 }
1281 },
1282 None => None,
1283 };
1284
1285 let task_meta = TaskMeta {
1286 task,
1287 next_at: t.next_at,
1288 deps: t.deps.into(),
1289 retry_count,
1290 retry,
1291 cron,
1292 rerun_requested: false,
1293 };
1294 self.add_queue(t.id, task_meta).await.map(|_| ())
1295 }
1296
1297 fn take_running(&self, id: TaskId) -> Option<TaskMeta<S>> {
1308 match self.tasks_running.lock() {
1309 Ok(mut running) => running.remove(&id),
1310 Err(poisoned) => {
1311 error!("Mutex poisoned: tasks_running (recovering)");
1312 poisoned.into_inner().remove(&id)
1313 }
1314 }
1315 }
1316
1317 fn clear_rerun_request(&self, id: TaskId) {
1325 let mut running = match self.tasks_running.lock() {
1326 Ok(guard) => guard,
1327 Err(poisoned) => {
1328 error!("Mutex poisoned: tasks_running (recovering)");
1329 poisoned.into_inner()
1330 }
1331 };
1332 if let Some(meta) = running.get_mut(&id) {
1333 meta.rerun_requested = false;
1334 }
1335 }
1336
1337 fn spawn_task(&self, state: S, task: Arc<dyn Task<S>>, id: TaskId, task_meta: TaskMeta<S>) {
1338 let tx_finish = self.tx_finish.clone();
1339 let store = self.store.clone();
1340 let scheduler = self.clone();
1341 tokio::spawn(async move {
1343 match task.run(&state).await {
1344 Ok(()) => {
1345 debug!("Task {} completed successfully", id);
1346 tx_finish.send(id).unwrap_or(());
1347 }
1348 Err(e) => {
1349 let is_retryable = e.is_retryable();
1350 if let Some(retry_policy) = &task_meta.retry {
1351 if is_retryable && retry_policy.should_retry(task_meta.retry_count) {
1352 let backoff = retry_policy.calculate_backoff(task_meta.retry_count);
1353 let next_at = Timestamp::from_now(backoff.cast_signed());
1354
1355 info!(
1356 "Task {} failed (attempt {}/{}). Scheduling retry in {} seconds: {}",
1357 id,
1358 task_meta.retry_count + 1,
1359 retry_policy.times,
1360 backoff,
1361 e
1362 );
1363
1364 if let Err(err) =
1366 store.update_task_error(id, &e.to_string(), Some(next_at)).await
1367 {
1368 error!(
1369 "Failed to persist error for task {}: {} - retry not durable",
1370 id, err
1371 );
1372 }
1373
1374 task.on_attempt_failed(&state, task_meta.retry_count, &e.to_string())
1375 .await;
1376
1377 let current_meta = match scheduler.tasks_running.lock() {
1388 Ok(mut tasks_running) => tasks_running.remove(&id),
1389 Err(poisoned) => {
1390 error!("Mutex poisoned: tasks_running (recovering)");
1391 poisoned.into_inner().remove(&id)
1392 }
1393 };
1394
1395 let mut retry_meta = current_meta.unwrap_or_else(|| task_meta.clone());
1401 retry_meta.retry_count = task_meta.retry_count + 1;
1402 retry_meta.next_at = Some(next_at);
1403 retry_meta.rerun_requested = false;
1410
1411 if let Err(err) = scheduler.add_queue(id, retry_meta).await {
1412 error!(
1413 "Failed to queue retry for task {}: {} - task lost!",
1414 id, err
1415 );
1416 }
1417 } else {
1418 if is_retryable {
1420 error!(
1421 "Task {} failed after {} retries: {}",
1422 id, task_meta.retry_count, e
1423 );
1424 } else {
1425 error!("Task {} failed permanently (non-retryable): {}", id, e);
1426 }
1427 let next_at = task_meta
1434 .cron
1435 .as_ref()
1436 .and_then(|c| c.next_execution(Timestamp::now()).ok());
1437 if let Err(err) =
1438 store.update_task_error(id, &e.to_string(), next_at).await
1439 {
1440 error!(
1441 "Failed to persist error for task {}: {} - retry not durable",
1442 id, err
1443 );
1444 }
1445 task.on_failed(&state, task_meta.retry_count, &e.to_string()).await;
1446 scheduler.clear_rerun_request(id);
1456 tx_finish.send(id).unwrap_or(());
1457 }
1458 } else {
1459 error!("Task {} failed: {}", id, e);
1463 let next_at = task_meta
1467 .cron
1468 .as_ref()
1469 .and_then(|c| c.next_execution(Timestamp::now()).ok());
1470 if let Err(err) = store.update_task_error(id, &e.to_string(), next_at).await
1471 {
1472 error!(
1473 "Failed to persist error for task {}: {} - retry not durable",
1474 id, err
1475 );
1476 }
1477 task.on_failed(&state, 0, &e.to_string()).await;
1478 scheduler.clear_rerun_request(id);
1479 tx_finish.send(id).unwrap_or(());
1480 }
1481 }
1482 }
1483 });
1484 }
1485
1486 pub async fn health_check(&self) -> ClResult<SchedulerHealth> {
1489 let waiting_count = lock!(self.tasks_waiting, "tasks_waiting")?.len();
1490 let scheduled_count = lock!(self.tasks_scheduled, "tasks_scheduled")?.len();
1491 let running_count = lock!(self.tasks_running, "tasks_running")?.len();
1492 let dependents_count = lock!(self.task_dependents, "task_dependents")?.len();
1493
1494 let mut stuck_tasks = Vec::new();
1496 let mut tasks_with_missing_deps = Vec::new();
1497
1498 {
1500 let waiting = lock!(self.tasks_waiting, "tasks_waiting")?;
1501 let _deps_map = lock!(self.task_dependents, "task_dependents")?;
1502
1503 for (id, task_meta) in waiting.iter() {
1504 if task_meta.deps.is_empty() {
1505 stuck_tasks.push(*id);
1506 warn!("SCHEDULER HEALTH: Task {} in waiting with no dependencies", id);
1507 } else {
1508 for dep in &task_meta.deps {
1514 let dep_exists = waiting.contains_key(dep)
1515 || self.tasks_running.lock().ok().is_some_and(|r| r.contains_key(dep))
1516 || self
1517 .tasks_scheduled
1518 .lock()
1519 .ok()
1520 .is_some_and(|s| s.iter().any(|((_, task_id), _)| task_id == dep));
1521
1522 if !dep_exists {
1523 tasks_with_missing_deps.push((*id, *dep));
1524 warn!(
1525 "SCHEDULER HEALTH: Task {} depends on non-existent task {}",
1526 id, dep
1527 );
1528 }
1529 }
1530 }
1531 }
1532 }
1533
1534 Ok(SchedulerHealth {
1535 waiting: waiting_count,
1536 scheduled: scheduled_count,
1537 running: running_count,
1538 dependents: dependents_count,
1539 stuck_tasks,
1540 tasks_with_missing_deps,
1541 })
1542 }
1543}
1544
1545#[derive(Debug, Clone)]
1547pub struct SchedulerHealth {
1548 pub waiting: usize,
1550 pub scheduled: usize,
1552 pub running: usize,
1554 pub dependents: usize,
1556 pub stuck_tasks: Vec<TaskId>,
1558 pub tasks_with_missing_deps: Vec<(TaskId, TaskId)>,
1560}
1561
1562#[cfg(test)]
1563mod tests {
1564 use super::*;
1565 use serde::{Deserialize, Serialize};
1566
1567 type State = Arc<Mutex<Vec<u8>>>;
1568
1569 #[derive(Debug, Serialize, Deserialize)]
1570 struct TestTask {
1571 num: u8,
1572 }
1573
1574 impl TestTask {
1575 pub fn new(num: u8) -> Arc<Self> {
1576 Arc::new(Self { num })
1577 }
1578 }
1579
1580 #[async_trait]
1581 impl Task<State> for TestTask {
1582 fn kind() -> &'static str {
1583 "test"
1584 }
1585
1586 fn build(_id: TaskId, ctx: &str) -> ClResult<Arc<dyn Task<State>>> {
1587 let num: u8 = ctx
1588 .parse()
1589 .map_err(|_| Error::Internal("test task context must be u8".into()))?;
1590 let task = TestTask::new(num);
1591 Ok(task)
1592 }
1593
1594 fn serialize(&self) -> String {
1595 self.num.to_string()
1596 }
1597
1598 fn kind_of(&self) -> &'static str {
1599 "test"
1600 }
1601
1602 async fn run(&self, state: &State) -> ClResult<()> {
1603 info!("Running task {}", self.num);
1604 tokio::time::sleep(std::time::Duration::from_millis(200 * u64::from(self.num))).await;
1605 info!("Completed task {}", self.num);
1606 state.lock().unwrap().push(self.num);
1607 Ok(())
1608 }
1609 }
1610
1611 #[derive(Debug, Clone)]
1612 struct FailingTask {
1613 id: u8,
1614 fail_count: u8,
1615 attempt: Arc<Mutex<u8>>,
1616 retried: Arc<Mutex<Vec<u16>>>,
1618 gave_up: Arc<Mutex<Vec<u16>>>,
1620 }
1621
1622 impl FailingTask {
1623 pub fn new(id: u8, fail_count: u8) -> Arc<Self> {
1624 Arc::new(Self {
1625 id,
1626 fail_count,
1627 attempt: Arc::new(Mutex::new(0)),
1628 retried: Arc::new(Mutex::new(Vec::new())),
1629 gave_up: Arc::new(Mutex::new(Vec::new())),
1630 })
1631 }
1632 }
1633
1634 #[async_trait]
1635 impl Task<State> for FailingTask {
1636 fn kind() -> &'static str {
1637 "failing"
1638 }
1639
1640 fn build(_id: TaskId, ctx: &str) -> ClResult<Arc<dyn Task<State>>> {
1641 let parts: Vec<&str> = ctx.split(',').collect();
1642 if parts.len() != 2 {
1643 return Err(Error::Internal("failing task context must have 2 parts".into()));
1644 }
1645 let id: u8 = parts[0]
1646 .parse()
1647 .map_err(|_| Error::Internal("failing task id must be u8".into()))?;
1648 let fail_count: u8 = parts[1]
1649 .parse()
1650 .map_err(|_| Error::Internal("failing task fail_count must be u8".into()))?;
1651 Ok(FailingTask::new(id, fail_count))
1652 }
1653
1654 fn serialize(&self) -> String {
1655 format!("{},{}", self.id, self.fail_count)
1656 }
1657
1658 fn kind_of(&self) -> &'static str {
1659 "failing"
1660 }
1661
1662 async fn run(&self, state: &State) -> ClResult<()> {
1663 let mut attempt = self.attempt.lock().unwrap();
1664 *attempt += 1;
1665 let current_attempt = *attempt;
1666
1667 info!("FailingTask {} - attempt {}/{}", self.id, current_attempt, self.fail_count + 1);
1668
1669 if current_attempt <= self.fail_count {
1670 error!("FailingTask {} failed on attempt {}", self.id, current_attempt);
1671 return Err(Error::ServiceUnavailable(format!("Task {} failed", self.id)));
1672 }
1673
1674 info!("FailingTask {} succeeded on attempt {}", self.id, current_attempt);
1675 state.lock().unwrap().push(self.id);
1676 Ok(())
1677 }
1678
1679 async fn on_attempt_failed(&self, _state: &State, attempt: u16, _last_error: &str) {
1680 self.retried.lock().unwrap().push(attempt);
1681 }
1682
1683 async fn on_failed(&self, _state: &State, attempts: u16, _last_error: &str) {
1684 self.gave_up.lock().unwrap().push(attempts);
1685 }
1686 }
1687
1688 #[test]
1689 fn test_calculate_backoff() {
1690 let policy = RetryPolicy::new((10, 43200), 50);
1691 assert_eq!(policy.calculate_backoff(0), 10);
1692 assert_eq!(policy.calculate_backoff(1), 20);
1693 assert_eq!(policy.calculate_backoff(4), 160);
1694 assert_eq!(policy.calculate_backoff(20), 43200);
1696
1697 let wide = RetryPolicy::new((10, 43200), 200);
1700 assert_eq!(wide.calculate_backoff(63), 43200);
1701 assert_eq!(wide.calculate_backoff(64), 43200);
1702 assert_eq!(wide.calculate_backoff(200), 43200);
1703 assert_eq!(wide.calculate_backoff(u16::MAX), 43200);
1704 }
1705
1706 #[tokio::test]
1707 pub async fn test_scheduler() {
1708 let _ = tracing_subscriber::fmt().try_init();
1709
1710 let task_store: Arc<dyn TaskStore<State>> = InMemoryTaskStore::new();
1711 let state: State = Arc::new(Mutex::new(Vec::new()));
1712 let scheduler = Scheduler::new(task_store);
1713 scheduler.start(state.clone());
1714 scheduler.register::<TestTask>().unwrap();
1715
1716 let _task1 = TestTask::new(1);
1717 let task2 = TestTask::new(1);
1718 let task3 = TestTask::new(1);
1719
1720 let task2_id = scheduler.task(task2).schedule_after(2).schedule().await.unwrap();
1721 let task3_id = scheduler.add(task3).await.unwrap();
1722 scheduler
1723 .task(TestTask::new(1))
1724 .depend_on(vec![task2_id, task3_id])
1725 .schedule()
1726 .await
1727 .unwrap();
1728
1729 tokio::time::sleep(std::time::Duration::from_secs(4)).await;
1730 let task4 = TestTask::new(1);
1731 let task5 = TestTask::new(1);
1732 scheduler.task(task4).schedule_after(2).schedule().await.unwrap();
1733 scheduler.task(task5).schedule_after(1).schedule().await.unwrap();
1734
1735 tokio::time::sleep(std::time::Duration::from_secs(3)).await;
1736
1737 let st = state.lock().unwrap();
1738 info!("res: {}", st.len());
1739 let str_vec = st.iter().map(std::string::ToString::to_string).collect::<Vec<String>>();
1740 assert_eq!(str_vec.join(":"), "1:1:1:1:1");
1741 }
1742
1743 #[tokio::test]
1744 pub async fn test_retry_with_backoff() {
1745 let _ = tracing_subscriber::fmt().try_init();
1746
1747 let task_store: Arc<dyn TaskStore<State>> = InMemoryTaskStore::new();
1748 let state: State = Arc::new(Mutex::new(Vec::new()));
1749 let scheduler = Scheduler::new(task_store);
1750 scheduler.start(state.clone());
1751 scheduler.register::<FailingTask>().unwrap();
1752
1753 let failing_task = FailingTask::new(42, 2);
1756 let retried = failing_task.retried.clone();
1757 let gave_up = failing_task.gave_up.clone();
1758 let retry_policy = RetryPolicy { wait_min_max: (1, 3600), times: 3 };
1759
1760 scheduler.task(failing_task).with_retry(retry_policy).schedule().await.unwrap();
1761
1762 tokio::time::sleep(std::time::Duration::from_secs(6)).await;
1769
1770 {
1771 let st = state.lock().unwrap();
1772 assert_eq!(st.len(), 1, "Task should have succeeded after retries");
1773 assert_eq!(st[0], 42);
1774 }
1775
1776 assert_eq!(
1780 retried.lock().unwrap().as_slice(),
1781 &[0, 1],
1782 "Both retried failures should report their zero-based attempt index"
1783 );
1784 assert!(gave_up.lock().unwrap().is_empty(), "on_failed is only for terminal failures");
1785 }
1786
1787 #[tokio::test]
1790 pub async fn test_builder_simple_schedule() {
1791 let task_store: Arc<dyn TaskStore<State>> = InMemoryTaskStore::new();
1792 let state: State = Arc::new(Mutex::new(Vec::new()));
1793 let scheduler = Scheduler::new(task_store);
1794 scheduler.start(state.clone());
1795 scheduler.register::<TestTask>().unwrap();
1796
1797 let task = TestTask::new(1);
1799 let id = scheduler.task(task).now().await.unwrap();
1800
1801 assert!(id > 0, "Task ID should be positive");
1802
1803 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
1804
1805 let st = state.lock().unwrap();
1806 assert_eq!(st.len(), 1, "Task should have executed");
1807 assert_eq!(st[0], 1);
1808 }
1809
1810 #[tokio::test]
1811 pub async fn test_builder_with_key() {
1812 let task_store: Arc<dyn TaskStore<State>> = InMemoryTaskStore::new();
1813 let state: State = Arc::new(Mutex::new(Vec::new()));
1814 let scheduler = Scheduler::new(task_store);
1815 scheduler.start(state.clone());
1816 scheduler.register::<TestTask>().unwrap();
1817
1818 let task = TestTask::new(1);
1820 let _id = scheduler.task(task).key("my-task-key").now().await.unwrap();
1821
1822 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
1823
1824 let st = state.lock().unwrap();
1825 assert_eq!(st.len(), 1);
1826 assert_eq!(st[0], 1);
1827 }
1828
1829 #[tokio::test]
1830 pub async fn test_builder_with_delay() {
1831 let task_store: Arc<dyn TaskStore<State>> = InMemoryTaskStore::new();
1832 let state: State = Arc::new(Mutex::new(Vec::new()));
1833 let scheduler = Scheduler::new(task_store);
1834 scheduler.start(state.clone());
1835 scheduler.register::<TestTask>().unwrap();
1836
1837 let task = TestTask::new(1);
1839 let _id = scheduler
1840 .task(task)
1841 .after(1) .await
1843 .unwrap();
1844
1845 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
1847 {
1848 let st = state.lock().unwrap();
1849 assert_eq!(st.len(), 0, "Task should not execute yet");
1850 }
1851
1852 tokio::time::sleep(std::time::Duration::from_millis(800)).await;
1854
1855 {
1856 let st = state.lock().unwrap();
1857 assert_eq!(st.len(), 1, "Task should have executed");
1858 assert_eq!(st[0], 1);
1859 }
1860 }
1861
1862 #[tokio::test]
1863 pub async fn test_builder_with_dependencies() {
1864 let task_store: Arc<dyn TaskStore<State>> = InMemoryTaskStore::new();
1865 let state: State = Arc::new(Mutex::new(Vec::new()));
1866 let scheduler = Scheduler::new(task_store);
1867 scheduler.start(state.clone());
1868 scheduler.register::<TestTask>().unwrap();
1869
1870 let task1 = TestTask::new(1);
1872 let id1 = scheduler.task(task1).now().await.unwrap();
1873
1874 let task2 = TestTask::new(1);
1876 let id2 = scheduler.task(task2).now().await.unwrap();
1877
1878 let task3 = TestTask::new(1);
1880 let _id3 = scheduler.task(task3).depend_on(vec![id1, id2]).schedule().await.unwrap();
1881
1882 tokio::time::sleep(std::time::Duration::from_millis(1500)).await;
1884
1885 let st = state.lock().unwrap();
1886 let str_vec = st.iter().map(std::string::ToString::to_string).collect::<Vec<String>>();
1888 assert_eq!(str_vec.join(":"), "1:1:1");
1889 }
1890
1891 #[tokio::test]
1892 pub async fn test_builder_with_retry() {
1893 let task_store: Arc<dyn TaskStore<State>> = InMemoryTaskStore::new();
1894 let state: State = Arc::new(Mutex::new(Vec::new()));
1895 let scheduler = Scheduler::new(task_store);
1896 scheduler.start(state.clone());
1897 scheduler.register::<FailingTask>().unwrap();
1898
1899 let failing_task = FailingTask::new(55, 1); let retry_policy = RetryPolicy { wait_min_max: (1, 3600), times: 3 };
1902
1903 let _id = scheduler.task(failing_task).with_retry(retry_policy).schedule().await.unwrap();
1904
1905 tokio::time::sleep(std::time::Duration::from_secs(3)).await;
1907
1908 let st = state.lock().unwrap();
1909 assert_eq!(st.len(), 1);
1910 assert_eq!(st[0], 55);
1911 }
1912
1913 #[tokio::test]
1914 pub async fn test_builder_with_automatic_retry() {
1915 let task_store: Arc<dyn TaskStore<State>> = InMemoryTaskStore::new();
1916 let state: State = Arc::new(Mutex::new(Vec::new()));
1917 let scheduler = Scheduler::new(task_store);
1918 scheduler.start(state.clone());
1919 scheduler.register::<FailingTask>().unwrap();
1920
1921 let failing_task = FailingTask::new(66, 1);
1923 let _id = scheduler.task(failing_task).with_automatic_retry().await.unwrap();
1924
1925 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
1928
1929 let st = state.lock().unwrap();
1931 let _ = st.len(); }
1935
1936 #[tokio::test]
1937 pub async fn test_builder_fluent_chaining() {
1938 let task_store: Arc<dyn TaskStore<State>> = InMemoryTaskStore::new();
1939 let state: State = Arc::new(Mutex::new(Vec::new()));
1940 let scheduler = Scheduler::new(task_store);
1941 scheduler.start(state.clone());
1942 scheduler.register::<TestTask>().unwrap();
1943
1944 let dep1 = scheduler.task(TestTask::new(1)).now().await.unwrap();
1946 let dep2 = scheduler.task(TestTask::new(1)).now().await.unwrap();
1947
1948 let retry_policy = RetryPolicy { wait_min_max: (1, 3600), times: 3 };
1950
1951 let task = TestTask::new(1);
1952 let _id = scheduler
1953 .task(task)
1954 .key("complex-task")
1955 .schedule_after(0) .depend_on(vec![dep1, dep2])
1957 .with_retry(retry_policy)
1958 .schedule()
1959 .await
1960 .unwrap();
1961
1962 tokio::time::sleep(std::time::Duration::from_millis(800)).await;
1963
1964 let st = state.lock().unwrap();
1965 let str_vec = st.iter().map(std::string::ToString::to_string).collect::<Vec<String>>();
1967 assert_eq!(str_vec.join(":"), "1:1:1");
1968 }
1969
1970 #[tokio::test]
1971 pub async fn test_builder_backward_compatibility() {
1972 let task_store: Arc<dyn TaskStore<State>> = InMemoryTaskStore::new();
1973 let state: State = Arc::new(Mutex::new(Vec::new()));
1974 let scheduler = Scheduler::new(task_store);
1975 scheduler.start(state.clone());
1976 scheduler.register::<TestTask>().unwrap();
1977
1978 let _id1 = scheduler.add(TestTask::new(1)).await.unwrap();
1980
1981 let _id2 = scheduler.task(TestTask::new(1)).now().await.unwrap();
1983
1984 tokio::time::sleep(std::time::Duration::from_millis(800)).await;
1985
1986 let st = state.lock().unwrap();
1987 assert_eq!(st.len(), 2);
1989 let str_vec = st.iter().map(std::string::ToString::to_string).collect::<Vec<String>>();
1990 assert_eq!(str_vec.join(":"), "1:1");
1991 }
1992
1993 #[tokio::test]
1996 pub async fn test_builder_pipeline_scenario() {
1997 let task_store: Arc<dyn TaskStore<State>> = InMemoryTaskStore::new();
1999 let state: State = Arc::new(Mutex::new(Vec::new()));
2000 let scheduler = Scheduler::new(task_store);
2001 scheduler.start(state.clone());
2002 scheduler.register::<TestTask>().unwrap();
2003
2004 let id1 = scheduler.task(TestTask::new(1)).key("stage-1").now().await.unwrap();
2006
2007 let id2 = scheduler.task(TestTask::new(1)).key("stage-2").after_task(id1).await.unwrap();
2009
2010 let _id3 = scheduler.task(TestTask::new(1)).key("stage-3").after_task(id2).await.unwrap();
2012
2013 tokio::time::sleep(std::time::Duration::from_millis(1200)).await;
2015
2016 let st = state.lock().unwrap();
2017 let str_vec = st.iter().map(std::string::ToString::to_string).collect::<Vec<String>>();
2019 assert_eq!(str_vec.join(":"), "1:1:1");
2020 }
2021
2022 #[tokio::test]
2023 pub async fn test_builder_multi_dependency_join() {
2024 let task_store: Arc<dyn TaskStore<State>> = InMemoryTaskStore::new();
2026 let state: State = Arc::new(Mutex::new(Vec::new()));
2027 let scheduler = Scheduler::new(task_store);
2028 scheduler.start(state.clone());
2029 scheduler.register::<TestTask>().unwrap();
2030
2031 let id1 = scheduler.task(TestTask::new(1)).now().await.unwrap();
2033 let id2 = scheduler.task(TestTask::new(1)).now().await.unwrap();
2034
2035 let _id3 = scheduler
2037 .task(TestTask::new(1))
2038 .depend_on(vec![id1, id2])
2039 .schedule()
2040 .await
2041 .unwrap();
2042
2043 tokio::time::sleep(std::time::Duration::from_secs(1)).await;
2044
2045 let st = state.lock().unwrap();
2046 let str_vec = st.iter().map(std::string::ToString::to_string).collect::<Vec<String>>();
2048 assert_eq!(str_vec.join(":"), "1:1:1");
2049 }
2050
2051 #[tokio::test]
2052 pub async fn test_builder_scheduled_task_with_dependencies() {
2053 let task_store: Arc<dyn TaskStore<State>> = InMemoryTaskStore::new();
2055 let state: State = Arc::new(Mutex::new(Vec::new()));
2056 let scheduler = Scheduler::new(task_store);
2057 scheduler.start(state.clone());
2058 scheduler.register::<TestTask>().unwrap();
2059
2060 let dep_id = scheduler.task(TestTask::new(1)).now().await.unwrap();
2062
2063 let ts = Timestamp::from_now(1);
2065 let _task_id = scheduler
2066 .task(TestTask::new(1))
2067 .schedule_at(ts)
2068 .depend_on(vec![dep_id])
2069 .schedule()
2070 .await
2071 .unwrap();
2072
2073 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
2075 {
2076 let st = state.lock().unwrap();
2077 assert_eq!(st.len(), 1); }
2079
2080 tokio::time::sleep(std::time::Duration::from_millis(800)).await;
2082
2083 {
2084 let st = state.lock().unwrap();
2085 let str_vec = st.iter().map(std::string::ToString::to_string).collect::<Vec<String>>();
2086 assert_eq!(str_vec.join(":"), "1:1");
2087 }
2088 }
2089
2090 #[tokio::test]
2091 pub async fn test_builder_mixed_features() {
2092 let task_store: Arc<dyn TaskStore<State>> = InMemoryTaskStore::new();
2094 let state: State = Arc::new(Mutex::new(Vec::new()));
2095 let scheduler = Scheduler::new(task_store);
2096 scheduler.start(state.clone());
2097 scheduler.register::<TestTask>().unwrap();
2098 scheduler.register::<FailingTask>().unwrap();
2099
2100 let id1 = scheduler.task(TestTask::new(1)).now().await.unwrap();
2102
2103 let _id2 = scheduler
2105 .task(TestTask::new(1))
2106 .key("critical-task")
2107 .schedule_after(0)
2108 .depend_on(vec![id1])
2109 .schedule()
2110 .await
2111 .unwrap();
2112
2113 let _id3 = scheduler
2115 .task(FailingTask::new(1, 0)) .key("retryable-task")
2117 .with_retry(RetryPolicy {
2118 wait_min_max: (1, 3600),
2119 times: 3,
2120 })
2121 .schedule()
2122 .await
2123 .unwrap();
2124
2125 tokio::time::sleep(std::time::Duration::from_millis(1200)).await;
2127
2128 let st = state.lock().unwrap();
2129 let str_vec = st.iter().map(std::string::ToString::to_string).collect::<Vec<String>>();
2131 assert_eq!(str_vec.join(":"), "1:1:1");
2132 }
2133
2134 #[tokio::test]
2135 pub async fn test_builder_builder_reuse_not_possible() {
2136 let task_store: Arc<dyn TaskStore<State>> = InMemoryTaskStore::new();
2138 let _state: State = Arc::new(Mutex::new(Vec::new()));
2139 let scheduler = Scheduler::new(task_store);
2140
2141 let task = TestTask::new(1);
2142 let builder = scheduler.task(task);
2143
2144 let _id = builder.now().await.unwrap();
2150 }
2154
2155 #[tokio::test]
2156 pub async fn test_builder_different_task_types() {
2157 let task_store: Arc<dyn TaskStore<State>> = InMemoryTaskStore::new();
2159 let state: State = Arc::new(Mutex::new(Vec::new()));
2160 let scheduler = Scheduler::new(task_store);
2161 scheduler.start(state.clone());
2162 scheduler.register::<TestTask>().unwrap();
2163 scheduler.register::<FailingTask>().unwrap();
2164
2165 let _id1 = scheduler.task(TestTask::new(1)).key("test-task").now().await.unwrap();
2167
2168 let _id2 = scheduler
2169 .task(FailingTask::new(1, 0)) .key("failing-task")
2171 .now()
2172 .await
2173 .unwrap();
2174
2175 let _id3 = scheduler.task(TestTask::new(1)).now().await.unwrap();
2176
2177 tokio::time::sleep(std::time::Duration::from_secs(1)).await;
2178
2179 let st = state.lock().unwrap();
2180 assert_eq!(st.len(), 3);
2181 let str_vec = st.iter().map(std::string::ToString::to_string).collect::<Vec<String>>();
2182 assert_eq!(str_vec.join(":"), "1:1:1");
2184 }
2185
2186 #[tokio::test]
2191 pub async fn test_builder_cron_placeholder_syntax() {
2192 let task_store: Arc<dyn TaskStore<State>> = InMemoryTaskStore::new();
2194 let state: State = Arc::new(Mutex::new(Vec::new()));
2195 let scheduler = Scheduler::new(task_store);
2196 scheduler.start(state.clone());
2197 scheduler.register::<TestTask>().unwrap();
2198
2199 let task = TestTask::new(1);
2201 let _id = scheduler
2202 .task(task)
2203 .key("cron-task")
2204 .cron("0 9 * * *") .schedule()
2206 .await
2207 .unwrap();
2208
2209 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
2213
2214 let st = state.lock().unwrap();
2215 assert_eq!(st.len(), 0); }
2219
2220 #[tokio::test]
2221 pub async fn test_builder_daily_at_placeholder() {
2222 let task_store: Arc<dyn TaskStore<State>> = InMemoryTaskStore::new();
2224 let state: State = Arc::new(Mutex::new(Vec::new()));
2225 let scheduler = Scheduler::new(task_store);
2226 scheduler.start(state.clone());
2227 scheduler.register::<TestTask>().unwrap();
2228
2229 let task = TestTask::new(1);
2231 let _id = scheduler
2232 .task(task)
2233 .key("daily-task")
2234 .daily_at(14, 30) .schedule()
2236 .await
2237 .unwrap();
2238
2239 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
2242
2243 let st = state.lock().unwrap();
2244 assert_eq!(st.len(), 0);
2247 }
2248
2249 #[tokio::test]
2250 pub async fn test_builder_weekly_at_placeholder() {
2251 let task_store: Arc<dyn TaskStore<State>> = InMemoryTaskStore::new();
2253 let state: State = Arc::new(Mutex::new(Vec::new()));
2254 let scheduler = Scheduler::new(task_store);
2255 scheduler.start(state.clone());
2256 scheduler.register::<TestTask>().unwrap();
2257
2258 let task = TestTask::new(1);
2260 let _id = scheduler
2261 .task(task)
2262 .key("weekly-task")
2263 .weekly_at(1, 9, 0) .schedule()
2265 .await
2266 .unwrap();
2267
2268 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
2271
2272 let st = state.lock().unwrap();
2273 assert_eq!(st.len(), 0);
2276 }
2277
2278 #[tokio::test]
2279 pub async fn test_builder_cron_with_retry() {
2280 let task_store: Arc<dyn TaskStore<State>> = InMemoryTaskStore::new();
2282 let state: State = Arc::new(Mutex::new(Vec::new()));
2283 let scheduler = Scheduler::new(task_store);
2284 scheduler.start(state.clone());
2285 scheduler.register::<TestTask>().unwrap();
2286
2287 let task = TestTask::new(1);
2289 let _id = scheduler
2290 .task(task)
2291 .key("reliable-scheduled-task")
2292 .daily_at(2, 0) .with_retry(RetryPolicy {
2294 wait_min_max: (60, 3600),
2295 times: 5,
2296 })
2297 .schedule()
2298 .await
2299 .unwrap();
2300
2301 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
2304
2305 let st = state.lock().unwrap();
2306 assert_eq!(st.len(), 0);
2309 }
2310
2311 #[test]
2314 fn test_cron_to_string() {
2315 let cron = CronSchedule::parse("*/5 * * * *").unwrap();
2317 assert_eq!(cron.to_cron_string(), "*/5 * * * *");
2318 }
2319
2320 #[tokio::test]
2321 pub async fn test_running_task_not_double_scheduled() {
2322 let _ = tracing_subscriber::fmt().try_init();
2323
2324 let task_store: Arc<dyn TaskStore<State>> = InMemoryTaskStore::new();
2325 let state: State = Arc::new(Mutex::new(Vec::new()));
2326 let scheduler = Scheduler::new(task_store);
2327 scheduler.start(state.clone());
2328 scheduler.register::<TestTask>().unwrap();
2329
2330 let task = TestTask::new(5); let task_id = scheduler.add(task.clone()).await.unwrap();
2333
2334 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
2336
2337 {
2339 let running = scheduler.tasks_running.lock().unwrap();
2340 assert!(running.contains_key(&task_id), "Task should be in running queue");
2341 }
2342
2343 let task_meta = TaskMeta {
2345 task: task.clone(),
2346 next_at: Some(Timestamp::now()),
2347 deps: vec![],
2348 retry_count: 0,
2349 retry: None,
2350 cron: None,
2351 rerun_requested: false,
2352 };
2353 let result = scheduler.add_queue(task_id, task_meta).await;
2354
2355 assert!(result.is_ok(), "add_queue should succeed");
2357
2358 {
2360 let sched_queue = scheduler.tasks_scheduled.lock().unwrap();
2361 let in_scheduled = sched_queue.iter().any(|((_, id), _)| *id == task_id);
2362 assert!(!in_scheduled, "Task should NOT be in scheduled queue while running");
2363 }
2364
2365 tokio::time::sleep(std::time::Duration::from_secs(2)).await;
2367
2368 let st = state.lock().unwrap();
2370 assert_eq!(st.len(), 1, "Only one task execution should have occurred");
2371 assert_eq!(st[0], 5);
2372 }
2373
2374 #[tokio::test]
2375 pub async fn test_running_task_metadata_updated() {
2376 let _ = tracing_subscriber::fmt().try_init();
2377
2378 let task_store: Arc<dyn TaskStore<State>> = InMemoryTaskStore::new();
2379 let state: State = Arc::new(Mutex::new(Vec::new()));
2380 let scheduler = Scheduler::new(task_store);
2381 scheduler.start(state.clone());
2382 scheduler.register::<TestTask>().unwrap();
2383
2384 let task = TestTask::new(5); let task_id = scheduler.add(task.clone()).await.unwrap();
2387
2388 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
2390
2391 {
2393 let running = scheduler.tasks_running.lock().unwrap();
2394 let meta = running.get(&task_id).expect("Task should be running");
2395 assert!(meta.cron.is_none(), "Task should have no cron initially");
2396 }
2397
2398 let cron = CronSchedule::parse("*/5 * * * *").unwrap();
2400 let task_meta_with_cron = TaskMeta {
2401 task: task.clone(),
2402 next_at: Some(Timestamp::now()),
2403 deps: vec![],
2404 retry_count: 0,
2405 retry: None,
2406 cron: Some(cron.clone()),
2407 rerun_requested: false,
2408 };
2409 let result = scheduler.add_queue(task_id, task_meta_with_cron).await;
2410
2411 assert!(result.is_ok(), "add_queue should succeed");
2413
2414 {
2416 let running = scheduler.tasks_running.lock().unwrap();
2417 let meta = running.get(&task_id).expect("Task should still be running");
2418 assert!(meta.cron.is_some(), "Task should now have cron after update");
2419 }
2420
2421 tokio::time::sleep(std::time::Duration::from_secs(2)).await;
2423 }
2424
2425 struct KeyedTaskStore {
2432 last_id: Mutex<TaskId>,
2433 by_key: Mutex<HashMap<String, TaskId>>,
2434 input: Mutex<HashMap<TaskId, (String, String)>>,
2435 finished: Mutex<Vec<TaskId>>,
2440 errors: Mutex<Vec<(TaskId, Option<Timestamp>)>>,
2444 finished_gate: Arc<tokio::sync::Semaphore>,
2449 update_gate: Arc<tokio::sync::Semaphore>,
2451 finished_entered: Arc<tokio::sync::Notify>,
2453 updated_entered: Arc<tokio::sync::Notify>,
2455 }
2456
2457 impl KeyedTaskStore {
2458 fn new() -> Arc<Self> {
2460 Self::with_gates(
2461 tokio::sync::Semaphore::MAX_PERMITS,
2462 tokio::sync::Semaphore::MAX_PERMITS,
2463 )
2464 }
2465
2466 fn new_gated_finished() -> Arc<Self> {
2468 Self::with_gates(0, tokio::sync::Semaphore::MAX_PERMITS)
2469 }
2470
2471 fn new_gated_update() -> Arc<Self> {
2473 Self::with_gates(tokio::sync::Semaphore::MAX_PERMITS, 0)
2474 }
2475
2476 fn with_gates(finished_permits: usize, update_permits: usize) -> Arc<Self> {
2477 Arc::new(Self {
2478 last_id: Mutex::new(0),
2479 by_key: Mutex::new(HashMap::new()),
2480 input: Mutex::new(HashMap::new()),
2481 finished: Mutex::new(Vec::new()),
2482 errors: Mutex::new(Vec::new()),
2483 finished_gate: Arc::new(tokio::sync::Semaphore::new(finished_permits)),
2484 update_gate: Arc::new(tokio::sync::Semaphore::new(update_permits)),
2485 finished_entered: Arc::new(tokio::sync::Notify::new()),
2486 updated_entered: Arc::new(tokio::sync::Notify::new()),
2487 })
2488 }
2489
2490 fn finished_ids(&self) -> Vec<TaskId> {
2491 self.finished.lock().unwrap().clone()
2492 }
2493
2494 fn error_calls(&self) -> Vec<(TaskId, Option<Timestamp>)> {
2495 self.errors.lock().unwrap().clone()
2496 }
2497
2498 fn release_finished(&self, n: usize) {
2499 self.finished_gate.add_permits(n);
2500 }
2501
2502 fn release_updates(&self, n: usize) {
2503 self.update_gate.add_permits(n);
2504 }
2505
2506 async fn await_finished_entered(&self) {
2510 tokio::time::timeout(
2511 std::time::Duration::from_secs(5),
2512 self.finished_entered.notified(),
2513 )
2514 .await
2515 .expect("expected `finished` to be entered");
2516 }
2517
2518 async fn await_updated_entered(&self) {
2520 tokio::time::timeout(
2521 std::time::Duration::from_secs(5),
2522 self.updated_entered.notified(),
2523 )
2524 .await
2525 .expect("expected `update_task` to be entered");
2526 }
2527 }
2528
2529 #[async_trait]
2530 impl<S: Clone> TaskStore<S> for KeyedTaskStore {
2531 async fn add(&self, task: &TaskMeta<S>, key: Option<&str>) -> ClResult<TaskId> {
2532 let id = {
2533 let mut last = self.last_id.lock().unwrap();
2534 *last += 1;
2535 *last
2536 };
2537 self.input
2538 .lock()
2539 .unwrap()
2540 .insert(id, (task.task.kind_of().to_owned(), task.task.serialize()));
2541 if let Some(key) = key {
2542 self.by_key.lock().unwrap().insert(key.to_owned(), id);
2543 }
2544 Ok(id)
2545 }
2546
2547 async fn find_by_key(&self, key: &str) -> ClResult<Option<(TaskId, TaskData)>> {
2548 let Some(id) = self.by_key.lock().unwrap().get(key).copied() else { return Ok(None) };
2549 let Some((kind, input)) = self.input.lock().unwrap().get(&id).cloned() else {
2550 return Ok(None);
2551 };
2552 Ok(Some((
2553 id,
2554 TaskData {
2555 id,
2556 kind: kind.into(),
2557 status: TaskStatus::Pending,
2558 input: input.into(),
2559 deps: Box::from([]),
2560 retry_data: None,
2561 cron_data: None,
2562 next_at: None,
2563 },
2564 )))
2565 }
2566
2567 async fn update_task(&self, id: TaskId, task: &TaskMeta<S>) -> ClResult<()> {
2568 self.updated_entered.notify_one();
2569 self.update_gate
2570 .acquire()
2571 .await
2572 .map(tokio::sync::SemaphorePermit::forget)
2573 .map_err(|_| Error::Internal("store gate closed".into()))?;
2574 if let Some(entry) = self.input.lock().unwrap().get_mut(&id) {
2575 entry.1 = task.task.serialize();
2576 }
2577 Ok(())
2578 }
2579
2580 async fn finished(&self, id: TaskId, _output: &str) -> ClResult<()> {
2581 self.finished_entered.notify_one();
2582 self.finished_gate
2583 .acquire()
2584 .await
2585 .map(tokio::sync::SemaphorePermit::forget)
2586 .map_err(|_| Error::Internal("store gate closed".into()))?;
2587 self.finished.lock().unwrap().push(id);
2588 Ok(())
2589 }
2590 async fn load(&self) -> ClResult<Vec<TaskData>> {
2591 Ok(vec![])
2592 }
2593 async fn update_task_error(
2594 &self,
2595 task_id: TaskId,
2596 _output: &str,
2597 next_at: Option<Timestamp>,
2598 ) -> ClResult<()> {
2599 self.errors.lock().unwrap().push((task_id, next_at));
2600 Ok(())
2601 }
2602 async fn find_completed_deps(&self, _deps: &[TaskId]) -> ClResult<Vec<TaskId>> {
2603 Ok(vec![])
2604 }
2605 }
2606
2607 #[derive(Debug)]
2615 struct GatedTask {
2616 param: u8,
2617 runs: Arc<Mutex<Vec<u8>>>,
2618 entered: Arc<tokio::sync::Notify>,
2619 gate: Arc<tokio::sync::Semaphore>,
2620 fail: bool,
2622 }
2623
2624 #[async_trait]
2625 impl Task<State> for GatedTask {
2626 fn kind() -> &'static str {
2627 "gated"
2628 }
2629 fn kind_of(&self) -> &'static str {
2630 Self::kind()
2631 }
2632 fn build(_id: TaskId, _ctx: &str) -> ClResult<Arc<dyn Task<State>>> {
2633 Err(Error::Internal("not rebuilt in this test".into()))
2634 }
2635 fn serialize(&self) -> String {
2636 self.param.to_string()
2637 }
2638 async fn run(&self, _state: &State) -> ClResult<()> {
2639 self.runs.lock().unwrap().push(self.param);
2640 self.entered.notify_one();
2641 let _permit = self.gate.acquire().await;
2642 if self.fail {
2643 return Err(Error::Internal("gated failure".into()));
2645 }
2646 Ok(())
2647 }
2648 }
2649
2650 struct Gated {
2651 runs: Arc<Mutex<Vec<u8>>>,
2652 entered: Arc<tokio::sync::Notify>,
2653 gate: Arc<tokio::sync::Semaphore>,
2654 }
2655
2656 impl Gated {
2657 fn new() -> Self {
2658 Self {
2659 runs: Arc::new(Mutex::new(Vec::new())),
2660 entered: Arc::new(tokio::sync::Notify::new()),
2661 gate: Arc::new(tokio::sync::Semaphore::new(0)),
2662 }
2663 }
2664
2665 fn task(&self, param: u8, fail: bool) -> Arc<GatedTask> {
2666 Arc::new(GatedTask {
2667 param,
2668 runs: Arc::clone(&self.runs),
2669 entered: Arc::clone(&self.entered),
2670 gate: Arc::clone(&self.gate),
2671 fail,
2672 })
2673 }
2674
2675 fn params(&self) -> Vec<u8> {
2676 self.runs.lock().unwrap().clone()
2677 }
2678
2679 async fn await_run(&self) {
2683 tokio::time::timeout(std::time::Duration::from_secs(5), self.entered.notified())
2684 .await
2685 .expect("expected a task body to be entered");
2686 }
2687 }
2688
2689 #[tokio::test]
2697 pub async fn a_running_keyed_one_shot_re_requested_in_flight_runs_again() {
2698 let _ = tracing_subscriber::fmt().try_init();
2699
2700 let store = KeyedTaskStore::new();
2701 let task_store: Arc<dyn TaskStore<State>> = store.clone();
2702 let state: State = Arc::new(Mutex::new(Vec::new()));
2703 let scheduler = Scheduler::new(task_store);
2704 scheduler.start(state.clone());
2705 scheduler.register::<GatedTask>().unwrap();
2706
2707 let gated = Gated::new();
2708 let first_id = scheduler.task(gated.task(1, false)).key("test.gated").now().await.unwrap();
2709 gated.await_run().await;
2710
2711 let second_id = scheduler.task(gated.task(2, false)).key("test.gated").now().await.unwrap();
2713 assert_eq!(second_id, first_id, "the key must resolve to the running task");
2714 assert_eq!(gated.params(), vec![1], "no second body may start alongside the first");
2715
2716 gated.gate.add_permits(1);
2718 gated.await_run().await;
2719 gated.gate.add_permits(1);
2720
2721 assert_eq!(
2722 gated.params(),
2723 vec![1, 2],
2724 "the re-requested run must happen, with the new parameters"
2725 );
2726
2727 for _ in 0..200 {
2734 if !store.finished_ids().is_empty() {
2735 break;
2736 }
2737 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
2738 }
2739 assert_eq!(
2740 store.finished_ids(),
2741 vec![first_id],
2742 "the re-requested run must reach `finished`, or the task is stuck pending"
2743 );
2744 assert!(
2745 !scheduler.tasks_running.lock().unwrap().contains_key(&first_id),
2746 "a finished task must not stay in the running map"
2747 );
2748 }
2749
2750 #[tokio::test]
2760 pub async fn a_running_keyed_one_shot_re_requested_with_identical_params_runs_again() {
2761 let _ = tracing_subscriber::fmt().try_init();
2762
2763 let store = KeyedTaskStore::new();
2764 let task_store: Arc<dyn TaskStore<State>> = store.clone();
2765 let state: State = Arc::new(Mutex::new(Vec::new()));
2766 let scheduler = Scheduler::new(task_store);
2767 scheduler.start(state.clone());
2768 scheduler.register::<GatedTask>().unwrap();
2769
2770 let gated = Gated::new();
2771 let first_id = scheduler.task(gated.task(1, false)).key("test.gated").now().await.unwrap();
2772 gated.await_run().await;
2773
2774 let second_id = scheduler.task(gated.task(1, false)).key("test.gated").now().await.unwrap();
2776 assert_eq!(second_id, first_id, "the key must resolve to the running task");
2777 assert_eq!(gated.params(), vec![1], "no second body may start alongside the first");
2778
2779 gated.gate.add_permits(1);
2780 gated.await_run().await;
2781 gated.gate.add_permits(1);
2782
2783 assert_eq!(
2784 gated.params(),
2785 vec![1, 1],
2786 "an identical re-request carries the same intent as a changed one"
2787 );
2788
2789 for _ in 0..200 {
2790 if !store.finished_ids().is_empty() {
2791 break;
2792 }
2793 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
2794 }
2795 assert_eq!(
2796 store.finished_ids(),
2797 vec![first_id],
2798 "the re-requested run must reach `finished`, or the task is stuck pending"
2799 );
2800 }
2801
2802 #[tokio::test]
2813 pub async fn a_failing_cron_run_keeps_the_row_pending() {
2814 let _ = tracing_subscriber::fmt().try_init();
2815
2816 let store = KeyedTaskStore::new();
2817 let task_store: Arc<dyn TaskStore<State>> = store.clone();
2818 let state: State = Arc::new(Mutex::new(Vec::new()));
2819 let scheduler = Scheduler::new(task_store);
2820 scheduler.start(state.clone());
2821 scheduler.register::<GatedTask>().unwrap();
2822
2823 let gated = Gated::new();
2824 let id = scheduler
2826 .task(gated.task(1, true))
2827 .key("test.cron.failing")
2828 .cron("*/5 * * * *")
2829 .run_on_startup()
2830 .schedule()
2831 .await
2832 .unwrap();
2833 gated.await_run().await;
2834
2835 gated.gate.add_permits(1);
2837 for _ in 0..200 {
2838 if !store.error_calls().is_empty() {
2839 break;
2840 }
2841 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
2842 }
2843
2844 let errors = store.error_calls();
2845 assert_eq!(errors.len(), 1, "the failed run must be persisted");
2846 assert_eq!(errors[0].0, id);
2847 assert!(
2848 errors[0].1.is_some(),
2849 "a cron task's row must keep a live next_at, not be stamped 'E'"
2850 );
2851 assert!(store.finished_ids().is_empty(), "a failed run does not finish the task");
2852 }
2853
2854 #[tokio::test]
2860 pub async fn a_retry_after_an_in_flight_update_uses_the_new_parameters() {
2861 let _ = tracing_subscriber::fmt().try_init();
2862
2863 let task_store: Arc<dyn TaskStore<State>> = KeyedTaskStore::new();
2864 let state: State = Arc::new(Mutex::new(Vec::new()));
2865 let scheduler = Scheduler::new(task_store);
2866 scheduler.start(state.clone());
2867 scheduler.register::<GatedTask>().unwrap();
2868
2869 let gated = Gated::new();
2870 let first_id = scheduler
2873 .task(gated.task(1, true))
2874 .key("test.gated")
2875 .with_retry(RetryPolicy::new((0, 0), 3))
2876 .now()
2877 .await
2878 .unwrap();
2879 gated.await_run().await;
2880
2881 let second_id = scheduler
2882 .task(gated.task(2, false))
2883 .key("test.gated")
2884 .with_retry(RetryPolicy::new((0, 0), 3))
2885 .now()
2886 .await
2887 .unwrap();
2888 assert_eq!(second_id, first_id);
2889
2890 gated.gate.add_permits(1);
2892 gated.await_run().await;
2893 gated.gate.add_permits(1);
2894
2895 assert_eq!(
2896 gated.params(),
2897 vec![1, 2],
2898 "the retry must run the parameters the in-flight update wrote"
2899 );
2900 }
2901
2902 #[tokio::test(flavor = "current_thread")]
2919 pub async fn a_re_request_landing_while_the_finish_handler_marks_finished_still_runs() {
2920 let _ = tracing_subscriber::fmt().try_init();
2921
2922 let store = KeyedTaskStore::new_gated_finished();
2923 let task_store: Arc<dyn TaskStore<State>> = store.clone();
2924 let state: State = Arc::new(Mutex::new(Vec::new()));
2925 let scheduler = Scheduler::new(task_store);
2926 scheduler.start(state.clone());
2927 scheduler.register::<GatedTask>().unwrap();
2928
2929 let gated = Gated::new();
2930 let id = scheduler.task(gated.task(1, false)).key("test.gated").now().await.unwrap();
2933 gated.await_run().await;
2934
2935 gated.gate.add_permits(1);
2938 store.await_finished_entered().await;
2939
2940 let second = scheduler.task(gated.task(2, false)).key("test.gated").now().await.unwrap();
2942 assert_eq!(second, id, "the key must resolve to the same task");
2943
2944 store.release_finished(8);
2946 gated.await_run().await;
2947 gated.gate.add_permits(1);
2948
2949 assert_eq!(
2950 gated.params(),
2951 vec![1, 2],
2952 "the request that landed during `finished` must still run, with the new parameters"
2953 );
2954
2955 for _ in 0..200 {
2956 if store.finished_ids().len() >= 2 {
2957 break;
2958 }
2959 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
2960 }
2961 assert!(
2967 store.finished_ids().contains(&id),
2968 "the re-requested run must reach `finished`, or the row stays 'P' forever"
2969 );
2970 assert!(
2971 !scheduler.tasks_running.lock().unwrap().contains_key(&id),
2972 "a finished task must not stay in the running map"
2973 );
2974 }
2975
2976 #[tokio::test]
2983 pub async fn a_cron_reschedule_uses_the_parameters_an_in_flight_update_wrote() {
2984 let _ = tracing_subscriber::fmt().try_init();
2985
2986 let store = KeyedTaskStore::new_gated_update();
2987 let task_store: Arc<dyn TaskStore<State>> = store.clone();
2988 let state: State = Arc::new(Mutex::new(Vec::new()));
2989 let scheduler = Scheduler::new(task_store);
2990 scheduler.start(state.clone());
2991 scheduler.register::<GatedTask>().unwrap();
2992
2993 let gated = Gated::new();
2994 let id = scheduler
2995 .task(gated.task(1, false))
2996 .key("test.cron")
2997 .cron("*/5 * * * *")
2998 .run_on_startup()
2999 .schedule()
3000 .await
3001 .unwrap();
3002 gated.await_run().await;
3003
3004 gated.gate.add_permits(1);
3007 store.await_updated_entered().await;
3008
3009 let releaser = {
3014 let store = Arc::clone(&store);
3015 tokio::spawn(async move {
3016 store.await_updated_entered().await;
3017 store.release_updates(8);
3018 })
3019 };
3020 let second = scheduler
3021 .task(gated.task(2, false))
3022 .key("test.cron")
3023 .cron("*/5 * * * *")
3024 .schedule()
3025 .await
3026 .unwrap();
3027 assert_eq!(second, id, "the key must resolve to the running task");
3028 releaser.await.unwrap();
3029 for _ in 0..200 {
3032 let queued = {
3033 let scheduled = scheduler.tasks_scheduled.lock().unwrap();
3034 scheduled.iter().any(|((_, tid), _)| *tid == id)
3035 };
3036 if queued {
3037 break;
3038 }
3039 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
3040 }
3041 let scheduled = scheduler.tasks_scheduled.lock().unwrap();
3042 let (_, meta) = scheduled
3043 .iter()
3044 .find(|((_, tid), _)| *tid == id)
3045 .expect("cron task must be rescheduled");
3046 assert_eq!(meta.task.serialize(), "2", "the cron reschedule used the stale snapshot");
3047 }
3048}
3049
3050