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 { state: self.state };
100 (handle, slot)
101 }
102
103 /// Splits this pair into a tracked result handle and completion endpoint.
104 ///
105 /// # Returns
106 ///
107 /// A [`TrackedTask`] for the caller and a [`TaskSlot`] for the runner.
108 #[inline]
109 pub fn into_tracked_parts(self) -> (TrackedTask<R, E>, TaskSlot<R, E>) {
110 let handle = TaskHandle::new(Arc::clone(&self.state), self.receiver);
111 let tracked = TrackedTask::new(handle);
112 let slot = TaskSlot { state: self.state };
113 (tracked, slot)
114 }
115}
116
117impl<R, E> Default for TaskEndpointPair<R, E> {
118 /// Creates a new unsplit task completion pair.
119 #[inline]
120 fn default() -> Self {
121 Self::new()
122 }
123}