qubit_executor/task/task_runner.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::panic::{
11 AssertUnwindSafe,
12 catch_unwind,
13};
14
15use qubit_function::Callable;
16
17use super::{
18 TaskExecutionError,
19 TaskResult,
20 running_task_slot::RunningTaskSlot,
21 task_slot::TaskSlot,
22};
23
24/// Runner that executes a callable task with standard task-handle semantics.
25///
26/// `TaskRunner` owns the accepted callable, converts task failures and panics
27/// into [`TaskExecutionError`], and can publish the final result through a
28/// [`TaskSlot`] endpoint.
29pub struct TaskRunner<C> {
30 /// Callable task owned by this runner.
31 task: C,
32}
33
34impl<C> TaskRunner<C> {
35 /// Creates a runner for the supplied callable task.
36 ///
37 /// # Parameters
38 ///
39 /// * `task` - Callable task to execute later.
40 ///
41 /// # Returns
42 ///
43 /// A runner that owns the callable task.
44 #[inline]
45 pub const fn new(task: C) -> Self {
46 Self { task }
47 }
48
49 /// Runs the callable and converts task failure and panic into a handle result.
50 ///
51 /// # Returns
52 ///
53 /// `Ok(R)` if the task succeeds. If the task returns `Err(E)` or panics, the
54 /// corresponding [`TaskExecutionError`] is returned.
55 pub fn call<R, E>(self) -> TaskResult<R, E>
56 where
57 C: Callable<R, E>,
58 {
59 let mut task = self.task;
60 match catch_unwind(AssertUnwindSafe(|| task.call())) {
61 Ok(Ok(value)) => Ok(value),
62 Ok(Err(err)) => Err(TaskExecutionError::Failed(err)),
63 Err(_) => Err(TaskExecutionError::Panicked),
64 }
65 }
66
67 /// Runs this task through a task completion endpoint.
68 ///
69 /// # Parameters
70 ///
71 /// * `completion` - Completion endpoint that publishes the final result.
72 ///
73 /// # Returns
74 ///
75 /// `true` if the task started and its result was published, or `false` if
76 /// the completion endpoint had already been completed before the runner
77 /// started.
78 #[inline]
79 pub fn run<R, E>(self, slot: TaskSlot<R, E>) -> bool
80 where
81 C: Callable<R, E>,
82 {
83 slot.start_and_complete(|| self.call())
84 }
85
86 /// Runs this task through an already-started task slot.
87 ///
88 /// # Parameters
89 ///
90 /// * `slot` - Running slot that has already won the pending-to-running
91 /// transition.
92 ///
93 /// # Returns
94 ///
95 /// `true` if the task result was published, or `false` if completion was
96 /// no longer possible.
97 #[inline]
98 pub fn run_started<R, E>(self, slot: RunningTaskSlot<R, E>) -> bool
99 where
100 C: Callable<R, E>,
101 {
102 slot.complete(self.call())
103 }
104
105 /// Runs this task and discards its final result.
106 ///
107 /// This is intended for detached runnable submissions whose caller only
108 /// observes acceptance. The callable is still wrapped in panic conversion,
109 /// but success, task failure, and panic results are intentionally dropped.
110 pub fn run_detached<R, E>(self)
111 where
112 C: Callable<R, E>,
113 {
114 let _ignored = self.call::<R, E>();
115 }
116}