1#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
37#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
38#![allow(clippy::multiple_crate_versions)]
39
40use std::{
41 cell::RefCell,
42 future::Future,
43 sync::{LazyLock, RwLock, atomic::AtomicU64},
44};
45
46use switchy::unsync::util::CancellationToken;
47
48static WORKER_THREAD_ID_COUNTER: LazyLock<AtomicU64> = LazyLock::new(|| AtomicU64::new(1));
49
50thread_local! {
51 static WORKER_THREAD_ID: RefCell<u64> = RefCell::new(WORKER_THREAD_ID_COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst));
52}
53
54#[must_use]
58pub fn worker_thread_id() -> u64 {
59 WORKER_THREAD_ID.with_borrow(|x| *x)
60}
61
62thread_local! {
63 static SIMULATOR_CANCELLATION_TOKEN: RefCell<RwLock<CancellationToken>> =
64 RefCell::new(RwLock::new(CancellationToken::new()));
65}
66
67pub fn reset_simulator_cancellation_token() {
76 SIMULATOR_CANCELLATION_TOKEN
77 .with_borrow_mut(|x| *x.write().unwrap() = CancellationToken::new());
78}
79
80#[must_use]
88pub fn is_simulator_cancelled() -> bool {
89 is_global_simulator_cancelled()
90 || SIMULATOR_CANCELLATION_TOKEN.with_borrow(|x| x.read().unwrap().is_cancelled())
91}
92
93pub fn cancel_simulation() {
102 SIMULATOR_CANCELLATION_TOKEN.with_borrow(|x| x.read().unwrap().cancel());
103}
104
105static GLOBAL_SIMULATOR_CANCELLATION_TOKEN: LazyLock<RwLock<CancellationToken>> =
106 LazyLock::new(|| RwLock::new(CancellationToken::new()));
107
108pub fn reset_global_simulator_cancellation_token() {
117 *GLOBAL_SIMULATOR_CANCELLATION_TOKEN.write().unwrap() = CancellationToken::new();
118}
119
120#[must_use]
128pub fn is_global_simulator_cancelled() -> bool {
129 GLOBAL_SIMULATOR_CANCELLATION_TOKEN
130 .read()
131 .unwrap()
132 .is_cancelled()
133}
134
135pub fn cancel_global_simulation() {
144 GLOBAL_SIMULATOR_CANCELLATION_TOKEN.read().unwrap().cancel();
145}
146
147pub async fn run_until_simulation_cancelled<F>(fut: F) -> Option<F::Output>
171where
172 F: Future,
173{
174 let global_token = GLOBAL_SIMULATOR_CANCELLATION_TOKEN.read().unwrap().clone();
175 let local_token = SIMULATOR_CANCELLATION_TOKEN.with_borrow(|x| x.read().unwrap().clone());
176
177 switchy::unsync::select! {
178 resp = fut => Some(resp),
179 () = global_token.cancelled() => None,
180 () = local_token.cancelled() => None,
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use std::sync::{
187 Arc,
188 atomic::{AtomicBool, Ordering},
189 };
190
191 use serial_test::serial;
192
193 use super::*;
194
195 #[test_log::test]
201 #[serial]
202 fn test_worker_thread_id_returns_unique_ids() {
203 let id1 = worker_thread_id();
204 let id2 = worker_thread_id();
205 assert_eq!(id1, id2);
207 }
208
209 #[test_log::test]
210 #[serial]
211 fn test_worker_thread_id_uniqueness_across_threads() {
212 let id1 = worker_thread_id();
213 let handle = std::thread::spawn(worker_thread_id);
214 let id2 = handle.join().unwrap();
215 assert_ne!(id1, id2);
217 }
218
219 #[test_log::test]
220 #[serial]
221 fn test_local_cancellation_isolated_between_threads() {
222 reset_global_simulator_cancellation_token();
224 reset_simulator_cancellation_token();
225
226 cancel_simulation();
228 assert!(is_simulator_cancelled());
229
230 let handle = std::thread::spawn(|| {
232 reset_simulator_cancellation_token();
234 is_simulator_cancelled()
235 });
236
237 let other_thread_cancelled = handle.join().unwrap();
238 assert!(
241 !other_thread_cancelled,
242 "Local cancellation should not affect other threads"
243 );
244 }
245
246 #[test_log::test]
247 #[serial]
248 fn test_reset_simulator_cancellation_token() {
249 reset_global_simulator_cancellation_token();
251 reset_simulator_cancellation_token();
252
253 cancel_simulation();
255 assert!(is_simulator_cancelled());
256
257 reset_simulator_cancellation_token();
259 assert!(!is_simulator_cancelled());
260 }
261
262 #[test_log::test]
263 #[serial]
264 fn test_cancel_simulation_sets_cancelled_state() {
265 reset_global_simulator_cancellation_token();
267 reset_simulator_cancellation_token();
268
269 assert!(!is_simulator_cancelled());
270
271 cancel_simulation();
272 assert!(is_simulator_cancelled());
273 }
274
275 #[test_log::test]
276 #[serial]
277 fn test_is_simulator_cancelled_respects_global_cancellation() {
278 reset_global_simulator_cancellation_token();
280 reset_simulator_cancellation_token();
281
282 assert!(!is_simulator_cancelled());
283
284 cancel_global_simulation();
285 assert!(is_simulator_cancelled());
287 }
288
289 #[test_log::test]
290 #[serial]
291 fn test_global_cancellation_independent_from_local() {
292 reset_global_simulator_cancellation_token();
294 reset_simulator_cancellation_token();
295
296 cancel_simulation();
297 assert!(!is_global_simulator_cancelled());
299 assert!(is_simulator_cancelled());
300 }
301
302 #[test_log::test]
303 #[serial]
304 fn test_reset_global_simulator_cancellation_token() {
305 reset_global_simulator_cancellation_token();
307 reset_simulator_cancellation_token();
308
309 cancel_global_simulation();
310
311 assert!(is_global_simulator_cancelled());
312
313 reset_global_simulator_cancellation_token();
314 assert!(!is_global_simulator_cancelled());
315 }
316
317 #[test_log::test(switchy_async::test)]
318 #[serial]
319 async fn test_run_until_simulation_cancelled_completes_normally() {
320 reset_global_simulator_cancellation_token();
322 reset_simulator_cancellation_token();
323
324 let result = run_until_simulation_cancelled(async { 42 }).await;
325 assert_eq!(result, Some(42));
326 }
327
328 #[test_log::test(switchy_async::test)]
329 #[serial]
330 async fn test_run_until_simulation_cancelled_with_local_cancellation() {
331 reset_global_simulator_cancellation_token();
333 reset_simulator_cancellation_token();
334
335 let cancel_task = async {
336 cancel_simulation();
337 };
338
339 let work_task = async {
340 std::future::pending::<()>().await;
342 42
343 };
344
345 cancel_task.await;
347 let result = run_until_simulation_cancelled(work_task).await;
348 assert_eq!(result, None);
349 }
350
351 #[test_log::test(switchy_async::test)]
352 #[serial]
353 async fn test_run_until_simulation_cancelled_with_global_cancellation() {
354 reset_global_simulator_cancellation_token();
356 reset_simulator_cancellation_token();
357
358 let cancel_task = async {
359 cancel_global_simulation();
360 };
361
362 let work_task = async {
363 std::future::pending::<()>().await;
365 42
366 };
367
368 cancel_task.await;
370 let result = run_until_simulation_cancelled(work_task).await;
371 assert_eq!(result, None);
372 }
373
374 #[test_log::test]
375 #[serial]
376 fn test_global_cancellation_affects_other_threads() {
377 reset_global_simulator_cancellation_token();
379 reset_simulator_cancellation_token();
380
381 assert!(!is_global_simulator_cancelled());
383
384 cancel_global_simulation();
386
387 let handle = std::thread::spawn(|| {
389 reset_simulator_cancellation_token();
391 is_simulator_cancelled()
393 });
394
395 let other_thread_sees_cancellation = handle.join().unwrap();
396 assert!(
397 other_thread_sees_cancellation,
398 "Global cancellation should be visible to all threads"
399 );
400 }
401
402 #[test_log::test]
403 #[serial]
404 fn test_worker_thread_ids_are_monotonically_increasing() {
405 let mut handles = Vec::new();
407 for _ in 0..5 {
408 handles.push(std::thread::spawn(worker_thread_id));
409 }
410
411 let mut ids: Vec<u64> = handles.into_iter().map(|h| h.join().unwrap()).collect();
412
413 ids.sort_unstable();
415 let original_len = ids.len();
416 ids.dedup();
417 assert_eq!(ids.len(), original_len, "All thread IDs should be unique");
418
419 assert!(ids.iter().all(|&id| id >= 1), "All IDs should be >= 1");
421 }
422
423 #[test_log::test]
424 #[serial]
425 fn test_is_simulator_cancelled_with_both_local_and_global_cancelled() {
426 reset_global_simulator_cancellation_token();
428 reset_simulator_cancellation_token();
429
430 cancel_simulation();
432 cancel_global_simulation();
433
434 assert!(is_simulator_cancelled());
436 assert!(is_global_simulator_cancelled());
437
438 reset_global_simulator_cancellation_token();
440 assert!(is_simulator_cancelled());
441 assert!(!is_global_simulator_cancelled());
442
443 reset_simulator_cancellation_token();
445 assert!(!is_simulator_cancelled());
446 }
447
448 #[test_log::test]
449 #[serial]
450 fn test_global_cancellation_from_multiple_threads_is_thread_safe() {
451 reset_global_simulator_cancellation_token();
453
454 let mut handles = Vec::new();
456 for _ in 0..10 {
457 handles.push(std::thread::spawn(|| {
458 cancel_global_simulation();
459 is_global_simulator_cancelled()
460 }));
461 }
462
463 for handle in handles {
465 let result = handle.join().unwrap();
466 assert!(result, "All threads should see global cancellation");
467 }
468
469 assert!(is_global_simulator_cancelled());
471 }
472
473 #[test_log::test(switchy_async::test)]
474 #[serial]
475 async fn test_run_until_simulation_cancelled_with_concurrent_cancellation() {
476 reset_global_simulator_cancellation_token();
478 reset_simulator_cancellation_token();
479
480 let work_started = Arc::new(AtomicBool::new(false));
481 let work_started_clone = Arc::clone(&work_started);
482
483 let work_task = async move {
485 work_started_clone.store(true, Ordering::SeqCst);
486 std::future::pending::<()>().await;
487 42
488 };
489
490 let result = switchy::unsync::select! {
492 result = run_until_simulation_cancelled(work_task) => result,
493 () = async {
494 while !work_started.load(Ordering::SeqCst) {
496 switchy::unsync::task::yield_now().await;
497 }
498 cancel_simulation();
500 } => None,
501 };
502
503 assert_eq!(result, None, "Task should be cancelled");
504 }
505}