1use std::marker::PhantomData;
30use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
31use std::sync::{Arc, Mutex, Weak};
32use std::time::Duration;
33
34#[cfg(feature = "native")]
35use asupersync::types::Time as NativeTime;
36#[cfg(feature = "native")]
37use asupersync::types::{CancelKind as NativeCancelKind, CancelReason as NativeCancelReason};
38#[cfg(feature = "native")]
39use asupersync::{Budget as NativeBudget, Cx as NativeCx};
40
41#[cfg(not(feature = "native"))]
42mod native_cx_shim {
43 use std::sync::atomic::{AtomicBool, Ordering};
44 use std::sync::{Arc, Mutex};
45
46 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
47 pub enum NativeCancelKind {
48 User,
49 Timeout,
50 Deadline,
51 PollQuota,
52 CostBudget,
53 FailFast,
54 RaceLost,
55 ParentCancelled,
56 Shutdown,
57 LinkedExit,
58 ResourceUnavailable,
59 }
60
61 #[derive(Debug, Clone, PartialEq, Eq)]
62 pub struct NativeCancelReason {
63 pub kind: NativeCancelKind,
64 }
65
66 impl NativeCancelReason {
67 #[must_use]
68 pub const fn timeout() -> Self {
69 Self {
70 kind: NativeCancelKind::Timeout,
71 }
72 }
73
74 #[must_use]
75 pub fn user(_message: impl Into<String>) -> Self {
76 Self {
77 kind: NativeCancelKind::User,
78 }
79 }
80
81 #[must_use]
82 pub const fn parent_cancelled() -> Self {
83 Self {
84 kind: NativeCancelKind::ParentCancelled,
85 }
86 }
87
88 #[must_use]
89 pub const fn resource_unavailable() -> Self {
90 Self {
91 kind: NativeCancelKind::ResourceUnavailable,
92 }
93 }
94 }
95
96 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
97 pub struct NativeCheckpointError;
98
99 #[derive(Debug, Default)]
100 struct NativeCxInner {
101 cancel_requested: AtomicBool,
102 cancel_reason: Mutex<Option<NativeCancelReason>>,
103 }
104
105 #[derive(Debug, Clone, Default)]
106 pub struct NativeCx {
107 inner: Arc<NativeCxInner>,
108 }
109
110 impl NativeCx {
111 #[must_use]
112 pub fn for_testing() -> Self {
113 Self::default()
114 }
115
116 pub fn set_cancel_requested(&self, requested: bool) {
117 self.inner
118 .cancel_requested
119 .store(requested, Ordering::Release);
120 if !requested {
121 *self
122 .inner
123 .cancel_reason
124 .lock()
125 .unwrap_or_else(std::sync::PoisonError::into_inner) = None;
126 }
127 }
128
129 pub fn set_cancel_reason(&self, reason: NativeCancelReason) {
130 *self
131 .inner
132 .cancel_reason
133 .lock()
134 .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(reason);
135 self.inner.cancel_requested.store(true, Ordering::Release);
136 }
137
138 #[must_use]
139 pub fn is_cancel_requested(&self) -> bool {
140 self.inner.cancel_requested.load(Ordering::Acquire)
141 }
142
143 #[must_use]
144 pub fn cancel_reason(&self) -> Option<NativeCancelReason> {
145 self.inner
146 .cancel_reason
147 .lock()
148 .unwrap_or_else(std::sync::PoisonError::into_inner)
149 .clone()
150 }
151
152 pub fn checkpoint(&self) -> std::result::Result<(), NativeCheckpointError> {
153 if self.is_cancel_requested() {
154 Err(NativeCheckpointError)
155 } else {
156 Ok(())
157 }
158 }
159 }
160}
161
162#[cfg(not(feature = "native"))]
163use native_cx_shim::NativeCx;
164
165use crate::eprocess::{EProcessDecision, EProcessOracle, EProcessSnapshot};
166
167pub const SQLITE_INTERRUPT: i32 = 9;
169
170pub const MAX_MASK_DEPTH: u32 = 64;
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
185pub enum CancelState {
186 Created,
187 Running,
188 CancelRequested,
189 Cancelling,
190 Finalizing,
191 Completed,
192}
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
199pub enum CancelReason {
200 Timeout = 0,
201 UserInterrupt = 1,
202 RegionClose = 2,
203 Abort = 3,
204}
205
206pub mod cap {
208 mod sealed {
209 pub trait Sealed {}
210
211 pub struct Bit<const V: bool>;
212
213 pub trait Le {}
214 impl Le for (Bit<false>, Bit<false>) {}
215 impl Le for (Bit<false>, Bit<true>) {}
216 impl Le for (Bit<true>, Bit<true>) {}
217 }
218
219 #[derive(Debug, Clone, Copy, Default)]
221 pub struct CapSet<
222 const SPAWN: bool,
223 const TIME: bool,
224 const RANDOM: bool,
225 const IO: bool,
226 const REMOTE: bool,
227 >;
228
229 impl<
230 const SPAWN: bool,
231 const TIME: bool,
232 const RANDOM: bool,
233 const IO: bool,
234 const REMOTE: bool,
235 > sealed::Sealed for CapSet<SPAWN, TIME, RANDOM, IO, REMOTE>
236 {
237 }
238
239 pub type All = CapSet<true, true, true, true, true>;
241 pub type None = CapSet<false, false, false, false, false>;
243
244 pub trait SubsetOf<Super>: sealed::Sealed {}
249
250 impl<
251 const S_SPAWN: bool,
252 const S_TIME: bool,
253 const S_RANDOM: bool,
254 const S_IO: bool,
255 const S_REMOTE: bool,
256 const P_SPAWN: bool,
257 const P_TIME: bool,
258 const P_RANDOM: bool,
259 const P_IO: bool,
260 const P_REMOTE: bool,
261 > SubsetOf<CapSet<P_SPAWN, P_TIME, P_RANDOM, P_IO, P_REMOTE>>
262 for CapSet<S_SPAWN, S_TIME, S_RANDOM, S_IO, S_REMOTE>
263 where
264 (sealed::Bit<S_SPAWN>, sealed::Bit<P_SPAWN>): sealed::Le,
265 (sealed::Bit<S_TIME>, sealed::Bit<P_TIME>): sealed::Le,
266 (sealed::Bit<S_RANDOM>, sealed::Bit<P_RANDOM>): sealed::Le,
267 (sealed::Bit<S_IO>, sealed::Bit<P_IO>): sealed::Le,
268 (sealed::Bit<S_REMOTE>, sealed::Bit<P_REMOTE>): sealed::Le,
269 {
270 }
271
272 pub trait HasSpawn: sealed::Sealed {}
273 impl<const TIME: bool, const RANDOM: bool, const IO: bool, const REMOTE: bool> HasSpawn
274 for CapSet<true, TIME, RANDOM, IO, REMOTE>
275 {
276 }
277
278 pub trait HasTime: sealed::Sealed {}
279 impl<const SPAWN: bool, const RANDOM: bool, const IO: bool, const REMOTE: bool> HasTime
280 for CapSet<SPAWN, true, RANDOM, IO, REMOTE>
281 {
282 }
283
284 pub trait HasRandom: sealed::Sealed {}
285 impl<const SPAWN: bool, const TIME: bool, const IO: bool, const REMOTE: bool> HasRandom
286 for CapSet<SPAWN, TIME, true, IO, REMOTE>
287 {
288 }
289
290 pub trait HasIo: sealed::Sealed {}
291 impl<const SPAWN: bool, const TIME: bool, const RANDOM: bool, const REMOTE: bool> HasIo
292 for CapSet<SPAWN, TIME, RANDOM, true, REMOTE>
293 {
294 }
295
296 pub trait HasRemote: sealed::Sealed {}
297 impl<const SPAWN: bool, const TIME: bool, const RANDOM: bool, const IO: bool> HasRemote
298 for CapSet<SPAWN, TIME, RANDOM, IO, true>
299 {
300 }
301}
302
303pub type FullCaps = cap::All;
305pub type StorageCaps = cap::CapSet<false, true, false, true, false>;
307pub type ComputeCaps = cap::None;
309
310#[derive(Debug, Clone, Copy, PartialEq, Eq)]
316pub struct Budget {
317 pub deadline: Option<Duration>,
318 pub poll_quota: u32,
319 pub cost_quota: Option<u64>,
320 pub priority: u8,
321}
322
323impl Budget {
324 pub const INFINITE: Self = Self {
326 deadline: None,
327 poll_quota: u32::MAX,
328 cost_quota: None,
329 priority: 0,
330 };
331
332 pub const MINIMAL: Self = Self {
334 deadline: None,
335 poll_quota: 100,
336 cost_quota: None,
337 priority: 0,
338 };
339
340 #[must_use]
341 pub const fn with_deadline(self, deadline: Duration) -> Self {
342 Self {
343 deadline: Some(deadline),
344 ..self
345 }
346 }
347
348 #[must_use]
349 pub const fn with_priority(self, priority: u8) -> Self {
350 Self { priority, ..self }
351 }
352
353 #[must_use]
354 pub const fn with_poll_quota(self, poll_quota: u32) -> Self {
355 Self { poll_quota, ..self }
356 }
357
358 #[must_use]
359 pub const fn with_cost_quota(self, cost_quota: u64) -> Self {
360 Self {
361 cost_quota: Some(cost_quota),
362 ..self
363 }
364 }
365
366 #[must_use]
368 pub fn meet(self, other: Self) -> Self {
369 Self {
370 deadline: match (self.deadline, other.deadline) {
371 (Some(a), Some(b)) => Some(a.min(b)),
372 (Some(a), None) => Some(a),
373 (None, Some(b)) => Some(b),
374 (None, None) => None,
375 },
376 poll_quota: self.poll_quota.min(other.poll_quota),
377 cost_quota: match (self.cost_quota, other.cost_quota) {
378 (Some(a), Some(b)) => Some(a.min(b)),
379 (Some(a), None) => Some(a),
380 (None, Some(b)) => Some(b),
381 (None, None) => None,
382 },
383 priority: self.priority.max(other.priority),
384 }
385 }
386}
387
388#[derive(Debug, Clone, Copy, PartialEq, Eq)]
389pub enum ErrorKind {
390 Cancelled,
391}
392
393#[derive(Debug, Clone, PartialEq, Eq)]
394pub struct Error {
395 kind: ErrorKind,
396}
397
398impl std::fmt::Display for Error {
399 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
400 match self.kind {
401 ErrorKind::Cancelled => write!(f, "operation cancelled"),
402 }
403 }
404}
405
406impl std::error::Error for Error {}
407
408impl Error {
409 #[must_use]
410 pub const fn cancelled() -> Self {
411 Self {
412 kind: ErrorKind::Cancelled,
413 }
414 }
415
416 #[must_use]
417 pub const fn kind(&self) -> ErrorKind {
418 self.kind
419 }
420
421 #[must_use]
422 pub const fn sqlite_error_code(&self) -> i32 {
423 match self.kind {
424 ErrorKind::Cancelled => SQLITE_INTERRUPT,
425 }
426 }
427}
428
429pub type Result<T, E = Error> = std::result::Result<T, E>;
430
431#[derive(Debug)]
432struct CxInner {
433 cancel_requested: AtomicBool,
434 cancel_state: Mutex<CancelState>,
435 cancel_reason: Mutex<Option<CancelReason>>,
436 mask_depth: AtomicU32,
437 children: Mutex<Vec<Weak<Self>>>,
438 last_checkpoint_msg: Mutex<Option<String>>,
439 last_eprocess_decision: Mutex<Option<EProcessDecision>>,
440 eprocess_oracle: std::sync::OnceLock<Arc<EProcessOracle>>,
441 #[cfg(feature = "native")]
442 attached_native_cx: Mutex<Option<NativeCx>>,
443 #[cfg(feature = "native")]
444 fallback_native_cx: std::sync::OnceLock<NativeCx>,
445 unix_millis: AtomicU64,
447}
448
449impl CxInner {
450 fn new() -> Self {
451 Self {
452 cancel_requested: AtomicBool::new(false),
453 cancel_state: Mutex::new(CancelState::Created),
454 cancel_reason: Mutex::new(None),
455 mask_depth: AtomicU32::new(0),
456 children: Mutex::new(Vec::new()),
457 last_checkpoint_msg: Mutex::new(None),
458 last_eprocess_decision: Mutex::new(None),
459 eprocess_oracle: std::sync::OnceLock::new(),
460 #[cfg(feature = "native")]
461 attached_native_cx: Mutex::new(None),
462 #[cfg(feature = "native")]
463 fallback_native_cx: std::sync::OnceLock::new(),
464 unix_millis: AtomicU64::new(0),
465 }
466 }
467}
468
469#[cfg(feature = "native")]
470#[must_use]
471fn local_reason_to_native(reason: CancelReason) -> NativeCancelReason {
472 match reason {
473 CancelReason::Timeout => NativeCancelReason::timeout(),
474 CancelReason::UserInterrupt => NativeCancelReason::user("sqlite interrupt"),
475 CancelReason::RegionClose => NativeCancelReason::parent_cancelled(),
476 CancelReason::Abort => NativeCancelReason::resource_unavailable(),
477 }
478}
479
480#[cfg(feature = "native")]
481#[must_use]
482fn native_reason_to_local(reason: &NativeCancelReason) -> CancelReason {
483 match reason.kind {
484 NativeCancelKind::User => CancelReason::UserInterrupt,
485 NativeCancelKind::Timeout
486 | NativeCancelKind::Deadline
487 | NativeCancelKind::PollQuota
488 | NativeCancelKind::CostBudget => CancelReason::Timeout,
489 NativeCancelKind::FailFast
490 | NativeCancelKind::RaceLost
491 | NativeCancelKind::ParentCancelled
492 | NativeCancelKind::Shutdown
493 | NativeCancelKind::LinkedExit => CancelReason::RegionClose,
494 NativeCancelKind::ResourceUnavailable => CancelReason::Abort,
495 }
496}
497
498#[cfg(feature = "native")]
499fn sync_native_cx_cancel(inner: &CxInner, reason: CancelReason) {
500 let attached_native = inner
501 .attached_native_cx
502 .lock()
503 .unwrap_or_else(std::sync::PoisonError::into_inner)
504 .as_ref()
505 .cloned();
506 if let Some(native) = attached_native {
507 native.set_cancel_reason(local_reason_to_native(reason));
508 }
509 if let Some(native) = inner.fallback_native_cx.get() {
510 native.set_cancel_reason(local_reason_to_native(reason));
511 }
512}
513
514#[cfg(feature = "native")]
515#[must_use]
516#[allow(dead_code)]
517fn native_budget_from_local(budget: Budget) -> NativeBudget {
518 let mut native_budget = NativeBudget::new()
519 .with_poll_quota(budget.poll_quota)
520 .with_priority(budget.priority);
521 if let Some(cost_quota) = budget.cost_quota {
522 native_budget = native_budget.with_cost_quota(cost_quota);
523 }
524 if let Some(deadline) = budget.deadline {
525 native_budget = native_budget.with_deadline(local_deadline_to_native_time(deadline));
526 }
527 native_budget
528}
529
530#[cfg(feature = "native")]
531#[must_use]
532#[allow(dead_code)]
533fn wall_clock_now_since_epoch() -> Duration {
534 std::time::SystemTime::now()
535 .duration_since(std::time::UNIX_EPOCH)
536 .unwrap_or(Duration::ZERO)
537}
538
539#[cfg(feature = "native")]
540#[must_use]
541#[allow(dead_code)]
542fn local_deadline_to_native_time(deadline: Duration) -> NativeTime {
543 let absolute_deadline = wall_clock_now_since_epoch()
544 .checked_add(deadline)
545 .unwrap_or(Duration::MAX);
546 let nanos = u64::try_from(absolute_deadline.as_nanos()).unwrap_or(u64::MAX);
547 NativeTime::from_nanos(nanos)
548}
549
550fn propagate_cancel(inner: &CxInner, reason: CancelReason) {
555 inner.cancel_requested.store(true, Ordering::Release);
557
558 {
560 let mut r = inner
561 .cancel_reason
562 .lock()
563 .unwrap_or_else(std::sync::PoisonError::into_inner);
564 match *r {
565 Some(existing) if existing >= reason => {}
566 _ => *r = Some(reason),
567 }
568 }
569
570 {
572 let mut state = inner
573 .cancel_state
574 .lock()
575 .unwrap_or_else(std::sync::PoisonError::into_inner);
576 if matches!(*state, CancelState::Created | CancelState::Running) {
577 *state = CancelState::CancelRequested;
578 }
579 }
580
581 #[cfg(feature = "native")]
584 sync_native_cx_cancel(inner, reason);
585
586 let children: Vec<Arc<CxInner>> = {
588 let mut guard = inner
589 .children
590 .lock()
591 .unwrap_or_else(std::sync::PoisonError::into_inner);
592 guard.retain(|child| child.strong_count() > 0);
593 guard.iter().filter_map(Weak::upgrade).collect()
594 };
595 for child in &children {
596 propagate_cancel(child, reason);
597 }
598}
599
600#[derive(Debug)]
606pub struct Cx<Caps: cap::SubsetOf<cap::All> = FullCaps> {
607 inner: Arc<CxInner>,
608 budget: Budget,
609 trace_id: u64,
610 decision_id: u64,
611 policy_id: u64,
612 _caps: PhantomData<fn() -> Caps>,
614}
615
616impl<Caps: cap::SubsetOf<cap::All>> Clone for Cx<Caps> {
617 fn clone(&self) -> Self {
618 Self {
619 inner: Arc::clone(&self.inner),
620 budget: self.budget,
621 trace_id: self.trace_id,
622 decision_id: self.decision_id,
623 policy_id: self.policy_id,
624 _caps: PhantomData,
625 }
626 }
627}
628
629impl Default for Cx<FullCaps> {
630 fn default() -> Self {
631 Self::new()
632 }
633}
634
635impl Cx<FullCaps> {
636 #[must_use]
637 pub fn new() -> Self {
638 Self::with_budget(Budget::INFINITE)
639 }
640}
641
642impl<Caps: cap::SubsetOf<cap::All>> Cx<Caps> {
643 #[cfg(all(feature = "native", test))]
644 #[must_use]
645 #[allow(dead_code)]
646 fn effective_native_cx(&self) -> NativeCx {
647 let attached_native = self
648 .inner
649 .attached_native_cx
650 .lock()
651 .unwrap_or_else(std::sync::PoisonError::into_inner)
652 .as_ref()
653 .cloned();
654 if let Some(native) = attached_native {
655 return native;
656 }
657
658 self.inner
659 .fallback_native_cx
660 .get_or_init(|| {
661 let native =
662 NativeCx::for_request_with_budget(native_budget_from_local(self.budget));
663 if let Some(reason) = self.cancel_reason() {
664 native.set_cancel_reason(local_reason_to_native(reason));
665 } else if self.is_cancel_requested() {
666 native.set_cancel_requested(true);
667 }
668 native
669 })
670 .clone()
671 }
672
673 #[cfg(feature = "native")]
674 #[must_use]
675 fn native_cx_for_checkpoint(&self) -> Option<NativeCx> {
676 let attached_native = self
677 .inner
678 .attached_native_cx
679 .lock()
680 .unwrap_or_else(std::sync::PoisonError::into_inner)
681 .as_ref()
682 .cloned();
683 attached_native.or_else(|| self.inner.fallback_native_cx.get().cloned())
684 }
685
686 #[must_use]
687 pub fn with_budget(budget: Budget) -> Self {
688 Self {
689 inner: Arc::new(CxInner::new()),
690 budget,
691 trace_id: 0,
692 decision_id: 0,
693 policy_id: 0,
694 _caps: PhantomData,
695 }
696 }
697
698 #[must_use]
699 pub fn budget(&self) -> Budget {
700 self.budget
701 }
702
703 #[must_use]
709 pub fn trace_id(&self) -> u64 {
710 self.trace_id
711 }
712
713 #[must_use]
715 pub fn decision_id(&self) -> u64 {
716 self.decision_id
717 }
718
719 #[must_use]
721 pub fn policy_id(&self) -> u64 {
722 self.policy_id
723 }
724
725 #[must_use]
729 pub fn with_trace_context(mut self, trace_id: u64, decision_id: u64, policy_id: u64) -> Self {
730 self.trace_id = trace_id;
731 self.decision_id = decision_id;
732 self.policy_id = policy_id;
733 self
734 }
735
736 #[must_use]
740 pub fn with_decision_id(mut self, decision_id: u64) -> Self {
741 self.decision_id = decision_id;
742 self
743 }
744
745 #[must_use]
747 pub fn with_policy_id(mut self, policy_id: u64) -> Self {
748 self.policy_id = policy_id;
749 self
750 }
751
752 #[must_use]
758 pub fn scope_with_budget(&self, child: Budget) -> Self {
759 Self {
760 inner: Arc::clone(&self.inner),
761 budget: self.budget.meet(child),
762 trace_id: self.trace_id,
763 decision_id: self.decision_id,
764 policy_id: self.policy_id,
765 _caps: PhantomData,
766 }
767 }
768
769 #[must_use]
771 pub fn cleanup_scope(&self) -> Self {
772 self.scope_with_budget(Budget::MINIMAL)
773 }
774
775 #[must_use]
779 pub fn restrict<NewCaps>(&self) -> Cx<NewCaps>
780 where
781 NewCaps: cap::SubsetOf<cap::All> + cap::SubsetOf<Caps>,
782 {
783 self.retype()
784 }
785
786 #[must_use]
788 fn retype<NewCaps>(&self) -> Cx<NewCaps>
789 where
790 NewCaps: cap::SubsetOf<cap::All>,
791 {
792 Cx {
793 inner: Arc::clone(&self.inner),
794 budget: self.budget,
795 trace_id: self.trace_id,
796 decision_id: self.decision_id,
797 policy_id: self.policy_id,
798 _caps: PhantomData,
799 }
800 }
801
802 #[must_use]
807 pub fn is_cancel_requested(&self) -> bool {
808 self.inner.cancel_requested.load(Ordering::Acquire)
809 }
810
811 pub fn cancel(&self) {
815 self.cancel_with_reason(CancelReason::UserInterrupt);
816 }
817
818 pub fn cancel_with_reason(&self, reason: CancelReason) {
825 propagate_cancel(&self.inner, reason);
826 }
827
828 #[must_use]
830 pub fn cancel_state(&self) -> CancelState {
831 *self
832 .inner
833 .cancel_state
834 .lock()
835 .unwrap_or_else(std::sync::PoisonError::into_inner)
836 }
837
838 #[must_use]
840 pub fn cancel_reason(&self) -> Option<CancelReason> {
841 *self
842 .inner
843 .cancel_reason
844 .lock()
845 .unwrap_or_else(std::sync::PoisonError::into_inner)
846 }
847
848 pub fn transition_to_running(&self) {
850 let mut state = self
851 .inner
852 .cancel_state
853 .lock()
854 .unwrap_or_else(std::sync::PoisonError::into_inner);
855 if *state == CancelState::Created {
856 *state = CancelState::Running;
857 }
858 }
859
860 pub fn transition_to_finalizing(&self) {
862 let mut state = self
863 .inner
864 .cancel_state
865 .lock()
866 .unwrap_or_else(std::sync::PoisonError::into_inner);
867 if *state == CancelState::Cancelling {
868 *state = CancelState::Finalizing;
869 }
870 }
871
872 pub fn transition_to_completed(&self) {
874 let mut state = self
875 .inner
876 .cancel_state
877 .lock()
878 .unwrap_or_else(std::sync::PoisonError::into_inner);
879 if matches!(*state, CancelState::Finalizing | CancelState::Running) {
880 *state = CancelState::Completed;
881 }
882 }
883
884 pub fn set_eprocess_oracle(&self, oracle: Arc<EProcessOracle>) {
886 let _ = self.inner.eprocess_oracle.set(oracle);
887 }
888
889 pub fn clear_eprocess_oracle(&self) {
891 }
894
895 #[cfg(feature = "native")]
897 pub fn set_native_cx(&self, native_cx: NativeCx) {
898 if let Some(reason) = self.cancel_reason() {
899 native_cx.set_cancel_reason(local_reason_to_native(reason));
900 } else if self.is_cancel_requested() {
901 native_cx.set_cancel_requested(true);
902 }
903 *self
904 .inner
905 .attached_native_cx
906 .lock()
907 .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(native_cx);
908 }
909
910 #[cfg(not(feature = "native"))]
912 pub fn set_native_cx<T>(&self, _native_cx: T) {}
913
914 #[cfg(feature = "native")]
916 #[must_use]
917 pub fn attached_native_cx(&self) -> Option<NativeCx> {
918 self.inner
919 .attached_native_cx
920 .lock()
921 .unwrap_or_else(std::sync::PoisonError::into_inner)
922 .clone()
923 }
924
925 #[cfg(not(feature = "native"))]
927 #[must_use]
928 pub fn attached_native_cx(&self) -> Option<NativeCx> {
929 None
930 }
931
932 #[cfg(feature = "native")]
934 pub fn clear_native_cx(&self) {
935 *self
936 .inner
937 .attached_native_cx
938 .lock()
939 .unwrap_or_else(std::sync::PoisonError::into_inner) = None;
940 }
941
942 #[cfg(not(feature = "native"))]
944 pub fn clear_native_cx(&self) {}
945
946 #[must_use]
947 fn maybe_cancel_via_eprocess(&self) -> bool {
948 let Some(oracle) = self.inner.eprocess_oracle.get() else {
949 return false;
950 };
951 let decision = oracle.decision(self.budget.priority);
952 self.record_eprocess_decision(decision.clone());
953 tracing::debug!(
954 target: "fsqlite::cx",
955 event = "eprocess_checkpoint",
956 trace_id = self.trace_id,
957 decision_id = self.decision_id,
958 policy_id = self.policy_id,
959 priority = decision.priority,
960 evalue = decision.snapshot.evalue,
961 threshold = decision.snapshot.rejection_threshold,
962 observations = decision.snapshot.observations,
963 priority_threshold = decision.snapshot.priority_threshold,
964 should_shed = decision.should_shed,
965 signal = ?decision.snapshot.last_signal
966 );
967 if decision.should_shed {
968 tracing::info!(
969 target: "fsqlite::cx",
970 event = "eprocess_shedding_triggered",
971 trace_id = self.trace_id,
972 decision_id = self.decision_id,
973 policy_id = self.policy_id,
974 priority = decision.priority,
975 evalue = decision.snapshot.evalue,
976 threshold = decision.snapshot.rejection_threshold,
977 signal = ?decision.snapshot.last_signal
978 );
979 self.cancel_with_reason(CancelReason::Abort);
980 return true;
981 }
982 false
983 }
984
985 #[cfg(feature = "native")]
986 #[must_use]
987 fn maybe_cancel_via_native_cx(&self, masked: bool) -> bool {
988 let Some(native) = self.native_cx_for_checkpoint() else {
989 return false;
990 };
991
992 if masked {
993 if native.is_cancel_requested() {
994 let reason = native
995 .cancel_reason()
996 .as_ref()
997 .map_or(CancelReason::Timeout, native_reason_to_local);
998 self.cancel_with_reason(reason);
999 return true;
1000 }
1001 return false;
1002 }
1003
1004 if native.checkpoint().is_err() {
1005 let reason = native
1006 .cancel_reason()
1007 .as_ref()
1008 .map_or(CancelReason::Timeout, native_reason_to_local);
1009 self.cancel_with_reason(reason);
1010 return true;
1011 }
1012 false
1013 }
1014
1015 pub fn checkpoint(&self) -> Result<()> {
1035 let cancel_requested = self.inner.cancel_requested.load(Ordering::Acquire);
1036 if !cancel_requested {
1037 if !self.maybe_cancel_via_eprocess() {
1040 #[cfg(feature = "native")]
1041 {
1042 let masked = self.inner.mask_depth.load(Ordering::Acquire) > 0;
1043 if !self.maybe_cancel_via_native_cx(masked) {
1044 return Ok(());
1045 }
1046 }
1047 #[cfg(not(feature = "native"))]
1048 {
1049 return Ok(());
1050 }
1051 }
1052 }
1053
1054 let masked = self.inner.mask_depth.load(Ordering::Acquire) > 0;
1057 if masked {
1058 return Ok(());
1059 }
1060
1061 {
1063 let mut state = self
1064 .inner
1065 .cancel_state
1066 .lock()
1067 .unwrap_or_else(std::sync::PoisonError::into_inner);
1068 if *state == CancelState::CancelRequested {
1069 *state = CancelState::Cancelling;
1070 }
1071 }
1072 Err(Error::cancelled())
1073 }
1074
1075 pub fn checkpoint_with(&self, msg: impl Into<String>) -> Result<()> {
1077 {
1078 let mut guard = self
1079 .inner
1080 .last_checkpoint_msg
1081 .lock()
1082 .unwrap_or_else(std::sync::PoisonError::into_inner);
1083 *guard = Some(msg.into());
1084 }
1085 self.checkpoint()
1086 }
1087
1088 #[must_use]
1089 pub fn last_checkpoint_message(&self) -> Option<String> {
1090 self.inner
1091 .last_checkpoint_msg
1092 .lock()
1093 .unwrap_or_else(std::sync::PoisonError::into_inner)
1094 .clone()
1095 }
1096
1097 #[must_use]
1099 pub fn last_eprocess_decision(&self) -> Option<EProcessDecision> {
1100 self.inner
1101 .last_eprocess_decision
1102 .lock()
1103 .unwrap_or_else(std::sync::PoisonError::into_inner)
1104 .clone()
1105 }
1106
1107 #[must_use]
1109 pub fn last_eprocess_snapshot(&self) -> Option<EProcessSnapshot> {
1110 self.last_eprocess_decision()
1111 .map(|decision| decision.snapshot)
1112 }
1113
1114 fn record_eprocess_decision(&self, decision: EProcessDecision) {
1115 *self
1116 .inner
1117 .last_eprocess_decision
1118 .lock()
1119 .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(decision);
1120 }
1121
1122 #[must_use]
1135 pub fn masked(&self) -> MaskGuard<'_> {
1136 let prev = self.inner.mask_depth.fetch_add(1, Ordering::AcqRel);
1137 if prev >= MAX_MASK_DEPTH {
1138 self.inner.mask_depth.fetch_sub(1, Ordering::Release);
1139 assert!(
1140 prev < MAX_MASK_DEPTH,
1141 "MAX_MASK_DEPTH ({MAX_MASK_DEPTH}) exceeded: mask nesting depth would be {}",
1142 prev + 1
1143 );
1144 }
1145 MaskGuard { inner: &self.inner }
1146 }
1147
1148 #[must_use]
1150 pub fn mask_depth(&self) -> u32 {
1151 self.inner.mask_depth.load(Ordering::Acquire)
1152 }
1153
1154 pub fn commit_section<R>(
1163 &self,
1164 poll_quota: u32,
1165 body: impl FnOnce(&CommitCtx) -> R,
1166 finalizer: impl FnOnce(),
1167 ) -> R {
1168 struct FinGuard<G: FnOnce()>(Option<G>);
1169 impl<G: FnOnce()> Drop for FinGuard<G> {
1170 fn drop(&mut self) {
1171 if let Some(f) = self.0.take() {
1172 f();
1173 }
1174 }
1175 }
1176
1177 let _mask = self.masked();
1178 let _fin = FinGuard(Some(finalizer));
1179 let ctx = CommitCtx::new(poll_quota);
1180 body(&ctx)
1181 }
1182
1183 #[must_use]
1191 pub fn create_child(&self) -> Self {
1192 let mut child = Self::with_budget(self.budget);
1193 child.trace_id = self.trace_id;
1194 child.decision_id = self.decision_id;
1195 child.policy_id = self.policy_id;
1196 if let Some(oracle) = self.inner.eprocess_oracle.get().cloned() {
1197 child.set_eprocess_oracle(oracle);
1198 }
1199 #[cfg(feature = "native")]
1200 if let Some(native_cx) = self.attached_native_cx() {
1201 child.set_native_cx(native_cx);
1202 }
1203 {
1204 let mut children = self
1205 .inner
1206 .children
1207 .lock()
1208 .unwrap_or_else(std::sync::PoisonError::into_inner);
1209 children.push(Arc::downgrade(&child.inner));
1210 }
1211 if let Some(reason) = self.cancel_reason() {
1212 child.cancel_with_reason(reason);
1213 } else if self.is_cancel_requested() {
1214 child.cancel();
1215 }
1216 child
1217 }
1218
1219 pub fn set_unix_millis_for_testing(&self, millis: u64)
1221 where
1222 Caps: cap::HasTime,
1223 {
1224 self.inner.unix_millis.store(millis, Ordering::Release);
1225 }
1226
1227 #[must_use]
1229 pub fn current_time_julian_day(&self) -> f64
1230 where
1231 Caps: cap::HasTime,
1232 {
1233 let millis = self.inner.unix_millis.load(Ordering::Acquire);
1234 #[allow(clippy::cast_precision_loss)]
1235 let secs = (millis as f64) / 1000.0;
1236 2_440_587.5 + (secs / 86_400.0)
1238 }
1239}
1240
1241#[derive(Debug)]
1249pub struct MaskGuard<'a> {
1250 inner: &'a CxInner,
1251}
1252
1253impl Drop for MaskGuard<'_> {
1254 fn drop(&mut self) {
1255 self.inner.mask_depth.fetch_sub(1, Ordering::Release);
1256 }
1257}
1258
1259#[derive(Debug)]
1267pub struct CommitCtx {
1268 poll_remaining: AtomicU32,
1269}
1270
1271impl CommitCtx {
1272 fn new(poll_quota: u32) -> Self {
1273 Self {
1274 poll_remaining: AtomicU32::new(poll_quota),
1275 }
1276 }
1277
1278 #[must_use]
1280 pub fn poll_remaining(&self) -> u32 {
1281 self.poll_remaining.load(Ordering::Acquire)
1282 }
1283
1284 pub fn tick(&self) -> bool {
1286 let prev = self.poll_remaining.load(Ordering::Acquire);
1287 if prev == 0 {
1288 return false;
1289 }
1290 self.poll_remaining.fetch_sub(1, Ordering::AcqRel);
1291 true
1292 }
1293}
1294
1295#[cfg(test)]
1296mod tests {
1297 use super::*;
1298 use crate::eprocess::{EProcessConfig, EProcessSignal};
1299 use std::path::{Path, PathBuf};
1300 use std::sync::{Arc, Weak};
1301
1302 #[test]
1303 fn test_cx_checkpoint_observes_cancellation() {
1304 let cx = Cx::new();
1305 assert!(cx.checkpoint().is_ok());
1306 cx.cancel();
1307 let err = cx.checkpoint().unwrap_err();
1308 assert_eq!(err.kind(), ErrorKind::Cancelled);
1309 assert_eq!(err.sqlite_error_code(), SQLITE_INTERRUPT);
1310 }
1311
1312 #[test]
1313 fn test_cx_capability_narrowing_compiles() {
1314 let cx = Cx::<FullCaps>::new();
1315 let _compute = cx.restrict::<ComputeCaps>();
1316 let _storage = cx.restrict::<StorageCaps>();
1317 }
1318
1319 #[test]
1320 fn test_cx_budget_meet_tightens() {
1321 let parent = Budget::INFINITE.with_deadline(Duration::from_millis(100));
1322 let child = Budget::INFINITE.with_deadline(Duration::from_millis(200));
1323 let effective = parent.meet(child);
1324 assert_eq!(effective.deadline, Some(Duration::from_millis(100)));
1325 }
1326
1327 #[test]
1328 fn test_cx_budget_priority_join() {
1329 let parent = Budget::INFINITE.with_priority(2);
1330 let child = Budget::INFINITE.with_priority(5);
1331 let effective = parent.meet(child);
1332 assert_eq!(effective.priority, 5);
1333 }
1334
1335 #[test]
1336 fn test_cx_scope_with_budget_cannot_loosen() {
1337 let cx =
1338 Cx::<FullCaps>::with_budget(Budget::INFINITE.with_deadline(Duration::from_millis(50)));
1339 let child = Budget::INFINITE.with_deadline(Duration::from_millis(100));
1340 let scoped = cx.scope_with_budget(child);
1341 assert_eq!(scoped.budget().deadline, Some(Duration::from_millis(50)));
1342 }
1343
1344 #[test]
1345 fn test_cx_checkpoint_with_message_records_message() {
1346 let cx = Cx::new();
1347 assert!(cx.checkpoint_with("vdbe pc=5").is_ok());
1348 assert_eq!(cx.last_checkpoint_message().as_deref(), Some("vdbe pc=5"));
1349 }
1350
1351 #[test]
1352 fn test_cx_cleanup_uses_minimal_budget() {
1353 let cx = Cx::<FullCaps>::with_budget(Budget::INFINITE.with_poll_quota(10_000));
1354 let cleanup = cx.cleanup_scope();
1355 assert_eq!(cleanup.budget(), Budget::MINIMAL);
1356 }
1357
1358 #[test]
1359 fn test_cx_restrict_storage_to_compute() {
1360 let cx = Cx::<FullCaps>::new();
1361 let storage = cx.restrict::<StorageCaps>();
1362 let _compute = storage.restrict::<ComputeCaps>();
1363 }
1364
1365 #[test]
1366 fn test_cx_restrict_is_zero_cost() {
1367 assert_eq!(
1370 std::mem::size_of::<Cx<FullCaps>>(),
1371 std::mem::size_of::<Cx<ComputeCaps>>()
1372 );
1373 }
1374
1375 #[test]
1376 fn test_budget_mixed_lattice() {
1377 let a = Budget {
1378 deadline: Some(Duration::from_millis(100)),
1379 poll_quota: 500,
1380 cost_quota: Some(1000),
1381 priority: 2,
1382 };
1383 let b = Budget {
1384 deadline: Some(Duration::from_millis(200)),
1385 poll_quota: 300,
1386 cost_quota: Some(2000),
1387 priority: 5,
1388 };
1389 let m = a.meet(b);
1390 assert_eq!(m.deadline, Some(Duration::from_millis(100)));
1392 assert_eq!(m.poll_quota, 300);
1393 assert_eq!(m.cost_quota, Some(1000));
1394 assert_eq!(m.priority, 5);
1396 }
1397
1398 #[test]
1399 fn test_budget_meet_commutative() {
1400 let a = Budget {
1401 deadline: Some(Duration::from_millis(50)),
1402 poll_quota: 400,
1403 cost_quota: Some(800),
1404 priority: 3,
1405 };
1406 let b = Budget {
1407 deadline: Some(Duration::from_millis(150)),
1408 poll_quota: 200,
1409 cost_quota: None,
1410 priority: 7,
1411 };
1412 assert_eq!(a.meet(b), b.meet(a));
1413 }
1414
1415 #[test]
1416 fn test_budget_meet_associative() {
1417 let a = Budget::INFINITE
1418 .with_deadline(Duration::from_millis(50))
1419 .with_poll_quota(100)
1420 .with_priority(1);
1421 let b = Budget::INFINITE
1422 .with_deadline(Duration::from_millis(150))
1423 .with_poll_quota(200)
1424 .with_priority(5);
1425 let c = Budget::INFINITE
1426 .with_deadline(Duration::from_millis(75))
1427 .with_poll_quota(50)
1428 .with_priority(3);
1429 assert_eq!(a.meet(b).meet(c), a.meet(b.meet(c)));
1430 }
1431
1432 #[test]
1433 fn test_budget_minimal_is_stricter_than_normal() {
1434 let normal = Budget::INFINITE.with_poll_quota(10_000);
1435 let effective = normal.meet(Budget::MINIMAL);
1436 assert_eq!(effective.poll_quota, Budget::MINIMAL.poll_quota);
1437 }
1438
1439 #[test]
1440 fn test_cx_cancel_shared_across_clones() {
1441 let cx1 = Cx::<FullCaps>::new();
1442 let cx2 = cx1.clone();
1443 assert!(!cx2.is_cancel_requested());
1444 cx1.cancel();
1445 assert!(cx2.is_cancel_requested());
1446 assert!(cx2.checkpoint().is_err());
1447 }
1448
1449 #[test]
1450 fn test_cx_cancel_shared_across_restrict() {
1451 let cx = Cx::<FullCaps>::new();
1452 let compute = cx.restrict::<ComputeCaps>();
1453 cx.cancel();
1454 assert!(compute.checkpoint().is_err());
1455 }
1456
1457 #[test]
1458 fn test_cx_current_time_julian_day() {
1459 let cx = Cx::<FullCaps>::new();
1460 cx.set_unix_millis_for_testing(0);
1462 let jd = cx.current_time_julian_day();
1463 assert!((jd - 2_440_587.5).abs() < 1e-10);
1464
1465 cx.set_unix_millis_for_testing(86_400_000);
1467 let jd = cx.current_time_julian_day();
1468 assert!((jd - 2_440_588.5).abs() < 1e-10);
1469 }
1470
1471 #[test]
1472 fn test_capset_is_zero_sized() {
1473 assert_eq!(std::mem::size_of::<cap::All>(), 0);
1474 assert_eq!(std::mem::size_of::<cap::None>(), 0);
1475 assert_eq!(
1476 std::mem::size_of::<cap::CapSet<true, false, true, false, true>>(),
1477 0
1478 );
1479 }
1480
1481 #[test]
1482 fn test_cx_checkpoint_not_cancelled() {
1483 let cx = Cx::new();
1484 assert!(cx.checkpoint().is_ok());
1485 assert!(cx.checkpoint_with("still going").is_ok());
1486 }
1487
1488 #[test]
1489 fn test_cx_checkpoint_maps_to_sqlite_interrupt() {
1490 let cx = Cx::new();
1491 cx.cancel();
1492 let err = cx.checkpoint().unwrap_err();
1493 assert_eq!(err.sqlite_error_code(), SQLITE_INTERRUPT);
1494 }
1495
1496 #[test]
1497 fn test_cx_checkpoint_eprocess_sheds_low_priority_context() {
1498 let cx = Cx::<FullCaps>::with_budget(Budget::INFINITE.with_priority(3));
1499 let oracle = Arc::new(EProcessOracle::new(
1500 EProcessConfig {
1501 p0: 0.1,
1502 lambda: 5.0,
1503 alpha: 0.05,
1504 max_evalue: 1e12,
1505 },
1506 1,
1507 ));
1508 let signal = EProcessSignal::new(1.0, 1.0, 1.0);
1509 oracle.observe_signal(signal);
1510 oracle.observe_signal(signal);
1511 cx.set_eprocess_oracle(oracle);
1512 let err = cx.checkpoint().unwrap_err();
1513 assert_eq!(err.kind(), ErrorKind::Cancelled);
1514 assert_eq!(cx.cancel_reason(), Some(CancelReason::Abort));
1515 let decision = cx
1516 .last_eprocess_decision()
1517 .expect("checkpoint should record an e-process decision");
1518 assert!(decision.should_shed);
1519 assert_eq!(decision.snapshot.last_signal, Some(signal));
1520 }
1521
1522 #[test]
1523 fn test_cx_checkpoint_eprocess_respects_priority_threshold() {
1524 let cx = Cx::<FullCaps>::with_budget(Budget::INFINITE.with_priority(1));
1525 let oracle = Arc::new(EProcessOracle::new(
1526 EProcessConfig {
1527 p0: 0.1,
1528 lambda: 5.0,
1529 alpha: 0.05,
1530 max_evalue: 1e12,
1531 },
1532 1,
1533 ));
1534 let signal = EProcessSignal::new(1.0, 1.0, 1.0);
1535 oracle.observe_signal(signal);
1536 oracle.observe_signal(signal);
1537 cx.set_eprocess_oracle(oracle);
1538 assert!(cx.checkpoint().is_ok());
1539 assert!(!cx.is_cancel_requested());
1540 let decision = cx
1541 .last_eprocess_decision()
1542 .expect("checkpoint should still record non-shedding decisions");
1543 assert!(!decision.should_shed);
1544 assert_eq!(decision.priority, 1);
1545 assert_eq!(decision.snapshot.last_signal, Some(signal));
1546 }
1547
1548 #[test]
1549 fn test_cx_checkpoint_eprocess_preserves_masking_semantics() {
1550 let cx = Cx::<FullCaps>::with_budget(Budget::INFINITE.with_priority(3));
1551 let oracle = Arc::new(EProcessOracle::new(
1552 EProcessConfig {
1553 p0: 0.1,
1554 lambda: 5.0,
1555 alpha: 0.05,
1556 max_evalue: 1e12,
1557 },
1558 1,
1559 ));
1560 let signal = EProcessSignal::new(1.0, 1.0, 1.0);
1561 oracle.observe_signal(signal);
1562 oracle.observe_signal(signal);
1563 cx.set_eprocess_oracle(oracle);
1564 {
1565 let _mask = cx.masked();
1566 assert!(cx.checkpoint().is_ok());
1567 assert!(cx.is_cancel_requested());
1568 assert_eq!(cx.cancel_state(), CancelState::CancelRequested);
1569 assert_eq!(
1570 cx.last_eprocess_snapshot()
1571 .expect("checkpoint should record the masked decision")
1572 .last_signal,
1573 Some(signal)
1574 );
1575 }
1576 let err = cx.checkpoint().unwrap_err();
1577 assert_eq!(err.kind(), ErrorKind::Cancelled);
1578 }
1579
1580 #[test]
1581 fn test_create_child_inherits_eprocess_oracle() {
1582 let parent = Cx::<FullCaps>::with_budget(Budget::INFINITE.with_priority(3));
1583 let oracle = Arc::new(EProcessOracle::new(
1584 EProcessConfig {
1585 p0: 0.1,
1586 lambda: 5.0,
1587 alpha: 0.05,
1588 max_evalue: 1e12,
1589 },
1590 1,
1591 ));
1592 let signal = EProcessSignal::new(1.0, 1.0, 1.0);
1593 oracle.observe_signal(signal);
1594 oracle.observe_signal(signal);
1595 parent.set_eprocess_oracle(oracle);
1596
1597 let child = parent.create_child();
1598 let err = child.checkpoint().unwrap_err();
1599 assert_eq!(err.kind(), ErrorKind::Cancelled);
1600 assert_eq!(child.cancel_reason(), Some(CancelReason::Abort));
1601 assert_eq!(
1602 child
1603 .last_eprocess_snapshot()
1604 .expect("child checkpoint should record inherited oracle decision")
1605 .last_signal,
1606 Some(signal)
1607 );
1608 }
1609
1610 #[test]
1611 fn test_create_child_inherits_preexisting_parent_cancellation() {
1612 let parent = Cx::<FullCaps>::new();
1613 parent.cancel_with_reason(CancelReason::RegionClose);
1614
1615 let child = parent.create_child();
1616 assert_eq!(child.cancel_reason(), Some(CancelReason::RegionClose));
1617 assert_eq!(child.cancel_state(), CancelState::CancelRequested);
1618
1619 let err = child.checkpoint().unwrap_err();
1620 assert_eq!(err.kind(), ErrorKind::Cancelled);
1621 }
1622
1623 #[cfg(feature = "native")]
1624 #[test]
1625 fn test_cx_checkpoint_native_cx_cancellation_maps_reason() {
1626 let cx = Cx::<FullCaps>::new();
1627 let native = NativeCx::for_testing();
1628 cx.set_native_cx(native.clone());
1629 native.set_cancel_reason(NativeCancelReason::timeout());
1630
1631 let err = cx.checkpoint().unwrap_err();
1632 assert_eq!(err.kind(), ErrorKind::Cancelled);
1633 assert_eq!(cx.cancel_reason(), Some(CancelReason::Timeout));
1634 }
1635
1636 #[cfg(feature = "native")]
1637 #[test]
1638 fn test_cx_cancel_reason_propagates_to_native_cx() {
1639 let cx = Cx::<FullCaps>::new();
1640 let native = NativeCx::for_testing();
1641 cx.set_native_cx(native.clone());
1642
1643 cx.cancel_with_reason(CancelReason::RegionClose);
1644 let reason = native
1645 .cancel_reason()
1646 .expect("native cancel reason must be set");
1647 assert_eq!(reason.kind, NativeCancelKind::ParentCancelled);
1648 }
1649
1650 #[cfg(feature = "native")]
1651 #[test]
1652 fn test_cx_checkpoint_native_cx_respects_local_masking() {
1653 let cx = Cx::<FullCaps>::new();
1654 let native = NativeCx::for_testing();
1655 cx.set_native_cx(native.clone());
1656 native.set_cancel_reason(NativeCancelReason::user("cancel"));
1657
1658 {
1659 let _mask = cx.masked();
1660 assert!(cx.checkpoint().is_ok());
1661 assert!(cx.is_cancel_requested());
1662 assert_eq!(cx.cancel_state(), CancelState::CancelRequested);
1663 }
1664
1665 let err = cx.checkpoint().unwrap_err();
1666 assert_eq!(err.kind(), ErrorKind::Cancelled);
1667 }
1668
1669 #[cfg(feature = "native")]
1670 #[test]
1671 fn test_cx_effective_native_cx_uses_fallback_without_marking_explicit_attachment() {
1672 let cx = Cx::<FullCaps>::with_budget(Budget::INFINITE.with_priority(7));
1673
1674 assert!(cx.attached_native_cx().is_none());
1675 let native = cx.effective_native_cx();
1676 assert!(cx.attached_native_cx().is_none());
1677 assert!(native.checkpoint().is_ok());
1678 }
1679
1680 #[cfg(feature = "native")]
1681 #[test]
1682 fn test_cx_checkpoint_without_native_context_does_not_create_fallback() {
1683 let cx = Cx::<FullCaps>::new();
1684
1685 assert!(cx.inner.fallback_native_cx.get().is_none());
1686 assert!(cx.checkpoint().is_ok());
1687 assert!(cx.inner.fallback_native_cx.get().is_none());
1688 }
1689
1690 #[cfg(feature = "native")]
1691 #[test]
1692 fn test_cx_set_native_cx_replaces_fallback_context() {
1693 let cx = Cx::<FullCaps>::new();
1694 let _ = cx.effective_native_cx();
1695
1696 let replacement = NativeCx::for_testing();
1697 cx.set_native_cx(replacement.clone());
1698 replacement.set_cancel_reason(NativeCancelReason::timeout());
1699
1700 let err = cx.checkpoint().unwrap_err();
1701 assert_eq!(err.kind(), ErrorKind::Cancelled);
1702 assert_eq!(cx.cancel_reason(), Some(CancelReason::Timeout));
1703 }
1704
1705 #[cfg(feature = "native")]
1706 #[test]
1707 fn test_create_child_copies_preexisting_cancellation_into_fallback_native_cx() {
1708 let parent = Cx::<FullCaps>::new();
1709 parent.cancel_with_reason(CancelReason::RegionClose);
1710
1711 let child = parent.create_child();
1712 let reason = child
1713 .effective_native_cx()
1714 .cancel_reason()
1715 .expect("fallback native cx should mirror inherited cancellation");
1716 assert_eq!(reason.kind, NativeCancelKind::ParentCancelled);
1717 }
1718
1719 #[cfg(feature = "native")]
1720 #[test]
1721 fn test_create_child_inherits_explicit_native_cx_attachment() {
1722 let parent = Cx::<FullCaps>::new();
1723 let native = NativeCx::for_testing();
1724 parent.set_native_cx(native.clone());
1725
1726 let child = parent.create_child();
1727 assert!(child.attached_native_cx().is_some());
1728
1729 native.set_cancel_reason(NativeCancelReason::timeout());
1730 let err = child
1731 .checkpoint()
1732 .expect_err("child should observe inherited native cancel");
1733 assert_eq!(err.kind(), ErrorKind::Cancelled);
1734 assert_eq!(child.cancel_reason(), Some(CancelReason::Timeout));
1735 }
1736
1737 #[test]
1738 fn test_budget_infinite_is_identity_for_meet() {
1739 let budget = Budget {
1740 deadline: Some(Duration::from_millis(42)),
1741 poll_quota: 500,
1742 cost_quota: Some(1000),
1743 priority: 7,
1744 };
1745 assert_eq!(budget.meet(Budget::INFINITE), budget);
1746 assert_eq!(Budget::INFINITE.meet(budget), budget);
1747 }
1748
1749 #[test]
1750 fn test_budget_none_constraints_propagate() {
1751 let a = Budget {
1752 deadline: None,
1753 poll_quota: u32::MAX,
1754 cost_quota: None,
1755 priority: 0,
1756 };
1757 let b = Budget {
1758 deadline: Some(Duration::from_millis(50)),
1759 poll_quota: 100,
1760 cost_quota: Some(500),
1761 priority: 3,
1762 };
1763 let m = a.meet(b);
1764 assert_eq!(m.deadline, Some(Duration::from_millis(50)));
1765 assert_eq!(m.poll_quota, 100);
1766 assert_eq!(m.cost_quota, Some(500));
1767 assert_eq!(m.priority, 3);
1768 }
1769
1770 #[test]
1771 fn test_cx_scope_budget_chains() {
1772 let cx = Cx::<FullCaps>::with_budget(
1773 Budget::INFINITE
1774 .with_deadline(Duration::from_millis(100))
1775 .with_poll_quota(1000),
1776 );
1777 let s1 = cx.scope_with_budget(Budget::INFINITE.with_deadline(Duration::from_millis(50)));
1779 assert_eq!(s1.budget().deadline, Some(Duration::from_millis(50)));
1780 assert_eq!(s1.budget().poll_quota, 1000);
1781
1782 let s2 = s1.scope_with_budget(Budget::INFINITE.with_poll_quota(200));
1784 assert_eq!(s2.budget().deadline, Some(Duration::from_millis(50)));
1785 assert_eq!(s2.budget().poll_quota, 200);
1786 }
1787
1788 fn collect_rs_files(dir: &Path, out: &mut Vec<PathBuf>) -> std::io::Result<()> {
1789 for entry in std::fs::read_dir(dir)? {
1790 let entry = entry?;
1791 let path = entry.path();
1792 if path.is_dir() {
1793 collect_rs_files(&path, out)?;
1794 } else if path.extension().is_some_and(|ext| ext == "rs") {
1795 out.push(path);
1796 }
1797 }
1798 Ok(())
1799 }
1800
1801 fn scan_file_outside_cfg_test_items(src: &str, patterns: &[&str]) -> Vec<(usize, String)> {
1802 let mut hits = Vec::new();
1803
1804 let mut brace_depth: i32 = 0;
1805 let mut pending_cfg_test = false;
1806 let mut pending_attr_paren_depth: i32 = 0;
1807 let mut skip_until_depth: Option<i32> = None;
1808
1809 for (idx, line) in src.lines().enumerate() {
1810 let trimmed = line.trim_start();
1811 let paren_delta = i32::try_from(line.matches('(').count()).unwrap_or(i32::MAX)
1812 - i32::try_from(line.matches(')').count()).unwrap_or(i32::MAX);
1813
1814 if skip_until_depth.is_none() {
1815 if trimmed.starts_with("#[cfg(test)]") && trimmed.contains('{') {
1817 pending_cfg_test = false;
1818 pending_attr_paren_depth = 0;
1819 skip_until_depth = Some(brace_depth);
1820 } else if trimmed.contains("fn test_") && trimmed.contains('{') {
1821 skip_until_depth = Some(brace_depth);
1822 } else if trimmed.starts_with("#[cfg(test)]") {
1823 pending_cfg_test = true;
1824 pending_attr_paren_depth = 0;
1825 } else if pending_cfg_test {
1826 if trimmed.starts_with("#[") || pending_attr_paren_depth > 0 {
1828 pending_attr_paren_depth =
1829 pending_attr_paren_depth.saturating_add(paren_delta);
1830 } else if trimmed.is_empty() || trimmed.starts_with("//") {
1831 } else if trimmed.contains('{') {
1833 pending_cfg_test = false;
1834 pending_attr_paren_depth = 0;
1835 skip_until_depth = Some(brace_depth);
1836 } else {
1837 pending_cfg_test = false;
1838 pending_attr_paren_depth = 0;
1839 }
1840 } else {
1841 for &pat in patterns {
1842 if line.contains(pat) {
1843 hits.push((idx + 1, pat.to_string()));
1844 }
1845 }
1846 }
1847 }
1848
1849 let opens = i32::try_from(line.matches('{').count()).unwrap_or(i32::MAX);
1851 let closes = i32::try_from(line.matches('}').count()).unwrap_or(i32::MAX);
1852 brace_depth = brace_depth.saturating_add(opens).saturating_sub(closes);
1853
1854 if let Some(until) = skip_until_depth {
1855 if brace_depth <= until {
1856 skip_until_depth = None;
1857 }
1858 }
1859 }
1860
1861 hits
1862 }
1863
1864 #[test]
1865 fn test_scan_file_outside_cfg_test_items_skips_cfg_test_functions_and_modules() {
1866 let src = r"
1867fn production_path() {
1868 let _ = Cx::new();
1869}
1870
1871#[cfg(test)]
1872fn test_only_helper() {
1873 let _ = Cx::new();
1874}
1875
1876#[cfg(test)]
1877mod tests {
1878 fn nested_test_helper() {
1879 let _ = Cx::default();
1880 }
1881}
1882";
1883
1884 let hits = scan_file_outside_cfg_test_items(src, &["Cx::new(", "Cx::default("]);
1885 assert_eq!(hits, vec![(3, "Cx::new(".to_string())]);
1886 }
1887
1888 #[test]
1889 fn test_no_direct_cx_constructors_in_runtime_production_code() {
1890 let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1891 let repo_root = manifest_dir
1892 .parent()
1893 .and_then(Path::parent)
1894 .expect("fsqlite-types manifest dir must be crates/<name>");
1895 let crates_dir = repo_root.join("crates");
1896 let runtime_crates = [
1897 "fsqlite-core",
1898 "fsqlite-vdbe",
1899 "fsqlite-btree",
1900 "fsqlite-pager",
1901 "fsqlite-wal",
1902 "fsqlite-mvcc",
1903 ];
1904 let forbidden = ["Cx::new(", "Cx::default("];
1905
1906 let mut violations: Vec<String> = Vec::new();
1907 let mut crate_dirs: Vec<PathBuf> = Vec::new();
1908 for entry in std::fs::read_dir(&crates_dir).expect("read crates/ dir") {
1909 let entry = entry.expect("read crates/ entry");
1910 let path = entry.path();
1911 if path.is_dir() {
1912 crate_dirs.push(path);
1913 }
1914 }
1915
1916 for crate_dir in crate_dirs {
1917 let crate_name = crate_dir
1918 .file_name()
1919 .and_then(|s| s.to_str())
1920 .unwrap_or("<unknown>");
1921 if !runtime_crates.contains(&crate_name) {
1922 continue;
1923 }
1924
1925 let src_dir = crate_dir.join("src");
1926 if !src_dir.is_dir() {
1927 continue;
1928 }
1929
1930 let mut files = Vec::new();
1931 collect_rs_files(&src_dir, &mut files).expect("collect rs files");
1932
1933 for file in files {
1934 if file
1935 .file_name()
1936 .and_then(|name| name.to_str())
1937 .is_some_and(|name| name.contains("test"))
1938 {
1939 continue;
1940 }
1941
1942 let src = std::fs::read_to_string(&file).expect("read file");
1943 let rel_path = file.strip_prefix(repo_root).unwrap_or(&file);
1944
1945 for (line, pat) in scan_file_outside_cfg_test_items(&src, &forbidden) {
1946 let line_text = src.lines().nth(line - 1).unwrap_or("").trim();
1947 let allowed_detached_root_constructor = rel_path
1948 == Path::new("crates/fsqlite-core/src/connection.rs")
1949 && pat == "Cx::new("
1950 && line_text.contains("Cx::new().with_trace_context(");
1951
1952 if allowed_detached_root_constructor {
1953 continue;
1954 }
1955
1956 violations.push(format!(
1957 "{crate_name}:{path}:{line} uses forbidden `{pat}` outside cfg(test) code: {line_text}",
1958 path = rel_path.display()
1959 ));
1960 }
1961 }
1962 }
1963
1964 assert!(
1965 violations.is_empty(),
1966 "direct `Cx::new()` / `Cx::default()` production-path violations:\n{}",
1967 violations.join("\n")
1968 );
1969 }
1970
1971 #[test]
1972 fn test_ambient_authority_audit_gate() {
1973 let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1976 let repo_root = manifest_dir
1977 .parent()
1978 .and_then(Path::parent)
1979 .expect("fsqlite-types manifest dir must be crates/<name>");
1980 let crates_dir = repo_root.join("crates");
1981
1982 let always_forbidden = [
1984 "SystemTime::now(",
1985 "Instant::now(",
1986 "thread_rng(",
1987 "getrandom",
1988 "std::net::",
1989 "std::thread::spawn",
1990 "tokio::spawn",
1991 ];
1992
1993 let non_vfs_forbidden = ["std::fs::"];
1995
1996 let exempt_crates = [
2023 "fsqlite-harness",
2024 "fsqlite-cli",
2025 "fsqlite-e2e",
2026 "fsqlite-observability",
2027 "fsqlite-core",
2028 "fsqlite-vdbe",
2029 "fsqlite-mvcc",
2030 "fsqlite-parser",
2031 "fsqlite-planner",
2032 "fsqlite-wal",
2033 "fsqlite-vfs",
2034 "fsqlite-types",
2035 "fsqlite-func",
2036 "fsqlite",
2037 "fsqlite-btree",
2038 "fsqlite-c-api",
2039 "fsqlite-pager",
2040 ];
2041
2042 let mut violations: Vec<String> = Vec::new();
2043 let mut crate_dirs: Vec<PathBuf> = Vec::new();
2044 for entry in std::fs::read_dir(&crates_dir).expect("read crates/ dir") {
2045 let entry = entry.expect("read crates/ entry");
2046 let path = entry.path();
2047 if path.is_dir() {
2048 crate_dirs.push(path);
2049 }
2050 }
2051
2052 for crate_dir in crate_dirs {
2053 let crate_name = crate_dir
2054 .file_name()
2055 .and_then(|s| s.to_str())
2056 .unwrap_or("<unknown>");
2057 if exempt_crates.contains(&crate_name) {
2058 continue;
2059 }
2060 let src_dir = crate_dir.join("src");
2061 if !src_dir.is_dir() {
2062 continue;
2063 }
2064
2065 let mut files = Vec::new();
2066 collect_rs_files(&src_dir, &mut files).expect("collect rs files");
2067
2068 for file in files {
2069 let src = std::fs::read_to_string(&file).expect("read file");
2070 for (line, pat) in scan_file_outside_cfg_test_items(&src, &always_forbidden) {
2071 violations.push(format!(
2072 "{crate_name}:{path}:{line} uses forbidden `{pat}`",
2073 path = file.display()
2074 ));
2075 }
2076
2077 if crate_name != "fsqlite-vfs" {
2078 for (line, pat) in scan_file_outside_cfg_test_items(&src, &non_vfs_forbidden) {
2079 violations.push(format!(
2080 "{crate_name}:{path}:{line} uses forbidden `{pat}` (non-vfs crate)",
2081 path = file.display()
2082 ));
2083 }
2084 }
2085 }
2086 }
2087
2088 assert!(
2089 violations.is_empty(),
2090 "ambient authority violations (outside cfg(test) modules):\n{}",
2091 violations.join("\n")
2092 );
2093 }
2094
2095 const BEAD_ID: &str = "bd-samf";
2100
2101 #[test]
2102 fn test_cancel_state_machine_all_transitions() {
2103 let cx = Cx::<FullCaps>::new();
2105 assert_eq!(
2106 cx.cancel_state(),
2107 CancelState::Created,
2108 "bead_id={BEAD_ID} initial_state"
2109 );
2110
2111 cx.transition_to_running();
2112 assert_eq!(
2113 cx.cancel_state(),
2114 CancelState::Running,
2115 "bead_id={BEAD_ID} after_start"
2116 );
2117
2118 cx.cancel_with_reason(CancelReason::UserInterrupt);
2119 assert_eq!(
2120 cx.cancel_state(),
2121 CancelState::CancelRequested,
2122 "bead_id={BEAD_ID} after_cancel"
2123 );
2124
2125 let err = cx.checkpoint();
2127 assert!(err.is_err(), "bead_id={BEAD_ID} checkpoint_returns_err");
2128 assert_eq!(
2129 cx.cancel_state(),
2130 CancelState::Cancelling,
2131 "bead_id={BEAD_ID} after_checkpoint_observation"
2132 );
2133
2134 cx.transition_to_finalizing();
2135 assert_eq!(
2136 cx.cancel_state(),
2137 CancelState::Finalizing,
2138 "bead_id={BEAD_ID} after_finalize_start"
2139 );
2140
2141 cx.transition_to_completed();
2142 assert_eq!(
2143 cx.cancel_state(),
2144 CancelState::Completed,
2145 "bead_id={BEAD_ID} after_complete"
2146 );
2147 }
2148
2149 #[test]
2150 fn test_cancel_propagates_to_children() {
2151 let parent = Cx::<FullCaps>::new();
2153 parent.transition_to_running();
2154
2155 let child1 = parent.create_child();
2156 child1.transition_to_running();
2157 let child2 = parent.create_child();
2158 child2.transition_to_running();
2159 let child3 = parent.create_child();
2160 child3.transition_to_running();
2161
2162 assert!(!child1.is_cancel_requested());
2163 assert!(!child2.is_cancel_requested());
2164 assert!(!child3.is_cancel_requested());
2165
2166 parent.cancel_with_reason(CancelReason::RegionClose);
2167
2168 assert!(
2170 child1.is_cancel_requested(),
2171 "bead_id={BEAD_ID} child1_cancelled"
2172 );
2173 assert!(
2174 child2.is_cancel_requested(),
2175 "bead_id={BEAD_ID} child2_cancelled"
2176 );
2177 assert!(
2178 child3.is_cancel_requested(),
2179 "bead_id={BEAD_ID} child3_cancelled"
2180 );
2181
2182 assert_eq!(child1.cancel_state(), CancelState::CancelRequested);
2184 assert_eq!(child2.cancel_state(), CancelState::CancelRequested);
2185 assert_eq!(child3.cancel_state(), CancelState::CancelRequested);
2186
2187 assert_eq!(child1.cancel_reason(), Some(CancelReason::RegionClose));
2189 }
2190
2191 #[test]
2192 fn test_dropped_children_are_pruned_from_parent_links() {
2193 let parent = Cx::<FullCaps>::new();
2194
2195 let live_child = parent.create_child();
2196 let dropped_child = parent.create_child();
2197 drop(dropped_child);
2198
2199 parent.cancel_with_reason(CancelReason::RegionClose);
2201
2202 let live_count = {
2203 let children = parent
2204 .inner
2205 .children
2206 .lock()
2207 .unwrap_or_else(std::sync::PoisonError::into_inner);
2208 children.iter().filter_map(Weak::upgrade).count()
2209 };
2210 assert_eq!(live_count, 1, "only the live child should remain linked");
2211 assert!(live_child.is_cancel_requested());
2212 }
2213
2214 #[test]
2215 fn test_cancel_idempotent_strongest_wins() {
2216 let cx = Cx::<FullCaps>::new();
2218 cx.transition_to_running();
2219
2220 cx.cancel_with_reason(CancelReason::Timeout);
2221 assert_eq!(
2222 cx.cancel_reason(),
2223 Some(CancelReason::Timeout),
2224 "bead_id={BEAD_ID} first_reason"
2225 );
2226
2227 cx.cancel_with_reason(CancelReason::Abort);
2229 assert_eq!(
2230 cx.cancel_reason(),
2231 Some(CancelReason::Abort),
2232 "bead_id={BEAD_ID} upgraded_reason"
2233 );
2234
2235 cx.cancel_with_reason(CancelReason::UserInterrupt);
2237 assert_eq!(
2238 cx.cancel_reason(),
2239 Some(CancelReason::Abort),
2240 "bead_id={BEAD_ID} reason_stays_strongest"
2241 );
2242 }
2243
2244 #[test]
2245 fn test_losers_drain_on_race() {
2246 use std::sync::atomic::AtomicBool;
2249
2250 let loser_cx = Cx::<FullCaps>::new();
2251 loser_cx.transition_to_running();
2252
2253 let obligation_resolved = Arc::new(AtomicBool::new(false));
2255 let ob_clone = Arc::clone(&obligation_resolved);
2256
2257 loser_cx.cancel_with_reason(CancelReason::RegionClose);
2259
2260 assert!(loser_cx.checkpoint().is_err());
2262 assert_eq!(loser_cx.cancel_state(), CancelState::Cancelling);
2263
2264 ob_clone.store(true, Ordering::Release);
2266 loser_cx.transition_to_finalizing();
2267 loser_cx.transition_to_completed();
2268
2269 assert!(
2270 obligation_resolved.load(Ordering::Acquire),
2271 "bead_id={BEAD_ID} loser_obligation_resolved"
2272 );
2273 assert_eq!(
2274 loser_cx.cancel_state(),
2275 CancelState::Completed,
2276 "bead_id={BEAD_ID} loser_drained"
2277 );
2278 }
2279
2280 #[test]
2281 fn test_vdbe_checkpoint_cancel_observed_at_next_opcode() {
2282 let cx = Cx::<FullCaps>::new();
2285 cx.transition_to_running();
2286
2287 let mut last_executed = 0u32;
2288 for opcode in 0..100u32 {
2289 if cx.checkpoint_with(format!("vdbe pc={opcode}")).is_err() {
2291 last_executed = opcode;
2292 break;
2293 }
2294 last_executed = opcode;
2296 if opcode == 50 {
2298 cx.cancel_with_reason(CancelReason::UserInterrupt);
2299 }
2300 }
2301
2302 assert_eq!(
2303 last_executed, 51,
2304 "bead_id={BEAD_ID} cancel_observed_at_opcode_51"
2305 );
2306 }
2307
2308 #[test]
2309 fn test_btree_checkpoint_cancel_within_one_node() {
2310 let cx = Cx::<FullCaps>::new();
2313 cx.transition_to_running();
2314
2315 let nodes = ["root", "internal_l", "internal_r", "leaf_a", "leaf_b"];
2316 let cancel_at = 2; let mut observed_at = None;
2318
2319 for (i, node) in nodes.iter().enumerate() {
2320 if cx.checkpoint_with(format!("btree node={node}")).is_err() {
2322 observed_at = Some(i);
2323 break;
2324 }
2325 if i == cancel_at {
2328 cx.cancel_with_reason(CancelReason::UserInterrupt);
2329 }
2330 }
2331
2332 assert_eq!(
2333 observed_at,
2334 Some(cancel_at + 1),
2335 "bead_id={BEAD_ID} btree_cancel_within_one_node"
2336 );
2337 }
2338
2339 #[test]
2340 fn test_masked_section_defers_cancel() {
2341 let cx = Cx::<FullCaps>::new();
2344 cx.transition_to_running();
2345
2346 cx.cancel_with_reason(CancelReason::UserInterrupt);
2347 assert!(cx.is_cancel_requested());
2348
2349 {
2351 let _guard = cx.masked();
2352 assert_eq!(cx.mask_depth(), 1);
2353
2354 assert!(
2356 cx.checkpoint().is_ok(),
2357 "bead_id={BEAD_ID} checkpoint_ok_while_masked"
2358 );
2359
2360 {
2362 let _inner = cx.masked();
2363 assert_eq!(cx.mask_depth(), 2);
2364 assert!(cx.checkpoint().is_ok());
2365 }
2366 assert_eq!(cx.mask_depth(), 1);
2367 }
2368 assert_eq!(cx.mask_depth(), 0);
2369
2370 assert!(
2372 cx.checkpoint().is_err(),
2373 "bead_id={BEAD_ID} checkpoint_err_after_mask_exit"
2374 );
2375 }
2376
2377 #[test]
2378 #[should_panic(expected = "MAX_MASK_DEPTH")]
2379 #[allow(clippy::collection_is_never_read)]
2380 fn test_max_mask_depth_exceeded_panics() {
2381 let cx = Cx::<FullCaps>::new();
2383 let mut guards = Vec::new();
2384 for _ in 0..MAX_MASK_DEPTH {
2385 guards.push(cx.masked());
2386 }
2387 let _overflow = cx.masked();
2389 }
2390
2391 #[test]
2392 fn test_commit_section_completes_under_cancel() {
2393 let cx = Cx::<FullCaps>::new();
2395 cx.transition_to_running();
2396
2397 let ops_completed = Arc::new(AtomicU32::new(0));
2398 let finalizer_ran = Arc::new(AtomicBool::new(false));
2399
2400 let ops = Arc::clone(&ops_completed);
2401 let fin = Arc::clone(&finalizer_ran);
2402
2403 cx.commit_section(
2404 10,
2405 |ctx| {
2406 assert!(ctx.tick());
2408 ops.fetch_add(1, Ordering::Release);
2409
2410 cx.cancel_with_reason(CancelReason::UserInterrupt);
2412
2413 assert!(ctx.tick());
2415 ops.fetch_add(1, Ordering::Release);
2416 assert!(
2417 cx.checkpoint().is_ok(),
2418 "bead_id={BEAD_ID} masked_during_commit"
2419 );
2420
2421 assert!(ctx.tick());
2423 ops.fetch_add(1, Ordering::Release);
2424 },
2425 move || {
2426 fin.store(true, Ordering::Release);
2427 },
2428 );
2429
2430 assert_eq!(
2431 ops_completed.load(Ordering::Acquire),
2432 3,
2433 "bead_id={BEAD_ID} all_ops_completed"
2434 );
2435 assert!(
2436 finalizer_ran.load(Ordering::Acquire),
2437 "bead_id={BEAD_ID} finalizer_ran"
2438 );
2439
2440 assert!(cx.checkpoint().is_err());
2442 }
2443
2444 #[test]
2445 fn test_commit_section_enforces_poll_quota() {
2446 let cx = Cx::<FullCaps>::new();
2448 cx.transition_to_running();
2449
2450 let ticks_succeeded = Arc::new(AtomicU32::new(0));
2451 let ts = Arc::clone(&ticks_succeeded);
2452
2453 cx.commit_section(
2454 3,
2455 |ctx| {
2456 assert_eq!(ctx.poll_remaining(), 3);
2457 for _ in 0..5 {
2458 if ctx.tick() {
2459 ts.fetch_add(1, Ordering::Release);
2460 }
2461 }
2462 },
2463 || {},
2464 );
2465
2466 assert_eq!(
2467 ticks_succeeded.load(Ordering::Acquire),
2468 3,
2469 "bead_id={BEAD_ID} poll_quota_enforced"
2470 );
2471 }
2472
2473 #[test]
2474 fn test_cancel_unaware_hot_loop_detected() {
2475 let cx = Cx::<FullCaps>::new();
2478 cx.transition_to_running();
2479
2480 let deadline = 100u32;
2483 let mut iterations_without_checkpoint = 0u32;
2484 let mut detected_unaware = false;
2485
2486 cx.cancel_with_reason(CancelReason::UserInterrupt);
2487
2488 for _i in 0..200u32 {
2489 iterations_without_checkpoint += 1;
2490 if iterations_without_checkpoint >= deadline {
2491 detected_unaware = true;
2492 break;
2493 }
2494 }
2496
2497 assert!(
2498 detected_unaware,
2499 "bead_id={BEAD_ID} cancel_unaware_loop_detected"
2500 );
2501
2502 let cx2 = Cx::<FullCaps>::new();
2504 cx2.transition_to_running();
2505 cx2.cancel_with_reason(CancelReason::UserInterrupt);
2506 let mut compliant_iters = 0u32;
2507 for _ in 0..200u32 {
2508 if cx2.checkpoint().is_err() {
2509 break;
2510 }
2511 compliant_iters += 1;
2512 }
2513 assert_eq!(
2514 compliant_iters, 0,
2515 "bead_id={BEAD_ID} compliant_loop_exits_immediately"
2516 );
2517 }
2518
2519 #[test]
2520 fn test_write_coordinator_commit_section() {
2521 let cx = Cx::<FullCaps>::new();
2524 cx.transition_to_running();
2525
2526 let proof_published = Arc::new(AtomicBool::new(false));
2527 let marker_published = Arc::new(AtomicBool::new(false));
2528 let reservation_released = Arc::new(AtomicBool::new(false));
2529
2530 let proof = Arc::clone(&proof_published);
2531 let marker = Arc::clone(&marker_published);
2532 let release = Arc::clone(&reservation_released);
2533
2534 cx.commit_section(
2535 10,
2536 |ctx| {
2537 assert!(ctx.tick());
2539
2540 cx.cancel_with_reason(CancelReason::RegionClose);
2542
2543 assert!(ctx.tick());
2545 proof.store(true, Ordering::Release);
2546 assert!(cx.checkpoint().is_ok());
2548
2549 assert!(ctx.tick());
2551 marker.store(true, Ordering::Release);
2552 },
2553 move || {
2554 release.store(true, Ordering::Release);
2556 },
2557 );
2558
2559 assert!(
2560 proof_published.load(Ordering::Acquire),
2561 "bead_id={BEAD_ID} proof_published"
2562 );
2563 assert!(
2564 marker_published.load(Ordering::Acquire),
2565 "bead_id={BEAD_ID} marker_published"
2566 );
2567 assert!(
2568 reservation_released.load(Ordering::Acquire),
2569 "bead_id={BEAD_ID} reservation_released"
2570 );
2571
2572 assert!(cx.checkpoint().is_err());
2574 }
2575
2576 #[test]
2581 fn test_trace_ids_default_to_zero() {
2582 let cx = Cx::<FullCaps>::new();
2583 assert_eq!(cx.trace_id(), 0);
2584 assert_eq!(cx.decision_id(), 0);
2585 assert_eq!(cx.policy_id(), 0);
2586 }
2587
2588 #[test]
2589 fn test_with_trace_context_sets_all_ids() {
2590 let cx = Cx::<FullCaps>::new().with_trace_context(42, 99, 7);
2591 assert_eq!(cx.trace_id(), 42);
2592 assert_eq!(cx.decision_id(), 99);
2593 assert_eq!(cx.policy_id(), 7);
2594 }
2595
2596 #[test]
2597 fn test_with_decision_id_preserves_other_ids() {
2598 let cx = Cx::<FullCaps>::new()
2599 .with_trace_context(10, 20, 30)
2600 .with_decision_id(55);
2601 assert_eq!(cx.trace_id(), 10);
2602 assert_eq!(cx.decision_id(), 55);
2603 assert_eq!(cx.policy_id(), 30);
2604 }
2605
2606 #[test]
2607 fn test_with_policy_id_preserves_other_ids() {
2608 let cx = Cx::<FullCaps>::new()
2609 .with_trace_context(100, 200, 300)
2610 .with_policy_id(88);
2611 assert_eq!(cx.trace_id(), 100);
2612 assert_eq!(cx.decision_id(), 200);
2613 assert_eq!(cx.policy_id(), 88);
2614 }
2615
2616 #[test]
2617 #[allow(clippy::redundant_clone)]
2618 fn test_clone_propagates_trace_ids() {
2619 let cx = Cx::<FullCaps>::new().with_trace_context(1, 2, 3);
2620 let cloned = cx.clone();
2621 assert_eq!(cloned.trace_id(), 1);
2622 assert_eq!(cloned.decision_id(), 2);
2623 assert_eq!(cloned.policy_id(), 3);
2624 }
2625
2626 #[test]
2627 fn test_restrict_propagates_trace_ids() {
2628 let cx = Cx::<FullCaps>::new();
2629 let compute = cx.restrict::<ComputeCaps>();
2630 assert_eq!(compute.trace_id(), 0);
2631 assert_eq!(compute.decision_id(), 0);
2632 assert_eq!(compute.policy_id(), 0);
2633 }
2634
2635 #[test]
2636 fn test_scope_with_budget_propagates_trace_ids() {
2637 let cx = Cx::<FullCaps>::new().with_trace_context(5, 6, 7);
2638 let scoped = cx.scope_with_budget(Budget::MINIMAL);
2639 assert_eq!(scoped.trace_id(), 5);
2640 assert_eq!(scoped.decision_id(), 6);
2641 assert_eq!(scoped.policy_id(), 7);
2642 assert_eq!(scoped.budget().poll_quota, Budget::MINIMAL.poll_quota);
2644 }
2645
2646 #[test]
2647 fn test_cleanup_scope_propagates_trace_ids() {
2648 let cx = Cx::<FullCaps>::new().with_trace_context(11, 22, 33);
2649 let cleanup = cx.cleanup_scope();
2650 assert_eq!(cleanup.trace_id(), 11);
2651 assert_eq!(cleanup.decision_id(), 22);
2652 assert_eq!(cleanup.policy_id(), 33);
2653 }
2654
2655 #[test]
2656 fn test_create_child_propagates_trace_ids() {
2657 let parent = Cx::<FullCaps>::new().with_trace_context(50, 60, 70);
2658 let child = parent.create_child();
2659 assert_eq!(child.trace_id(), 50);
2660 assert_eq!(child.decision_id(), 60);
2661 assert_eq!(child.policy_id(), 70);
2662 parent.cancel();
2664 assert!(parent.is_cancel_requested());
2665 assert!(child.is_cancel_requested()); }
2667
2668 #[test]
2669 fn test_trace_ids_independent_across_children() {
2670 let parent = Cx::<FullCaps>::new().with_trace_context(1, 2, 3);
2671 let child1 = parent.create_child().with_decision_id(100);
2672 let child2 = parent.create_child().with_decision_id(200);
2673 assert_eq!(child1.trace_id(), 1);
2675 assert_eq!(child2.trace_id(), 1);
2676 assert_eq!(child1.decision_id(), 100);
2677 assert_eq!(child2.decision_id(), 200);
2678 assert_eq!(parent.decision_id(), 2);
2680 }
2681
2682 #[test]
2683 fn test_with_budget_starts_at_zero_trace_ids() {
2684 let cx = Cx::<FullCaps>::with_budget(Budget::MINIMAL);
2685 assert_eq!(cx.trace_id(), 0);
2686 assert_eq!(cx.decision_id(), 0);
2687 assert_eq!(cx.policy_id(), 0);
2688 }
2689}