Skip to main content

framework_timer/
runtime.rs

1use crate::{TimerNameLike, TimerRegistration, TimerResult, TimerTarget, TimerWrapper, timer_iter};
2use anyhow::{Result, anyhow};
3use framework_core::current_millis;
4use futures_util::FutureExt;
5use std::any::Any;
6use std::collections::HashMap;
7use std::panic::AssertUnwindSafe;
8use std::sync::{Arc, OnceLock};
9use tokio::sync::Semaphore;
10use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel};
11use tokio::task::{Id, JoinSet};
12use tokio::time::{Duration, sleep};
13
14static WAKE_SENDER: OnceLock<UnboundedSender<String>> = OnceLock::new();
15
16/// 定时任务运行时的基础配置。
17#[derive(Clone, Copy, Debug, Default)]
18pub struct TimerOption {
19    /// 同时运行的任务数上限;`None` 表示不设上限。
20    pub max_concurrent_tasks: Option<usize>,
21    /// 未在宏上单独指定时,任务 panic 后是否立即重试。
22    pub retry_immediately_on_panic: bool,
23}
24
25struct TimerState {
26    name: String,
27    registration: &'static TimerRegistration,
28    next_run_at: i64,
29    running: bool,
30}
31
32/// 启动所有已注册的定时任务。
33///
34/// 调度器等待至最近任务的下次执行时间,并将到期任务交给独立 Tokio 子任务执行,
35/// 因此某个任务的执行不会阻塞其他任务的调度。
36pub fn start(wrapper: Option<TimerWrapper>, option: TimerOption) {
37    let semaphore = option.max_concurrent_tasks.map(|count| {
38        assert!(count > 0, "max_concurrent_tasks 必须大于 0");
39        Arc::new(Semaphore::new(count))
40    });
41    let states = timer_iter()
42        .flat_map(|registration| {
43            (1..=registration.count).map(move |index| TimerState {
44                name: if registration.count == 1 {
45                    registration.name.to_owned()
46                } else {
47                    format!("{}_{index}", registration.name)
48                },
49                registration,
50                next_run_at: 0,
51                running: false,
52            })
53        })
54        .collect::<Vec<_>>();
55    let (wake_sender, wake_receiver) = unbounded_channel();
56    WAKE_SENDER
57        .set(wake_sender)
58        .expect("定时任务运行时只能启动一次");
59
60    tokio::spawn(run_scheduler(
61        states,
62        wake_receiver,
63        wrapper,
64        option,
65        semaphore,
66    ));
67}
68
69/// 立即唤醒名称匹配的任一空闲定时任务实例。
70pub fn wake(name: impl TimerNameLike) {
71    let Some(sender) = WAKE_SENDER.get() else {
72        return;
73    };
74    let _ = sender.send(name.timer_name().to_owned());
75}
76
77async fn run_scheduler(
78    mut states: Vec<TimerState>,
79    mut wake_receiver: UnboundedReceiver<String>,
80    wrapper: Option<TimerWrapper>,
81    option: TimerOption,
82    semaphore: Option<Arc<Semaphore>>,
83) {
84    let mut tasks = JoinSet::new();
85    let mut task_indices = HashMap::new();
86
87    loop {
88        let wait_millis = dispatch_due_tasks(
89            &mut states,
90            &mut tasks,
91            &mut task_indices,
92            &wrapper,
93            semaphore.as_ref(),
94            current_millis().expect("获取当前毫秒时间戳失败"),
95        );
96
97        match wait_millis {
98            Some(wait_millis) => {
99                tokio::select! {
100                    Some(name) = wake_receiver.recv() => wake_timer(&mut states, name),
101                    result = tasks.join_next_with_id(), if !tasks.is_empty() => {
102                        if let Some(result) = result {
103                            handle_task_result(result, &mut task_indices, &mut states, option);
104                        }
105                    }
106                    _ = sleep(Duration::from_millis(wait_millis)) => {}
107                    else => return,
108                }
109            }
110            None => {
111                tokio::select! {
112                    Some(name) = wake_receiver.recv() => wake_timer(&mut states, name),
113                    result = tasks.join_next_with_id() => {
114                        if let Some(result) = result {
115                            handle_task_result(result, &mut task_indices, &mut states, option);
116                        }
117                    }
118                    else => return,
119                }
120            }
121        }
122    }
123}
124
125fn wake_timer(states: &mut [TimerState], name: String) {
126    if let Some(state) = states
127        .iter_mut()
128        .find(|state| !state.running && state.registration.name == name)
129    {
130        state.next_run_at = -10086;
131    }
132}
133
134fn dispatch_due_tasks(
135    states: &mut [TimerState],
136    tasks: &mut JoinSet<Result<TimerResult>>,
137    task_indices: &mut HashMap<Id, usize>,
138    wrapper: &Option<TimerWrapper>,
139    semaphore: Option<&Arc<Semaphore>>,
140    now: i64,
141) -> Option<u64> {
142    let mut next_run_at: Option<i64> = None;
143    for (index, state) in states.iter_mut().enumerate() {
144        if state.running {
145            continue;
146        }
147        if state.next_run_at > now {
148            next_run_at =
149                Some(next_run_at.map_or(state.next_run_at, |next| next.min(state.next_run_at)));
150            continue;
151        }
152
153        state.running = true;
154        let target = state.registration.target;
155        let wrapper = wrapper.clone();
156        let semaphore = semaphore.cloned();
157        let task = tasks.spawn(async move {
158            execute_timer(target, wrapper.as_ref(), semaphore.as_ref()).await
159        });
160        task_indices.insert(task.id(), index);
161    }
162    next_run_at.map(|next_run_at| next_run_at.saturating_sub(now).max(0) as u64)
163}
164
165fn handle_task_result(
166    result: Result<(Id, Result<TimerResult>), tokio::task::JoinError>,
167    task_indices: &mut HashMap<Id, usize>,
168    states: &mut [TimerState],
169    option: TimerOption,
170) {
171    let (task_id, result) = match result {
172        Ok((task_id, result)) => (task_id, result),
173        Err(error) => {
174            log::error!("定时任务 Tokio 子任务执行失败:{error:?}");
175            (
176                error.id(),
177                Err(anyhow!("定时任务 Tokio 子任务执行失败:{error:?}")),
178            )
179        }
180    };
181    let index = task_indices
182        .remove(&task_id)
183        .expect("定时任务缺少任务实例索引");
184    let state = &mut states[index];
185    state.running = false;
186
187    let now = current_millis().expect("获取当前毫秒时间戳失败");
188    let next = match result {
189        Ok(result) => {
190            if let Some(error) = result.error {
191                log::error!("定时任务 {} 执行失败:{error:?}", state.name);
192            }
193            match result.next {
194                None => now.saturating_add(state.registration.interval),
195                Some(v) => v,
196            }
197        }
198        Err(error) => {
199            log::error!("定时任务 {} 发生 panic: {error:?}", state.name);
200            if state
201                .registration
202                .retry_immediately_on_panic
203                .unwrap_or(option.retry_immediately_on_panic)
204            {
205                0
206            } else {
207                now.saturating_add(state.registration.interval)
208            }
209        }
210    };
211    state.next_run_at = next
212}
213
214async fn execute_timer(
215    target: TimerTarget,
216    wrapper: Option<&TimerWrapper>,
217    semaphore: Option<&Arc<Semaphore>>,
218) -> Result<TimerResult> {
219    let _permit = match semaphore {
220        Some(semaphore) => Some(
221            semaphore
222                .acquire()
223                .await
224                .map_err(|_| anyhow!("定时任务并发控制 Semaphore 已关闭"))?,
225        ),
226        None => None,
227    };
228
229    let future = match wrapper {
230        Some(wrapper) => wrapper(target),
231        None => target(),
232    };
233
234    AssertUnwindSafe(future)
235        .catch_unwind()
236        .await
237        .map_err(panic_to_error)
238}
239
240fn panic_to_error(payload: Box<dyn Any + Send>) -> anyhow::Error {
241    let message = if let Some(message) = payload.downcast_ref::<&str>() {
242        *message
243    } else if let Some(message) = payload.downcast_ref::<String>() {
244        message.as_str()
245    } else {
246        "未知 panic payload"
247    };
248
249    anyhow!("定时任务执行发生 panic: {message}")
250}