Skip to main content

qubit_executor/task/
task_endpoint_pair.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::sync::Arc;
11
12use oneshot::Receiver;
13use oneshot::channel;
14
15use crate::hook::{
16    TaskHook,
17    next_task_id,
18};
19
20use super::task_execution_error::TaskResult;
21use super::task_handle::TaskHandle;
22use super::task_slot::TaskSlot;
23use super::task_state::TaskState;
24use super::tracked_task::TrackedTask;
25
26/// One-shot pair of endpoints for a task submission.
27///
28/// A pair owns the shared task completion endpoint and the result receiver
29/// until it is split into caller-facing and runner-facing endpoints. Pairs
30/// created with [`Self::new`] do not install a lifecycle hook, avoiding the
31/// allocation and dynamic dispatch cost of a no-op hook on the default path.
32///
33/// Custom executors using this SPI should call [`TaskSlot::accept`] or the
34/// crate-internal handle acceptance path only after submission has succeeded.
35/// Dropping a runner slot before acceptance releases result waiters with
36/// `Dropped` but does not emit hook lifecycle events. Once a service has
37/// accepted a task, it should call [`TaskSlot::cancel_unstarted`] rather than
38/// dropping the slot when it intentionally removes unstarted work, so result
39/// handles observe cancellation instead of an abandoned runner endpoint.
40pub struct TaskEndpointPair<R, E> {
41    /// Receiver consumed by the caller-facing handle.
42    receiver: Receiver<TaskResult<R, E>>,
43    /// Shared completion state consumed by the runner-facing endpoint.
44    state: Arc<TaskState<R, E>>,
45}
46
47impl<R, E> TaskEndpointPair<R, E> {
48    /// Creates a new unsplit task completion pair without lifecycle hooks.
49    ///
50    /// # Returns
51    ///
52    /// A pair that can be split once into its handle and completion endpoints.
53    #[inline]
54    pub fn new() -> Self {
55        Self::with_optional_hook(None)
56    }
57
58    /// Creates a new unsplit task completion pair with a lifecycle hook.
59    ///
60    /// # Parameters
61    ///
62    /// * `hook` - Hook notified about this task's lifecycle.
63    ///
64    /// # Returns
65    ///
66    /// A pair that can be split once into its handle and runner slot.
67    #[inline]
68    pub fn with_hook(hook: Arc<dyn TaskHook>) -> Self {
69        Self::with_optional_hook(Some(hook))
70    }
71
72    /// Creates a new unsplit task completion pair with an optional lifecycle hook.
73    ///
74    /// # Parameters
75    ///
76    /// * `hook` - Hook notified about this task's lifecycle after acceptance, or
77    ///   `None` for the fast path with no hook allocation or callback dispatch.
78    ///
79    /// # Returns
80    ///
81    /// A pair that can be split once into its handle and runner slot.
82    #[inline]
83    pub(crate) fn with_optional_hook(hook: Option<Arc<dyn TaskHook>>) -> Self {
84        let (sender, receiver) = channel();
85        Self {
86            receiver,
87            state: Arc::new(TaskState::new(next_task_id(), sender, hook)),
88        }
89    }
90
91    /// Splits this pair into a result handle and completion endpoint.
92    ///
93    /// # Returns
94    ///
95    /// A [`TaskHandle`] for the caller and a [`TaskSlot`] for the runner.
96    #[inline]
97    pub fn into_parts(self) -> (TaskHandle<R, E>, TaskSlot<R, E>) {
98        let handle = TaskHandle::new(Arc::clone(&self.state), self.receiver);
99        let slot = TaskSlot {
100            state: Some(self.state),
101        };
102        (handle, slot)
103    }
104
105    /// Splits this pair into a tracked result handle and completion endpoint.
106    ///
107    /// # Returns
108    ///
109    /// A [`TrackedTask`] for the caller and a [`TaskSlot`] for the runner.
110    #[inline]
111    pub fn into_tracked_parts(self) -> (TrackedTask<R, E>, TaskSlot<R, E>) {
112        let handle = TaskHandle::new(Arc::clone(&self.state), self.receiver);
113        let tracked = TrackedTask::new(handle);
114        let slot = TaskSlot {
115            state: Some(self.state),
116        };
117        (tracked, slot)
118    }
119}
120
121impl<R, E> Default for TaskEndpointPair<R, E> {
122    /// Creates a new unsplit task completion pair.
123    #[inline]
124    fn default() -> Self {
125        Self::new()
126    }
127}