Skip to main content

qubit_executor/hook/
task_hook.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2025 - 2026 Haixing Hu.
4 *
5 *    SPDX-License-Identifier: Apache-2.0
6 *
7 *    Licensed under the Apache License, Version 2.0.
8 *
9 ******************************************************************************/
10use std::{
11    panic::{
12        AssertUnwindSafe,
13        catch_unwind,
14    },
15    sync::Arc,
16};
17
18use crate::{
19    TaskStatus,
20    service::SubmissionError,
21};
22
23use super::TaskId;
24
25/// Observes task lifecycle events emitted by executors and executor services.
26///
27/// Hook events follow a strict lifecycle ordering for each task. A rejected
28/// submission emits only [`Self::on_rejected`] and never receives a task id. An
29/// accepted submission emits [`Self::on_accepted`] before any later
30/// [`Self::on_started`] or [`Self::on_finished`] event for the same task.
31/// `on_started` is emitted only when the task actually begins running; a task
32/// cancelled before start or abandoned by its accepted runner endpoint may emit
33/// `on_finished` without `on_started`.
34///
35/// Executors contain hook panics so hook implementations cannot break task
36/// execution or result publication.
37pub trait TaskHook: Send + Sync + 'static {
38    /// Called after a task is accepted.
39    ///
40    /// # Parameters
41    ///
42    /// * `task_id` - Identifier assigned to the accepted task.
43    #[inline]
44    fn on_accepted(&self, _task_id: TaskId) {}
45
46    /// Called when a submitted task is rejected.
47    ///
48    /// Rejected tasks do not have task ids and never emit started or finished
49    /// events.
50    ///
51    /// # Parameters
52    ///
53    /// * `error` - Submission error explaining the rejection.
54    #[inline]
55    fn on_rejected(&self, _error: &SubmissionError) {}
56
57    /// Called immediately before an accepted task starts running.
58    ///
59    /// This callback is emitted after [`Self::on_accepted`] for the same task.
60    ///
61    /// # Parameters
62    ///
63    /// * `task_id` - Identifier assigned to the accepted task.
64    #[inline]
65    fn on_started(&self, _task_id: TaskId) {}
66
67    /// Called after an accepted task reaches a terminal status.
68    ///
69    /// This callback is emitted after [`Self::on_accepted`] for the same task.
70    /// It is normally emitted after [`Self::on_started`], except for pre-start
71    /// cancellation or accepted runner-endpoint abandonment.
72    ///
73    /// # Parameters
74    ///
75    /// * `task_id` - Identifier assigned to the accepted task.
76    /// * `status` - Terminal status observed for the task.
77    #[inline]
78    fn on_finished(&self, _task_id: TaskId, _status: TaskStatus) {}
79}
80
81/// Calls a task hook and contains hook panics.
82///
83/// # Parameters
84///
85/// * `callback` - Hook callback to invoke.
86fn contain_hook_panic(callback: impl FnOnce()) {
87    if catch_unwind(AssertUnwindSafe(callback)).is_err() {
88        log::warn!("task hook panicked");
89    }
90}
91
92/// Notifies a hook that a task was accepted.
93///
94/// # Parameters
95///
96/// * `hook` - Hook to notify.
97/// * `task_id` - Accepted task id.
98#[inline]
99pub(crate) fn notify_accepted(hook: &dyn TaskHook, task_id: TaskId) {
100    contain_hook_panic(|| hook.on_accepted(task_id));
101}
102
103/// Notifies a hook that a task was rejected.
104///
105/// # Parameters
106///
107/// * `hook` - Hook to notify.
108/// * `error` - Submission error explaining the rejection.
109#[inline]
110pub(crate) fn notify_rejected(hook: &dyn TaskHook, error: &SubmissionError) {
111    contain_hook_panic(|| hook.on_rejected(error));
112}
113
114/// Notifies an optional hook that a task was rejected.
115///
116/// # Parameters
117///
118/// * `hook` - Optional hook to notify.
119/// * `error` - Submission error explaining the rejection.
120#[inline]
121pub(crate) fn notify_rejected_optional(hook: Option<&Arc<dyn TaskHook>>, error: &SubmissionError) {
122    if let Some(hook) = hook {
123        notify_rejected(hook.as_ref(), error);
124    }
125}
126
127/// Notifies a hook that a task started.
128///
129/// # Parameters
130///
131/// * `hook` - Hook to notify.
132/// * `task_id` - Started task id.
133#[inline]
134pub(crate) fn notify_started(hook: &dyn TaskHook, task_id: TaskId) {
135    contain_hook_panic(|| hook.on_started(task_id));
136}
137
138/// Notifies a hook that a task finished.
139///
140/// # Parameters
141///
142/// * `hook` - Hook to notify.
143/// * `task_id` - Finished task id.
144/// * `status` - Terminal task status.
145#[inline]
146pub(crate) fn notify_finished(hook: &dyn TaskHook, task_id: TaskId, status: TaskStatus) {
147    contain_hook_panic(|| hook.on_finished(task_id, status));
148}