1use std::future::Future;
30use std::marker::PhantomData;
31use std::pin::Pin;
32#[cfg(feature = "native")]
33use std::sync::atomic::AtomicU8;
34use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
35use std::sync::{Arc, Mutex, Weak};
36use std::task::{Context as TaskContext, Poll, Waker};
37use std::time::Duration;
38
39use crate::sync_primitives::SystemTime;
40
41#[cfg(feature = "native")]
42use asupersync::types::Time as NativeTime;
43#[cfg(feature = "native")]
44use asupersync::types::{CancelKind as NativeCancelKind, CancelReason as NativeCancelReason};
45#[cfg(feature = "native")]
46use asupersync::{Budget as NativeBudget, Cx as NativeCx};
47
48#[cfg(not(feature = "native"))]
49mod native_cx_shim {
50 use std::sync::atomic::{AtomicBool, Ordering};
51 use std::sync::{Arc, Mutex};
52
53 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
54 pub enum NativeCancelKind {
55 User,
56 Timeout,
57 Deadline,
58 PollQuota,
59 CostBudget,
60 FailFast,
61 RaceLost,
62 ParentCancelled,
63 Shutdown,
64 LinkedExit,
65 ResourceUnavailable,
66 }
67
68 #[derive(Debug, Clone, PartialEq, Eq)]
69 pub struct NativeCancelReason {
70 pub kind: NativeCancelKind,
71 }
72
73 impl NativeCancelReason {
74 #[must_use]
75 pub const fn timeout() -> Self {
76 Self {
77 kind: NativeCancelKind::Timeout,
78 }
79 }
80
81 #[must_use]
82 pub fn user(_message: impl Into<String>) -> Self {
83 Self {
84 kind: NativeCancelKind::User,
85 }
86 }
87
88 #[must_use]
89 pub const fn parent_cancelled() -> Self {
90 Self {
91 kind: NativeCancelKind::ParentCancelled,
92 }
93 }
94
95 #[must_use]
96 pub const fn resource_unavailable() -> Self {
97 Self {
98 kind: NativeCancelKind::ResourceUnavailable,
99 }
100 }
101 }
102
103 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
104 pub struct NativeCheckpointError;
105
106 #[derive(Debug, Default)]
107 struct NativeCxInner {
108 cancel_requested: AtomicBool,
109 cancel_reason: Mutex<Option<NativeCancelReason>>,
110 }
111
112 #[derive(Debug, Clone, Default)]
113 pub struct NativeCx {
114 inner: Arc<NativeCxInner>,
115 }
116
117 impl NativeCx {
118 #[must_use]
119 pub fn for_testing() -> Self {
120 Self::default()
121 }
122
123 pub fn set_cancel_requested(&self, requested: bool) {
124 self.inner
125 .cancel_requested
126 .store(requested, Ordering::Release);
127 if !requested {
128 *self
129 .inner
130 .cancel_reason
131 .lock()
132 .unwrap_or_else(std::sync::PoisonError::into_inner) = None;
133 }
134 }
135
136 pub fn set_cancel_reason(&self, reason: NativeCancelReason) {
137 *self
138 .inner
139 .cancel_reason
140 .lock()
141 .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(reason);
142 self.inner.cancel_requested.store(true, Ordering::Release);
143 }
144
145 #[must_use]
146 pub fn is_cancel_requested(&self) -> bool {
147 self.inner.cancel_requested.load(Ordering::Acquire)
148 }
149
150 #[must_use]
151 pub fn cancel_reason(&self) -> Option<NativeCancelReason> {
152 self.inner
153 .cancel_reason
154 .lock()
155 .unwrap_or_else(std::sync::PoisonError::into_inner)
156 .clone()
157 }
158
159 pub fn checkpoint(&self) -> std::result::Result<(), NativeCheckpointError> {
160 if self.is_cancel_requested() {
161 Err(NativeCheckpointError)
162 } else {
163 Ok(())
164 }
165 }
166 }
167}
168
169#[cfg(not(feature = "native"))]
170use native_cx_shim::NativeCx;
171
172use crate::eprocess::{EProcessDecision, EProcessOracle, EProcessSnapshot};
173
174pub const SQLITE_INTERRUPT: i32 = 9;
176
177pub const MAX_MASK_DEPTH: u32 = 64;
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
192pub enum CancelState {
193 Created,
194 Running,
195 CancelRequested,
196 Cancelling,
197 Finalizing,
198 Completed,
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
206pub enum CancelReason {
207 Timeout = 0,
208 UserInterrupt = 1,
209 RegionClose = 2,
210 Abort = 3,
211}
212
213pub mod cap {
215 mod sealed {
216 pub trait Sealed {}
217
218 pub struct Bit<const V: bool>;
219
220 pub trait Le {}
221 impl Le for (Bit<false>, Bit<false>) {}
222 impl Le for (Bit<false>, Bit<true>) {}
223 impl Le for (Bit<true>, Bit<true>) {}
224 }
225
226 #[derive(Debug, Clone, Copy, Default)]
228 pub struct CapSet<
229 const SPAWN: bool,
230 const TIME: bool,
231 const RANDOM: bool,
232 const IO: bool,
233 const REMOTE: bool,
234 >;
235
236 impl<
237 const SPAWN: bool,
238 const TIME: bool,
239 const RANDOM: bool,
240 const IO: bool,
241 const REMOTE: bool,
242 > sealed::Sealed for CapSet<SPAWN, TIME, RANDOM, IO, REMOTE>
243 {
244 }
245
246 pub type All = CapSet<true, true, true, true, true>;
248 pub type None = CapSet<false, false, false, false, false>;
250
251 pub trait SubsetOf<Super>: sealed::Sealed {}
256
257 impl<
258 const S_SPAWN: bool,
259 const S_TIME: bool,
260 const S_RANDOM: bool,
261 const S_IO: bool,
262 const S_REMOTE: bool,
263 const P_SPAWN: bool,
264 const P_TIME: bool,
265 const P_RANDOM: bool,
266 const P_IO: bool,
267 const P_REMOTE: bool,
268 > SubsetOf<CapSet<P_SPAWN, P_TIME, P_RANDOM, P_IO, P_REMOTE>>
269 for CapSet<S_SPAWN, S_TIME, S_RANDOM, S_IO, S_REMOTE>
270 where
271 (sealed::Bit<S_SPAWN>, sealed::Bit<P_SPAWN>): sealed::Le,
272 (sealed::Bit<S_TIME>, sealed::Bit<P_TIME>): sealed::Le,
273 (sealed::Bit<S_RANDOM>, sealed::Bit<P_RANDOM>): sealed::Le,
274 (sealed::Bit<S_IO>, sealed::Bit<P_IO>): sealed::Le,
275 (sealed::Bit<S_REMOTE>, sealed::Bit<P_REMOTE>): sealed::Le,
276 {
277 }
278
279 pub trait HasSpawn: sealed::Sealed {}
280 impl<const TIME: bool, const RANDOM: bool, const IO: bool, const REMOTE: bool> HasSpawn
281 for CapSet<true, TIME, RANDOM, IO, REMOTE>
282 {
283 }
284
285 pub trait HasTime: sealed::Sealed {}
286 impl<const SPAWN: bool, const RANDOM: bool, const IO: bool, const REMOTE: bool> HasTime
287 for CapSet<SPAWN, true, RANDOM, IO, REMOTE>
288 {
289 }
290
291 pub trait HasRandom: sealed::Sealed {}
292 impl<const SPAWN: bool, const TIME: bool, const IO: bool, const REMOTE: bool> HasRandom
293 for CapSet<SPAWN, TIME, true, IO, REMOTE>
294 {
295 }
296
297 pub trait HasIo: sealed::Sealed {}
298 impl<const SPAWN: bool, const TIME: bool, const RANDOM: bool, const REMOTE: bool> HasIo
299 for CapSet<SPAWN, TIME, RANDOM, true, REMOTE>
300 {
301 }
302
303 pub trait HasRemote: sealed::Sealed {}
304 impl<const SPAWN: bool, const TIME: bool, const RANDOM: bool, const IO: bool> HasRemote
305 for CapSet<SPAWN, TIME, RANDOM, IO, true>
306 {
307 }
308}
309
310pub type FullCaps = cap::All;
312pub type StorageCaps = cap::CapSet<false, true, false, true, false>;
314pub type ComputeCaps = cap::None;
316
317#[derive(Debug, Clone, Copy, PartialEq, Eq)]
323pub struct Budget {
324 pub deadline: Option<Duration>,
327 pub poll_quota: u32,
328 pub cost_quota: Option<u64>,
329 pub priority: u8,
330}
331
332impl Budget {
333 pub const INFINITE: Self = Self {
335 deadline: None,
336 poll_quota: u32::MAX,
337 cost_quota: None,
338 priority: 0,
339 };
340
341 pub const MINIMAL: Self = Self {
343 deadline: None,
344 poll_quota: 100,
345 cost_quota: None,
346 priority: 0,
347 };
348
349 #[must_use]
350 pub const fn with_deadline(self, deadline: Duration) -> Self {
351 Self {
352 deadline: Some(deadline),
353 ..self
354 }
355 }
356
357 #[must_use]
358 pub const fn with_priority(self, priority: u8) -> Self {
359 Self { priority, ..self }
360 }
361
362 #[must_use]
363 pub const fn with_poll_quota(self, poll_quota: u32) -> Self {
364 Self { poll_quota, ..self }
365 }
366
367 #[must_use]
368 pub const fn with_cost_quota(self, cost_quota: u64) -> Self {
369 Self {
370 cost_quota: Some(cost_quota),
371 ..self
372 }
373 }
374
375 #[must_use]
377 pub fn meet(self, other: Self) -> Self {
378 Self {
379 deadline: match (self.deadline, other.deadline) {
380 (Some(a), Some(b)) => Some(a.min(b)),
381 (Some(a), None) => Some(a),
382 (None, Some(b)) => Some(b),
383 (None, None) => None,
384 },
385 poll_quota: self.poll_quota.min(other.poll_quota),
386 cost_quota: match (self.cost_quota, other.cost_quota) {
387 (Some(a), Some(b)) => Some(a.min(b)),
388 (Some(a), None) => Some(a),
389 (None, Some(b)) => Some(b),
390 (None, None) => None,
391 },
392 priority: self.priority.max(other.priority),
393 }
394 }
395}
396
397#[derive(Debug, Clone, Copy, PartialEq, Eq)]
398pub enum ErrorKind {
399 Cancelled,
400}
401
402#[derive(Debug, Clone, PartialEq, Eq)]
403pub struct Error {
404 kind: ErrorKind,
405}
406
407impl std::fmt::Display for Error {
408 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
409 match self.kind {
410 ErrorKind::Cancelled => write!(f, "operation cancelled"),
411 }
412 }
413}
414
415impl std::error::Error for Error {}
416
417impl Error {
418 #[must_use]
419 pub const fn cancelled() -> Self {
420 Self {
421 kind: ErrorKind::Cancelled,
422 }
423 }
424
425 #[must_use]
426 pub const fn kind(&self) -> ErrorKind {
427 self.kind
428 }
429
430 #[must_use]
431 pub const fn sqlite_error_code(&self) -> i32 {
432 match self.kind {
433 ErrorKind::Cancelled => SQLITE_INTERRUPT,
434 }
435 }
436}
437
438pub type Result<T, E = Error> = std::result::Result<T, E>;
439
440#[derive(Debug)]
441struct LocalCancelWaiter {
442 id: u64,
443 waker: Waker,
444}
445
446#[derive(Debug, Default)]
447struct LocalCancelWaiters {
448 next_id: u64,
449 entries: Vec<LocalCancelWaiter>,
450}
451
452impl LocalCancelWaiters {
453 fn allocate_id(&mut self) -> u64 {
454 loop {
455 let id = self.next_id;
456 self.next_id = self.next_id.wrapping_add(1);
457 if self.entries.iter().all(|waiter| waiter.id != id) {
458 return id;
459 }
460 }
461 }
462
463 fn remove(&mut self, id: u64) -> Option<LocalCancelWaiter> {
464 self.entries
465 .iter()
466 .position(|waiter| waiter.id == id)
467 .map(|position| self.entries.swap_remove(position))
468 }
469}
470
471#[derive(Debug)]
472struct CxInner {
473 cancel_requested: AtomicBool,
474 #[cfg(feature = "native")]
479 native_cancel_reason: AtomicU8,
480 cancel_state: Mutex<CancelState>,
481 cancel_reason: Mutex<Option<CancelReason>>,
482 mask_depth: AtomicU32,
483 cancel_dispatch_gate: Arc<Mutex<()>>,
484 local_cancel_waiters: Mutex<LocalCancelWaiters>,
485 children: Mutex<Vec<Weak<Self>>>,
486 last_checkpoint_msg: Mutex<Option<String>>,
487 last_eprocess_decision: Mutex<Option<EProcessDecision>>,
488 eprocess_oracle: std::sync::OnceLock<Arc<EProcessOracle>>,
489 #[cfg(feature = "native")]
490 attached_native_cx: Mutex<Option<NativeCx>>,
491 #[cfg(feature = "native")]
492 fallback_native_cx: std::sync::OnceLock<NativeCx>,
493 blocking_io_inline_safe: AtomicBool,
500 unix_millis: AtomicU64,
507 unix_millis_is_fixed: AtomicBool,
508}
509
510impl CxInner {
511 fn new(cancel_dispatch_gate: Arc<Mutex<()>>) -> Self {
512 Self {
513 cancel_requested: AtomicBool::new(false),
514 #[cfg(feature = "native")]
515 native_cancel_reason: AtomicU8::new(0),
516 cancel_state: Mutex::new(CancelState::Created),
517 cancel_reason: Mutex::new(None),
518 mask_depth: AtomicU32::new(0),
519 cancel_dispatch_gate,
520 local_cancel_waiters: Mutex::new(LocalCancelWaiters::default()),
521 children: Mutex::new(Vec::new()),
522 last_checkpoint_msg: Mutex::new(None),
523 last_eprocess_decision: Mutex::new(None),
524 eprocess_oracle: std::sync::OnceLock::new(),
525 #[cfg(feature = "native")]
526 attached_native_cx: Mutex::new(None),
527 #[cfg(feature = "native")]
528 fallback_native_cx: std::sync::OnceLock::new(),
529 blocking_io_inline_safe: AtomicBool::new(false),
530 unix_millis: AtomicU64::new(0),
531 unix_millis_is_fixed: AtomicBool::new(false),
532 }
533 }
534}
535
536#[cfg(feature = "native")]
537#[must_use]
538fn local_reason_to_native(reason: CancelReason) -> NativeCancelReason {
539 match reason {
540 CancelReason::Timeout => NativeCancelReason::timeout(),
541 CancelReason::UserInterrupt => NativeCancelReason::user("sqlite interrupt"),
542 CancelReason::RegionClose => NativeCancelReason::parent_cancelled(),
543 CancelReason::Abort => NativeCancelReason::resource_unavailable(),
544 }
545}
546
547#[cfg(feature = "native")]
548#[must_use]
549fn native_reason_to_local(reason: &NativeCancelReason) -> CancelReason {
550 match reason.kind {
551 NativeCancelKind::User => CancelReason::UserInterrupt,
552 NativeCancelKind::Timeout
553 | NativeCancelKind::Deadline
554 | NativeCancelKind::PollQuota
555 | NativeCancelKind::CostBudget => CancelReason::Timeout,
556 NativeCancelKind::FailFast
557 | NativeCancelKind::RaceLost
558 | NativeCancelKind::ParentCancelled
559 | NativeCancelKind::Shutdown
560 | NativeCancelKind::LinkedExit => CancelReason::RegionClose,
561 NativeCancelKind::ResourceUnavailable => CancelReason::Abort,
562 }
563}
564
565#[cfg(feature = "native")]
566const fn encode_native_cancel_reason(reason: CancelReason) -> u8 {
567 match reason {
568 CancelReason::Timeout => 1,
569 CancelReason::UserInterrupt => 2,
570 CancelReason::RegionClose => 3,
571 CancelReason::Abort => 4,
572 }
573}
574
575#[cfg(feature = "native")]
576fn decode_native_cancel_reason(encoded: u8) -> Option<CancelReason> {
577 match encoded {
578 0 => None,
579 1 => Some(CancelReason::Timeout),
580 2 => Some(CancelReason::UserInterrupt),
581 3 => Some(CancelReason::RegionClose),
582 4 => Some(CancelReason::Abort),
583 _ => unreachable!("invalid native cancellation reason rank"),
584 }
585}
586
587#[cfg(feature = "native")]
588fn record_native_cancel_reason(inner: &CxInner, reason: CancelReason) {
589 inner
590 .native_cancel_reason
591 .fetch_max(encode_native_cancel_reason(reason), Ordering::AcqRel);
592}
593
594#[cfg(feature = "native")]
595fn mirrored_native_cancel_reason(inner: &CxInner) -> Option<CancelReason> {
596 decode_native_cancel_reason(inner.native_cancel_reason.load(Ordering::Acquire))
597}
598
599#[cfg(feature = "native")]
600fn sync_one_native_cx_cancel(inner: &CxInner, native: &NativeCx) {
601 let mut encoded = inner.native_cancel_reason.load(Ordering::Acquire);
602 while let Some(reason) = decode_native_cancel_reason(encoded) {
603 native.set_cancel_reason(local_reason_to_native(reason));
606 let latest = inner.native_cancel_reason.load(Ordering::Acquire);
607 if latest == encoded {
608 break;
609 }
610 encoded = latest;
611 }
612}
613
614#[cfg(feature = "native")]
615#[must_use]
616#[allow(dead_code)]
617fn native_budget_from_local_at(budget: Budget, now: NativeTime) -> NativeBudget {
618 let mut native_budget = NativeBudget::new()
619 .with_poll_quota(budget.poll_quota)
620 .with_priority(budget.priority);
621 if let Some(cost_quota) = budget.cost_quota {
622 native_budget = native_budget.with_cost_quota(cost_quota);
623 }
624 if let Some(timeout) = budget.deadline {
625 native_budget = native_budget.with_timeout(now, timeout);
626 }
627 native_budget
628}
629
630#[derive(Debug, Clone, Copy, PartialEq, Eq)]
631enum NativeCancelPropagation {
632 LocalAndNative,
633 LocalOnly,
634}
635
636#[must_use]
637fn local_cancellation_matches(inner: &CxInner, respect_mask: bool) -> bool {
638 inner.cancel_requested.load(Ordering::Acquire)
639 && (!respect_mask || inner.mask_depth.load(Ordering::Acquire) == 0)
640}
641
642fn take_local_cancel_waiters(inner: &CxInner) -> Vec<LocalCancelWaiter> {
643 let mut waiters = inner
644 .local_cancel_waiters
645 .lock()
646 .unwrap_or_else(std::sync::PoisonError::into_inner);
647 std::mem::take(&mut waiters.entries)
648}
649
650fn capture_cancel_callback_panic(
651 first_panic: &mut Option<Box<dyn std::any::Any + Send>>,
652 callback: impl FnOnce(),
653) {
654 if let Err(payload) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(callback)) {
655 if first_panic.is_none() {
656 *first_panic = Some(payload);
657 } else {
658 std::mem::forget(payload);
662 }
663 }
664}
665
666fn dispatch_local_cancel_waiters(
667 waiters: Vec<LocalCancelWaiter>,
668 first_panic: &mut Option<Box<dyn std::any::Any + Send>>,
669) {
670 for waiter in waiters {
675 capture_cancel_callback_panic(first_panic, || waiter.waker.wake_by_ref());
676 capture_cancel_callback_panic(first_panic, || drop(waiter));
677 }
678}
679
680fn publish_cancel_state(
681 inner: &CxInner,
682 local_reason: CancelReason,
683 native_reason: Option<CancelReason>,
684) -> CancelReason {
685 #[cfg(not(feature = "native"))]
686 let _ = native_reason;
687
688 let effective_local_reason = {
690 let mut r = inner
691 .cancel_reason
692 .lock()
693 .unwrap_or_else(std::sync::PoisonError::into_inner);
694 match *r {
695 Some(existing) if existing >= local_reason => existing,
696 _ => {
697 *r = Some(local_reason);
698 local_reason
699 }
700 }
701 };
702
703 {
705 let mut state = inner
706 .cancel_state
707 .lock()
708 .unwrap_or_else(std::sync::PoisonError::into_inner);
709 if matches!(*state, CancelState::Created | CancelState::Running) {
710 *state = CancelState::CancelRequested;
711 }
712 }
713
714 #[cfg(feature = "native")]
719 if let Some(reason) = native_reason {
720 record_native_cancel_reason(inner, reason);
721 }
722
723 inner.cancel_requested.store(true, Ordering::Release);
727
728 effective_local_reason
729}
730
731fn try_append_live_children(
732 inner: &CxInner,
733 descendants: &mut Vec<Arc<CxInner>>,
734) -> std::result::Result<(), std::collections::TryReserveError> {
735 let mut children = inner
736 .children
737 .lock()
738 .unwrap_or_else(std::sync::PoisonError::into_inner);
739 children.retain(|child| child.strong_count() > 0);
740 descendants.try_reserve(children.len())?;
741 for child in children.iter().filter_map(Weak::upgrade) {
742 descendants.push(child);
743 }
744 Ok(())
745}
746
747#[must_use]
748fn local_cancel_waiter_count(inner: &CxInner) -> usize {
749 inner
750 .local_cancel_waiters
751 .lock()
752 .unwrap_or_else(std::sync::PoisonError::into_inner)
753 .entries
754 .len()
755}
756
757fn drain_local_cancel_waiters_into(inner: &CxInner, waiters: &mut Vec<LocalCancelWaiter>) {
758 let mut registered_waiters = inner
759 .local_cancel_waiters
760 .lock()
761 .unwrap_or_else(std::sync::PoisonError::into_inner);
762 debug_assert!(
763 waiters.capacity() - waiters.len() >= registered_waiters.entries.len(),
764 "cancellation waiter capacity must be reserved before publication"
765 );
766 waiters.append(&mut registered_waiters.entries);
767}
768
769#[cfg(feature = "native")]
770struct NativeCancelTarget {
771 descendant_index: Option<usize>,
772 native_cx: NativeCx,
773}
774
775#[cfg(feature = "native")]
776fn append_native_cancel_targets(
777 inner: &CxInner,
778 descendant_index: Option<usize>,
779 targets: &mut Vec<NativeCancelTarget>,
780) {
781 let attached_native = inner
782 .attached_native_cx
783 .lock()
784 .unwrap_or_else(std::sync::PoisonError::into_inner)
785 .as_ref()
786 .cloned();
787 if let Some(native_cx) = attached_native {
788 targets.push(NativeCancelTarget {
789 descendant_index,
790 native_cx,
791 });
792 }
793
794 if let Some(native_cx) = inner.fallback_native_cx.get().cloned() {
795 targets.push(NativeCancelTarget {
796 descendant_index,
797 native_cx,
798 });
799 }
800}
801
802fn propagate_cancel_tree(
807 inner: &CxInner,
808 reason: CancelReason,
809 native_propagation: NativeCancelPropagation,
810) {
811 let mut descendants = Vec::new();
812 let mut waiters = Vec::new();
813 #[cfg(feature = "native")]
814 let mut native_targets = Vec::new();
815 let mut reserve_error = None;
816 {
817 let _dispatch = inner
821 .cancel_dispatch_gate
822 .lock()
823 .unwrap_or_else(std::sync::PoisonError::into_inner);
824 let native_reason = match native_propagation {
825 NativeCancelPropagation::LocalAndNative => Some(reason),
826 NativeCancelPropagation::LocalOnly => None,
827 };
828
829 if let Err(error) = try_append_live_children(inner, &mut descendants) {
830 reserve_error = Some(error);
831 }
832 let mut cursor = 0;
833 while reserve_error.is_none() && cursor < descendants.len() {
834 let node = Arc::clone(&descendants[cursor]);
835 if let Err(error) = try_append_live_children(&node, &mut descendants) {
836 reserve_error = Some(error);
837 break;
838 }
839 cursor += 1;
840 }
841
842 if reserve_error.is_none() {
843 let waiter_count = descendants
847 .iter()
848 .fold(local_cancel_waiter_count(inner), |count, node| {
849 count.saturating_add(local_cancel_waiter_count(node))
850 });
851 if let Err(error) = waiters.try_reserve(waiter_count) {
852 reserve_error = Some(error);
853 }
854 }
855
856 #[cfg(feature = "native")]
857 if reserve_error.is_none() && native_propagation == NativeCancelPropagation::LocalAndNative
858 {
859 let target_capacity = descendants.len().saturating_mul(2).saturating_add(2);
860 if let Err(error) = native_targets.try_reserve(target_capacity) {
861 reserve_error = Some(error);
862 } else {
863 append_native_cancel_targets(inner, None, &mut native_targets);
864 for (index, node) in descendants.iter().enumerate() {
865 append_native_cancel_targets(node, Some(index), &mut native_targets);
866 }
867 }
868 }
869
870 if reserve_error.is_none() {
871 let propagated_local_reason = publish_cancel_state(inner, reason, native_reason);
872 drain_local_cancel_waiters_into(inner, &mut waiters);
873
874 #[cfg(feature = "native")]
875 let propagated_native_reason = match native_propagation {
876 NativeCancelPropagation::LocalAndNative => mirrored_native_cancel_reason(inner),
877 NativeCancelPropagation::LocalOnly => None,
878 };
879 #[cfg(not(feature = "native"))]
880 let propagated_native_reason = None;
881
882 for node in &descendants {
883 publish_cancel_state(node, propagated_local_reason, propagated_native_reason);
884 drain_local_cancel_waiters_into(node, &mut waiters);
885 }
886 }
887 }
888
889 if let Some(error) = reserve_error {
894 panic!("failed to reserve cancellation propagation storage: {error}");
895 }
896
897 let mut first_panic = None;
898 #[cfg(feature = "native")]
899 for target in &native_targets {
900 let target_inner = target
901 .descendant_index
902 .map_or(inner, |index| &descendants[index]);
903 capture_cancel_callback_panic(&mut first_panic, || {
904 sync_one_native_cx_cancel(target_inner, &target.native_cx);
905 });
906 }
907 dispatch_local_cancel_waiters(waiters, &mut first_panic);
908 if let Some(payload) = first_panic {
909 std::panic::resume_unwind(payload);
910 }
911}
912
913fn propagate_cancel(inner: &CxInner, reason: CancelReason) {
914 propagate_cancel_tree(inner, reason, NativeCancelPropagation::LocalAndNative);
915}
916
917fn propagate_local_cancel(inner: &CxInner, reason: CancelReason) {
918 propagate_cancel_tree(inner, reason, NativeCancelPropagation::LocalOnly);
919}
920
921#[derive(Debug)]
931#[must_use = "dropping the relay discards the authority to cancel the derived operation"]
932pub struct LocalCancelRelay {
933 inner: Weak<CxInner>,
934}
935
936impl LocalCancelRelay {
937 #[must_use]
943 pub fn cancel_local(&self, reason: CancelReason) -> bool {
944 let Some(inner) = self.inner.upgrade() else {
945 return false;
946 };
947 propagate_local_cancel(&inner, reason);
948 true
949 }
950}
951
952#[derive(Debug)]
962#[must_use = "futures do nothing unless polled or awaited"]
963pub struct LocalCancellation<'a> {
964 inner: &'a CxInner,
965 waiter_id: Option<u64>,
966 respect_mask: bool,
967}
968
969impl LocalCancellation<'_> {
970 fn unregister(&mut self) {
971 let Some(id) = self.waiter_id.take() else {
972 return;
973 };
974 let retired = {
975 let mut waiters = self
976 .inner
977 .local_cancel_waiters
978 .lock()
979 .unwrap_or_else(std::sync::PoisonError::into_inner);
980 waiters.remove(id)
981 };
982 drop(retired);
984 }
985}
986
987impl Future for LocalCancellation<'_> {
988 type Output = ();
989
990 fn poll(mut self: Pin<&mut Self>, task_cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
991 let mut prepared_waker = None;
992 let this = self.as_mut().get_mut();
993 let inner = this.inner;
994
995 loop {
996 if local_cancellation_matches(inner, this.respect_mask) {
997 this.unregister();
998 drop(prepared_waker);
999 return Poll::Ready(());
1000 }
1001
1002 let dispatch = inner
1008 .cancel_dispatch_gate
1009 .lock()
1010 .unwrap_or_else(std::sync::PoisonError::into_inner);
1011 let mut waiters = inner
1012 .local_cancel_waiters
1013 .lock()
1014 .unwrap_or_else(std::sync::PoisonError::into_inner);
1015 if local_cancellation_matches(inner, this.respect_mask) {
1016 let retired = this.waiter_id.take().and_then(|id| waiters.remove(id));
1017 drop(waiters);
1018 drop(dispatch);
1019 drop(retired);
1020 drop(prepared_waker);
1021 return Poll::Ready(());
1022 }
1023
1024 let existing_position = this.waiter_id.and_then(|id| {
1025 waiters
1026 .entries
1027 .iter()
1028 .position(|registered| registered.id == id)
1029 });
1030 if existing_position
1031 .is_some_and(|position| waiters.entries[position].waker.will_wake(task_cx.waker()))
1032 {
1033 drop(waiters);
1034 drop(dispatch);
1035 drop(prepared_waker);
1036 return Poll::Pending;
1037 }
1038
1039 if existing_position.is_none()
1040 && let Err(error) = waiters.entries.try_reserve(1)
1041 {
1042 drop(waiters);
1043 drop(dispatch);
1044 drop(prepared_waker);
1045 panic!("failed to reserve local-cancellation waiter storage: {error}");
1046 }
1047
1048 let Some(new_waker) = prepared_waker.take() else {
1049 drop(waiters);
1052 drop(dispatch);
1053 prepared_waker = Some(task_cx.waker().clone());
1054 continue;
1055 };
1056
1057 let retired = if let Some(position) = existing_position {
1058 Some(std::mem::replace(
1059 &mut waiters.entries[position].waker,
1060 new_waker,
1061 ))
1062 } else {
1063 let id = waiters.allocate_id();
1064 waiters.entries.push(LocalCancelWaiter {
1065 id,
1066 waker: new_waker,
1067 });
1068 this.waiter_id = Some(id);
1069 None
1070 };
1071 drop(waiters);
1072 drop(dispatch);
1073 drop(retired);
1076 return Poll::Pending;
1077 }
1078 }
1079}
1080
1081impl Drop for LocalCancellation<'_> {
1082 fn drop(&mut self) {
1083 self.unregister();
1084 }
1085}
1086
1087#[derive(Debug)]
1093pub struct Cx<Caps: cap::SubsetOf<cap::All> = FullCaps> {
1094 inner: Arc<CxInner>,
1095 budget: Budget,
1096 trace_id: u64,
1097 decision_id: u64,
1098 policy_id: u64,
1099 _caps: PhantomData<fn() -> Caps>,
1101}
1102
1103impl<Caps: cap::SubsetOf<cap::All>> Clone for Cx<Caps> {
1104 fn clone(&self) -> Self {
1105 Self {
1106 inner: Arc::clone(&self.inner),
1107 budget: self.budget,
1108 trace_id: self.trace_id,
1109 decision_id: self.decision_id,
1110 policy_id: self.policy_id,
1111 _caps: PhantomData,
1112 }
1113 }
1114}
1115
1116impl Default for Cx<FullCaps> {
1117 fn default() -> Self {
1118 Self::new()
1119 }
1120}
1121
1122impl Cx<FullCaps> {
1123 #[must_use]
1124 pub fn new() -> Self {
1125 Self::with_budget(Budget::INFINITE)
1126 }
1127}
1128
1129impl<Caps: cap::SubsetOf<cap::All>> Cx<Caps> {
1130 #[cfg(all(feature = "native", test))]
1131 #[must_use]
1132 #[allow(dead_code)]
1133 fn effective_native_cx(&self) -> NativeCx {
1134 let native = {
1135 let _dispatch = self
1136 .inner
1137 .cancel_dispatch_gate
1138 .lock()
1139 .unwrap_or_else(std::sync::PoisonError::into_inner);
1140 let attached_native = self
1141 .inner
1142 .attached_native_cx
1143 .lock()
1144 .unwrap_or_else(std::sync::PoisonError::into_inner)
1145 .as_ref()
1146 .cloned();
1147 attached_native.unwrap_or_else(|| {
1148 self.inner
1149 .fallback_native_cx
1150 .get_or_init(|| {
1151 NativeCx::for_request_with_budget(native_budget_from_local_at(
1152 self.budget,
1153 asupersync::time::wall_now(),
1154 ))
1155 })
1156 .clone()
1157 })
1158 };
1159 sync_one_native_cx_cancel(&self.inner, &native);
1163 native
1164 }
1165
1166 #[cfg(feature = "native")]
1167 #[must_use]
1168 fn native_cx_for_checkpoint(&self) -> Option<NativeCx> {
1169 let attached_native = self
1170 .inner
1171 .attached_native_cx
1172 .lock()
1173 .unwrap_or_else(std::sync::PoisonError::into_inner)
1174 .as_ref()
1175 .cloned();
1176 attached_native.or_else(|| self.inner.fallback_native_cx.get().cloned())
1177 }
1178
1179 #[must_use]
1180 pub fn with_budget(budget: Budget) -> Self {
1181 Self::with_budget_and_cancel_dispatch(budget, Arc::new(Mutex::new(())))
1182 }
1183
1184 fn with_budget_and_cancel_dispatch(
1185 budget: Budget,
1186 cancel_dispatch_gate: Arc<Mutex<()>>,
1187 ) -> Self {
1188 Self {
1189 inner: Arc::new(CxInner::new(cancel_dispatch_gate)),
1190 budget,
1191 trace_id: 0,
1192 decision_id: 0,
1193 policy_id: 0,
1194 _caps: PhantomData,
1195 }
1196 }
1197
1198 #[must_use]
1199 pub fn budget(&self) -> Budget {
1200 self.budget
1201 }
1202
1203 #[must_use]
1209 pub fn trace_id(&self) -> u64 {
1210 self.trace_id
1211 }
1212
1213 #[must_use]
1215 pub fn decision_id(&self) -> u64 {
1216 self.decision_id
1217 }
1218
1219 #[must_use]
1221 pub fn policy_id(&self) -> u64 {
1222 self.policy_id
1223 }
1224
1225 #[must_use]
1229 pub fn with_trace_context(mut self, trace_id: u64, decision_id: u64, policy_id: u64) -> Self {
1230 self.trace_id = trace_id;
1231 self.decision_id = decision_id;
1232 self.policy_id = policy_id;
1233 self
1234 }
1235
1236 #[must_use]
1240 pub fn with_decision_id(mut self, decision_id: u64) -> Self {
1241 self.decision_id = decision_id;
1242 self
1243 }
1244
1245 #[must_use]
1247 pub fn with_policy_id(mut self, policy_id: u64) -> Self {
1248 self.policy_id = policy_id;
1249 self
1250 }
1251
1252 #[must_use]
1258 pub fn scope_with_budget(&self, child: Budget) -> Self {
1259 Self {
1260 inner: Arc::clone(&self.inner),
1261 budget: self.budget.meet(child),
1262 trace_id: self.trace_id,
1263 decision_id: self.decision_id,
1264 policy_id: self.policy_id,
1265 _caps: PhantomData,
1266 }
1267 }
1268
1269 #[must_use]
1271 pub fn cleanup_scope(&self) -> Self {
1272 self.scope_with_budget(Budget::MINIMAL)
1273 }
1274
1275 #[must_use]
1279 pub fn restrict<NewCaps>(&self) -> Cx<NewCaps>
1280 where
1281 NewCaps: cap::SubsetOf<cap::All> + cap::SubsetOf<Caps>,
1282 {
1283 self.retype()
1284 }
1285
1286 #[must_use]
1288 fn retype<NewCaps>(&self) -> Cx<NewCaps>
1289 where
1290 NewCaps: cap::SubsetOf<cap::All>,
1291 {
1292 Cx {
1293 inner: Arc::clone(&self.inner),
1294 budget: self.budget,
1295 trace_id: self.trace_id,
1296 decision_id: self.decision_id,
1297 policy_id: self.policy_id,
1298 _caps: PhantomData,
1299 }
1300 }
1301
1302 #[must_use]
1307 pub fn is_cancel_requested(&self) -> bool {
1308 self.inner.cancel_requested.load(Ordering::Acquire)
1309 }
1310
1311 pub fn wait_for_local_cancellation(&self) -> LocalCancellation<'_> {
1317 LocalCancellation {
1318 inner: &self.inner,
1319 waiter_id: None,
1320 respect_mask: true,
1321 }
1322 }
1323
1324 pub fn wait_for_local_cancel_request(&self) -> LocalCancellation<'_> {
1330 LocalCancellation {
1331 inner: &self.inner,
1332 waiter_id: None,
1333 respect_mask: false,
1334 }
1335 }
1336
1337 pub fn cancel(&self) {
1341 self.cancel_with_reason(CancelReason::UserInterrupt);
1342 }
1343
1344 pub fn cancel_with_reason(&self, reason: CancelReason) {
1351 propagate_cancel(&self.inner, reason);
1352 }
1353
1354 #[must_use]
1356 pub fn cancel_state(&self) -> CancelState {
1357 *self
1358 .inner
1359 .cancel_state
1360 .lock()
1361 .unwrap_or_else(std::sync::PoisonError::into_inner)
1362 }
1363
1364 #[must_use]
1366 pub fn cancel_reason(&self) -> Option<CancelReason> {
1367 *self
1368 .inner
1369 .cancel_reason
1370 .lock()
1371 .unwrap_or_else(std::sync::PoisonError::into_inner)
1372 }
1373
1374 pub fn transition_to_running(&self) {
1376 let mut state = self
1377 .inner
1378 .cancel_state
1379 .lock()
1380 .unwrap_or_else(std::sync::PoisonError::into_inner);
1381 if *state == CancelState::Created {
1382 *state = CancelState::Running;
1383 }
1384 }
1385
1386 pub fn transition_to_finalizing(&self) {
1388 let mut state = self
1389 .inner
1390 .cancel_state
1391 .lock()
1392 .unwrap_or_else(std::sync::PoisonError::into_inner);
1393 if *state == CancelState::Cancelling {
1394 *state = CancelState::Finalizing;
1395 }
1396 }
1397
1398 pub fn transition_to_completed(&self) {
1400 let mut state = self
1401 .inner
1402 .cancel_state
1403 .lock()
1404 .unwrap_or_else(std::sync::PoisonError::into_inner);
1405 if matches!(*state, CancelState::Finalizing | CancelState::Running) {
1406 *state = CancelState::Completed;
1407 }
1408 }
1409
1410 pub fn set_eprocess_oracle(&self, oracle: Arc<EProcessOracle>) {
1412 let _ = self.inner.eprocess_oracle.set(oracle);
1413 }
1414
1415 pub fn clear_eprocess_oracle(&self) {
1417 }
1420
1421 #[cfg(feature = "native")]
1423 pub fn set_native_cx(&self, native_cx: NativeCx) {
1424 let retired = {
1425 let _dispatch = self
1426 .inner
1427 .cancel_dispatch_gate
1428 .lock()
1429 .unwrap_or_else(std::sync::PoisonError::into_inner);
1430 let mut attached = self
1431 .inner
1432 .attached_native_cx
1433 .lock()
1434 .unwrap_or_else(std::sync::PoisonError::into_inner);
1435 attached.replace(native_cx.clone())
1436 };
1437 sync_one_native_cx_cancel(&self.inner, &native_cx);
1441 drop(retired);
1445 }
1446
1447 #[cfg(not(feature = "native"))]
1449 pub fn set_native_cx<T>(&self, _native_cx: T) {}
1450
1451 #[cfg(feature = "native")]
1453 #[must_use]
1454 pub fn attached_native_cx(&self) -> Option<NativeCx> {
1455 self.inner
1456 .attached_native_cx
1457 .lock()
1458 .unwrap_or_else(std::sync::PoisonError::into_inner)
1459 .clone()
1460 }
1461
1462 #[cfg(feature = "native")]
1470 #[must_use]
1471 pub fn native_spawn_budget(&self, native_cx: &NativeCx) -> NativeBudget {
1472 let local = native_budget_from_local_at(self.budget, native_cx.now_for_observability());
1473 native_cx.budget().meet(local)
1474 }
1475
1476 #[cfg(not(feature = "native"))]
1478 #[must_use]
1479 pub fn attached_native_cx(&self) -> Option<NativeCx> {
1480 None
1481 }
1482
1483 #[cfg(feature = "native")]
1485 pub fn clear_native_cx(&self) {
1486 let retired = {
1487 let _dispatch = self
1488 .inner
1489 .cancel_dispatch_gate
1490 .lock()
1491 .unwrap_or_else(std::sync::PoisonError::into_inner);
1492 self.inner
1493 .attached_native_cx
1494 .lock()
1495 .unwrap_or_else(std::sync::PoisonError::into_inner)
1496 .take()
1497 };
1498 drop(retired);
1500 }
1501
1502 pub fn mark_blocking_io_inline_safe(&self) {
1507 self.inner
1508 .blocking_io_inline_safe
1509 .store(true, Ordering::Release);
1510 }
1511
1512 #[must_use]
1515 pub fn blocking_io_inline_safe(&self) -> bool {
1516 self.inner.blocking_io_inline_safe.load(Ordering::Acquire)
1517 }
1518
1519 #[cfg(not(feature = "native"))]
1521 pub fn clear_native_cx(&self) {}
1522
1523 #[must_use]
1524 fn maybe_cancel_via_eprocess(&self) -> bool {
1525 let Some(oracle) = self.inner.eprocess_oracle.get() else {
1526 return false;
1527 };
1528 let decision = oracle.decision(self.budget.priority);
1529 self.record_eprocess_decision(decision.clone());
1530 tracing::debug!(
1531 target: "fsqlite::cx",
1532 event = "eprocess_checkpoint",
1533 trace_id = self.trace_id,
1534 decision_id = self.decision_id,
1535 policy_id = self.policy_id,
1536 priority = decision.priority,
1537 evalue = decision.snapshot.evalue,
1538 threshold = decision.snapshot.rejection_threshold,
1539 observations = decision.snapshot.observations,
1540 priority_threshold = decision.snapshot.priority_threshold,
1541 should_shed = decision.should_shed,
1542 signal = ?decision.snapshot.last_signal
1543 );
1544 if decision.should_shed {
1545 tracing::info!(
1546 target: "fsqlite::cx",
1547 event = "eprocess_shedding_triggered",
1548 trace_id = self.trace_id,
1549 decision_id = self.decision_id,
1550 policy_id = self.policy_id,
1551 priority = decision.priority,
1552 evalue = decision.snapshot.evalue,
1553 threshold = decision.snapshot.rejection_threshold,
1554 signal = ?decision.snapshot.last_signal
1555 );
1556 self.cancel_with_reason(CancelReason::Abort);
1557 return true;
1558 }
1559 false
1560 }
1561
1562 #[cfg(feature = "native")]
1563 #[must_use]
1564 fn maybe_cancel_via_native_cx(&self, masked: bool) -> bool {
1565 let Some(native) = self.native_cx_for_checkpoint() else {
1566 return false;
1567 };
1568
1569 if masked {
1570 if native.is_cancel_requested() {
1571 let reason = native
1572 .cancel_reason()
1573 .as_ref()
1574 .map_or(CancelReason::Timeout, native_reason_to_local);
1575 self.cancel_with_reason(reason);
1576 return true;
1577 }
1578 return false;
1579 }
1580
1581 if native.checkpoint().is_err() {
1582 let reason = native
1583 .cancel_reason()
1584 .as_ref()
1585 .map_or(CancelReason::Timeout, native_reason_to_local);
1586 self.cancel_with_reason(reason);
1587 return true;
1588 }
1589 false
1590 }
1591
1592 pub fn checkpoint(&self) -> Result<()> {
1612 let cancel_requested = self.inner.cancel_requested.load(Ordering::Acquire);
1613 if !cancel_requested {
1614 if !self.maybe_cancel_via_eprocess() {
1617 #[cfg(feature = "native")]
1618 {
1619 let masked = self.inner.mask_depth.load(Ordering::Acquire) > 0;
1620 if !self.maybe_cancel_via_native_cx(masked) {
1621 return Ok(());
1622 }
1623 }
1624 #[cfg(not(feature = "native"))]
1625 {
1626 return Ok(());
1627 }
1628 }
1629 }
1630
1631 let masked = self.inner.mask_depth.load(Ordering::Acquire) > 0;
1634 if masked {
1635 return Ok(());
1636 }
1637
1638 {
1640 let mut state = self
1641 .inner
1642 .cancel_state
1643 .lock()
1644 .unwrap_or_else(std::sync::PoisonError::into_inner);
1645 if *state == CancelState::CancelRequested {
1646 *state = CancelState::Cancelling;
1647 }
1648 }
1649 Err(Error::cancelled())
1650 }
1651
1652 pub fn checkpoint_with(&self, msg: impl Into<String>) -> Result<()> {
1654 {
1655 let mut guard = self
1656 .inner
1657 .last_checkpoint_msg
1658 .lock()
1659 .unwrap_or_else(std::sync::PoisonError::into_inner);
1660 *guard = Some(msg.into());
1661 }
1662 self.checkpoint()
1663 }
1664
1665 #[must_use]
1666 pub fn last_checkpoint_message(&self) -> Option<String> {
1667 self.inner
1668 .last_checkpoint_msg
1669 .lock()
1670 .unwrap_or_else(std::sync::PoisonError::into_inner)
1671 .clone()
1672 }
1673
1674 #[must_use]
1676 pub fn last_eprocess_decision(&self) -> Option<EProcessDecision> {
1677 self.inner
1678 .last_eprocess_decision
1679 .lock()
1680 .unwrap_or_else(std::sync::PoisonError::into_inner)
1681 .clone()
1682 }
1683
1684 #[must_use]
1686 pub fn last_eprocess_snapshot(&self) -> Option<EProcessSnapshot> {
1687 self.last_eprocess_decision()
1688 .map(|decision| decision.snapshot)
1689 }
1690
1691 fn record_eprocess_decision(&self, decision: EProcessDecision) {
1692 *self
1693 .inner
1694 .last_eprocess_decision
1695 .lock()
1696 .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(decision);
1697 }
1698
1699 #[must_use]
1712 pub fn masked(&self) -> MaskGuard<'_> {
1713 let prev = self.inner.mask_depth.fetch_add(1, Ordering::AcqRel);
1714 if prev >= MAX_MASK_DEPTH {
1715 self.inner.mask_depth.fetch_sub(1, Ordering::Release);
1716 assert!(
1717 prev < MAX_MASK_DEPTH,
1718 "MAX_MASK_DEPTH ({MAX_MASK_DEPTH}) exceeded: mask nesting depth would be {}",
1719 prev + 1
1720 );
1721 }
1722 MaskGuard { inner: &self.inner }
1723 }
1724
1725 #[must_use]
1727 pub fn mask_depth(&self) -> u32 {
1728 self.inner.mask_depth.load(Ordering::Acquire)
1729 }
1730
1731 pub fn commit_section<R>(
1740 &self,
1741 poll_quota: u32,
1742 body: impl FnOnce(&CommitCtx) -> R,
1743 finalizer: impl FnOnce(),
1744 ) -> R {
1745 struct FinGuard<G: FnOnce()>(Option<G>);
1746 impl<G: FnOnce()> Drop for FinGuard<G> {
1747 fn drop(&mut self) {
1748 if let Some(f) = self.0.take() {
1749 f();
1750 }
1751 }
1752 }
1753
1754 let _mask = self.masked();
1755 let _fin = FinGuard(Some(finalizer));
1756 let ctx = CommitCtx::new(poll_quota);
1757 body(&ctx)
1758 }
1759
1760 #[must_use]
1768 pub fn create_child(&self) -> Self {
1769 self.create_child_with_runtime_affinity(true)
1770 }
1771
1772 #[must_use]
1781 pub fn create_child_for_spawn(&self) -> Self {
1782 self.create_child_with_runtime_affinity(false)
1783 }
1784
1785 fn create_child_with_runtime_affinity(&self, inherit_runtime_affinity: bool) -> Self {
1786 let mut child = Self::with_budget_and_cancel_dispatch(
1787 self.budget,
1788 Arc::clone(&self.inner.cancel_dispatch_gate),
1789 );
1790 child.trace_id = self.trace_id;
1791 child.decision_id = self.decision_id;
1792 child.policy_id = self.policy_id;
1793 if self.inner.unix_millis_is_fixed.load(Ordering::Acquire) {
1794 let unix_millis = self.inner.unix_millis.load(Ordering::Acquire);
1795 child
1796 .inner
1797 .unix_millis
1798 .store(unix_millis, Ordering::Release);
1799 child
1800 .inner
1801 .unix_millis_is_fixed
1802 .store(true, Ordering::Release);
1803 }
1804 if let Some(oracle) = self.inner.eprocess_oracle.get().cloned() {
1805 child.set_eprocess_oracle(oracle);
1806 }
1807 if inherit_runtime_affinity && self.blocking_io_inline_safe() {
1811 child.mark_blocking_io_inline_safe();
1812 }
1813
1814 #[cfg(feature = "native")]
1815 let native_to_sync = {
1816 let _dispatch = self
1817 .inner
1818 .cancel_dispatch_gate
1819 .lock()
1820 .unwrap_or_else(std::sync::PoisonError::into_inner);
1821 let attached_native = inherit_runtime_affinity
1822 .then(|| {
1823 self.inner
1824 .attached_native_cx
1825 .lock()
1826 .unwrap_or_else(std::sync::PoisonError::into_inner)
1827 .clone()
1828 })
1829 .flatten();
1830 if let Some(native) = attached_native.as_ref() {
1831 *child
1832 .inner
1833 .attached_native_cx
1834 .lock()
1835 .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(native.clone());
1836 }
1837
1838 let local_reason = self.cancel_reason();
1839 let native_reason = mirrored_native_cancel_reason(&self.inner);
1840 if let Some(local_reason) = local_reason.or(native_reason) {
1841 publish_cancel_state(&child.inner, local_reason, native_reason);
1842 }
1843
1844 let mut children = self
1847 .inner
1848 .children
1849 .lock()
1850 .unwrap_or_else(std::sync::PoisonError::into_inner);
1851 if children.len() == children.capacity() {
1852 children.retain(|registered| registered.strong_count() > 0);
1853 }
1854 children.push(Arc::downgrade(&child.inner));
1855
1856 attached_native.filter(|_| native_reason.is_some())
1857 };
1858
1859 #[cfg(not(feature = "native"))]
1860 {
1861 let _dispatch = self
1862 .inner
1863 .cancel_dispatch_gate
1864 .lock()
1865 .unwrap_or_else(std::sync::PoisonError::into_inner);
1866 if let Some(reason) = self.cancel_reason() {
1867 publish_cancel_state(&child.inner, reason, None);
1868 }
1869 let mut children = self
1870 .inner
1871 .children
1872 .lock()
1873 .unwrap_or_else(std::sync::PoisonError::into_inner);
1874 if children.len() == children.capacity() {
1875 children.retain(|registered| registered.strong_count() > 0);
1876 }
1877 children.push(Arc::downgrade(&child.inner));
1878 }
1879
1880 #[cfg(feature = "native")]
1881 if let Some(native) = native_to_sync {
1882 sync_one_native_cx_cancel(&child.inner, &native);
1883 }
1884
1885 child
1886 }
1887
1888 pub fn create_child_with_local_cancel_relay(&self) -> (Self, LocalCancelRelay) {
1896 let child = self.create_child();
1897 let relay = LocalCancelRelay {
1898 inner: Arc::downgrade(&child.inner),
1899 };
1900 (child, relay)
1901 }
1902
1903 pub fn set_unix_millis_for_testing(&self, millis: u64)
1905 where
1906 Caps: cap::HasTime,
1907 {
1908 self.inner.unix_millis.store(millis, Ordering::Release);
1909 self.inner
1910 .unix_millis_is_fixed
1911 .store(true, Ordering::Release);
1912 }
1913
1914 #[must_use]
1919 pub fn current_time_unix_millis(&self) -> u64
1920 where
1921 Caps: cap::HasTime,
1922 {
1923 if self.inner.unix_millis_is_fixed.load(Ordering::Acquire) {
1924 return self.inner.unix_millis.load(Ordering::Acquire);
1925 }
1926
1927 u64::try_from(
1928 SystemTime::now()
1929 .duration_since(SystemTime::UNIX_EPOCH)
1930 .unwrap_or_default()
1931 .as_millis(),
1932 )
1933 .unwrap_or(u64::MAX)
1934 }
1935
1936 #[must_use]
1938 pub fn current_time_julian_day(&self) -> f64
1939 where
1940 Caps: cap::HasTime,
1941 {
1942 let millis = self.current_time_unix_millis();
1943 #[allow(clippy::cast_precision_loss)]
1944 let secs = (millis as f64) / 1000.0;
1945 2_440_587.5 + (secs / 86_400.0)
1947 }
1948}
1949
1950#[derive(Debug)]
1958pub struct MaskGuard<'a> {
1959 inner: &'a CxInner,
1960}
1961
1962impl Drop for MaskGuard<'_> {
1963 fn drop(&mut self) {
1964 let previous = self.inner.mask_depth.fetch_sub(1, Ordering::AcqRel);
1965 debug_assert!(previous > 0, "mask depth underflow");
1966 if previous == 1 {
1967 let waiters = {
1968 let _dispatch = self
1969 .inner
1970 .cancel_dispatch_gate
1971 .lock()
1972 .unwrap_or_else(std::sync::PoisonError::into_inner);
1973 if self.inner.mask_depth.load(Ordering::Acquire) == 0
1974 && self.inner.cancel_requested.load(Ordering::Acquire)
1975 {
1976 take_local_cancel_waiters(self.inner)
1977 } else {
1978 Vec::new()
1979 }
1980 };
1981 let mut first_panic = None;
1982 dispatch_local_cancel_waiters(waiters, &mut first_panic);
1983 if let Some(payload) = first_panic {
1984 std::mem::forget(payload);
1987 }
1988 }
1989 }
1990}
1991
1992#[derive(Debug)]
2000pub struct CommitCtx {
2001 poll_remaining: AtomicU32,
2002}
2003
2004impl CommitCtx {
2005 fn new(poll_quota: u32) -> Self {
2006 Self {
2007 poll_remaining: AtomicU32::new(poll_quota),
2008 }
2009 }
2010
2011 #[must_use]
2013 pub fn poll_remaining(&self) -> u32 {
2014 self.poll_remaining.load(Ordering::Acquire)
2015 }
2016
2017 pub fn tick(&self) -> bool {
2019 let prev = self.poll_remaining.load(Ordering::Acquire);
2020 if prev == 0 {
2021 return false;
2022 }
2023 self.poll_remaining.fetch_sub(1, Ordering::AcqRel);
2024 true
2025 }
2026}
2027
2028#[cfg(test)]
2029mod tests {
2030 use super::*;
2031 use crate::eprocess::{EProcessConfig, EProcessSignal};
2032 use std::path::{Path, PathBuf};
2033 use std::sync::atomic::{AtomicBool, AtomicUsize};
2034 use std::sync::{Arc, Barrier, Weak};
2035
2036 #[derive(Debug, Default)]
2037 struct CountingWake(AtomicUsize);
2038
2039 impl std::task::Wake for CountingWake {
2040 fn wake(self: Arc<Self>) {
2041 self.0.fetch_add(1, Ordering::AcqRel);
2042 }
2043
2044 fn wake_by_ref(self: &Arc<Self>) {
2045 self.0.fetch_add(1, Ordering::AcqRel);
2046 }
2047 }
2048
2049 #[derive(Debug)]
2050 struct DescendantStateProbeWake {
2051 descendant: Weak<CxInner>,
2052 wake_count: AtomicUsize,
2053 saw_descendant_cancelled: AtomicBool,
2054 dispatch_gate_was_unlocked: AtomicBool,
2055 }
2056
2057 impl DescendantStateProbeWake {
2058 fn observe(&self) {
2059 let descendant = self
2060 .descendant
2061 .upgrade()
2062 .expect("observed descendant should remain alive");
2063 self.saw_descendant_cancelled.store(
2064 descendant.cancel_requested.load(Ordering::Acquire),
2065 Ordering::Release,
2066 );
2067 let dispatch_guard = descendant
2068 .cancel_dispatch_gate
2069 .try_lock()
2070 .expect("cancellation callbacks must run outside the family phase gate");
2071 self.dispatch_gate_was_unlocked
2072 .store(true, Ordering::Release);
2073 drop(dispatch_guard);
2074 self.wake_count.fetch_add(1, Ordering::AcqRel);
2075 }
2076 }
2077
2078 impl std::task::Wake for DescendantStateProbeWake {
2079 fn wake(self: Arc<Self>) {
2080 self.observe();
2081 }
2082
2083 fn wake_by_ref(self: &Arc<Self>) {
2084 self.observe();
2085 }
2086 }
2087
2088 #[derive(Debug)]
2089 struct ReentrantFamilyWake {
2090 cx: Cx<FullCaps>,
2091 wake_count: AtomicUsize,
2092 child_inherited_cancellation: AtomicBool,
2093 }
2094
2095 impl ReentrantFamilyWake {
2096 fn exercise(&self) {
2097 self.cx.cancel_with_reason(CancelReason::Abort);
2098 let child = self.cx.create_child();
2099 self.child_inherited_cancellation.store(
2100 child.cancel_reason() == Some(CancelReason::Abort),
2101 Ordering::Release,
2102 );
2103 let mask = self.cx.masked();
2104 drop(mask);
2105 self.wake_count.fetch_add(1, Ordering::AcqRel);
2106 }
2107 }
2108
2109 impl std::task::Wake for ReentrantFamilyWake {
2110 fn wake(self: Arc<Self>) {
2111 self.exercise();
2112 }
2113
2114 fn wake_by_ref(self: &Arc<Self>) {
2115 self.exercise();
2116 }
2117 }
2118
2119 #[derive(Debug, Default)]
2120 struct PanicWake;
2121
2122 impl std::task::Wake for PanicWake {
2123 fn wake(self: Arc<Self>) {
2124 panic!("intentional cancellation-waker panic");
2125 }
2126
2127 fn wake_by_ref(self: &Arc<Self>) {
2128 panic!("intentional cancellation-waker panic");
2129 }
2130 }
2131
2132 #[derive(Debug)]
2133 struct RegistryProbeWake {
2134 inner: Weak<CxInner>,
2135 wake_count: AtomicUsize,
2136 registry_was_unlocked: AtomicBool,
2137 }
2138
2139 impl RegistryProbeWake {
2140 fn probe_registry(&self) {
2141 let inner = self
2142 .inner
2143 .upgrade()
2144 .expect("observed context should remain alive");
2145 let registry_guard = inner
2146 .local_cancel_waiters
2147 .try_lock()
2148 .expect("waker callbacks must run after releasing the waiter registry");
2149 self.registry_was_unlocked.store(true, Ordering::Release);
2150 drop(registry_guard);
2151 self.wake_count.fetch_add(1, Ordering::AcqRel);
2152 }
2153 }
2154
2155 impl std::task::Wake for RegistryProbeWake {
2156 fn wake(self: Arc<Self>) {
2157 self.probe_registry();
2158 }
2159
2160 fn wake_by_ref(self: &Arc<Self>) {
2161 self.probe_registry();
2162 }
2163 }
2164
2165 fn local_cancel_waiter_count<Caps: cap::SubsetOf<cap::All>>(cx: &Cx<Caps>) -> usize {
2166 cx.inner
2167 .local_cancel_waiters
2168 .lock()
2169 .unwrap_or_else(std::sync::PoisonError::into_inner)
2170 .entries
2171 .len()
2172 }
2173
2174 #[test]
2175 fn test_cx_checkpoint_observes_cancellation() {
2176 let cx = Cx::new();
2177 assert_eq!(local_cancel_waiter_count(&cx), 0);
2178 assert!(cx.checkpoint().is_ok());
2179 cx.cancel();
2180 assert_eq!(local_cancel_waiter_count(&cx), 0);
2181 let err = cx.checkpoint().unwrap_err();
2182 assert_eq!(err.kind(), ErrorKind::Cancelled);
2183 assert_eq!(err.sqlite_error_code(), SQLITE_INTERRUPT);
2184 }
2185
2186 #[test]
2187 fn test_cx_capability_narrowing_compiles() {
2188 let cx = Cx::<FullCaps>::new();
2189 let _compute = cx.restrict::<ComputeCaps>();
2190 let _storage = cx.restrict::<StorageCaps>();
2191 }
2192
2193 #[test]
2194 fn test_cx_budget_meet_tightens() {
2195 let parent = Budget::INFINITE.with_deadline(Duration::from_millis(100));
2196 let child = Budget::INFINITE.with_deadline(Duration::from_millis(200));
2197 let effective = parent.meet(child);
2198 assert_eq!(effective.deadline, Some(Duration::from_millis(100)));
2199 }
2200
2201 #[test]
2202 fn test_cx_budget_priority_join() {
2203 let parent = Budget::INFINITE.with_priority(2);
2204 let child = Budget::INFINITE.with_priority(5);
2205 let effective = parent.meet(child);
2206 assert_eq!(effective.priority, 5);
2207 }
2208
2209 #[cfg(feature = "native")]
2210 #[test]
2211 fn bd_2jpu6_2_native_budget_translation_uses_supplied_clock_domain() {
2212 let now = NativeTime::from_nanos(1_000);
2213 let local = Budget::INFINITE.with_deadline(Duration::from_nanos(250));
2214
2215 let native = native_budget_from_local_at(local, now);
2216
2217 assert_eq!(native.deadline, Some(NativeTime::from_nanos(1_250)));
2218 }
2219
2220 #[cfg(feature = "native")]
2221 #[test]
2222 fn bd_2jpu6_2_native_spawn_budget_meets_parent_bounds_and_priority() {
2223 let parent = NativeBudget::INFINITE
2224 .with_poll_quota(80)
2225 .with_cost_quota(900)
2226 .with_priority(9);
2227 let native_cx = NativeCx::for_testing_with_budget(parent);
2228 let local = Cx::<FullCaps>::with_budget(
2229 Budget::INFINITE
2230 .with_poll_quota(60)
2231 .with_cost_quota(700)
2232 .with_priority(3),
2233 );
2234
2235 let effective = local.native_spawn_budget(&native_cx);
2236
2237 assert_eq!(effective.poll_quota, 60);
2238 assert_eq!(effective.cost_quota, Some(700));
2239 assert_eq!(effective.priority, 9);
2240 }
2241
2242 #[test]
2243 fn test_cx_scope_with_budget_cannot_loosen() {
2244 let cx =
2245 Cx::<FullCaps>::with_budget(Budget::INFINITE.with_deadline(Duration::from_millis(50)));
2246 let child = Budget::INFINITE.with_deadline(Duration::from_millis(100));
2247 let scoped = cx.scope_with_budget(child);
2248 assert_eq!(scoped.budget().deadline, Some(Duration::from_millis(50)));
2249 }
2250
2251 #[test]
2252 fn test_cx_checkpoint_with_message_records_message() {
2253 let cx = Cx::new();
2254 assert!(cx.checkpoint_with("vdbe pc=5").is_ok());
2255 assert_eq!(cx.last_checkpoint_message().as_deref(), Some("vdbe pc=5"));
2256 }
2257
2258 #[test]
2259 fn test_cx_cleanup_uses_minimal_budget() {
2260 let cx = Cx::<FullCaps>::with_budget(Budget::INFINITE.with_poll_quota(10_000));
2261 let cleanup = cx.cleanup_scope();
2262 assert_eq!(cleanup.budget(), Budget::MINIMAL);
2263 }
2264
2265 #[test]
2266 fn test_cx_restrict_storage_to_compute() {
2267 let cx = Cx::<FullCaps>::new();
2268 let storage = cx.restrict::<StorageCaps>();
2269 let _compute = storage.restrict::<ComputeCaps>();
2270 }
2271
2272 #[test]
2273 fn test_cx_restrict_is_zero_cost() {
2274 assert_eq!(
2277 std::mem::size_of::<Cx<FullCaps>>(),
2278 std::mem::size_of::<Cx<ComputeCaps>>()
2279 );
2280 }
2281
2282 #[test]
2283 fn test_budget_mixed_lattice() {
2284 let a = Budget {
2285 deadline: Some(Duration::from_millis(100)),
2286 poll_quota: 500,
2287 cost_quota: Some(1000),
2288 priority: 2,
2289 };
2290 let b = Budget {
2291 deadline: Some(Duration::from_millis(200)),
2292 poll_quota: 300,
2293 cost_quota: Some(2000),
2294 priority: 5,
2295 };
2296 let m = a.meet(b);
2297 assert_eq!(m.deadline, Some(Duration::from_millis(100)));
2299 assert_eq!(m.poll_quota, 300);
2300 assert_eq!(m.cost_quota, Some(1000));
2301 assert_eq!(m.priority, 5);
2303 }
2304
2305 #[test]
2306 fn test_budget_meet_commutative() {
2307 let a = Budget {
2308 deadline: Some(Duration::from_millis(50)),
2309 poll_quota: 400,
2310 cost_quota: Some(800),
2311 priority: 3,
2312 };
2313 let b = Budget {
2314 deadline: Some(Duration::from_millis(150)),
2315 poll_quota: 200,
2316 cost_quota: None,
2317 priority: 7,
2318 };
2319 assert_eq!(a.meet(b), b.meet(a));
2320 }
2321
2322 #[test]
2323 fn test_budget_meet_associative() {
2324 let a = Budget::INFINITE
2325 .with_deadline(Duration::from_millis(50))
2326 .with_poll_quota(100)
2327 .with_priority(1);
2328 let b = Budget::INFINITE
2329 .with_deadline(Duration::from_millis(150))
2330 .with_poll_quota(200)
2331 .with_priority(5);
2332 let c = Budget::INFINITE
2333 .with_deadline(Duration::from_millis(75))
2334 .with_poll_quota(50)
2335 .with_priority(3);
2336 assert_eq!(a.meet(b).meet(c), a.meet(b.meet(c)));
2337 }
2338
2339 #[test]
2340 fn test_budget_minimal_is_stricter_than_normal() {
2341 let normal = Budget::INFINITE.with_poll_quota(10_000);
2342 let effective = normal.meet(Budget::MINIMAL);
2343 assert_eq!(effective.poll_quota, Budget::MINIMAL.poll_quota);
2344 }
2345
2346 #[test]
2347 fn test_cx_cancel_shared_across_clones() {
2348 let cx1 = Cx::<FullCaps>::new();
2349 let cx2 = cx1.clone();
2350 assert!(!cx2.is_cancel_requested());
2351 cx1.cancel();
2352 assert!(cx2.is_cancel_requested());
2353 assert!(cx2.checkpoint().is_err());
2354 }
2355
2356 #[test]
2357 fn test_cx_cancel_shared_across_restrict() {
2358 let cx = Cx::<FullCaps>::new();
2359 let compute = cx.restrict::<ComputeCaps>();
2360 cx.cancel();
2361 assert!(compute.checkpoint().is_err());
2362 }
2363
2364 fn system_time_unix_millis() -> u64 {
2365 u64::try_from(
2366 SystemTime::now()
2367 .duration_since(SystemTime::UNIX_EPOCH)
2368 .unwrap_or_default()
2369 .as_millis(),
2370 )
2371 .unwrap_or(u64::MAX)
2372 }
2373
2374 #[test]
2375 fn test_cx_current_time_uses_live_clock_by_default() {
2376 let cx = Cx::<FullCaps>::new();
2377 let observed = cx.current_time_unix_millis();
2378 let expected = system_time_unix_millis();
2379
2380 assert!(
2381 observed.abs_diff(expected) <= 60_000,
2382 "default Cx clock must be live: observed={observed}, expected approximately {expected}"
2383 );
2384 }
2385
2386 #[test]
2387 fn test_cx_fixed_unix_millis_supports_full_u64_domain() {
2388 let cx = Cx::<FullCaps>::new();
2389
2390 cx.set_unix_millis_for_testing(0);
2391 assert_eq!(cx.current_time_unix_millis(), 0);
2392
2393 cx.set_unix_millis_for_testing(u64::MAX);
2394 assert_eq!(cx.current_time_unix_millis(), u64::MAX);
2395 }
2396
2397 #[test]
2398 fn test_cx_current_time_julian_day_uses_fixed_unix_millis() {
2399 let cx = Cx::<FullCaps>::new();
2400
2401 cx.set_unix_millis_for_testing(0);
2403 let jd = cx.current_time_julian_day();
2404 assert!((jd - 2_440_587.5).abs() < 1e-10);
2405
2406 cx.set_unix_millis_for_testing(86_400_000);
2408 let jd = cx.current_time_julian_day();
2409 assert!((jd - 2_440_588.5).abs() < 1e-10);
2410 }
2411
2412 #[test]
2413 fn test_cx_children_inherit_fixed_or_live_clock_state() {
2414 let fixed_parent = Cx::<FullCaps>::new();
2415 fixed_parent.set_unix_millis_for_testing(0);
2416 let fixed_child = fixed_parent.create_child();
2417 let fixed_spawn_child = fixed_parent.create_child_for_spawn();
2418
2419 assert_eq!(fixed_child.current_time_unix_millis(), 0);
2420 assert_eq!(fixed_spawn_child.current_time_unix_millis(), 0);
2421
2422 fixed_parent.set_unix_millis_for_testing(86_400_000);
2425 assert_eq!(fixed_child.current_time_unix_millis(), 0);
2426
2427 let live_parent = Cx::<FullCaps>::new();
2428 let live_child = live_parent.create_child();
2429 let observed = live_child.current_time_unix_millis();
2430 let expected = system_time_unix_millis();
2431 assert!(
2432 observed.abs_diff(expected) <= 60_000,
2433 "child of live Cx must remain live: observed={observed}, expected approximately {expected}"
2434 );
2435 }
2436
2437 #[test]
2438 fn test_cx_fixed_clock_updates_publish_complete_values() {
2439 const FIRST: u64 = 0xAAAA_AAAA_AAAA_AAAA;
2440 const SECOND: u64 = 0x5555_5555_5555_5555;
2441
2442 let cx = Cx::<FullCaps>::new();
2443 cx.set_unix_millis_for_testing(FIRST);
2444 let writer_cx = cx.clone();
2445 let writer = std::thread::spawn(move || {
2446 for _ in 0..1_000 {
2447 writer_cx.set_unix_millis_for_testing(FIRST);
2448 writer_cx.set_unix_millis_for_testing(SECOND);
2449 }
2450 });
2451
2452 for _ in 0..1_000 {
2453 let observed = cx.current_time_unix_millis();
2454 assert!(matches!(observed, FIRST | SECOND));
2455 }
2456 writer.join().expect("clock writer must not panic");
2457 assert_eq!(cx.current_time_unix_millis(), SECOND);
2458 }
2459
2460 #[test]
2461 fn test_capset_is_zero_sized() {
2462 assert_eq!(std::mem::size_of::<cap::All>(), 0);
2463 assert_eq!(std::mem::size_of::<cap::None>(), 0);
2464 assert_eq!(
2465 std::mem::size_of::<cap::CapSet<true, false, true, false, true>>(),
2466 0
2467 );
2468 }
2469
2470 #[test]
2471 fn test_cx_checkpoint_not_cancelled() {
2472 let cx = Cx::new();
2473 assert!(cx.checkpoint().is_ok());
2474 assert!(cx.checkpoint_with("still going").is_ok());
2475 }
2476
2477 #[test]
2478 fn test_cx_checkpoint_maps_to_sqlite_interrupt() {
2479 let cx = Cx::new();
2480 cx.cancel();
2481 let err = cx.checkpoint().unwrap_err();
2482 assert_eq!(err.sqlite_error_code(), SQLITE_INTERRUPT);
2483 }
2484
2485 #[test]
2486 fn test_cx_checkpoint_eprocess_sheds_low_priority_context() {
2487 let cx = Cx::<FullCaps>::with_budget(Budget::INFINITE.with_priority(3));
2488 let oracle = Arc::new(EProcessOracle::new(
2489 EProcessConfig {
2490 p0: 0.1,
2491 lambda: 5.0,
2492 alpha: 0.05,
2493 max_evalue: 1e12,
2494 },
2495 1,
2496 ));
2497 let signal = EProcessSignal::new(1.0, 1.0, 1.0);
2498 oracle.observe_signal(signal);
2499 oracle.observe_signal(signal);
2500 cx.set_eprocess_oracle(oracle);
2501 let err = cx.checkpoint().unwrap_err();
2502 assert_eq!(err.kind(), ErrorKind::Cancelled);
2503 assert_eq!(cx.cancel_reason(), Some(CancelReason::Abort));
2504 let decision = cx
2505 .last_eprocess_decision()
2506 .expect("checkpoint should record an e-process decision");
2507 assert!(decision.should_shed);
2508 assert_eq!(decision.snapshot.last_signal, Some(signal));
2509 }
2510
2511 #[test]
2512 fn test_cx_checkpoint_eprocess_respects_priority_threshold() {
2513 let cx = Cx::<FullCaps>::with_budget(Budget::INFINITE.with_priority(1));
2514 let oracle = Arc::new(EProcessOracle::new(
2515 EProcessConfig {
2516 p0: 0.1,
2517 lambda: 5.0,
2518 alpha: 0.05,
2519 max_evalue: 1e12,
2520 },
2521 1,
2522 ));
2523 let signal = EProcessSignal::new(1.0, 1.0, 1.0);
2524 oracle.observe_signal(signal);
2525 oracle.observe_signal(signal);
2526 cx.set_eprocess_oracle(oracle);
2527 assert!(cx.checkpoint().is_ok());
2528 assert!(!cx.is_cancel_requested());
2529 let decision = cx
2530 .last_eprocess_decision()
2531 .expect("checkpoint should still record non-shedding decisions");
2532 assert!(!decision.should_shed);
2533 assert_eq!(decision.priority, 1);
2534 assert_eq!(decision.snapshot.last_signal, Some(signal));
2535 }
2536
2537 #[test]
2538 fn test_cx_checkpoint_eprocess_preserves_masking_semantics() {
2539 let cx = Cx::<FullCaps>::with_budget(Budget::INFINITE.with_priority(3));
2540 let oracle = Arc::new(EProcessOracle::new(
2541 EProcessConfig {
2542 p0: 0.1,
2543 lambda: 5.0,
2544 alpha: 0.05,
2545 max_evalue: 1e12,
2546 },
2547 1,
2548 ));
2549 let signal = EProcessSignal::new(1.0, 1.0, 1.0);
2550 oracle.observe_signal(signal);
2551 oracle.observe_signal(signal);
2552 cx.set_eprocess_oracle(oracle);
2553 {
2554 let _mask = cx.masked();
2555 assert!(cx.checkpoint().is_ok());
2556 assert!(cx.is_cancel_requested());
2557 assert_eq!(cx.cancel_state(), CancelState::CancelRequested);
2558 assert_eq!(
2559 cx.last_eprocess_snapshot()
2560 .expect("checkpoint should record the masked decision")
2561 .last_signal,
2562 Some(signal)
2563 );
2564 }
2565 let err = cx.checkpoint().unwrap_err();
2566 assert_eq!(err.kind(), ErrorKind::Cancelled);
2567 }
2568
2569 #[test]
2570 fn test_create_child_inherits_eprocess_oracle() {
2571 let parent = Cx::<FullCaps>::with_budget(Budget::INFINITE.with_priority(3));
2572 let oracle = Arc::new(EProcessOracle::new(
2573 EProcessConfig {
2574 p0: 0.1,
2575 lambda: 5.0,
2576 alpha: 0.05,
2577 max_evalue: 1e12,
2578 },
2579 1,
2580 ));
2581 let signal = EProcessSignal::new(1.0, 1.0, 1.0);
2582 oracle.observe_signal(signal);
2583 oracle.observe_signal(signal);
2584 parent.set_eprocess_oracle(oracle);
2585
2586 let child = parent.create_child();
2587 let err = child.checkpoint().unwrap_err();
2588 assert_eq!(err.kind(), ErrorKind::Cancelled);
2589 assert_eq!(child.cancel_reason(), Some(CancelReason::Abort));
2590 assert_eq!(
2591 child
2592 .last_eprocess_snapshot()
2593 .expect("child checkpoint should record inherited oracle decision")
2594 .last_signal,
2595 Some(signal)
2596 );
2597 }
2598
2599 #[test]
2600 fn spawn_child_preserves_logical_context_without_thread_affinity() {
2601 let budget = Budget {
2602 deadline: Some(Duration::from_secs(7)),
2603 poll_quota: 123,
2604 cost_quota: Some(456),
2605 priority: 3,
2606 };
2607 let parent = Cx::<FullCaps>::with_budget(budget).with_trace_context(50, 60, 70);
2608 let oracle = Arc::new(EProcessOracle::new(
2609 EProcessConfig {
2610 p0: 0.1,
2611 lambda: 5.0,
2612 alpha: 0.05,
2613 max_evalue: 1e12,
2614 },
2615 1,
2616 ));
2617 parent.set_eprocess_oracle(Arc::clone(&oracle));
2618 parent.mark_blocking_io_inline_safe();
2619
2620 let child = parent.create_child_for_spawn();
2621 assert_eq!(child.budget(), budget);
2622 assert_eq!(child.trace_id(), 50);
2623 assert_eq!(child.decision_id(), 60);
2624 assert_eq!(child.policy_id(), 70);
2625 assert!(
2626 Arc::ptr_eq(
2627 child
2628 .inner
2629 .eprocess_oracle
2630 .get()
2631 .expect("spawn child should inherit the e-process oracle"),
2632 &oracle
2633 ),
2634 "spawn child must retain the exact logical policy oracle"
2635 );
2636 assert!(
2637 !child.blocking_io_inline_safe(),
2638 "spawn child must not inherit an OS-thread-only I/O permission"
2639 );
2640
2641 parent.cancel_with_reason(CancelReason::RegionClose);
2642 assert_eq!(child.cancel_reason(), Some(CancelReason::RegionClose));
2643 }
2644
2645 #[test]
2646 fn test_create_child_inherits_preexisting_parent_cancellation() {
2647 let parent = Cx::<FullCaps>::new();
2648 parent.cancel_with_reason(CancelReason::RegionClose);
2649
2650 let child = parent.create_child();
2651 assert_eq!(child.cancel_reason(), Some(CancelReason::RegionClose));
2652 assert_eq!(child.cancel_state(), CancelState::CancelRequested);
2653
2654 let err = child.checkpoint().unwrap_err();
2655 assert_eq!(err.kind(), ErrorKind::Cancelled);
2656 }
2657
2658 #[test]
2659 fn local_cancel_relay_is_subtree_scoped_and_reason_monotone() {
2660 let root = Cx::<FullCaps>::new();
2661 let sibling = root.create_child();
2662 let (operation, relay) = root.create_child_with_local_cancel_relay();
2663 let existing_descendant = operation.create_child();
2664
2665 assert!(relay.cancel_local(CancelReason::Timeout));
2666 assert!(relay.cancel_local(CancelReason::Abort));
2667 assert!(relay.cancel_local(CancelReason::UserInterrupt));
2668
2669 assert_eq!(operation.cancel_reason(), Some(CancelReason::Abort));
2670 assert_eq!(
2671 existing_descendant.cancel_reason(),
2672 Some(CancelReason::Abort)
2673 );
2674 assert!(operation.checkpoint().is_err());
2675 assert!(existing_descendant.checkpoint().is_err());
2676
2677 assert!(root.checkpoint().is_ok());
2678 assert!(sibling.checkpoint().is_ok());
2679 assert!(!root.is_cancel_requested());
2680 assert!(!sibling.is_cancel_requested());
2681
2682 let late_descendant = operation.create_child();
2683 assert_eq!(late_descendant.cancel_reason(), Some(CancelReason::Abort));
2684 assert!(late_descendant.checkpoint().is_err());
2685 assert!(root.checkpoint().is_ok());
2686 }
2687
2688 #[test]
2689 fn local_cancel_relay_is_weak_and_cross_thread_safe() {
2690 fn assert_send_sync<T: Send + Sync>() {}
2691 assert_send_sync::<LocalCancelRelay>();
2692
2693 let root = Cx::<FullCaps>::new();
2694 let (operation, relay) = root.create_child_with_local_cancel_relay();
2695 let cancel_thread =
2696 std::thread::spawn(move || relay.cancel_local(CancelReason::RegionClose));
2697 assert!(
2698 cancel_thread
2699 .join()
2700 .expect("cancel thread should not panic"),
2701 "live operation should accept a relayed cancellation"
2702 );
2703 assert_eq!(operation.cancel_reason(), Some(CancelReason::RegionClose));
2704
2705 let (dropped_operation, dropped_relay) = root.create_child_with_local_cancel_relay();
2706 drop(dropped_operation);
2707 assert!(
2708 !dropped_relay.cancel_local(CancelReason::Abort),
2709 "a weak relay must become inert after its target is dropped"
2710 );
2711 assert!(root.checkpoint().is_ok());
2712 }
2713
2714 #[test]
2715 fn local_cancellation_future_wakes_for_local_relay() {
2716 let root = Cx::<FullCaps>::new();
2717 let (operation, relay) = root.create_child_with_local_cancel_relay();
2718 let wake_count = Arc::new(CountingWake::default());
2719 let waker = Waker::from(Arc::clone(&wake_count));
2720 let mut task_cx = TaskContext::from_waker(&waker);
2721 let mut cancellation = std::pin::pin!(operation.wait_for_local_cancellation());
2722
2723 assert_eq!(
2724 cancellation.as_mut().poll(&mut task_cx),
2725 Poll::Pending,
2726 "uncancelled operation should register one waiter"
2727 );
2728 assert_eq!(local_cancel_waiter_count(&operation), 1);
2729
2730 assert!(relay.cancel_local(CancelReason::RegionClose));
2731 assert_eq!(
2732 wake_count.0.load(Ordering::Acquire),
2733 1,
2734 "local relay cancellation should wake the registered future"
2735 );
2736 assert_eq!(cancellation.as_mut().poll(&mut task_cx), Poll::Ready(()));
2737 assert_eq!(
2738 local_cancel_waiter_count(&operation),
2739 0,
2740 "ready future must leave no stale registration"
2741 );
2742 assert!(root.checkpoint().is_ok());
2743 }
2744
2745 #[test]
2746 fn dropping_local_cancellation_future_unregisters_waiter() {
2747 let cx = Cx::<FullCaps>::new();
2748 let wake_count = Arc::new(CountingWake::default());
2749 let waker = Waker::from(wake_count);
2750 let mut task_cx = TaskContext::from_waker(&waker);
2751
2752 {
2753 let mut cancellation = std::pin::pin!(cx.wait_for_local_cancellation());
2754 assert_eq!(cancellation.as_mut().poll(&mut task_cx), Poll::Pending);
2755 assert_eq!(local_cancel_waiter_count(&cx), 1);
2756 }
2757
2758 assert_eq!(
2759 local_cancel_waiter_count(&cx),
2760 0,
2761 "dropping a pending future must remove its waker"
2762 );
2763 }
2764
2765 #[test]
2766 fn already_cancelled_local_future_never_registers_a_waiter() {
2767 let cx = Cx::<FullCaps>::new();
2768 cx.cancel();
2769 assert_eq!(local_cancel_waiter_count(&cx), 0);
2770
2771 let wake_count = Arc::new(CountingWake::default());
2772 let waker = Waker::from(wake_count);
2773 let mut task_cx = TaskContext::from_waker(&waker);
2774 let mut cancellation = std::pin::pin!(cx.wait_for_local_cancellation());
2775 assert_eq!(cancellation.as_mut().poll(&mut task_cx), Poll::Ready(()));
2776 assert_eq!(
2777 local_cancel_waiter_count(&cx),
2778 0,
2779 "an already-ready first poll must not register a waiter"
2780 );
2781 }
2782
2783 #[test]
2784 fn repoll_replaces_the_registered_waker() {
2785 let cx = Cx::<FullCaps>::new();
2786 let first_wake_count = Arc::new(CountingWake::default());
2787 let first_waker = Waker::from(Arc::clone(&first_wake_count));
2788 let mut first_task_cx = TaskContext::from_waker(&first_waker);
2789 let second_wake_count = Arc::new(CountingWake::default());
2790 let second_waker = Waker::from(Arc::clone(&second_wake_count));
2791 let mut second_task_cx = TaskContext::from_waker(&second_waker);
2792 let mut cancellation = std::pin::pin!(cx.wait_for_local_cancellation());
2793
2794 assert_eq!(
2795 cancellation.as_mut().poll(&mut first_task_cx),
2796 Poll::Pending
2797 );
2798 assert_eq!(
2799 cancellation.as_mut().poll(&mut second_task_cx),
2800 Poll::Pending
2801 );
2802 assert_eq!(local_cancel_waiter_count(&cx), 1);
2803
2804 cx.cancel();
2805 assert_eq!(
2806 first_wake_count.0.load(Ordering::Acquire),
2807 0,
2808 "a replaced waker must not be invoked"
2809 );
2810 assert_eq!(
2811 second_wake_count.0.load(Ordering::Acquire),
2812 1,
2813 "only the most recently registered waker should be invoked"
2814 );
2815 assert_eq!(
2816 cancellation.as_mut().poll(&mut second_task_cx),
2817 Poll::Ready(())
2818 );
2819 }
2820
2821 #[test]
2822 fn local_cancellation_waker_runs_outside_the_registry_lock() {
2823 let cx = Cx::<FullCaps>::new();
2824 let probe = Arc::new(RegistryProbeWake {
2825 inner: Arc::downgrade(&cx.inner),
2826 wake_count: AtomicUsize::new(0),
2827 registry_was_unlocked: AtomicBool::new(false),
2828 });
2829 let waker = Waker::from(Arc::clone(&probe));
2830 let mut task_cx = TaskContext::from_waker(&waker);
2831 let mut cancellation = std::pin::pin!(cx.wait_for_local_cancellation());
2832
2833 assert_eq!(cancellation.as_mut().poll(&mut task_cx), Poll::Pending);
2834 cx.cancel();
2835 assert!(
2836 probe.registry_was_unlocked.load(Ordering::Acquire),
2837 "wake callback should acquire the registry without reentrant deadlock"
2838 );
2839 assert_eq!(probe.wake_count.load(Ordering::Acquire), 1);
2840 assert_eq!(cancellation.as_mut().poll(&mut task_cx), Poll::Ready(()));
2841 }
2842
2843 #[test]
2844 fn cancellation_publishes_the_complete_subtree_before_waking_observers() {
2845 let root = Cx::<FullCaps>::new();
2846 let descendant = root.create_child();
2847 let probe = Arc::new(DescendantStateProbeWake {
2848 descendant: Arc::downgrade(&descendant.inner),
2849 wake_count: AtomicUsize::new(0),
2850 saw_descendant_cancelled: AtomicBool::new(false),
2851 dispatch_gate_was_unlocked: AtomicBool::new(false),
2852 });
2853 let waker = Waker::from(Arc::clone(&probe));
2854 let mut task_cx = TaskContext::from_waker(&waker);
2855 let mut cancellation = std::pin::pin!(root.wait_for_local_cancellation());
2856
2857 assert_eq!(cancellation.as_mut().poll(&mut task_cx), Poll::Pending);
2858 root.cancel();
2859
2860 assert_eq!(probe.wake_count.load(Ordering::Acquire), 1);
2861 assert!(
2862 probe.saw_descendant_cancelled.load(Ordering::Acquire),
2863 "reentrant observers must never see a half-published cancellation tree"
2864 );
2865 assert!(
2866 probe.dispatch_gate_was_unlocked.load(Ordering::Acquire),
2867 "callbacks must be able to re-enter family cancellation machinery"
2868 );
2869 assert_eq!(cancellation.as_mut().poll(&mut task_cx), Poll::Ready(()));
2870 assert!(descendant.checkpoint().is_err());
2871 }
2872
2873 #[test]
2874 fn panicking_cancellation_waker_does_not_suppress_other_observers() {
2875 let root = Cx::<FullCaps>::new();
2876 let descendant = root.create_child();
2877 let panic_waker = Waker::from(Arc::new(PanicWake));
2878 let mut panic_task_cx = TaskContext::from_waker(&panic_waker);
2879 let wake_count = Arc::new(CountingWake::default());
2880 let counting_waker = Waker::from(Arc::clone(&wake_count));
2881 let mut counting_task_cx = TaskContext::from_waker(&counting_waker);
2882 let mut panicking = std::pin::pin!(root.wait_for_local_cancellation());
2883 let mut counting = std::pin::pin!(root.wait_for_local_cancellation());
2884
2885 assert_eq!(panicking.as_mut().poll(&mut panic_task_cx), Poll::Pending);
2886 assert_eq!(counting.as_mut().poll(&mut counting_task_cx), Poll::Pending);
2887 let cancel_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2888 root.cancel();
2889 }));
2890
2891 assert!(
2892 cancel_result.is_err(),
2893 "the first callback panic must be resumed after notification completes"
2894 );
2895 assert_eq!(
2896 wake_count.0.load(Ordering::Acquire),
2897 1,
2898 "one panicking observer must not suppress later observers"
2899 );
2900 assert!(descendant.checkpoint().is_err());
2901 assert_eq!(panicking.as_mut().poll(&mut panic_task_cx), Poll::Ready(()));
2902 assert_eq!(
2903 counting.as_mut().poll(&mut counting_task_cx),
2904 Poll::Ready(())
2905 );
2906 assert_eq!(local_cancel_waiter_count(&root), 0);
2907 }
2908
2909 #[test]
2910 fn cancellation_waker_can_reenter_family_state_without_deadlock() {
2911 let root = Cx::<FullCaps>::new();
2912 let probe = Arc::new(ReentrantFamilyWake {
2913 cx: root.clone(),
2914 wake_count: AtomicUsize::new(0),
2915 child_inherited_cancellation: AtomicBool::new(false),
2916 });
2917 let waker = Waker::from(Arc::clone(&probe));
2918 let mut task_cx = TaskContext::from_waker(&waker);
2919 let mut cancellation = std::pin::pin!(root.wait_for_local_cancellation());
2920
2921 assert_eq!(cancellation.as_mut().poll(&mut task_cx), Poll::Pending);
2922 root.cancel();
2923
2924 assert_eq!(probe.wake_count.load(Ordering::Acquire), 1);
2925 assert!(
2926 probe.child_inherited_cancellation.load(Ordering::Acquire),
2927 "a child created reentrantly must be initialized before it is linked"
2928 );
2929 assert_eq!(root.cancel_reason(), Some(CancelReason::Abort));
2930 assert_eq!(cancellation.as_mut().poll(&mut task_cx), Poll::Ready(()));
2931 }
2932
2933 #[test]
2934 fn repeated_local_waiter_poll_and_drop_accumulates_nothing() {
2935 let cx = Cx::<FullCaps>::new();
2936 let initial_children = cx
2937 .inner
2938 .children
2939 .lock()
2940 .unwrap_or_else(std::sync::PoisonError::into_inner)
2941 .len();
2942 let wake_count = Arc::new(CountingWake::default());
2943 let waker = Waker::from(wake_count);
2944 let mut task_cx = TaskContext::from_waker(&waker);
2945
2946 for _ in 0..256 {
2947 let mut cancellation = std::pin::pin!(cx.wait_for_local_cancellation());
2948 assert_eq!(cancellation.as_mut().poll(&mut task_cx), Poll::Pending);
2949 }
2950
2951 assert_eq!(
2952 local_cancel_waiter_count(&cx),
2953 0,
2954 "dropped local wait futures must not accumulate registry entries"
2955 );
2956 assert_eq!(
2957 cx.inner
2958 .children
2959 .lock()
2960 .unwrap_or_else(std::sync::PoisonError::into_inner)
2961 .len(),
2962 initial_children,
2963 "local wait futures must not allocate child contexts"
2964 );
2965 }
2966
2967 #[test]
2968 fn local_cancellation_future_defers_while_masked_and_wakes_on_unmask() {
2969 let root = Cx::<FullCaps>::new();
2970 let (operation, relay) = root.create_child_with_local_cancel_relay();
2971 let mask = operation.masked();
2972 let wake_count = Arc::new(CountingWake::default());
2973 let waker = Waker::from(Arc::clone(&wake_count));
2974 let mut task_cx = TaskContext::from_waker(&waker);
2975 let mut cancellation = std::pin::pin!(operation.wait_for_local_cancellation());
2976
2977 assert_eq!(cancellation.as_mut().poll(&mut task_cx), Poll::Pending);
2978 assert!(relay.cancel_local(CancelReason::Abort));
2979 assert_eq!(wake_count.0.load(Ordering::Acquire), 1);
2980
2981 assert_eq!(
2982 cancellation.as_mut().poll(&mut task_cx),
2983 Poll::Pending,
2984 "masking must defer cancellation observation"
2985 );
2986 assert_eq!(
2987 local_cancel_waiter_count(&operation),
2988 1,
2989 "a masked future must remain registered for the unmask boundary"
2990 );
2991
2992 drop(mask);
2993 assert_eq!(
2994 wake_count.0.load(Ordering::Acquire),
2995 2,
2996 "outermost unmask must wake a deferred cancellation observer"
2997 );
2998 assert_eq!(cancellation.as_mut().poll(&mut task_cx), Poll::Ready(()));
2999 }
3000
3001 #[test]
3002 fn local_cancel_request_future_is_ready_while_masked() {
3003 let root = Cx::<FullCaps>::new();
3004 let (operation, relay) = root.create_child_with_local_cancel_relay();
3005 let mask = operation.masked();
3006 let wake_count = Arc::new(CountingWake::default());
3007 let waker = Waker::from(Arc::clone(&wake_count));
3008 let mut task_cx = TaskContext::from_waker(&waker);
3009 let mut request = std::pin::pin!(operation.wait_for_local_cancel_request());
3010
3011 assert_eq!(request.as_mut().poll(&mut task_cx), Poll::Pending);
3012 assert!(relay.cancel_local(CancelReason::UserInterrupt));
3013 assert_eq!(wake_count.0.load(Ordering::Acquire), 1);
3014 assert_eq!(
3015 request.as_mut().poll(&mut task_cx),
3016 Poll::Ready(()),
3017 "raw request notification must not reinterpret masking policy"
3018 );
3019 assert!(
3020 operation.checkpoint().is_ok(),
3021 "the context checkpoint itself must continue to defer while masked"
3022 );
3023 drop(mask);
3024 assert!(operation.checkpoint().is_err());
3025 }
3026
3027 #[test]
3028 fn local_cancellation_registration_race_leaves_no_stale_waiter() {
3029 for _ in 0..64 {
3030 let root = Cx::<FullCaps>::new();
3031 let (operation, relay) = root.create_child_with_local_cancel_relay();
3032 let barrier = Arc::new(Barrier::new(2));
3033 let cancel_barrier = Arc::clone(&barrier);
3034 let cancel_thread = std::thread::spawn(move || {
3035 cancel_barrier.wait();
3036 relay.cancel_local(CancelReason::UserInterrupt)
3037 });
3038
3039 let wake_count = Arc::new(CountingWake::default());
3040 let waker = Waker::from(Arc::clone(&wake_count));
3041 let mut task_cx = TaskContext::from_waker(&waker);
3042 let mut cancellation = std::pin::pin!(operation.wait_for_local_cancellation());
3043 barrier.wait();
3044 let first_poll = cancellation.as_mut().poll(&mut task_cx);
3045
3046 assert!(
3047 cancel_thread
3048 .join()
3049 .expect("cancel thread should not panic")
3050 );
3051 if first_poll == Poll::Pending {
3052 assert!(
3053 wake_count.0.load(Ordering::Acquire) > 0,
3054 "a waiter registered during cancellation must be notified"
3055 );
3056 }
3057 assert_eq!(cancellation.as_mut().poll(&mut task_cx), Poll::Ready(()));
3058 assert_eq!(
3059 local_cancel_waiter_count(&operation),
3060 0,
3061 "registration/cancellation race must not strand a waker"
3062 );
3063 }
3064 }
3065
3066 #[test]
3067 fn local_cancel_relay_handles_deep_subtrees_on_a_small_stack() {
3068 let root = Cx::<FullCaps>::new();
3069 let (operation, relay) = root.create_child_with_local_cancel_relay();
3070 let mut chain = Vec::with_capacity(8_193);
3071 chain.push(operation);
3072 for _ in 0..8_192 {
3073 let child = chain
3074 .last()
3075 .expect("chain must contain its root")
3076 .create_child();
3077 chain.push(child);
3078 }
3079
3080 let cancel_thread = std::thread::Builder::new()
3081 .name("local-cancel-deep-tree".to_owned())
3082 .stack_size(256 * 1024)
3083 .spawn(move || relay.cancel_local(CancelReason::RegionClose))
3084 .expect("small-stack cancellation thread should spawn");
3085 assert!(
3086 cancel_thread
3087 .join()
3088 .expect("iterative cancellation traversal must not overflow")
3089 );
3090 assert_eq!(
3091 chain.last().and_then(Cx::cancel_reason),
3092 Some(CancelReason::RegionClose)
3093 );
3094 assert!(root.checkpoint().is_ok());
3095 }
3096
3097 #[cfg(feature = "native")]
3098 #[test]
3099 fn test_cx_checkpoint_native_cx_cancellation_maps_reason() {
3100 let cx = Cx::<FullCaps>::new();
3101 let native = NativeCx::for_testing();
3102 cx.set_native_cx(native.clone());
3103 native.set_cancel_reason(NativeCancelReason::timeout());
3104
3105 let err = cx.checkpoint().unwrap_err();
3106 assert_eq!(err.kind(), ErrorKind::Cancelled);
3107 assert_eq!(cx.cancel_reason(), Some(CancelReason::Timeout));
3108 }
3109
3110 #[cfg(feature = "native")]
3111 #[test]
3112 fn test_cx_cancel_reason_propagates_to_native_cx() {
3113 let cx = Cx::<FullCaps>::new();
3114 let native = NativeCx::for_testing();
3115 cx.set_native_cx(native.clone());
3116
3117 cx.cancel_with_reason(CancelReason::RegionClose);
3118 let reason = native
3119 .cancel_reason()
3120 .expect("native cancel reason must be set");
3121 assert_eq!(reason.kind, NativeCancelKind::ParentCancelled);
3122 }
3123
3124 #[cfg(feature = "native")]
3125 #[test]
3126 fn local_cancel_relay_preserves_shared_native_context_for_late_children() {
3127 let root = Cx::<FullCaps>::new();
3128 let native = NativeCx::for_testing();
3129 root.set_native_cx(native.clone());
3130 let sibling = root.create_child();
3131 let (operation, relay) = root.create_child_with_local_cancel_relay();
3132 let existing_descendant = operation.create_child();
3133
3134 assert!(relay.cancel_local(CancelReason::Abort));
3135 assert!(operation.checkpoint().is_err());
3136 assert!(existing_descendant.checkpoint().is_err());
3137 assert!(root.checkpoint().is_ok());
3138 assert!(sibling.checkpoint().is_ok());
3139 assert!(
3140 native.checkpoint().is_ok(),
3141 "local operation cancellation must not poison shared native I/O state"
3142 );
3143
3144 let late_descendant = operation.create_child();
3145 assert!(late_descendant.checkpoint().is_err());
3146 assert!(
3147 native.checkpoint().is_ok(),
3148 "a descendant created after local cancellation must inherit locally"
3149 );
3150
3151 root.cancel_with_reason(CancelReason::RegionClose);
3152 assert!(
3153 native.is_cancel_requested(),
3154 "later ordinary root cancellation must still cross the native boundary"
3155 );
3156 }
3157
3158 #[cfg(feature = "native")]
3159 #[test]
3160 fn local_cancel_relay_preserves_native_contexts_attached_after_cancellation() {
3161 let root = Cx::<FullCaps>::new();
3162 let (operation, relay) = root.create_child_with_local_cancel_relay();
3163 assert!(relay.cancel_local(CancelReason::Abort));
3164
3165 let fallback = operation.effective_native_cx();
3166 assert!(
3167 fallback.checkpoint().is_ok(),
3168 "local cancellation must not taint a later fallback native context"
3169 );
3170
3171 let replacement = NativeCx::for_testing();
3172 operation.set_native_cx(replacement.clone());
3173 assert!(
3174 replacement.checkpoint().is_ok(),
3175 "local cancellation must not taint a later explicit native context"
3176 );
3177 assert!(operation.checkpoint().is_err());
3178 }
3179
3180 #[cfg(feature = "native")]
3181 #[test]
3182 fn local_reason_never_leaks_through_later_ordinary_cancellation() {
3183 let root = Cx::<FullCaps>::new();
3184 let shared_native = NativeCx::for_testing();
3185 root.set_native_cx(shared_native.clone());
3186 let (operation, relay) = root.create_child_with_local_cancel_relay();
3187
3188 assert!(relay.cancel_local(CancelReason::Abort));
3189 root.cancel_with_reason(CancelReason::Timeout);
3190
3191 assert_eq!(operation.cancel_reason(), Some(CancelReason::Abort));
3192 assert_eq!(
3193 shared_native
3194 .cancel_reason()
3195 .expect("ordinary cancellation must reach shared native")
3196 .kind,
3197 NativeCancelKind::Timeout,
3198 "the stronger local-only Abort must not cross the native boundary"
3199 );
3200
3201 let late_descendant = operation.create_child();
3202 assert_eq!(
3203 late_descendant.cancel_reason(),
3204 Some(CancelReason::Abort),
3205 "late descendants inherit the aggregate local reason"
3206 );
3207 assert_eq!(
3208 shared_native
3209 .cancel_reason()
3210 .expect("late-child registration must retain ordinary reason")
3211 .kind,
3212 NativeCancelKind::Timeout
3213 );
3214
3215 operation.clear_native_cx();
3216 let fallback = operation.effective_native_cx();
3217 assert_eq!(
3218 fallback
3219 .cancel_reason()
3220 .expect("late fallback must receive ordinary reason")
3221 .kind,
3222 NativeCancelKind::Timeout
3223 );
3224
3225 let replacement = NativeCx::for_testing();
3226 operation.set_native_cx(replacement.clone());
3227 assert_eq!(
3228 replacement
3229 .cancel_reason()
3230 .expect("late explicit attachment must receive ordinary reason")
3231 .kind,
3232 NativeCancelKind::Timeout
3233 );
3234 }
3235
3236 #[cfg(feature = "native")]
3237 #[test]
3238 fn weaker_ordinary_reason_cannot_downgrade_native_cancellation() {
3239 let cx = Cx::<FullCaps>::new();
3240 let native = NativeCx::for_testing();
3241 cx.set_native_cx(native.clone());
3242
3243 cx.cancel_with_reason(CancelReason::Abort);
3244 cx.cancel_with_reason(CancelReason::Timeout);
3245
3246 assert_eq!(cx.cancel_reason(), Some(CancelReason::Abort));
3247 assert_eq!(
3248 native
3249 .cancel_reason()
3250 .expect("native reason must remain present")
3251 .kind,
3252 NativeCancelKind::ResourceUnavailable
3253 );
3254 }
3255
3256 #[cfg(feature = "native")]
3257 #[test]
3258 fn native_attachment_racing_ordinary_cancellation_never_misses_reason() {
3259 for _ in 0..128 {
3260 let cx = Cx::<FullCaps>::new();
3261 let cancel_cx = cx.clone();
3262 let attach_cx = cx.clone();
3263 let native = NativeCx::for_testing();
3264 let attached_native = native.clone();
3265 let gate = Arc::new(std::sync::Barrier::new(3));
3266 let cancel_gate = Arc::clone(&gate);
3267 let attach_gate = Arc::clone(&gate);
3268
3269 let cancel_thread = std::thread::spawn(move || {
3270 cancel_gate.wait();
3271 cancel_cx.cancel_with_reason(CancelReason::RegionClose);
3272 });
3273 let attach_thread = std::thread::spawn(move || {
3274 attach_gate.wait();
3275 attach_cx.set_native_cx(attached_native);
3276 });
3277 gate.wait();
3278 cancel_thread.join().expect("cancellation must not panic");
3279 attach_thread.join().expect("attachment must not panic");
3280
3281 assert_eq!(
3282 native
3283 .cancel_reason()
3284 .expect("racing attachment must observe cancellation")
3285 .kind,
3286 NativeCancelKind::ParentCancelled
3287 );
3288 }
3289 }
3290
3291 #[cfg(feature = "native")]
3292 #[test]
3293 fn racing_native_replacements_each_synchronize_the_exact_supplied_handle() {
3294 for _ in 0..128 {
3295 let cx = Cx::<FullCaps>::new();
3296 cx.cancel_with_reason(CancelReason::RegionClose);
3297
3298 let native_a = NativeCx::for_testing();
3299 let native_b = NativeCx::for_testing();
3300 let setter_a = cx.clone();
3301 let setter_b = cx.clone();
3302 let supplied_a = native_a.clone();
3303 let supplied_b = native_b.clone();
3304 let gate = Arc::new(std::sync::Barrier::new(3));
3305 let gate_a = Arc::clone(&gate);
3306 let gate_b = Arc::clone(&gate);
3307
3308 let thread_a = std::thread::spawn(move || {
3309 gate_a.wait();
3310 setter_a.set_native_cx(supplied_a);
3311 });
3312 let thread_b = std::thread::spawn(move || {
3313 gate_b.wait();
3314 setter_b.set_native_cx(supplied_b);
3315 });
3316 gate.wait();
3317 thread_a.join().expect("first replacement must not panic");
3318 thread_b.join().expect("second replacement must not panic");
3319
3320 for native in [&native_a, &native_b] {
3321 assert_eq!(
3322 native
3323 .cancel_reason()
3324 .expect("each exact supplied handle must be synchronized")
3325 .kind,
3326 NativeCancelKind::ParentCancelled
3327 );
3328 }
3329 }
3330 }
3331
3332 #[cfg(feature = "native")]
3333 #[test]
3334 fn fallback_creation_racing_ordinary_cancellation_never_misses_reason() {
3335 for _ in 0..128 {
3336 let cx = Cx::<FullCaps>::new();
3337 let cancel_cx = cx.clone();
3338 let fallback_cx = cx.clone();
3339 let gate = Arc::new(std::sync::Barrier::new(3));
3340 let cancel_gate = Arc::clone(&gate);
3341 let fallback_gate = Arc::clone(&gate);
3342
3343 let cancel_thread = std::thread::spawn(move || {
3344 cancel_gate.wait();
3345 cancel_cx.cancel_with_reason(CancelReason::RegionClose);
3346 });
3347 let fallback_thread = std::thread::spawn(move || {
3348 fallback_gate.wait();
3349 fallback_cx.effective_native_cx()
3350 });
3351 gate.wait();
3352 cancel_thread.join().expect("cancellation must not panic");
3353 let native = fallback_thread
3354 .join()
3355 .expect("fallback creation must not panic");
3356
3357 assert_eq!(
3358 native
3359 .cancel_reason()
3360 .expect("racing fallback must observe cancellation")
3361 .kind,
3362 NativeCancelKind::ParentCancelled
3363 );
3364 }
3365 }
3366
3367 #[cfg(feature = "native")]
3368 #[test]
3369 fn ordinary_cancel_before_native_attachment_is_mirrored_after_registration() {
3370 let cx = Cx::<FullCaps>::new();
3371 cx.cancel_with_reason(CancelReason::RegionClose);
3372
3373 let native = NativeCx::for_testing();
3374 cx.set_native_cx(native.clone());
3375 let reason = native
3376 .cancel_reason()
3377 .expect("ordinary local cancellation must mirror to a later attachment");
3378 assert_eq!(reason.kind, NativeCancelKind::ParentCancelled);
3379 }
3380
3381 #[cfg(feature = "native")]
3382 #[test]
3383 fn test_cx_checkpoint_native_cx_respects_local_masking() {
3384 let cx = Cx::<FullCaps>::new();
3385 let native = NativeCx::for_testing();
3386 cx.set_native_cx(native.clone());
3387 native.set_cancel_reason(NativeCancelReason::user("cancel"));
3388
3389 {
3390 let _mask = cx.masked();
3391 assert!(cx.checkpoint().is_ok());
3392 assert!(cx.is_cancel_requested());
3393 assert_eq!(cx.cancel_state(), CancelState::CancelRequested);
3394 }
3395
3396 let err = cx.checkpoint().unwrap_err();
3397 assert_eq!(err.kind(), ErrorKind::Cancelled);
3398 }
3399
3400 #[cfg(feature = "native")]
3401 #[test]
3402 fn test_cx_effective_native_cx_uses_fallback_without_marking_explicit_attachment() {
3403 let cx = Cx::<FullCaps>::with_budget(Budget::INFINITE.with_priority(7));
3404
3405 assert!(cx.attached_native_cx().is_none());
3406 let native = cx.effective_native_cx();
3407 assert!(cx.attached_native_cx().is_none());
3408 assert!(native.checkpoint().is_ok());
3409 }
3410
3411 #[cfg(feature = "native")]
3412 #[test]
3413 fn test_cx_checkpoint_without_native_context_does_not_create_fallback() {
3414 let cx = Cx::<FullCaps>::new();
3415
3416 assert!(cx.inner.fallback_native_cx.get().is_none());
3417 assert!(cx.checkpoint().is_ok());
3418 assert!(cx.inner.fallback_native_cx.get().is_none());
3419 }
3420
3421 #[cfg(feature = "native")]
3422 #[test]
3423 fn test_cx_set_native_cx_replaces_fallback_context() {
3424 let cx = Cx::<FullCaps>::new();
3425 let _ = cx.effective_native_cx();
3426
3427 let replacement = NativeCx::for_testing();
3428 cx.set_native_cx(replacement.clone());
3429 replacement.set_cancel_reason(NativeCancelReason::timeout());
3430
3431 let err = cx.checkpoint().unwrap_err();
3432 assert_eq!(err.kind(), ErrorKind::Cancelled);
3433 assert_eq!(cx.cancel_reason(), Some(CancelReason::Timeout));
3434 }
3435
3436 #[cfg(feature = "native")]
3437 #[test]
3438 fn test_create_child_copies_preexisting_cancellation_into_fallback_native_cx() {
3439 let parent = Cx::<FullCaps>::new();
3440 parent.cancel_with_reason(CancelReason::RegionClose);
3441
3442 let child = parent.create_child();
3443 let reason = child
3444 .effective_native_cx()
3445 .cancel_reason()
3446 .expect("fallback native cx should mirror inherited cancellation");
3447 assert_eq!(reason.kind, NativeCancelKind::ParentCancelled);
3448 }
3449
3450 #[cfg(feature = "native")]
3451 #[test]
3452 fn test_create_child_inherits_explicit_native_cx_attachment() {
3453 let parent = Cx::<FullCaps>::new();
3454 let native = NativeCx::for_testing();
3455 parent.set_native_cx(native.clone());
3456
3457 let child = parent.create_child();
3458 assert!(child.attached_native_cx().is_some());
3459
3460 native.set_cancel_reason(NativeCancelReason::timeout());
3461 let err = child
3462 .checkpoint()
3463 .expect_err("child should observe inherited native cancel");
3464 assert_eq!(err.kind(), ErrorKind::Cancelled);
3465 assert_eq!(child.cancel_reason(), Some(CancelReason::Timeout));
3466 }
3467
3468 #[cfg(feature = "native")]
3469 #[test]
3470 fn spawn_child_does_not_carry_a_task_affine_native_context() {
3471 let parent = Cx::<FullCaps>::new();
3472 parent.set_native_cx(NativeCx::for_testing());
3473 let child = parent.create_child_for_spawn();
3474
3475 assert!(
3476 child.attached_native_cx().is_none(),
3477 "spawn child must start without the caller task's native context"
3478 );
3479 assert!(
3480 child.inner.fallback_native_cx.get().is_none(),
3481 "spawn child must not invent a fallback context before task entry"
3482 );
3483
3484 let task_native = NativeCx::for_testing();
3485 child.set_native_cx(task_native);
3486 assert!(
3487 child.attached_native_cx().is_some(),
3488 "the spawned task must be able to attach its own native context"
3489 );
3490 }
3491
3492 #[test]
3493 fn test_budget_infinite_is_identity_for_meet() {
3494 let budget = Budget {
3495 deadline: Some(Duration::from_millis(42)),
3496 poll_quota: 500,
3497 cost_quota: Some(1000),
3498 priority: 7,
3499 };
3500 assert_eq!(budget.meet(Budget::INFINITE), budget);
3501 assert_eq!(Budget::INFINITE.meet(budget), budget);
3502 }
3503
3504 #[test]
3505 fn test_budget_none_constraints_propagate() {
3506 let a = Budget {
3507 deadline: None,
3508 poll_quota: u32::MAX,
3509 cost_quota: None,
3510 priority: 0,
3511 };
3512 let b = Budget {
3513 deadline: Some(Duration::from_millis(50)),
3514 poll_quota: 100,
3515 cost_quota: Some(500),
3516 priority: 3,
3517 };
3518 let m = a.meet(b);
3519 assert_eq!(m.deadline, Some(Duration::from_millis(50)));
3520 assert_eq!(m.poll_quota, 100);
3521 assert_eq!(m.cost_quota, Some(500));
3522 assert_eq!(m.priority, 3);
3523 }
3524
3525 #[test]
3526 fn test_cx_scope_budget_chains() {
3527 let cx = Cx::<FullCaps>::with_budget(
3528 Budget::INFINITE
3529 .with_deadline(Duration::from_millis(100))
3530 .with_poll_quota(1000),
3531 );
3532 let s1 = cx.scope_with_budget(Budget::INFINITE.with_deadline(Duration::from_millis(50)));
3534 assert_eq!(s1.budget().deadline, Some(Duration::from_millis(50)));
3535 assert_eq!(s1.budget().poll_quota, 1000);
3536
3537 let s2 = s1.scope_with_budget(Budget::INFINITE.with_poll_quota(200));
3539 assert_eq!(s2.budget().deadline, Some(Duration::from_millis(50)));
3540 assert_eq!(s2.budget().poll_quota, 200);
3541 }
3542
3543 fn collect_rs_files(dir: &Path, out: &mut Vec<PathBuf>) -> std::io::Result<()> {
3544 for entry in std::fs::read_dir(dir)? {
3545 let entry = entry?;
3546 let path = entry.path();
3547 if path.is_dir() {
3548 collect_rs_files(&path, out)?;
3549 } else if path.extension().is_some_and(|ext| ext == "rs") {
3550 out.push(path);
3551 }
3552 }
3553 Ok(())
3554 }
3555
3556 fn scan_file_outside_cfg_test_items(src: &str, patterns: &[&str]) -> Vec<(usize, String)> {
3557 let mut hits = Vec::new();
3558
3559 let mut brace_depth: i32 = 0;
3560 let mut pending_cfg_test = false;
3561 let mut pending_attr_paren_depth: i32 = 0;
3562 let mut skip_until_depth: Option<i32> = None;
3563
3564 for (idx, line) in src.lines().enumerate() {
3565 let trimmed = line.trim_start();
3566 let paren_delta = i32::try_from(line.matches('(').count()).unwrap_or(i32::MAX)
3567 - i32::try_from(line.matches(')').count()).unwrap_or(i32::MAX);
3568
3569 if skip_until_depth.is_none() {
3570 if trimmed.starts_with("#[cfg(test)]") && trimmed.contains('{') {
3572 pending_cfg_test = false;
3573 pending_attr_paren_depth = 0;
3574 skip_until_depth = Some(brace_depth);
3575 } else if trimmed.contains("fn test_") && trimmed.contains('{') {
3576 skip_until_depth = Some(brace_depth);
3577 } else if trimmed.starts_with("#[cfg(test)]") {
3578 pending_cfg_test = true;
3579 pending_attr_paren_depth = 0;
3580 } else if pending_cfg_test {
3581 if trimmed.starts_with("#[") || pending_attr_paren_depth > 0 {
3583 pending_attr_paren_depth =
3584 pending_attr_paren_depth.saturating_add(paren_delta);
3585 } else if trimmed.is_empty() || trimmed.starts_with("//") {
3586 } else if trimmed.contains('{') {
3588 pending_cfg_test = false;
3589 pending_attr_paren_depth = 0;
3590 skip_until_depth = Some(brace_depth);
3591 } else {
3592 pending_cfg_test = false;
3593 pending_attr_paren_depth = 0;
3594 }
3595 } else {
3596 for &pat in patterns {
3597 if line.contains(pat) {
3598 hits.push((idx + 1, pat.to_string()));
3599 }
3600 }
3601 }
3602 }
3603
3604 let opens = i32::try_from(line.matches('{').count()).unwrap_or(i32::MAX);
3606 let closes = i32::try_from(line.matches('}').count()).unwrap_or(i32::MAX);
3607 brace_depth = brace_depth.saturating_add(opens).saturating_sub(closes);
3608
3609 if let Some(until) = skip_until_depth
3610 && brace_depth <= until
3611 {
3612 skip_until_depth = None;
3613 }
3614 }
3615
3616 hits
3617 }
3618
3619 #[test]
3620 fn test_scan_file_outside_cfg_test_items_skips_cfg_test_functions_and_modules() {
3621 let src = r"
3622fn production_path() {
3623 let _ = Cx::new();
3624}
3625
3626#[cfg(test)]
3627fn test_only_helper() {
3628 let _ = Cx::new();
3629}
3630
3631#[cfg(test)]
3632mod tests {
3633 fn nested_test_helper() {
3634 let _ = Cx::default();
3635 }
3636}
3637";
3638
3639 let hits = scan_file_outside_cfg_test_items(src, &["Cx::new(", "Cx::default("]);
3640 assert_eq!(hits, vec![(3, "Cx::new(".to_string())]);
3641 }
3642
3643 #[test]
3644 fn test_no_direct_cx_constructors_in_runtime_production_code() {
3645 let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
3646 let repo_root = manifest_dir
3647 .parent()
3648 .and_then(Path::parent)
3649 .expect("fsqlite-types manifest dir must be crates/<name>");
3650 let crates_dir = repo_root.join("crates");
3651 let runtime_crates = [
3652 "fsqlite-core",
3653 "fsqlite-vdbe",
3654 "fsqlite-btree",
3655 "fsqlite-pager",
3656 "fsqlite-wal",
3657 "fsqlite-mvcc",
3658 ];
3659 let forbidden = ["Cx::new(", "Cx::default("];
3660
3661 let mut violations: Vec<String> = Vec::new();
3662 let mut crate_dirs: Vec<PathBuf> = Vec::new();
3663 for entry in std::fs::read_dir(&crates_dir).expect("read crates/ dir") {
3664 let entry = entry.expect("read crates/ entry");
3665 let path = entry.path();
3666 if path.is_dir() {
3667 crate_dirs.push(path);
3668 }
3669 }
3670
3671 for crate_dir in crate_dirs {
3672 let crate_name = crate_dir
3673 .file_name()
3674 .and_then(|s| s.to_str())
3675 .unwrap_or("<unknown>");
3676 if !runtime_crates.contains(&crate_name) {
3677 continue;
3678 }
3679
3680 let src_dir = crate_dir.join("src");
3681 if !src_dir.is_dir() {
3682 continue;
3683 }
3684
3685 let mut files = Vec::new();
3686 collect_rs_files(&src_dir, &mut files).expect("collect rs files");
3687
3688 for file in files {
3689 if file
3690 .file_name()
3691 .and_then(|name| name.to_str())
3692 .is_some_and(|name| name.contains("test"))
3693 {
3694 continue;
3695 }
3696
3697 let src = std::fs::read_to_string(&file).expect("read file");
3698 let rel_path = file.strip_prefix(repo_root).unwrap_or(&file);
3699
3700 for (line, pat) in scan_file_outside_cfg_test_items(&src, &forbidden) {
3701 let line_text = src.lines().nth(line - 1).unwrap_or("").trim();
3702 let allowed_detached_root_constructor = rel_path
3703 == Path::new("crates/fsqlite-core/src/connection.rs")
3704 && pat == "Cx::new("
3705 && line_text.contains("Cx::new().with_trace_context(");
3706
3707 if allowed_detached_root_constructor {
3708 continue;
3709 }
3710
3711 violations.push(format!(
3712 "{crate_name}:{path}:{line} uses forbidden `{pat}` outside cfg(test) code: {line_text}",
3713 path = rel_path.display()
3714 ));
3715 }
3716 }
3717 }
3718
3719 assert!(
3720 violations.is_empty(),
3721 "direct `Cx::new()` / `Cx::default()` production-path violations:\n{}",
3722 violations.join("\n")
3723 );
3724 }
3725
3726 #[test]
3727 fn test_ambient_authority_audit_gate() {
3728 let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
3731 let repo_root = manifest_dir
3732 .parent()
3733 .and_then(Path::parent)
3734 .expect("fsqlite-types manifest dir must be crates/<name>");
3735 let crates_dir = repo_root.join("crates");
3736
3737 let always_forbidden = [
3739 "SystemTime::now(",
3740 "Instant::now(",
3741 "thread_rng(",
3742 "getrandom",
3743 "std::net::",
3744 "std::thread::spawn",
3745 "tokio::spawn",
3746 ];
3747
3748 let non_vfs_forbidden = ["std::fs::"];
3750
3751 let exempt_crates = [
3777 "fsqlite-harness",
3778 "fsqlite-cli",
3779 "fsqlite-e2e",
3780 "fsqlite-observability",
3781 "fsqlite-core",
3782 "fsqlite-vdbe",
3783 "fsqlite-mvcc",
3784 "fsqlite-parser",
3785 "fsqlite-planner",
3786 "fsqlite-wal",
3787 "fsqlite-vfs",
3788 "fsqlite-types",
3789 "fsqlite-func",
3790 "fsqlite",
3791 "fsqlite-btree",
3792 "fsqlite-c-api",
3793 "fsqlite-pager",
3794 ];
3795
3796 let mut violations: Vec<String> = Vec::new();
3797 let mut crate_dirs: Vec<PathBuf> = Vec::new();
3798 for entry in std::fs::read_dir(&crates_dir).expect("read crates/ dir") {
3799 let entry = entry.expect("read crates/ entry");
3800 let path = entry.path();
3801 if path.is_dir() {
3802 crate_dirs.push(path);
3803 }
3804 }
3805
3806 for crate_dir in crate_dirs {
3807 let crate_name = crate_dir
3808 .file_name()
3809 .and_then(|s| s.to_str())
3810 .unwrap_or("<unknown>");
3811 if exempt_crates.contains(&crate_name) {
3812 continue;
3813 }
3814 let src_dir = crate_dir.join("src");
3815 if !src_dir.is_dir() {
3816 continue;
3817 }
3818
3819 let mut files = Vec::new();
3820 collect_rs_files(&src_dir, &mut files).expect("collect rs files");
3821
3822 for file in files {
3823 let src = std::fs::read_to_string(&file).expect("read file");
3824 for (line, pat) in scan_file_outside_cfg_test_items(&src, &always_forbidden) {
3825 violations.push(format!(
3826 "{crate_name}:{path}:{line} uses forbidden `{pat}`",
3827 path = file.display()
3828 ));
3829 }
3830
3831 if crate_name != "fsqlite-vfs" {
3832 for (line, pat) in scan_file_outside_cfg_test_items(&src, &non_vfs_forbidden) {
3833 violations.push(format!(
3834 "{crate_name}:{path}:{line} uses forbidden `{pat}` (non-vfs crate)",
3835 path = file.display()
3836 ));
3837 }
3838 }
3839 }
3840 }
3841
3842 assert!(
3843 violations.is_empty(),
3844 "ambient authority violations (outside cfg(test) modules):\n{}",
3845 violations.join("\n")
3846 );
3847 }
3848
3849 const BEAD_ID: &str = "bd-samf";
3854
3855 #[test]
3856 fn test_cancel_state_machine_all_transitions() {
3857 let cx = Cx::<FullCaps>::new();
3859 assert_eq!(
3860 cx.cancel_state(),
3861 CancelState::Created,
3862 "bead_id={BEAD_ID} initial_state"
3863 );
3864
3865 cx.transition_to_running();
3866 assert_eq!(
3867 cx.cancel_state(),
3868 CancelState::Running,
3869 "bead_id={BEAD_ID} after_start"
3870 );
3871
3872 cx.cancel_with_reason(CancelReason::UserInterrupt);
3873 assert_eq!(
3874 cx.cancel_state(),
3875 CancelState::CancelRequested,
3876 "bead_id={BEAD_ID} after_cancel"
3877 );
3878
3879 let err = cx.checkpoint();
3881 assert!(err.is_err(), "bead_id={BEAD_ID} checkpoint_returns_err");
3882 assert_eq!(
3883 cx.cancel_state(),
3884 CancelState::Cancelling,
3885 "bead_id={BEAD_ID} after_checkpoint_observation"
3886 );
3887
3888 cx.transition_to_finalizing();
3889 assert_eq!(
3890 cx.cancel_state(),
3891 CancelState::Finalizing,
3892 "bead_id={BEAD_ID} after_finalize_start"
3893 );
3894
3895 cx.transition_to_completed();
3896 assert_eq!(
3897 cx.cancel_state(),
3898 CancelState::Completed,
3899 "bead_id={BEAD_ID} after_complete"
3900 );
3901 }
3902
3903 #[test]
3904 fn test_cancel_propagates_to_children() {
3905 let parent = Cx::<FullCaps>::new();
3907 parent.transition_to_running();
3908
3909 let child1 = parent.create_child();
3910 child1.transition_to_running();
3911 let child2 = parent.create_child();
3912 child2.transition_to_running();
3913 let child3 = parent.create_child();
3914 child3.transition_to_running();
3915
3916 assert!(!child1.is_cancel_requested());
3917 assert!(!child2.is_cancel_requested());
3918 assert!(!child3.is_cancel_requested());
3919
3920 parent.cancel_with_reason(CancelReason::RegionClose);
3921
3922 assert!(
3924 child1.is_cancel_requested(),
3925 "bead_id={BEAD_ID} child1_cancelled"
3926 );
3927 assert!(
3928 child2.is_cancel_requested(),
3929 "bead_id={BEAD_ID} child2_cancelled"
3930 );
3931 assert!(
3932 child3.is_cancel_requested(),
3933 "bead_id={BEAD_ID} child3_cancelled"
3934 );
3935
3936 assert_eq!(child1.cancel_state(), CancelState::CancelRequested);
3938 assert_eq!(child2.cancel_state(), CancelState::CancelRequested);
3939 assert_eq!(child3.cancel_state(), CancelState::CancelRequested);
3940
3941 assert_eq!(child1.cancel_reason(), Some(CancelReason::RegionClose));
3943 }
3944
3945 #[test]
3946 fn test_dropped_children_are_pruned_from_parent_links() {
3947 let parent = Cx::<FullCaps>::new();
3948
3949 let live_child = parent.create_child();
3950 let dropped_child = parent.create_child();
3951 drop(dropped_child);
3952
3953 parent.cancel_with_reason(CancelReason::RegionClose);
3955
3956 let live_count = {
3957 let children = parent
3958 .inner
3959 .children
3960 .lock()
3961 .unwrap_or_else(std::sync::PoisonError::into_inner);
3962 children.iter().filter_map(Weak::upgrade).count()
3963 };
3964 assert_eq!(live_count, 1, "only the live child should remain linked");
3965 assert!(live_child.is_cancel_requested());
3966 }
3967
3968 #[test]
3969 fn dropped_children_are_pruned_before_registry_growth_without_cancellation() {
3970 let parent = Cx::<FullCaps>::new();
3971 drop(parent.create_child());
3972 let initial_capacity = parent
3973 .inner
3974 .children
3975 .lock()
3976 .unwrap_or_else(std::sync::PoisonError::into_inner)
3977 .capacity();
3978 assert!(initial_capacity > 0);
3979
3980 for _ in 0..4_096 {
3981 drop(parent.create_child());
3982 }
3983
3984 let children = parent
3985 .inner
3986 .children
3987 .lock()
3988 .unwrap_or_else(std::sync::PoisonError::into_inner);
3989 assert_eq!(
3990 children.capacity(),
3991 initial_capacity,
3992 "historical dead children must not grow an uncancelled family registry"
3993 );
3994 assert!(
3995 children.len() <= initial_capacity,
3996 "only the current bounded batch of dead weak links may remain"
3997 );
3998 }
3999
4000 #[test]
4001 fn test_cancel_idempotent_strongest_wins() {
4002 let cx = Cx::<FullCaps>::new();
4004 cx.transition_to_running();
4005
4006 cx.cancel_with_reason(CancelReason::Timeout);
4007 assert_eq!(
4008 cx.cancel_reason(),
4009 Some(CancelReason::Timeout),
4010 "bead_id={BEAD_ID} first_reason"
4011 );
4012
4013 cx.cancel_with_reason(CancelReason::Abort);
4015 assert_eq!(
4016 cx.cancel_reason(),
4017 Some(CancelReason::Abort),
4018 "bead_id={BEAD_ID} upgraded_reason"
4019 );
4020
4021 cx.cancel_with_reason(CancelReason::UserInterrupt);
4023 assert_eq!(
4024 cx.cancel_reason(),
4025 Some(CancelReason::Abort),
4026 "bead_id={BEAD_ID} reason_stays_strongest"
4027 );
4028 }
4029
4030 #[test]
4031 fn test_losers_drain_on_race() {
4032 use std::sync::atomic::AtomicBool;
4035
4036 let loser_cx = Cx::<FullCaps>::new();
4037 loser_cx.transition_to_running();
4038
4039 let obligation_resolved = Arc::new(AtomicBool::new(false));
4041 let ob_clone = Arc::clone(&obligation_resolved);
4042
4043 loser_cx.cancel_with_reason(CancelReason::RegionClose);
4045
4046 assert!(loser_cx.checkpoint().is_err());
4048 assert_eq!(loser_cx.cancel_state(), CancelState::Cancelling);
4049
4050 ob_clone.store(true, Ordering::Release);
4052 loser_cx.transition_to_finalizing();
4053 loser_cx.transition_to_completed();
4054
4055 assert!(
4056 obligation_resolved.load(Ordering::Acquire),
4057 "bead_id={BEAD_ID} loser_obligation_resolved"
4058 );
4059 assert_eq!(
4060 loser_cx.cancel_state(),
4061 CancelState::Completed,
4062 "bead_id={BEAD_ID} loser_drained"
4063 );
4064 }
4065
4066 #[test]
4067 fn test_vdbe_checkpoint_cancel_observed_at_next_opcode() {
4068 let cx = Cx::<FullCaps>::new();
4071 cx.transition_to_running();
4072
4073 let mut last_executed = 0u32;
4074 for opcode in 0..100u32 {
4075 if cx.checkpoint_with(format!("vdbe pc={opcode}")).is_err() {
4077 last_executed = opcode;
4078 break;
4079 }
4080 last_executed = opcode;
4082 if opcode == 50 {
4084 cx.cancel_with_reason(CancelReason::UserInterrupt);
4085 }
4086 }
4087
4088 assert_eq!(
4089 last_executed, 51,
4090 "bead_id={BEAD_ID} cancel_observed_at_opcode_51"
4091 );
4092 }
4093
4094 #[test]
4095 fn test_btree_checkpoint_cancel_within_one_node() {
4096 let cx = Cx::<FullCaps>::new();
4099 cx.transition_to_running();
4100
4101 let nodes = ["root", "internal_l", "internal_r", "leaf_a", "leaf_b"];
4102 let cancel_at = 2; let mut observed_at = None;
4104
4105 for (i, node) in nodes.iter().enumerate() {
4106 if cx.checkpoint_with(format!("btree node={node}")).is_err() {
4108 observed_at = Some(i);
4109 break;
4110 }
4111 if i == cancel_at {
4114 cx.cancel_with_reason(CancelReason::UserInterrupt);
4115 }
4116 }
4117
4118 assert_eq!(
4119 observed_at,
4120 Some(cancel_at + 1),
4121 "bead_id={BEAD_ID} btree_cancel_within_one_node"
4122 );
4123 }
4124
4125 #[test]
4126 fn test_masked_section_defers_cancel() {
4127 let cx = Cx::<FullCaps>::new();
4130 cx.transition_to_running();
4131
4132 cx.cancel_with_reason(CancelReason::UserInterrupt);
4133 assert!(cx.is_cancel_requested());
4134
4135 {
4137 let _guard = cx.masked();
4138 assert_eq!(cx.mask_depth(), 1);
4139
4140 assert!(
4142 cx.checkpoint().is_ok(),
4143 "bead_id={BEAD_ID} checkpoint_ok_while_masked"
4144 );
4145
4146 {
4148 let _inner = cx.masked();
4149 assert_eq!(cx.mask_depth(), 2);
4150 assert!(cx.checkpoint().is_ok());
4151 }
4152 assert_eq!(cx.mask_depth(), 1);
4153 }
4154 assert_eq!(cx.mask_depth(), 0);
4155
4156 assert!(
4158 cx.checkpoint().is_err(),
4159 "bead_id={BEAD_ID} checkpoint_err_after_mask_exit"
4160 );
4161 }
4162
4163 #[test]
4164 #[should_panic(expected = "MAX_MASK_DEPTH")]
4165 #[allow(clippy::collection_is_never_read)]
4166 fn test_max_mask_depth_exceeded_panics() {
4167 let cx = Cx::<FullCaps>::new();
4169 let mut guards = Vec::new();
4170 for _ in 0..MAX_MASK_DEPTH {
4171 guards.push(cx.masked());
4172 }
4173 let _overflow = cx.masked();
4175 }
4176
4177 #[test]
4178 fn test_commit_section_completes_under_cancel() {
4179 let cx = Cx::<FullCaps>::new();
4181 cx.transition_to_running();
4182
4183 let ops_completed = Arc::new(AtomicU32::new(0));
4184 let finalizer_ran = Arc::new(AtomicBool::new(false));
4185
4186 let ops = Arc::clone(&ops_completed);
4187 let fin = Arc::clone(&finalizer_ran);
4188
4189 cx.commit_section(
4190 10,
4191 |ctx| {
4192 assert!(ctx.tick());
4194 ops.fetch_add(1, Ordering::Release);
4195
4196 cx.cancel_with_reason(CancelReason::UserInterrupt);
4198
4199 assert!(ctx.tick());
4201 ops.fetch_add(1, Ordering::Release);
4202 assert!(
4203 cx.checkpoint().is_ok(),
4204 "bead_id={BEAD_ID} masked_during_commit"
4205 );
4206
4207 assert!(ctx.tick());
4209 ops.fetch_add(1, Ordering::Release);
4210 },
4211 move || {
4212 fin.store(true, Ordering::Release);
4213 },
4214 );
4215
4216 assert_eq!(
4217 ops_completed.load(Ordering::Acquire),
4218 3,
4219 "bead_id={BEAD_ID} all_ops_completed"
4220 );
4221 assert!(
4222 finalizer_ran.load(Ordering::Acquire),
4223 "bead_id={BEAD_ID} finalizer_ran"
4224 );
4225
4226 assert!(cx.checkpoint().is_err());
4228 }
4229
4230 #[test]
4231 fn test_commit_section_enforces_poll_quota() {
4232 let cx = Cx::<FullCaps>::new();
4234 cx.transition_to_running();
4235
4236 let ticks_succeeded = Arc::new(AtomicU32::new(0));
4237 let ts = Arc::clone(&ticks_succeeded);
4238
4239 cx.commit_section(
4240 3,
4241 |ctx| {
4242 assert_eq!(ctx.poll_remaining(), 3);
4243 for _ in 0..5 {
4244 if ctx.tick() {
4245 ts.fetch_add(1, Ordering::Release);
4246 }
4247 }
4248 },
4249 || {},
4250 );
4251
4252 assert_eq!(
4253 ticks_succeeded.load(Ordering::Acquire),
4254 3,
4255 "bead_id={BEAD_ID} poll_quota_enforced"
4256 );
4257 }
4258
4259 #[test]
4260 fn test_cancel_unaware_hot_loop_detected() {
4261 let cx = Cx::<FullCaps>::new();
4264 cx.transition_to_running();
4265
4266 let deadline = 100u32;
4269 let mut iterations_without_checkpoint = 0u32;
4270 let mut detected_unaware = false;
4271
4272 cx.cancel_with_reason(CancelReason::UserInterrupt);
4273
4274 for _i in 0..200u32 {
4275 iterations_without_checkpoint += 1;
4276 if iterations_without_checkpoint >= deadline {
4277 detected_unaware = true;
4278 break;
4279 }
4280 }
4282
4283 assert!(
4284 detected_unaware,
4285 "bead_id={BEAD_ID} cancel_unaware_loop_detected"
4286 );
4287
4288 let cx2 = Cx::<FullCaps>::new();
4290 cx2.transition_to_running();
4291 cx2.cancel_with_reason(CancelReason::UserInterrupt);
4292 let mut compliant_iters = 0u32;
4293 for _ in 0..200u32 {
4294 if cx2.checkpoint().is_err() {
4295 break;
4296 }
4297 compliant_iters += 1;
4298 }
4299 assert_eq!(
4300 compliant_iters, 0,
4301 "bead_id={BEAD_ID} compliant_loop_exits_immediately"
4302 );
4303 }
4304
4305 #[test]
4306 fn test_write_coordinator_commit_section() {
4307 let cx = Cx::<FullCaps>::new();
4310 cx.transition_to_running();
4311
4312 let proof_published = Arc::new(AtomicBool::new(false));
4313 let marker_published = Arc::new(AtomicBool::new(false));
4314 let reservation_released = Arc::new(AtomicBool::new(false));
4315
4316 let proof = Arc::clone(&proof_published);
4317 let marker = Arc::clone(&marker_published);
4318 let release = Arc::clone(&reservation_released);
4319
4320 cx.commit_section(
4321 10,
4322 |ctx| {
4323 assert!(ctx.tick());
4325
4326 cx.cancel_with_reason(CancelReason::RegionClose);
4328
4329 assert!(ctx.tick());
4331 proof.store(true, Ordering::Release);
4332 assert!(cx.checkpoint().is_ok());
4334
4335 assert!(ctx.tick());
4337 marker.store(true, Ordering::Release);
4338 },
4339 move || {
4340 release.store(true, Ordering::Release);
4342 },
4343 );
4344
4345 assert!(
4346 proof_published.load(Ordering::Acquire),
4347 "bead_id={BEAD_ID} proof_published"
4348 );
4349 assert!(
4350 marker_published.load(Ordering::Acquire),
4351 "bead_id={BEAD_ID} marker_published"
4352 );
4353 assert!(
4354 reservation_released.load(Ordering::Acquire),
4355 "bead_id={BEAD_ID} reservation_released"
4356 );
4357
4358 assert!(cx.checkpoint().is_err());
4360 }
4361
4362 #[test]
4367 fn test_trace_ids_default_to_zero() {
4368 let cx = Cx::<FullCaps>::new();
4369 assert_eq!(cx.trace_id(), 0);
4370 assert_eq!(cx.decision_id(), 0);
4371 assert_eq!(cx.policy_id(), 0);
4372 }
4373
4374 #[test]
4375 fn test_with_trace_context_sets_all_ids() {
4376 let cx = Cx::<FullCaps>::new().with_trace_context(42, 99, 7);
4377 assert_eq!(cx.trace_id(), 42);
4378 assert_eq!(cx.decision_id(), 99);
4379 assert_eq!(cx.policy_id(), 7);
4380 }
4381
4382 #[test]
4383 fn test_with_decision_id_preserves_other_ids() {
4384 let cx = Cx::<FullCaps>::new()
4385 .with_trace_context(10, 20, 30)
4386 .with_decision_id(55);
4387 assert_eq!(cx.trace_id(), 10);
4388 assert_eq!(cx.decision_id(), 55);
4389 assert_eq!(cx.policy_id(), 30);
4390 }
4391
4392 #[test]
4393 fn test_with_policy_id_preserves_other_ids() {
4394 let cx = Cx::<FullCaps>::new()
4395 .with_trace_context(100, 200, 300)
4396 .with_policy_id(88);
4397 assert_eq!(cx.trace_id(), 100);
4398 assert_eq!(cx.decision_id(), 200);
4399 assert_eq!(cx.policy_id(), 88);
4400 }
4401
4402 #[test]
4403 #[allow(clippy::redundant_clone)]
4404 fn test_clone_propagates_trace_ids() {
4405 let cx = Cx::<FullCaps>::new().with_trace_context(1, 2, 3);
4406 let cloned = cx.clone();
4407 assert_eq!(cloned.trace_id(), 1);
4408 assert_eq!(cloned.decision_id(), 2);
4409 assert_eq!(cloned.policy_id(), 3);
4410 }
4411
4412 #[test]
4413 fn test_restrict_propagates_trace_ids() {
4414 let cx = Cx::<FullCaps>::new();
4415 let compute = cx.restrict::<ComputeCaps>();
4416 assert_eq!(compute.trace_id(), 0);
4417 assert_eq!(compute.decision_id(), 0);
4418 assert_eq!(compute.policy_id(), 0);
4419 }
4420
4421 #[test]
4422 fn test_scope_with_budget_propagates_trace_ids() {
4423 let cx = Cx::<FullCaps>::new().with_trace_context(5, 6, 7);
4424 let scoped = cx.scope_with_budget(Budget::MINIMAL);
4425 assert_eq!(scoped.trace_id(), 5);
4426 assert_eq!(scoped.decision_id(), 6);
4427 assert_eq!(scoped.policy_id(), 7);
4428 assert_eq!(scoped.budget().poll_quota, Budget::MINIMAL.poll_quota);
4430 }
4431
4432 #[test]
4433 fn test_cleanup_scope_propagates_trace_ids() {
4434 let cx = Cx::<FullCaps>::new().with_trace_context(11, 22, 33);
4435 let cleanup = cx.cleanup_scope();
4436 assert_eq!(cleanup.trace_id(), 11);
4437 assert_eq!(cleanup.decision_id(), 22);
4438 assert_eq!(cleanup.policy_id(), 33);
4439 }
4440
4441 #[test]
4442 fn test_create_child_propagates_trace_ids() {
4443 let parent = Cx::<FullCaps>::new().with_trace_context(50, 60, 70);
4444 let child = parent.create_child();
4445 assert_eq!(child.trace_id(), 50);
4446 assert_eq!(child.decision_id(), 60);
4447 assert_eq!(child.policy_id(), 70);
4448 parent.cancel();
4450 assert!(parent.is_cancel_requested());
4451 assert!(child.is_cancel_requested()); }
4453
4454 #[test]
4455 fn test_trace_ids_independent_across_children() {
4456 let parent = Cx::<FullCaps>::new().with_trace_context(1, 2, 3);
4457 let child1 = parent.create_child().with_decision_id(100);
4458 let child2 = parent.create_child().with_decision_id(200);
4459 assert_eq!(child1.trace_id(), 1);
4461 assert_eq!(child2.trace_id(), 1);
4462 assert_eq!(child1.decision_id(), 100);
4463 assert_eq!(child2.decision_id(), 200);
4464 assert_eq!(parent.decision_id(), 2);
4466 }
4467
4468 #[test]
4469 fn test_with_budget_starts_at_zero_trace_ids() {
4470 let cx = Cx::<FullCaps>::with_budget(Budget::MINIMAL);
4471 assert_eq!(cx.trace_id(), 0);
4472 assert_eq!(cx.decision_id(), 0);
4473 assert_eq!(cx.policy_id(), 0);
4474 }
4475 #[test]
4478 fn blocking_io_inline_safe_defaults_false() {
4479 let cx = Cx::new();
4480 assert!(!cx.blocking_io_inline_safe());
4481 }
4482
4483 #[test]
4484 fn blocking_io_inline_safe_shared_through_clone() {
4485 let cx = Cx::new();
4486 let clone = cx.clone();
4487 cx.mark_blocking_io_inline_safe();
4488 assert!(clone.blocking_io_inline_safe());
4489 }
4490
4491 #[test]
4492 fn blocking_io_inline_safe_inherited_by_create_child() {
4493 let cx = Cx::new();
4494 cx.mark_blocking_io_inline_safe();
4495 let child = cx.create_child();
4496 assert!(child.blocking_io_inline_safe());
4497 }
4498
4499 #[test]
4500 fn blocking_io_inline_safe_not_invented_by_child_of_unset_parent() {
4501 let cx = Cx::new();
4502 let child = cx.create_child();
4503 assert!(!child.blocking_io_inline_safe());
4504 child.mark_blocking_io_inline_safe();
4507 assert!(!cx.blocking_io_inline_safe());
4508 }
4509}