1use std::future::Future;
16use std::num::NonZeroUsize;
17use std::pin::Pin;
18use std::sync::atomic::AtomicU64;
19use std::sync::atomic::Ordering;
20use std::sync::Arc;
21use std::time::Duration;
22use std::time::Instant;
23
24use dashmap::mapref::entry::Entry;
25use dashmap::DashMap;
26use parking_lot::Mutex;
27use serde::Serialize;
28use tokio::sync::Notify;
29use tokio_util::sync::CancellationToken;
30
31use crate::error::RuntimeError;
32use crate::error::RuntimeResult;
33use crate::operation::OperationContext;
34use crate::shutdown_report::ShutdownReport;
35use crate::task_group::TaskGroup;
36use crate::task_group::TaskId;
37use crate::task_group::TaskKind;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
41pub enum ScheduleMode {
42 FixedDelay,
44 FixedRateNoOverlap,
46 FixedRateAllowOverlap,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
52pub enum ScheduledTaskConcurrency {
53 Serial,
55 Bounded(NonZeroUsize),
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
61pub enum MissedTickPolicy {
62 Skip,
64 CoalesceLatest,
66 BoundedCatchUp(NonZeroUsize),
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
72pub struct ScheduledExecutionPolicy {
73 pub concurrency: ScheduledTaskConcurrency,
75 pub missed_ticks: MissedTickPolicy,
77}
78
79impl ScheduledExecutionPolicy {
80 #[must_use]
82 pub const fn serial(missed_ticks: MissedTickPolicy) -> Self {
83 Self {
84 concurrency: ScheduledTaskConcurrency::Serial,
85 missed_ticks,
86 }
87 }
88
89 #[must_use]
91 pub const fn bounded(concurrency: NonZeroUsize, missed_ticks: MissedTickPolicy) -> Self {
92 Self {
93 concurrency: ScheduledTaskConcurrency::Bounded(concurrency),
94 missed_ticks,
95 }
96 }
97}
98
99impl Default for ScheduledExecutionPolicy {
100 fn default() -> Self {
102 Self::serial(MissedTickPolicy::Skip)
103 }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
108pub enum ScheduledTaskControl {
109 Continue,
111 Stop,
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub enum ScheduledTaskRegistrationOutcome {
121 Scheduled(TaskId),
123 AlreadyPresent,
125}
126
127#[derive(Debug, Clone)]
133pub struct ScheduledTaskConfig {
134 pub name: String,
136 pub initial_delay: Duration,
138 pub period: Duration,
140 pub mode: ScheduleMode,
142 pub max_run_time: Option<Duration>,
147}
148
149impl ScheduledTaskConfig {
150 pub fn fixed_delay(name: impl Into<String>, period: Duration) -> Self {
152 Self {
153 name: name.into(),
154 initial_delay: Duration::ZERO,
155 period,
156 mode: ScheduleMode::FixedDelay,
157 max_run_time: None,
158 }
159 }
160
161 pub fn fixed_rate_no_overlap(name: impl Into<String>, period: Duration) -> Self {
163 Self {
164 mode: ScheduleMode::FixedRateNoOverlap,
165 ..Self::fixed_delay(name, period)
166 }
167 }
168
169 pub fn fixed_rate(name: impl Into<String>, period: Duration) -> Self {
172 Self {
173 mode: ScheduleMode::FixedRateAllowOverlap,
174 ..Self::fixed_delay(name, period)
175 }
176 }
177
178 #[must_use]
180 pub fn with_initial_delay(mut self, initial_delay: Duration) -> Self {
181 self.initial_delay = initial_delay;
182 self
183 }
184
185 fn run_limit(&self, policy: ScheduledExecutionPolicy) -> RuntimeResult<usize> {
187 if self.period.is_zero() {
188 return Err(RuntimeError::unsupported(
189 crate::RuntimeOperation::RegisterScheduledTask,
190 ));
191 }
192 match (self.mode, policy.concurrency) {
193 (ScheduleMode::FixedDelay | ScheduleMode::FixedRateNoOverlap, ScheduledTaskConcurrency::Serial) => Ok(1),
194 (ScheduleMode::FixedRateAllowOverlap, ScheduledTaskConcurrency::Bounded(max_runs)) => Ok(max_runs.get()),
195 (ScheduleMode::FixedDelay | ScheduleMode::FixedRateNoOverlap, ScheduledTaskConcurrency::Bounded(_))
196 | (ScheduleMode::FixedRateAllowOverlap, ScheduledTaskConcurrency::Serial) => Err(
197 RuntimeError::unsupported(crate::RuntimeOperation::RegisterScheduledTask),
198 ),
199 }
200 }
201}
202
203#[derive(Debug, Clone)]
205pub struct ScheduledTaskGroup {
206 group: TaskGroup,
207 schedules: Arc<DashMap<Arc<str>, Arc<ScheduledTaskMetrics>>>,
208}
209
210#[derive(Debug, Clone)]
215pub struct ScheduledTaskObserver {
216 schedules: std::sync::Weak<DashMap<Arc<str>, Arc<ScheduledTaskMetrics>>>,
217 names: Arc<[Arc<str>]>,
218}
219
220impl ScheduledTaskObserver {
221 pub fn snapshot(&self) -> Vec<ScheduledTaskSnapshot> {
223 let Some(schedules) = self.schedules.upgrade() else {
224 return Vec::new();
225 };
226 self.names
227 .iter()
228 .filter_map(|name| schedules.get(name).map(|entry| entry.value().snapshot()))
229 .collect()
230 }
231}
232
233#[derive(Debug)]
234struct ScheduledTaskMetrics {
235 config: ScheduledTaskConfig,
236 max_concurrency: usize,
237 pending_runs: AtomicU64,
238 completion: Notify,
239 active_runs: AtomicU64,
240 runs: AtomicU64,
241 skips: AtomicU64,
242 overlaps: AtomicU64,
243 failures: AtomicU64,
244 last_drift_ms: AtomicU64,
245 last_elapsed_ms: AtomicU64,
246 max_elapsed_ms: AtomicU64,
247}
248
249#[derive(Debug, Clone, Serialize)]
251pub struct ScheduledTaskSnapshot {
252 pub name: String,
254 pub mode: ScheduleMode,
256 pub running: bool,
258 pub active_runs: u64,
260 pub runs: u64,
262 pub skips: u64,
264 pub overlaps: u64,
266 pub failures: u64,
268 pub last_drift_ms: u64,
270 pub last_elapsed_ms: u64,
272 pub max_elapsed_ms: u64,
274}
275
276#[derive(Clone, Copy)]
278enum ScheduleBinding<'a> {
279 Group,
281 Operation(&'a OperationContext),
283}
284
285impl ScheduleBinding<'_> {
286 fn cancellation_token(self, group: &TaskGroup) -> CancellationToken {
287 match self {
288 Self::Group => group.cancellation_token(),
289 Self::Operation(operation) => operation.cancellation_token(),
290 }
291 }
292
293 fn run_spawner(self, group: &TaskGroup) -> RunSpawner {
294 RunSpawner {
295 group: group.clone(),
296 operation: match self {
297 Self::Group => None,
298 Self::Operation(operation) => Some(operation.with_task_kind(TaskKind::ScheduledRun)),
299 },
300 }
301 }
302}
303
304struct RunSpawner {
306 group: TaskGroup,
307 operation: Option<OperationContext>,
308}
309
310impl RunSpawner {
311 fn spawn<F>(&self, name: String, future: F) -> RuntimeResult<TaskId>
312 where
313 F: Future<Output = ()> + Send + 'static,
314 {
315 match &self.operation {
316 Some(operation) => self.group.spawn_operation(operation, name, future),
317 None => self.group.spawn(name, TaskKind::ScheduledRun, future),
318 }
319 }
320}
321
322type ScheduledRun = Pin<Box<dyn Future<Output = ScheduledTaskControl> + Send + 'static>>;
324
325type ScheduledTaskFn = Box<dyn FnMut() -> ScheduledRun + Send + 'static>;
332
333fn erase_task<F, Fut>(mut task: F) -> ScheduledTaskFn
335where
336 F: FnMut() -> Fut + Send + 'static,
337 Fut: Future<Output = ()> + Send + 'static,
338{
339 Box::new(move || {
340 let run = task();
341 Box::pin(async move {
342 run.await;
343 ScheduledTaskControl::Continue
344 })
345 })
346}
347
348fn erase_controlled_task<F, Fut>(mut task: F) -> ScheduledTaskFn
350where
351 F: FnMut() -> Fut + Send + 'static,
352 Fut: Future<Output = ScheduledTaskControl> + Send + 'static,
353{
354 Box::new(move || Box::pin(task()))
355}
356
357impl ScheduledTaskGroup {
358 pub fn new(group: TaskGroup) -> Self {
360 Self {
361 group,
362 schedules: Arc::new(DashMap::with_shard_amount(4)),
364 }
365 }
366
367 pub fn group(&self) -> &TaskGroup {
369 &self.group
370 }
371
372 pub fn schedule<F, Fut>(
386 &self,
387 config: ScheduledTaskConfig,
388 policy: ScheduledExecutionPolicy,
389 task: F,
390 ) -> RuntimeResult<ScheduledTaskRegistrationOutcome>
391 where
392 F: FnMut() -> Fut + Send + 'static,
393 Fut: Future<Output = ()> + Send + 'static,
394 {
395 self.schedule_erased(ScheduleBinding::Group, config, policy, erase_task(task))
396 }
397
398 pub fn schedule_operation<F, Fut>(
407 &self,
408 operation: &OperationContext,
409 config: ScheduledTaskConfig,
410 policy: ScheduledExecutionPolicy,
411 task: F,
412 ) -> RuntimeResult<ScheduledTaskRegistrationOutcome>
413 where
414 F: FnMut() -> Fut + Send + 'static,
415 Fut: Future<Output = ()> + Send + 'static,
416 {
417 self.schedule_erased(ScheduleBinding::Operation(operation), config, policy, erase_task(task))
418 }
419
420 pub fn schedule_controlled<F, Fut>(
430 &self,
431 config: ScheduledTaskConfig,
432 task: F,
433 ) -> RuntimeResult<ScheduledTaskRegistrationOutcome>
434 where
435 F: FnMut() -> Fut + Send + 'static,
436 Fut: Future<Output = ScheduledTaskControl> + Send + 'static,
437 {
438 if config.mode != ScheduleMode::FixedDelay {
439 return Err(RuntimeError::unsupported(
440 crate::RuntimeOperation::RegisterScheduledTask,
441 ));
442 }
443 config.run_limit(ScheduledExecutionPolicy::default())?;
444 self.register_fixed_delay(ScheduleBinding::Group, config, erase_controlled_task(task))
445 }
446
447 fn schedule_erased(
448 &self,
449 binding: ScheduleBinding<'_>,
450 config: ScheduledTaskConfig,
451 policy: ScheduledExecutionPolicy,
452 task: ScheduledTaskFn,
453 ) -> RuntimeResult<ScheduledTaskRegistrationOutcome> {
454 let max_concurrency = config.run_limit(policy)?;
455 if config.mode == ScheduleMode::FixedDelay {
456 return self.register_fixed_delay(binding, config, task);
457 }
458 self.register_fixed_rate(binding, config, max_concurrency, policy.missed_ticks, task)
459 }
460
461 fn register_fixed_delay(
462 &self,
463 binding: ScheduleBinding<'_>,
464 config: ScheduledTaskConfig,
465 task: ScheduledTaskFn,
466 ) -> RuntimeResult<ScheduledTaskRegistrationOutcome> {
467 let name: Arc<str> = Arc::from(config.name.as_str());
468 let Some(metrics) = self.register(name.clone(), config.clone(), 1) else {
469 return Ok(ScheduledTaskRegistrationOutcome::AlreadyPresent);
470 };
471 let token = binding.cancellation_token(&self.group);
472 let driver = async move {
473 fixed_delay_driver(token, config, metrics, task).await;
474 };
475 self.spawn_driver(binding, name, driver)
476 }
477
478 fn register_fixed_rate(
479 &self,
480 binding: ScheduleBinding<'_>,
481 config: ScheduledTaskConfig,
482 max_concurrency: usize,
483 missed_ticks: MissedTickPolicy,
484 task: ScheduledTaskFn,
485 ) -> RuntimeResult<ScheduledTaskRegistrationOutcome> {
486 let name: Arc<str> = Arc::from(config.name.as_str());
487 let Some(metrics) = self.register(name.clone(), config.clone(), max_concurrency) else {
488 return Ok(ScheduledTaskRegistrationOutcome::AlreadyPresent);
489 };
490 let token = binding.cancellation_token(&self.group);
491 let runs = binding.run_spawner(&self.group);
492 let task = Arc::new(Mutex::new(task));
494 let run_name: Arc<str> = Arc::from(format!("scheduled-run:{name}"));
495 let driver = async move {
496 fixed_rate_driver(token, config, metrics, missed_ticks, runs, task, run_name).await;
497 };
498 self.spawn_driver(binding, name, driver)
499 }
500
501 fn spawn_driver<F>(
502 &self,
503 binding: ScheduleBinding<'_>,
504 name: Arc<str>,
505 driver: F,
506 ) -> RuntimeResult<ScheduledTaskRegistrationOutcome>
507 where
508 F: Future<Output = ()> + Send + 'static,
509 {
510 let driver_name = format!("scheduled-driver:{name}");
511 let spawn_result = match binding {
512 ScheduleBinding::Group => self.group.spawn(driver_name, TaskKind::ScheduledDriver, driver),
513 ScheduleBinding::Operation(operation) => self.group.spawn_operation(
514 &operation.with_task_kind(TaskKind::ScheduledDriver),
515 driver_name,
516 driver,
517 ),
518 };
519 if spawn_result.is_err() {
520 self.schedules.remove(&name);
521 }
522 spawn_result.map(ScheduledTaskRegistrationOutcome::Scheduled)
523 }
524
525 pub fn snapshot(&self) -> Vec<ScheduledTaskSnapshot> {
527 self.schedules.iter().map(|entry| entry.value().snapshot()).collect()
528 }
529
530 pub fn observer(&self, names: &[&str]) -> ScheduledTaskObserver {
535 let mut names: Vec<Arc<str>> = names.iter().map(|name| Arc::from(*name)).collect();
536 names.sort_unstable();
537 names.dedup();
538 ScheduledTaskObserver {
539 schedules: Arc::downgrade(&self.schedules),
540 names: names.into(),
541 }
542 }
543
544 pub fn clear_completed(&self) -> RuntimeResult<()> {
551 if self.group.task_count() != 0 {
552 return Err(RuntimeError::context_unavailable(
553 crate::RuntimeOperation::ClearCompletedSchedules,
554 ));
555 }
556 self.schedules.clear();
557 Ok(())
558 }
559
560 pub async fn shutdown(&self, timeout: Duration) -> ShutdownReport {
562 self.group.shutdown(timeout).await
563 }
564
565 pub async fn shutdown_until(&self, deadline: crate::ShutdownDeadline) -> ShutdownReport {
567 self.group.shutdown_until(deadline).await
568 }
569
570 fn register(
571 &self,
572 name: Arc<str>,
573 config: ScheduledTaskConfig,
574 max_concurrency: usize,
575 ) -> Option<Arc<ScheduledTaskMetrics>> {
576 let metrics = Arc::new(ScheduledTaskMetrics {
577 config,
578 max_concurrency,
579 pending_runs: AtomicU64::new(0),
580 completion: Notify::new(),
581 active_runs: AtomicU64::new(0),
582 runs: AtomicU64::new(0),
583 skips: AtomicU64::new(0),
584 overlaps: AtomicU64::new(0),
585 failures: AtomicU64::new(0),
586 last_drift_ms: AtomicU64::new(0),
587 last_elapsed_ms: AtomicU64::new(0),
588 max_elapsed_ms: AtomicU64::new(0),
589 });
590
591 match self.schedules.entry(name.clone()) {
592 Entry::Occupied(_) => None,
593 Entry::Vacant(entry) => {
594 entry.insert(metrics.clone());
595 Some(metrics)
596 }
597 }
598 }
599}
600
601impl ScheduledTaskMetrics {
602 fn try_reserve_run(self: &Arc<Self>) -> Option<ScheduledRunGuard> {
603 let mut active = self.active_runs.load(Ordering::Acquire);
604 loop {
605 if active >= self.max_concurrency as u64 {
606 return None;
607 }
608 match self
609 .active_runs
610 .compare_exchange_weak(active, active + 1, Ordering::AcqRel, Ordering::Acquire)
611 {
612 Ok(_) => {
613 if active > 0 {
614 self.overlaps.fetch_add(1, Ordering::Relaxed);
615 }
616 return Some(ScheduledRunGuard::reserved(self.clone()));
617 }
618 Err(observed) => active = observed,
619 }
620 }
621 }
622
623 fn take_pending_run(&self) -> bool {
624 self.pending_runs
625 .fetch_update(Ordering::AcqRel, Ordering::Acquire, |pending| pending.checked_sub(1))
626 .is_ok()
627 }
628
629 fn queue_missed_ticks(&self, missed: u64, policy: MissedTickPolicy) {
630 if missed == 0 {
631 return;
632 }
633 let skipped = match policy {
634 MissedTickPolicy::Skip => missed,
635 MissedTickPolicy::CoalesceLatest => {
636 let previously_pending = self.pending_runs.swap(1, Ordering::AcqRel);
637 if previously_pending == 0 {
638 missed.saturating_sub(1)
639 } else {
640 missed
641 }
642 }
643 MissedTickPolicy::BoundedCatchUp(limit) => {
644 let limit = limit.get() as u64;
645 let mut added = 0;
646 while added < missed {
647 let pending = self.pending_runs.load(Ordering::Acquire);
648 if pending >= limit {
649 break;
650 }
651 if self
652 .pending_runs
653 .compare_exchange_weak(pending, pending + 1, Ordering::AcqRel, Ordering::Acquire)
654 .is_ok()
655 {
656 added += 1;
657 }
658 }
659 missed.saturating_sub(added)
660 }
661 };
662 self.skips.fetch_add(skipped, Ordering::Relaxed);
663 }
664
665 fn begin_serial_run(self: &Arc<Self>, expected_at: Instant) -> ScheduledRunGuard {
666 self.active_runs.fetch_add(1, Ordering::AcqRel);
667 self.record_drift(expected_at);
668 ScheduledRunGuard::reserved(self.clone())
669 }
670
671 fn record_drift(&self, expected_at: Instant) {
672 let drift_ms = Instant::now().saturating_duration_since(expected_at).as_millis() as u64;
673 self.last_drift_ms.store(drift_ms, Ordering::Relaxed);
674 }
675
676 fn snapshot(&self) -> ScheduledTaskSnapshot {
677 let active_runs = self.active_runs.load(Ordering::Acquire);
678 ScheduledTaskSnapshot {
679 name: self.config.name.clone(),
680 mode: self.config.mode,
681 running: active_runs > 0,
682 active_runs,
683 runs: self.runs.load(Ordering::Relaxed),
684 skips: self.skips.load(Ordering::Relaxed),
685 overlaps: self.overlaps.load(Ordering::Relaxed),
686 failures: self.failures.load(Ordering::Relaxed),
687 last_drift_ms: self.last_drift_ms.load(Ordering::Relaxed),
688 last_elapsed_ms: self.last_elapsed_ms.load(Ordering::Relaxed),
689 max_elapsed_ms: self.max_elapsed_ms.load(Ordering::Relaxed),
690 }
691 }
692}
693
694struct ScheduledRunGuard {
697 metrics: Arc<ScheduledTaskMetrics>,
698 started_at: Option<Instant>,
699 outcome: ScheduledRunOutcome,
700}
701
702#[derive(Clone, Copy)]
703enum ScheduledRunOutcome {
704 Completed,
705 TimedOut,
706 Panicked,
707 Cancelled,
708 RejectedBeforeStart,
709}
710
711impl ScheduledRunGuard {
712 fn reserved(metrics: Arc<ScheduledTaskMetrics>) -> Self {
713 Self {
714 metrics,
715 started_at: None,
716 outcome: ScheduledRunOutcome::RejectedBeforeStart,
717 }
718 }
719
720 fn start(&mut self) {
721 self.started_at = Some(Instant::now());
722 self.outcome = ScheduledRunOutcome::Cancelled;
723 }
724
725 fn finish(mut self, timed_out: bool) {
726 self.outcome = if timed_out {
727 ScheduledRunOutcome::TimedOut
728 } else {
729 ScheduledRunOutcome::Completed
730 };
731 }
732}
733
734impl Drop for ScheduledRunGuard {
735 fn drop(&mut self) {
736 if let Some(started_at) = self.started_at {
737 let elapsed_ms = started_at.elapsed().as_millis() as u64;
738 self.metrics.last_elapsed_ms.store(elapsed_ms, Ordering::Relaxed);
739 self.metrics.max_elapsed_ms.fetch_max(elapsed_ms, Ordering::Relaxed);
740 }
741 let outcome = if std::thread::panicking() {
742 ScheduledRunOutcome::Panicked
743 } else {
744 self.outcome
745 };
746 match outcome {
747 ScheduledRunOutcome::Completed => {
748 self.metrics.runs.fetch_add(1, Ordering::Relaxed);
749 }
750 ScheduledRunOutcome::TimedOut
751 | ScheduledRunOutcome::Panicked
752 | ScheduledRunOutcome::Cancelled
753 | ScheduledRunOutcome::RejectedBeforeStart => {
754 self.metrics.failures.fetch_add(1, Ordering::Relaxed);
755 }
756 }
757 self.metrics.active_runs.fetch_sub(1, Ordering::AcqRel);
758 self.metrics.completion.notify_one();
759 }
760}
761
762fn spawn_bounded_run(
764 runs: &RunSpawner,
765 mut run: ScheduledRunGuard,
766 task: &Arc<Mutex<ScheduledTaskFn>>,
767 name: &Arc<str>,
768 max_run_time: Option<Duration>,
769) -> bool {
770 let task = task.clone();
771 runs.spawn(name.to_string(), async move {
772 run.start();
773 let future = {
774 let mut task = task.lock();
775 (task)()
776 };
777 let (_, timed_out) = run_with_optional_timeout(future, max_run_time).await;
779 run.finish(timed_out);
780 })
781 .is_ok()
782}
783
784async fn fixed_rate_driver(
785 token: CancellationToken,
786 config: ScheduledTaskConfig,
787 metrics: Arc<ScheduledTaskMetrics>,
788 missed_ticks: MissedTickPolicy,
789 runs: RunSpawner,
790 task: Arc<Mutex<ScheduledTaskFn>>,
791 run_name: Arc<str>,
792) {
793 let period = config.period;
794 let max_run_time = config.max_run_time;
795 let mut expected_tick = Instant::now() + config.initial_delay;
796 loop {
797 if token.is_cancelled() {
798 return;
799 }
800 tokio::select! {
801 biased;
802 _ = token.cancelled() => return,
803 _ = metrics.completion.notified() => {}
804 _ = tokio::time::sleep_until(tokio::time::Instant::from_std(expected_tick)) => {
805 let now = Instant::now();
806 metrics.record_drift(expected_tick);
807 let overdue = now.saturating_duration_since(expected_tick);
808 let total_missed = 1u64.saturating_add(
809 u64::try_from(overdue.as_nanos() / period.as_nanos()).unwrap_or(u64::MAX),
810 );
811 let mut remaining = total_missed;
812 while remaining > 0 {
813 let Some(run) = metrics.try_reserve_run() else { break; };
814 if !spawn_bounded_run(&runs, run, &task, &run_name, max_run_time) {
815 break;
816 }
817 remaining -= 1;
818 }
819 metrics.queue_missed_ticks(remaining, missed_ticks);
820 let advance = period.saturating_mul(u32::try_from(total_missed).unwrap_or(u32::MAX));
821 expected_tick = expected_tick.checked_add(advance).unwrap_or(now);
822 }
823 }
824 while metrics.pending_runs.load(Ordering::Acquire) > 0 {
825 let Some(run) = metrics.try_reserve_run() else {
826 break;
827 };
828 if !metrics.take_pending_run() {
829 break;
830 }
831 if !spawn_bounded_run(&runs, run, &task, &run_name, max_run_time) {
832 break;
833 }
834 }
835 }
836}
837
838async fn run_with_optional_timeout(run: ScheduledRun, max_run_time: Option<Duration>) -> (ScheduledTaskControl, bool) {
840 if let Some(timeout) = max_run_time {
841 match tokio::time::timeout(timeout, run).await {
842 Ok(control) => (control, false),
843 Err(_) => (ScheduledTaskControl::Continue, true),
844 }
845 } else {
846 (run.await, false)
847 }
848}
849
850async fn fixed_delay_driver(
853 token: CancellationToken,
854 config: ScheduledTaskConfig,
855 metrics: Arc<ScheduledTaskMetrics>,
856 mut task: ScheduledTaskFn,
857) {
858 if !sleep_or_cancel(&token, config.initial_delay).await {
859 return;
860 }
861
862 loop {
863 if token.is_cancelled() {
864 return;
865 }
866
867 let started_at = Instant::now();
868 let mut run = metrics.begin_serial_run(started_at);
869 run.start();
870 let (control, timed_out) = run_with_optional_timeout(task(), config.max_run_time).await;
871 run.finish(timed_out);
872 if control == ScheduledTaskControl::Stop {
873 return;
874 }
875
876 if !sleep_or_cancel(&token, config.period).await {
877 return;
878 }
879 }
880}
881
882async fn sleep_or_cancel(token: &CancellationToken, duration: Duration) -> bool {
883 if duration.is_zero() {
884 return !token.is_cancelled();
885 }
886
887 tokio::select! {
888 _ = token.cancelled() => false,
889 _ = tokio::time::sleep(duration) => true,
890 }
891}
892
893#[cfg(test)]
894mod tests {
895 use std::sync::atomic::AtomicUsize;
896 use std::sync::atomic::Ordering;
897 use std::sync::Arc;
898 use std::time::Duration;
899
900 use tokio::sync::Semaphore;
901
902 use super::*;
903 use crate::RuntimeContext;
904
905 fn panicking_run(construction: bool) -> impl Future<Output = ()> + Send {
906 assert!(!construction, "injected run construction panic");
907 async { panic!("injected run poll panic") }
908 }
909
910 fn panicking_controlled_run(construction: bool) -> impl Future<Output = ScheduledTaskControl> + Send {
911 assert!(!construction, "injected controlled construction panic");
912 async { panic!("injected controlled poll panic") }
913 }
914
915 fn two() -> NonZeroUsize {
916 NonZeroUsize::new(2).unwrap()
917 }
918
919 #[tokio::test]
920 async fn every_schedule_entry_settles_construction_and_poll_panics() {
921 for construction in [false, true] {
922 for entry in 0..6 {
923 let context = RuntimeContext::from_current("scheduled-panic");
924 let scheduled = ScheduledTaskGroup::new(context.root_group().clone());
925 let operation = OperationContext::without_deadline(TaskKind::ScheduledDriver);
926 let delay = ScheduledTaskConfig::fixed_delay("panic", Duration::from_secs(60));
927 let serial = ScheduledTaskConfig::fixed_rate_no_overlap("panic", Duration::from_secs(60));
928 let overlapping = ScheduledTaskConfig::fixed_rate("panic", Duration::from_secs(60));
929 let result = match entry {
930 0 => scheduled.schedule(delay, ScheduledExecutionPolicy::default(), move || {
931 panicking_run(construction)
932 }),
933 1 => scheduled.schedule(serial, ScheduledExecutionPolicy::default(), move || {
934 panicking_run(construction)
935 }),
936 2 => scheduled.schedule(
937 overlapping,
938 ScheduledExecutionPolicy::bounded(two(), MissedTickPolicy::Skip),
939 move || panicking_run(construction),
940 ),
941 3 => scheduled.schedule_controlled(delay, move || panicking_controlled_run(construction)),
942 4 => scheduled.schedule_operation(
943 &operation,
944 delay,
945 ScheduledExecutionPolicy::default(),
946 move || panicking_run(construction),
947 ),
948 _ => scheduled.schedule_operation(
949 &operation,
950 serial,
951 ScheduledExecutionPolicy::default(),
952 move || panicking_run(construction),
953 ),
954 };
955 result.unwrap();
956 tokio::time::timeout(Duration::from_secs(5), async {
957 loop {
958 let snapshot = &scheduled.snapshot()[0];
959 if snapshot.failures == 1 && snapshot.active_runs == 0 {
960 break;
961 }
962 tokio::task::yield_now().await;
963 }
964 })
965 .await
966 .unwrap_or_else(|_| {
967 panic!(
968 "entry {entry}, construction {construction} did not settle: {:?}",
969 scheduled.snapshot()
970 )
971 });
972 let snapshot = &scheduled.snapshot()[0];
973 assert_eq!(snapshot.active_runs, 0, "entry {entry}, construction {construction}");
974 assert!(!snapshot.running);
975 assert_eq!(snapshot.failures, 1);
976 assert_eq!(snapshot.runs, 0);
977 let report = context.shutdown_tasks(Duration::from_secs(1)).await;
978 assert_eq!(report.panicked, 1);
979 assert_eq!(report.leaked, 0);
980 }
981 }
982 }
983
984 #[tokio::test]
985 async fn a_policy_that_contradicts_the_mode_is_rejected() {
986 let context = RuntimeContext::from_current("scheduled-policy-contract");
987 let scheduled = ScheduledTaskGroup::new(context.root_group().clone());
988 let period = Duration::from_secs(60);
989 let bounded = ScheduledExecutionPolicy::bounded(two(), MissedTickPolicy::Skip);
990 let serial = ScheduledExecutionPolicy::default();
991
992 for (config, policy) in [
993 (ScheduledTaskConfig::fixed_delay("delay-bounded", period), bounded),
994 (
995 ScheduledTaskConfig::fixed_rate_no_overlap("no-overlap-bounded", period),
996 bounded,
997 ),
998 (ScheduledTaskConfig::fixed_rate("overlap-serial", period), serial),
999 (ScheduledTaskConfig::fixed_delay("zero-period", Duration::ZERO), serial),
1000 ] {
1001 let name = config.name.clone();
1002 let error = scheduled.schedule(config, policy, || async {}).unwrap_err();
1003 assert_eq!(
1004 error.condition(),
1005 rocketmq_error::CanonicalCondition::Unimplemented,
1006 "{name}: {error}"
1007 );
1008 }
1009 let error = scheduled
1010 .schedule_controlled(
1011 ScheduledTaskConfig::fixed_rate_no_overlap("controlled-rate", period),
1012 || async { ScheduledTaskControl::Stop },
1013 )
1014 .unwrap_err();
1015 assert_eq!(error.condition(), rocketmq_error::CanonicalCondition::Unimplemented);
1016 assert!(scheduled.snapshot().is_empty());
1017 assert!(context.shutdown_tasks(Duration::from_secs(1)).await.is_healthy());
1018 }
1019
1020 #[tokio::test]
1021 async fn a_reserved_run_settles_if_rejected_or_aborted_before_first_poll() {
1022 let context = RuntimeContext::from_current("run-guard");
1023 let scheduled = ScheduledTaskGroup::new(context.root_group().clone());
1024 let name: Arc<str> = Arc::from("guard");
1025 let metrics = scheduled
1026 .register(
1027 name.clone(),
1028 ScheduledTaskConfig::fixed_rate_no_overlap("guard", Duration::from_secs(1)),
1029 1,
1030 )
1031 .unwrap();
1032 let run = metrics.try_reserve_run().unwrap();
1033 let (id, handle) = scheduled
1034 .group
1035 .spawn_with_handle("never-polled", TaskKind::ScheduledRun, async move {
1036 let mut run = run;
1037 run.start();
1038 std::future::pending::<()>().await;
1039 run.finish(false);
1040 })
1041 .unwrap();
1042 handle.abort();
1043 assert!(handle.await.unwrap_err().is_cancelled());
1044 assert!(scheduled.group.wait_task(id, Duration::ZERO).await);
1045 assert_eq!(metrics.snapshot().active_runs, 0);
1046 assert_eq!(metrics.snapshot().failures, 1);
1047 assert!(context.shutdown_tasks(Duration::from_secs(1)).await.is_healthy());
1048 let run = metrics.try_reserve_run().unwrap();
1049 let runs = ScheduleBinding::Group.run_spawner(&scheduled.group);
1050 assert!(!spawn_bounded_run(
1051 &runs,
1052 run,
1053 &Arc::new(Mutex::new(erase_task(|| async {}))),
1054 &name,
1055 None
1056 ));
1057 assert_eq!(metrics.snapshot().active_runs, 0);
1058 assert_eq!(metrics.snapshot().failures, 2);
1059 }
1060
1061 #[tokio::test]
1062 async fn operation_cancellation_settles_an_active_scheduled_run() {
1063 let context = RuntimeContext::from_current("scheduled-operation-cancel");
1064 let scheduled = ScheduledTaskGroup::new(context.root_group().clone());
1065 let operation = OperationContext::without_deadline(TaskKind::ScheduledDriver);
1066 let (started_tx, started_rx) = tokio::sync::oneshot::channel();
1067 let mut started = Some(started_tx);
1068 scheduled
1069 .schedule_operation(
1070 &operation,
1071 ScheduledTaskConfig::fixed_delay("cancel", Duration::from_secs(1)),
1072 ScheduledExecutionPolicy::default(),
1073 move || {
1074 let started = started.take();
1075 async move {
1076 if let Some(started) = started {
1077 let _ = started.send(());
1078 }
1079 std::future::pending::<()>().await;
1080 }
1081 },
1082 )
1083 .unwrap();
1084 started_rx.await.unwrap();
1085 assert!(operation
1086 .cancel_and_wait(&scheduled.group, Duration::from_secs(1))
1087 .await
1088 .unwrap());
1089 assert_eq!(scheduled.snapshot()[0].active_runs, 0);
1090 assert_eq!(scheduled.snapshot()[0].failures, 1);
1091 assert_eq!(scheduled.snapshot()[0].runs, 0);
1092 }
1093
1094 #[tokio::test(start_paused = true)]
1095 async fn fixed_delay_waits_for_the_first_delay_and_then_for_each_completion() {
1096 for operation_owned in [false, true] {
1097 let context = RuntimeContext::from_current("fixed-delay-contract");
1098 let scheduled = ScheduledTaskGroup::new(context.root_group().clone());
1099 let operation = OperationContext::without_deadline(TaskKind::ScheduledDriver);
1100 let config = ScheduledTaskConfig::fixed_delay("serial", Duration::from_secs(3))
1101 .with_initial_delay(Duration::from_secs(2));
1102 let release = Arc::new(Semaphore::new(0));
1103 let task_release = release.clone();
1104 let (starts, mut started) = tokio::sync::mpsc::unbounded_channel();
1105 let mut count = 0;
1106 let task = move || {
1107 count += 1;
1108 let count = count;
1109 let starts = starts.clone();
1110 let release = task_release.clone();
1111 async move {
1112 starts.send((count, tokio::time::Instant::now())).unwrap();
1113 if count == 1 {
1114 release.acquire().await.unwrap().forget();
1115 }
1116 }
1117 };
1118 let begin = tokio::time::Instant::now();
1119 let result = if operation_owned {
1120 scheduled.schedule_operation(&operation, config.clone(), ScheduledExecutionPolicy::default(), task)
1121 } else {
1122 scheduled.schedule(config.clone(), ScheduledExecutionPolicy::default(), task)
1123 };
1124 assert!(matches!(
1125 result.unwrap(),
1126 ScheduledTaskRegistrationOutcome::Scheduled(_)
1127 ));
1128 assert_eq!(
1129 scheduled
1130 .schedule(config, ScheduledExecutionPolicy::default(), || async {
1131 panic!("duplicate ran")
1132 })
1133 .unwrap(),
1134 ScheduledTaskRegistrationOutcome::AlreadyPresent,
1135 );
1136 let (count, first) = started.recv().await.unwrap();
1137 assert_eq!(count, 1);
1138 assert_eq!(first - begin, Duration::from_secs(2));
1139 tokio::time::advance(Duration::from_secs(10)).await;
1140 assert!(started.try_recv().is_err());
1141 assert_eq!(scheduled.snapshot()[0].active_runs, 1);
1142 let completion = tokio::time::Instant::now();
1143 release.add_permits(1);
1144 let (count, second) = started.recv().await.unwrap();
1145 assert_eq!(count, 2);
1146 assert_eq!(second - completion, Duration::from_secs(3));
1147 assert!(context.shutdown_tasks(Duration::from_secs(1)).await.is_healthy());
1148 let snapshot = &scheduled.snapshot()[0];
1149 assert_eq!(snapshot.runs, 2);
1150 assert_eq!(snapshot.active_runs, 0);
1151 assert_eq!(snapshot.failures, 0);
1152 }
1153 }
1154
1155 #[tokio::test(start_paused = true)]
1156 async fn a_controlled_schedule_ends_after_a_stop() {
1157 let context = RuntimeContext::from_current("scheduled-controlled-stop");
1158 let scheduled = ScheduledTaskGroup::new(context.root_group().clone());
1159 let calls = Arc::new(AtomicUsize::new(0));
1160 let task_calls = calls.clone();
1161 scheduled
1162 .schedule_controlled(
1163 ScheduledTaskConfig::fixed_delay("self-stopping", Duration::from_secs(1)),
1164 move || {
1165 let call = task_calls.fetch_add(1, Ordering::AcqRel) + 1;
1166 async move {
1167 if call == 2 {
1168 ScheduledTaskControl::Stop
1169 } else {
1170 ScheduledTaskControl::Continue
1171 }
1172 }
1173 },
1174 )
1175 .unwrap();
1176
1177 for _ in 0..5 {
1178 for _ in 0..20 {
1179 tokio::task::yield_now().await;
1180 }
1181 tokio::time::advance(Duration::from_secs(1)).await;
1182 }
1183 for _ in 0..20 {
1184 tokio::task::yield_now().await;
1185 }
1186 assert_eq!(calls.load(Ordering::Acquire), 2);
1187 let snapshot = &scheduled.snapshot()[0];
1188 assert_eq!(snapshot.runs, 2);
1189 assert_eq!(snapshot.active_runs, 0);
1190 assert_eq!(scheduled.group().task_count(), 0, "the driver ends after a stop");
1191 assert!(context.shutdown_tasks(Duration::from_secs(1)).await.is_healthy());
1192 }
1193
1194 #[tokio::test(start_paused = true)]
1195 async fn a_timed_out_run_releases_its_slot_once_before_the_next_controlled_run() {
1196 let context = RuntimeContext::from_current("scheduled-timeout");
1197 let scheduled = ScheduledTaskGroup::new(context.root_group().clone());
1198 let (second_tx, second_rx) = tokio::sync::oneshot::channel();
1199 let mut second_tx = Some(second_tx);
1200 let mut calls = 0;
1201 let mut config = ScheduledTaskConfig::fixed_delay("timeout", Duration::from_secs(1));
1202 config.max_run_time = Some(Duration::from_secs(1));
1203 scheduled
1204 .schedule_controlled(config, move || {
1205 calls += 1;
1206 let second = if calls == 2 { second_tx.take() } else { None };
1207 async move {
1208 if let Some(second) = second {
1209 let _ = second.send(());
1210 ScheduledTaskControl::Stop
1211 } else {
1212 std::future::pending().await
1213 }
1214 }
1215 })
1216 .unwrap();
1217 second_rx.await.unwrap();
1218 let report = context.shutdown_tasks(Duration::from_secs(1)).await;
1219 assert!(report.is_healthy());
1220 let snapshot = &scheduled.snapshot()[0];
1221 assert_eq!(snapshot.active_runs, 0);
1222 assert_eq!(snapshot.runs, 1);
1223 assert_eq!(snapshot.failures, 1);
1224 }
1225
1226 #[tokio::test(start_paused = true)]
1227 async fn coalesced_serial_schedule_never_exceeds_one_active_and_one_pending_run() {
1228 let context = RuntimeContext::try_from_current("bounded-schedule").unwrap();
1229 let scheduled = ScheduledTaskGroup::new(context.root_group().clone());
1230 let active = Arc::new(AtomicUsize::new(0));
1231 let max_active = Arc::new(AtomicUsize::new(0));
1232 let calls = Arc::new(AtomicUsize::new(0));
1233 let release = Arc::new(Semaphore::new(0));
1234
1235 let task_active = active.clone();
1236 let task_max_active = max_active.clone();
1237 let task_calls = calls.clone();
1238 let task_release = release.clone();
1239 scheduled
1240 .schedule(
1241 ScheduledTaskConfig::fixed_rate_no_overlap("bounded-coalesce", Duration::from_secs(1)),
1242 ScheduledExecutionPolicy::serial(MissedTickPolicy::CoalesceLatest),
1243 move || {
1244 let task_active = task_active.clone();
1245 let task_max_active = task_max_active.clone();
1246 let task_calls = task_calls.clone();
1247 let task_release = task_release.clone();
1248 async move {
1249 let current = task_active.fetch_add(1, Ordering::AcqRel) + 1;
1250 task_max_active.fetch_max(current, Ordering::AcqRel);
1251 task_calls.fetch_add(1, Ordering::AcqRel);
1252 let permit = task_release.acquire().await.unwrap();
1253 drop(permit);
1254 task_active.fetch_sub(1, Ordering::AcqRel);
1255 }
1256 },
1257 )
1258 .unwrap();
1259
1260 tokio::time::advance(Duration::from_secs(1)).await;
1261 for _ in 0..20 {
1262 tokio::task::yield_now().await;
1263 }
1264 assert_eq!(calls.load(Ordering::Acquire), 1);
1265
1266 tokio::time::advance(Duration::from_secs(5)).await;
1267 for _ in 0..20 {
1268 tokio::task::yield_now().await;
1269 }
1270 let snapshot = scheduled
1271 .snapshot()
1272 .into_iter()
1273 .find(|snapshot| snapshot.name == "bounded-coalesce")
1274 .unwrap();
1275 assert_eq!(snapshot.active_runs, 1);
1276 assert!(snapshot.skips >= 4);
1277 assert_eq!(calls.load(Ordering::Acquire), 1);
1278
1279 release.add_permits(1);
1280 for _ in 0..20 {
1281 tokio::task::yield_now().await;
1282 }
1283 assert_eq!(calls.load(Ordering::Acquire), 2);
1284 assert_eq!(max_active.load(Ordering::Acquire), 1);
1285
1286 release.add_permits(1);
1287 for _ in 0..20 {
1288 tokio::task::yield_now().await;
1289 }
1290 assert_eq!(active.load(Ordering::Acquire), 0);
1291 assert!(context.shutdown_tasks(Duration::from_secs(1)).await.is_healthy());
1292 }
1293
1294 #[tokio::test(start_paused = true)]
1295 async fn overlapping_runs_stay_within_the_policy_bound() {
1296 let context = RuntimeContext::from_current("scheduled-overlap-bound");
1297 let scheduled = ScheduledTaskGroup::new(context.root_group().clone());
1298 let active = Arc::new(AtomicUsize::new(0));
1299 let max_active = Arc::new(AtomicUsize::new(0));
1300 let release = Arc::new(Semaphore::new(0));
1301
1302 let task_active = active.clone();
1303 let task_max_active = max_active.clone();
1304 let task_release = release.clone();
1305 scheduled
1306 .schedule(
1307 ScheduledTaskConfig::fixed_rate("overlap", Duration::from_secs(1)),
1308 ScheduledExecutionPolicy::bounded(two(), MissedTickPolicy::Skip),
1309 move || {
1310 let task_active = task_active.clone();
1311 let task_max_active = task_max_active.clone();
1312 let task_release = task_release.clone();
1313 async move {
1314 let current = task_active.fetch_add(1, Ordering::AcqRel) + 1;
1315 task_max_active.fetch_max(current, Ordering::AcqRel);
1316 drop(task_release.acquire().await.unwrap());
1317 task_active.fetch_sub(1, Ordering::AcqRel);
1318 }
1319 },
1320 )
1321 .unwrap();
1322
1323 for _ in 0..5 {
1324 tokio::time::advance(Duration::from_secs(1)).await;
1325 for _ in 0..20 {
1326 tokio::task::yield_now().await;
1327 }
1328 }
1329 let snapshot = &scheduled.snapshot()[0];
1330 assert_eq!(max_active.load(Ordering::Acquire), 2);
1331 assert_eq!(snapshot.active_runs, 2);
1332 assert_eq!(snapshot.overlaps, 1);
1333 assert!(snapshot.skips >= 3, "{snapshot:?}");
1334
1335 release.add_permits(2);
1336 for _ in 0..20 {
1337 tokio::task::yield_now().await;
1338 }
1339 assert!(context.shutdown_tasks(Duration::from_secs(1)).await.is_healthy());
1340 }
1341}