Skip to main content

rocketmq_runtime/
scheduled.rs

1// Copyright 2023 The RocketMQ Rust Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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/// Identifies the schedule mode state.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
41pub enum ScheduleMode {
42    /// Waits `period` after each run completes.
43    FixedDelay,
44    /// Ticks every `period`; runs never overlap.
45    FixedRateNoOverlap,
46    /// Ticks every `period`; runs overlap up to the policy's bound.
47    FixedRateAllowOverlap,
48}
49
50/// Maximum concurrent runs admitted for one bounded schedule.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
52pub enum ScheduledTaskConcurrency {
53    /// At most one run is active.
54    Serial,
55    /// At most `max_runs` runs are active.
56    Bounded(NonZeroUsize),
57}
58
59/// Policy for ticks that arrive while all run slots are occupied.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
61pub enum MissedTickPolicy {
62    /// Discard the missed tick.
63    Skip,
64    /// Retain at most one pending intent and run it when a slot becomes free.
65    CoalesceLatest,
66    /// Retain at most `max_pending` intents and run them as slots become free.
67    BoundedCatchUp(NonZeroUsize),
68}
69
70/// Bounded execution policy for one scheduled task.
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
72pub struct ScheduledExecutionPolicy {
73    /// Maximum concurrent runs.
74    pub concurrency: ScheduledTaskConcurrency,
75    /// Behavior for missed ticks.
76    pub missed_ticks: MissedTickPolicy,
77}
78
79impl ScheduledExecutionPolicy {
80    /// Creates a serial policy.
81    #[must_use]
82    pub const fn serial(missed_ticks: MissedTickPolicy) -> Self {
83        Self {
84            concurrency: ScheduledTaskConcurrency::Serial,
85            missed_ticks,
86        }
87    }
88
89    /// Creates a bounded-overlap policy.
90    #[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    /// One run at a time; a tick that arrives while it runs is skipped.
101    fn default() -> Self {
102        Self::serial(MissedTickPolicy::Skip)
103    }
104}
105
106/// Identifies the scheduled task control state.
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
108pub enum ScheduledTaskControl {
109    /// Represents the continue case.
110    Continue,
111    /// Represents the stop case.
112    Stop,
113}
114
115/// Describes the result of registering a scheduled task.
116///
117/// A duplicate schedule name is a normal outcome. The existing registration,
118/// its driver, and its metrics remain unchanged.
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub enum ScheduledTaskRegistrationOutcome {
121    /// The task was registered and its driver was started.
122    Scheduled(TaskId),
123    /// A task with the requested name was already registered.
124    AlreadyPresent,
125}
126
127/// Name, cadence and run limit of one schedule.
128///
129/// The mode chosen by the constructor is the only input that decides how
130/// runs are timed and whether they may overlap. The policy passed at
131/// registration must agree with it.
132#[derive(Debug, Clone)]
133pub struct ScheduledTaskConfig {
134    /// The name value.
135    pub name: String,
136    /// Delay before the first run; zero means the first run is immediately due.
137    pub initial_delay: Duration,
138    /// Delay between completions or fixed-rate tick interval, according to mode.
139    pub period: Duration,
140    /// The mode value.
141    pub mode: ScheduleMode,
142    /// Optional limit on each asynchronous run, starting when it is polled.
143    ///
144    /// Expiry drops the run future; it cannot interrupt blocking work already
145    /// started by that future.
146    pub max_run_time: Option<Duration>,
147}
148
149impl ScheduledTaskConfig {
150    /// Creates a schedule that waits `period` after each run completes.
151    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    /// Creates a schedule that ticks every `period` and never overlaps runs.
162    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    /// Creates a schedule that ticks every `period` and overlaps runs up to
170    /// the bound of its policy.
171    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    /// Sets the delay before the first run.
179    #[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    /// Checks the configuration against `policy` and returns the run limit.
186    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/// Represents scheduled task group.
204#[derive(Debug, Clone)]
205pub struct ScheduledTaskGroup {
206    group: TaskGroup,
207    schedules: Arc<DashMap<Arc<str>, Arc<ScheduledTaskMetrics>>>,
208}
209
210/// Reads an explicitly selected set of schedules without retaining their owner.
211///
212/// Lookup cost is bounded by the names supplied at construction. A selected
213/// schedule may be registered later; missing registrations are omitted.
214#[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    /// Reads the selected registrations using only in-memory metric state.
222    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/// Represents scheduled task snapshot.
250#[derive(Debug, Clone, Serialize)]
251pub struct ScheduledTaskSnapshot {
252    /// The name value.
253    pub name: String,
254    /// The mode value.
255    pub mode: ScheduleMode,
256    /// Whether running.
257    pub running: bool,
258    /// The active runs value.
259    pub active_runs: u64,
260    /// Runs that returned normally, including a controlled stop.
261    pub runs: u64,
262    /// The skips value.
263    pub skips: u64,
264    /// The overlaps value.
265    pub overlaps: u64,
266    /// Reserved runs that timed out, panicked, were cancelled, or could not start.
267    pub failures: u64,
268    /// The last drift duration in milliseconds.
269    pub last_drift_ms: u64,
270    /// The last elapsed duration in milliseconds.
271    pub last_elapsed_ms: u64,
272    /// The max elapsed duration in milliseconds.
273    pub max_elapsed_ms: u64,
274}
275
276/// Where a schedule's driver and runs are registered.
277#[derive(Clone, Copy)]
278enum ScheduleBinding<'a> {
279    /// The fixed component owner; its cancellation ends the schedule.
280    Group,
281    /// A bounded operation under the component owner.
282    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
304/// Spawns the runs of one fixed-rate schedule.
305struct 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
322/// One run of a registered task.
323type ScheduledRun = Pin<Box<dyn Future<Output = ScheduledTaskControl> + Send + 'static>>;
324
325/// A registered task after type erasure; each call starts the next run.
326///
327/// Erasing the task where it is registered compiles the drivers, the run
328/// adapter and the task-group submission they use once in this crate instead
329/// of once per registration site, and keeps a large closure or run future out
330/// of the driver's own state.
331type ScheduledTaskFn = Box<dyn FnMut() -> ScheduledRun + Send + 'static>;
332
333/// Erases a task whose runs return nothing; every run continues the schedule.
334fn 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
348/// Erases a task whose runs decide whether the schedule continues.
349fn 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    /// Creates a new `ScheduledTaskGroup`.
359    pub fn new(group: TaskGroup) -> Self {
360        Self {
361            group,
362            // Registrations are rare, so a few shards suffice.
363            schedules: Arc::new(DashMap::with_shard_amount(4)),
364        }
365    }
366
367    /// Returns the group.
368    pub fn group(&self) -> &TaskGroup {
369        &self.group
370    }
371
372    /// Schedules `task` with the cadence of `config` and the run limits of `policy`.
373    ///
374    /// The driver and runs belong to this group and stop when it is cancelled.
375    /// A fixed-delay schedule runs `task` on its driver; a fixed-rate schedule
376    /// starts each run as its own task once a run slot is free. A duplicate
377    /// name leaves the existing registration unchanged.
378    ///
379    /// # Errors
380    ///
381    /// Returns an unsupported error when the period is zero or `policy`
382    /// contradicts the mode of `config`: fixed-delay and non-overlapping
383    /// schedules need a serial policy, an overlapping schedule needs a bounded
384    /// one. Returns an operational error when the driver cannot be spawned.
385    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    /// Schedules `task` like [`Self::schedule`] as part of a bounded operation.
399    ///
400    /// The driver and runs are registered with this group's fixed component
401    /// owner and stop when the operation is cancelled or reaches its deadline.
402    ///
403    /// # Errors
404    ///
405    /// Returns the same errors as [`Self::schedule`].
406    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    /// Schedules fixed-delay work that can end its own schedule.
421    ///
422    /// The schedule stops after a run returns [`ScheduledTaskControl::Stop`].
423    ///
424    /// # Errors
425    ///
426    /// Returns an unsupported error when the period is zero or `config` is not
427    /// a fixed-delay schedule, and an operational error when the driver cannot
428    /// be spawned.
429    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        // Runs only lock the closure to create their future, never while it runs.
493        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    /// Returns the snapshot.
526    pub fn snapshot(&self) -> Vec<ScheduledTaskSnapshot> {
527        self.schedules.iter().map(|entry| entry.value().snapshot()).collect()
528    }
529
530    /// Observes a fixed selection of schedule names without keeping tasks alive.
531    ///
532    /// Duplicate names are removed. Callers should select component-level jobs,
533    /// not names derived from requests or resources.
534    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    /// Clears completed schedule registrations so a fixed component owner can
545    /// start a new operation generation with the same schedule names.
546    ///
547    /// # Errors
548    ///
549    /// Returns an error while the component group still has active tasks.
550    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    /// Shuts down the owned service.
561    pub async fn shutdown(&self, timeout: Duration) -> ShutdownReport {
562        self.group.shutdown(timeout).await
563    }
564
565    /// Shuts down the owned service using one absolute deadline.
566    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
694// Reservation owns settlement even before a submitted future is first polled.
695// Drop covers construction/poll/destructor panics, rejection, and cancellation.
696struct 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
762/// Starts one reserved run; returns `false` if the owner rejected it.
763fn 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        // Only a controlled fixed-delay schedule can stop itself.
778        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
838/// Awaits `run` within `max_run_time`; returns its control and whether it timed out.
839async 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
850// Group and operation schedules share timing and settlement; their driver
851// registration keeps the distinct component or operation cancellation boundary.
852async 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}