1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
use crate::{Promise, PromiseRejection};
impl<T, E> Promise<T, E>
where
T: Send + 'static,
E: PromiseRejection,
{
/// Spawns this [`Promise`] on the ambient runtime to run to completion in
/// the background, discarding its outcome.
///
/// This is fire-and-forget: it consumes the [`Promise`], satisfying the
/// `#[must_use]` obligation without an `.await`. The inner future
/// continues running in the background after this call returns, and its
/// `Ok`/`Err` result is dropped. Panics are caught by the [`Promise`]
/// itself and turned into a rejection, which is likewise dropped, so a
/// detached [`Promise`] never propagates a panic to the executor.
///
/// Dispatch is selected at compile time based on which runtime features
/// are enabled, with a runtime check when both are on:
///
/// - Only `tokio` enabled: spawns via `tokio::spawn` and drops the
/// `JoinHandle`, which detaches the task.
/// - Only `smol` enabled: spawns via `smol::spawn` and calls
/// `smol::Task::detach`; without this the task would be cancelled on
/// drop.
/// - Both enabled: dispatches to the tokio path when called from within a
/// tokio runtime context (detected via
/// `tokio::runtime::Handle::try_current`), otherwise to the smol path.
///
/// Requires at least one of the `tokio` or `smol` features; if neither is
/// enabled this method does not exist and call sites fail to compile.
///
/// # Panics
///
/// Panics if only the `tokio` feature is enabled and this method is
/// called outside of a tokio runtime context, propagated from
/// `tokio::spawn`. With the `smol` feature enabled there is no such
/// requirement: outside a tokio runtime context the promise is spawned
/// on smol's global executor instead.
pub fn detach(self) {
#[cfg(all(feature = "tokio", feature = "smol"))]
if tokio::runtime::Handle::try_current().is_ok() {
drop(tokio::spawn(self));
} else {
smol::spawn(self).detach();
}
#[cfg(all(feature = "tokio", not(feature = "smol")))]
drop(tokio::spawn(self));
#[cfg(all(feature = "smol", not(feature = "tokio")))]
smol::spawn(self).detach();
}
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use crate::{Promise, PromiseRejection, TaskFailure};
#[derive(Debug)]
#[allow(dead_code)]
enum E {
AlreadyConsumed,
TaskFailed(TaskFailure),
}
impl PromiseRejection for E {
fn already_consumed() -> Self {
Self::AlreadyConsumed
}
fn task_failed(failure: TaskFailure) -> Self {
Self::TaskFailed(failure)
}
}
#[cfg(feature = "tokio")]
#[test]
fn runs_detached_via_tokio() {
let rt = tokio::runtime::Builder::new_current_thread()
.build()
.expect("build current-thread tokio runtime");
let ran = Arc::new(AtomicBool::new(false));
let flag = ran.clone();
rt.block_on(async move {
Promise::<(), E>::lazy(async move {
flag.store(true, Ordering::Relaxed);
Ok(())
})
.detach();
for _ in 0..5 {
tokio::task::yield_now().await;
}
});
assert!(
ran.load(Ordering::Relaxed),
"detached promise must run without being awaited"
);
}
#[cfg(all(feature = "smol", not(feature = "tokio")))]
#[test]
fn runs_detached_via_smol() {
let ran = Arc::new(AtomicBool::new(false));
let inner_flag = ran.clone();
let wait_flag = ran.clone();
smol::block_on(async move {
Promise::<(), E>::lazy(async move {
inner_flag.store(true, Ordering::Relaxed);
Ok(())
})
.detach();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while !wait_flag.load(Ordering::Relaxed) && std::time::Instant::now() < deadline {
std::thread::sleep(std::time::Duration::from_millis(1));
}
});
assert!(
ran.load(Ordering::Relaxed),
"detached promise must run without being awaited"
);
}
}