1use parking_lot::Mutex;
2use std::sync::{
3 atomic::{AtomicBool, Ordering},
4 Arc,
5};
6
7mod effect;
8pub use effect::spawn_effect;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
11#[serde(transparent)]
12pub struct WorkerId(u64);
13impl WorkerId {
14 pub const fn new(value: u64) -> Self {
15 Self(value)
16 }
17 pub const fn get(self) -> u64 {
18 self.0
19 }
20}
21#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
22#[serde(transparent)]
23pub struct WorkerIds(u64);
24impl WorkerIds {
25 pub fn allocate(&mut self) -> Result<WorkerId, Failure> {
26 let next = self.0.checked_add(1).ok_or_else(|| {
27 Failure::new(
28 FailureKind::IdentityExhausted,
29 "worker request IDs exhausted",
30 )
31 })?;
32 self.0 = next;
33 Ok(WorkerId::new(next))
34 }
35}
36#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
37pub enum FailureKind {
38 IdentityExhausted,
39 ThreadStart,
40 Panic,
41 Io,
42 Spawn,
43 Wait,
44 Exit,
45 InvalidInput,
46 Unavailable,
47 Protocol,
48 Disconnected,
49}
50#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
51pub struct Failure {
52 pub kind: FailureKind,
53 pub message: String,
54}
55impl Failure {
56 pub fn new(kind: FailureKind, message: impl Into<String>) -> Self {
57 Self {
58 kind,
59 message: message.into(),
60 }
61 }
62}
63#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
64pub enum CancelReason {
65 Superseded,
66 OwnerClosed,
67 Dismissed,
68 Shutdown,
69}
70#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
71pub enum Outcome<T> {
72 Success(T),
73 Failed {
74 failure: Failure,
75 partial: Option<T>,
76 },
77 Cancelled(CancelReason),
78}
79impl<T> Outcome<T> {
80 pub fn failed(kind: FailureKind, message: impl Into<String>) -> Self {
81 Self::Failed {
82 failure: Failure::new(kind, message),
83 partial: None,
84 }
85 }
86}
87#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
88pub struct Ticket<K> {
89 pub request: WorkerId,
90 pub key: K,
91}
92#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
93pub struct Completion<K, T> {
94 pub ticket: Ticket<K>,
95 pub outcome: Outcome<T>,
96}
97#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
98pub enum Load<K> {
99 Idle,
100 Running(Ticket<K>),
101 Ready(K),
102 Failed { key: K, failure: Failure },
103 Cancelled { key: K, reason: CancelReason },
104}
105impl<K: PartialEq> Load<K> {
106 pub fn owns(&self, ticket: &Ticket<K>) -> bool {
107 matches!(self, Self::Running(current) if current == ticket)
108 }
109 pub fn covers(&self, key: &K) -> bool {
110 match self {
111 Self::Idle => false,
112 Self::Running(t) => &t.key == key,
113 Self::Ready(k) | Self::Failed { key: k, .. } | Self::Cancelled { key: k, .. } => {
114 k == key
115 }
116 }
117 }
118 pub fn retry_failed(&mut self) {
119 if matches!(self, Self::Failed { .. } | Self::Cancelled { .. }) {
120 *self = Self::Idle;
121 }
122 }
123}
124
125type Resource = Box<dyn FnOnce() -> Result<(), Failure> + Send>;
126#[derive(Default)]
127struct Cancellation {
128 cancelled: AtomicBool,
129 resource: Mutex<Option<Resource>>,
130}
131#[derive(Clone)]
132pub struct CancelToken(Arc<Cancellation>);
133fn invoke(resource: Resource) -> Result<(), Failure> {
134 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(resource)) {
135 Ok(result) => result,
136 Err(_) => Err(Failure::new(
137 FailureKind::Panic,
138 "cancellation resource panicked",
139 )),
140 }
141}
142impl CancelToken {
143 pub fn is_cancelled(&self) -> bool {
144 self.0.cancelled.load(Ordering::Acquire)
145 }
146
147 pub fn register_cancel_resource(
152 &self,
153 resource: impl FnOnce() -> Result<(), Failure> + Send + 'static,
154 ) -> Result<(), Failure> {
155 let resource: Resource = Box::new(resource);
156 {
157 let mut slot = self.0.resource.lock();
158 if !self.is_cancelled() {
159 if slot.is_some() {
160 return Err(Failure::new(
161 FailureKind::Protocol,
162 "cancellation resource already registered",
163 ));
164 }
165 *slot = Some(resource);
166 return Ok(());
167 }
168 }
169 invoke(resource)
170 }
171
172 pub fn clear_cancel_resource(&self) {
175 let resource = self.0.resource.lock().take();
176 drop(resource);
177 }
178
179 fn cancel_resource(&self) -> Result<(), Failure> {
180 let resource = {
181 let mut slot = self.0.resource.lock();
182 self.0.cancelled.store(true, Ordering::Release);
183 slot.take()
184 };
185 match resource {
186 Some(resource) => invoke(resource),
187 None => Ok(()),
188 }
189 }
190}
191type Emitter<T> = Arc<Mutex<Option<Box<dyn FnOnce(Outcome<T>) + Send>>>>;
192fn finish<T>(emitter: &Emitter<T>, outcome: Outcome<T>) {
193 let emit = emitter.lock().take();
194 if let Some(emit) = emit {
195 emit(outcome);
196 }
197}
198pub struct CancelHandle {
199 cancel: Option<Box<dyn FnOnce(CancelReason) + Send>>,
200}
201impl CancelHandle {
202 pub fn cancel(mut self, reason: CancelReason) {
203 if let Some(cancel) = self.cancel.take() {
204 cancel(reason);
205 }
206 }
207}
208impl Drop for CancelHandle {
209 fn drop(&mut self) {
210 if let Some(cancel) = self.cancel.take() {
211 cancel(CancelReason::OwnerClosed);
212 }
213 }
214}
215#[must_use]
218pub struct PreparedWork<T> {
219 token: CancelToken,
220 emitter: Emitter<T>,
221}
222
223pub fn prepare<T: Send + 'static>(
224 emit: impl FnOnce(Outcome<T>) + Send + 'static,
225) -> PreparedWork<T> {
226 PreparedWork {
227 token: CancelToken(Arc::new(Cancellation::default())),
228 emitter: Arc::new(Mutex::new(Some(Box::new(emit)))),
229 }
230}
231
232impl<T: Send + 'static> PreparedWork<T> {
233 pub fn cancel_handle(&self) -> CancelHandle {
236 let cancel_token = self.token.clone();
237 let cancel_emitter = self.emitter.clone();
238 CancelHandle {
239 cancel: Some(Box::new(move |reason| {
240 let emit = cancel_emitter.lock().take();
241 if let Some(emit) = emit {
242 let outcome = match cancel_token.cancel_resource() {
243 Ok(()) => Outcome::Cancelled(reason),
244 Err(failure) => Outcome::Failed {
245 failure,
246 partial: None,
247 },
248 };
249 emit(outcome);
250 }
251 })),
252 }
253 }
254
255 pub fn run(self, work: impl FnOnce(CancelToken) -> Outcome<T>) {
256 if self.token.is_cancelled() {
257 return;
258 }
259 let outcome = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
260 work(self.token.clone())
261 })) {
262 Ok(outcome) => outcome,
263 Err(_) => match self.token.cancel_resource() {
264 Ok(()) => Outcome::failed(FailureKind::Panic, "worker panicked"),
265 Err(failure) => Outcome::Failed {
266 failure,
267 partial: None,
268 },
269 },
270 };
271 self.token.clear_cancel_resource();
272 finish(&self.emitter, outcome);
273 }
274}
275
276pub fn spawn<T: Send + 'static>(
277 name: &'static str,
278 emit: impl FnOnce(Outcome<T>) + Send + 'static,
279 work: impl FnOnce(CancelToken) -> Outcome<T> + Send + 'static,
280) -> CancelHandle {
281 let prepared = prepare(emit);
282 let handle = prepared.cancel_handle();
283 let emitter = prepared.emitter.clone();
284 if let Err(error) = std::thread::Builder::new()
285 .name(name.into())
286 .spawn(move || prepared.run(work))
287 {
288 finish(
289 &emitter,
290 Outcome::failed(FailureKind::ThreadStart, error.to_string()),
291 );
292 }
293 handle
294}
295
296pub fn spawn_scoped<'scope, 'env, T: Send + 'static>(
299 scope: &'scope std::thread::Scope<'scope, 'env>,
300 name: &'static str,
301 emit: impl FnOnce(Outcome<T>) + Send + 'static,
302 work: impl FnOnce(CancelToken) -> Outcome<T> + Send + 'scope,
303) -> CancelHandle {
304 let prepared = prepare(emit);
305 let handle = prepared.cancel_handle();
306 let emitter = prepared.emitter.clone();
307 if let Err(error) = std::thread::Builder::new()
308 .name(name.into())
309 .spawn_scoped(scope, move || prepared.run(work))
310 {
311 finish(
312 &emitter,
313 Outcome::failed(FailureKind::ThreadStart, error.to_string()),
314 );
315 }
316 handle
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322 use std::sync::mpsc::channel;
323
324 #[test]
325 fn cancellation_reserves_terminal_before_callback_and_worker_success() {
326 let (ready_tx, ready_rx) = channel();
327 let (entered_tx, entered_rx) = channel();
328 let (release_tx, release_rx) = channel();
329 let (work_tx, work_rx) = channel();
330 let (done_tx, done_rx) = channel();
331 let (tx, rx) = channel();
332 let handle = spawn(
333 "race",
334 move |result| {
335 tx.send(result).unwrap();
336 },
337 move |token| {
338 token
339 .register_cancel_resource(move || {
340 entered_tx.send(()).unwrap();
341 release_rx.recv().unwrap();
342 Err(Failure::new(FailureKind::Io, "cleanup failed"))
343 })
344 .unwrap();
345 ready_tx.send(()).unwrap();
346 work_rx.recv().unwrap();
347 done_tx.send(()).unwrap();
348 Outcome::Success(())
349 },
350 );
351 ready_rx.recv().unwrap();
352 let cancel = std::thread::spawn(move || handle.cancel(CancelReason::Dismissed));
353 entered_rx.recv().unwrap();
354 work_tx.send(()).unwrap();
355 done_rx.recv().unwrap();
356 assert!(rx.try_recv().is_err());
357 release_tx.send(()).unwrap();
358 cancel.join().unwrap();
359 assert!(
360 matches!(rx.recv().unwrap(), Outcome::Failed { failure, .. } if failure.kind == FailureKind::Io)
361 );
362 assert!(rx.recv().is_err());
363 }
364
365 #[test]
366 fn late_registration_runs_immediately_and_success_wins_when_already_published() {
367 let token = CancelToken(Arc::new(Cancellation::default()));
368 token.cancel_resource().unwrap();
369 let (tx, rx) = channel();
370 token
371 .register_cancel_resource(move || {
372 tx.send(()).unwrap();
373 Ok(())
374 })
375 .unwrap();
376 rx.recv().unwrap();
377 let (tx, rx) = channel();
378 let handle = spawn(
379 "success",
380 move |result| {
381 tx.send(result).unwrap();
382 },
383 |_| Outcome::Success(7),
384 );
385 assert!(matches!(rx.recv().unwrap(), Outcome::Success(7)));
386 drop(handle);
387 assert!(rx.recv().is_err());
388 }
389
390 #[test]
391 fn queued_cancellation_prevents_work_from_starting() {
392 let (tx, rx) = channel();
393 let prepared: PreparedWork<()> = prepare(move |outcome| {
394 tx.send(outcome).unwrap();
395 });
396 prepared.cancel_handle().cancel(CancelReason::Superseded);
397 prepared.run(|_| panic!("cancelled queued work executed"));
398 assert!(matches!(
399 rx.recv().unwrap(),
400 Outcome::Cancelled(CancelReason::Superseded)
401 ));
402 assert!(rx.recv().is_err());
403 }
404
405 #[test]
406 fn scoped_worker_cancellation_does_not_skip_physical_join() {
407 let (control_tx, control_rx) = channel();
408 let (entered_tx, entered_rx) = channel();
409 let (release_tx, release_rx) = channel();
410 let (exited_tx, exited_rx) = channel();
411 let (tx, rx) = channel();
412 let scope_owner = std::thread::spawn(move || {
413 std::thread::scope(|scope| {
414 let handle = spawn_scoped(
415 scope,
416 "scoped-reader",
417 move |outcome| {
418 tx.send(outcome).unwrap();
419 },
420 move |_| {
421 entered_tx.send(()).unwrap();
422 release_rx.recv().unwrap();
423 Outcome::Success(())
424 },
425 );
426 assert!(control_tx.send(handle).is_ok());
427 });
428 exited_tx.send(()).unwrap();
429 });
430 let handle = control_rx.recv().unwrap();
431 entered_rx.recv().unwrap();
432 handle.cancel(CancelReason::Dismissed);
433 assert!(matches!(
434 rx.recv().unwrap(),
435 Outcome::Cancelled(CancelReason::Dismissed)
436 ));
437 assert!(exited_rx.try_recv().is_err());
438 release_tx.send(()).unwrap();
439 exited_rx.recv().unwrap();
440 scope_owner.join().unwrap();
441 }
442}