1use crate::time::{Duration, Instant};
16use std::collections::{BTreeMap, BTreeSet};
17use std::error::Error;
18use std::fmt::Debug;
19use std::fmt::{self, Display, Formatter};
20use std::future::Future;
21use std::num::NonZeroUsize;
22use std::pin::Pin;
23use std::sync::atomic::{AtomicU64, AtomicU8, Ordering};
24use std::sync::{Arc, Mutex, Weak};
25use std::task::{Context, Poll, Waker};
26
27const NOT_CANCELLED: u8 = 0;
28const CANCELLED: u8 = 1;
29const RESOURCE_EXHAUSTED: u8 = 2;
30
31#[derive(Debug, Clone)]
32pub struct RuntimeCancellationToken {
33 state: Arc<RuntimeCancellationState>,
34}
35
36#[derive(Debug)]
37struct RuntimeCancellationState {
38 cancellation_reason: AtomicU8,
39 parents: [Option<RuntimeCancellationToken>; 2],
40 next_waiter_id: AtomicU64,
41 waiters: Mutex<BTreeMap<u64, Waker>>,
42}
43
44impl Default for RuntimeCancellationToken {
45 fn default() -> Self {
46 Self {
47 state: Arc::new(RuntimeCancellationState {
48 cancellation_reason: AtomicU8::new(NOT_CANCELLED),
49 parents: [None, None],
50 next_waiter_id: AtomicU64::new(1),
51 waiters: Mutex::new(BTreeMap::new()),
52 }),
53 }
54 }
55}
56
57impl RuntimeCancellationToken {
58 pub fn new() -> Self {
59 Self::default()
60 }
61
62 pub fn cancel(&self) -> bool {
63 self.cancel_with_code(CANCELLED)
64 }
65
66 pub fn cancel_for_resource_exhaustion(&self) -> bool {
70 self.cancel_with_code(RESOURCE_EXHAUSTED)
71 }
72
73 fn cancel_with_code(&self, reason: u8) -> bool {
74 if self
75 .state
76 .cancellation_reason
77 .compare_exchange(NOT_CANCELLED, reason, Ordering::AcqRel, Ordering::Acquire)
78 .is_err()
79 {
80 return false;
81 }
82 let waiters = {
83 let mut waiters = self
84 .state
85 .waiters
86 .lock()
87 .unwrap_or_else(|poisoned| poisoned.into_inner());
88 std::mem::take(&mut *waiters)
89 };
90 for (_, waiter) in waiters {
91 waiter.wake();
92 }
93 true
94 }
95
96 pub fn is_cancelled(&self) -> bool {
97 self.reason().is_some()
98 }
99
100 fn reason(&self) -> Option<RuntimeCancellationReason> {
101 match self.state.cancellation_reason.load(Ordering::Acquire) {
102 CANCELLED => Some(RuntimeCancellationReason::Cancelled),
103 RESOURCE_EXHAUSTED => Some(RuntimeCancellationReason::ResourceExhausted),
104 _ => self
105 .state
106 .parents
107 .iter()
108 .flatten()
109 .find_map(RuntimeCancellationToken::reason),
110 }
111 }
112
113 pub fn child(&self) -> Self {
114 Self {
115 state: Arc::new(RuntimeCancellationState {
116 cancellation_reason: AtomicU8::new(NOT_CANCELLED),
117 parents: [Some(self.clone()), None],
118 next_waiter_id: AtomicU64::new(1),
119 waiters: Mutex::new(BTreeMap::new()),
120 }),
121 }
122 }
123
124 fn linked(&self, other: &Self) -> Self {
125 if Arc::ptr_eq(&self.state, &other.state) {
126 return self.child();
127 }
128 Self {
129 state: Arc::new(RuntimeCancellationState {
130 cancellation_reason: AtomicU8::new(NOT_CANCELLED),
131 parents: [Some(self.clone()), Some(other.clone())],
132 next_waiter_id: AtomicU64::new(1),
133 waiters: Mutex::new(BTreeMap::new()),
134 }),
135 }
136 }
137
138 pub fn cancelled(&self) -> RuntimeCancellationFuture<'_> {
140 RuntimeCancellationFuture {
141 token: self,
142 registrations: Vec::new(),
143 }
144 }
145}
146
147struct RuntimeCancellationRegistration {
148 state: Weak<RuntimeCancellationState>,
149 waiter_id: u64,
150}
151
152pub struct RuntimeCancellationFuture<'a> {
154 token: &'a RuntimeCancellationToken,
155 registrations: Vec<RuntimeCancellationRegistration>,
156}
157
158impl Future for RuntimeCancellationFuture<'_> {
159 type Output = ();
160
161 fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
162 if self.token.is_cancelled() {
163 return Poll::Ready(());
164 }
165
166 if self.registrations.is_empty() {
167 let mut pending = vec![self.token];
168 let mut visited = BTreeSet::new();
169 while let Some(current) = pending.pop() {
170 if !visited.insert(Arc::as_ptr(¤t.state)) {
171 continue;
172 }
173 let waiter_id = current.state.next_waiter_id.fetch_add(1, Ordering::Relaxed);
174 current
175 .state
176 .waiters
177 .lock()
178 .unwrap_or_else(|poisoned| poisoned.into_inner())
179 .insert(waiter_id, context.waker().clone());
180 self.registrations.push(RuntimeCancellationRegistration {
181 state: Arc::downgrade(¤t.state),
182 waiter_id,
183 });
184 pending.extend(current.state.parents.iter().flatten());
185 }
186 } else {
187 for registration in &self.registrations {
188 if let Some(state) = registration.state.upgrade()
189 && let Some(waiter) = state
190 .waiters
191 .lock()
192 .unwrap_or_else(|poisoned| poisoned.into_inner())
193 .get_mut(®istration.waiter_id)
194 && !waiter.will_wake(context.waker())
195 {
196 *waiter = context.waker().clone();
197 }
198 }
199 }
200
201 if self.token.is_cancelled() {
202 Poll::Ready(())
203 } else {
204 Poll::Pending
205 }
206 }
207}
208
209impl Drop for RuntimeCancellationFuture<'_> {
210 fn drop(&mut self) {
211 for registration in &self.registrations {
212 if let Some(state) = registration.state.upgrade() {
213 state
214 .waiters
215 .lock()
216 .unwrap_or_else(|poisoned| poisoned.into_inner())
217 .remove(®istration.waiter_id);
218 }
219 }
220 }
221}
222
223pub trait RuntimeIoWavePermit: Debug + Send {}
225
226impl<T: Debug + Send> RuntimeIoWavePermit for T {}
227
228pub trait RuntimeIoWaveController: Debug + Send + Sync {
230 fn try_acquire(
235 &self,
236 slots: NonZeroUsize,
237 context: &RuntimeTaskContext,
238 ) -> Result<Option<Box<dyn RuntimeIoWavePermit>>, RuntimeIoWaveError>;
239
240 fn acquire(
241 &self,
242 slots: NonZeroUsize,
243 context: &RuntimeTaskContext,
244 ) -> Result<Box<dyn RuntimeIoWavePermit>, RuntimeIoWaveError>;
245}
246
247#[derive(Debug, Clone, Copy, PartialEq, Eq)]
248pub enum RuntimeIoWaveError {
249 Stopped(RuntimeCancellationReason),
250 ReservationExceeded {
251 requested_slots: usize,
252 reserved_slots: usize,
253 },
254}
255
256#[derive(Debug)]
257pub enum RuntimeIoWaveTryAcquire {
258 Acquired(Option<Box<dyn RuntimeIoWavePermit>>),
260 Pending,
262}
263
264impl Display for RuntimeIoWaveError {
265 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
266 match self {
267 Self::Stopped(reason) => write!(formatter, "runtime I/O wave stopped: {reason}"),
268 Self::ReservationExceeded {
269 requested_slots,
270 reserved_slots,
271 } => write!(
272 formatter,
273 "runtime I/O wave requested {requested_slots} slots, exceeding the {reserved_slots}-slot reservation"
274 ),
275 }
276 }
277}
278
279impl Error for RuntimeIoWaveError {
280 fn source(&self) -> Option<&(dyn Error + 'static)> {
281 match self {
282 Self::Stopped(reason) => Some(reason),
283 Self::ReservationExceeded { .. } => None,
284 }
285 }
286}
287
288mod memory;
289mod work;
290pub use memory::{
291 RuntimeMemoryController, RuntimeMemoryError, RuntimeMemoryPermit, RuntimeMemoryTarget,
292};
293
294#[derive(Debug, Clone)]
295pub struct RuntimeTaskContext {
296 cancellation: RuntimeCancellationToken,
297 deadline: Option<Instant>,
298 admitted_parallelism: NonZeroUsize,
299 executor_thread_limit: Option<NonZeroUsize>,
300 memory_reservation: Option<RuntimeMemoryReservation>,
301 memory_controller: Option<Arc<dyn RuntimeMemoryController>>,
302 memory_target: Option<Arc<dyn RuntimeMemoryTarget>>,
303 io_wave_controller: Option<Arc<dyn RuntimeIoWaveController>>,
304 work_budget: Option<Arc<work::RuntimeWorkBudget>>,
305}
306
307#[derive(Debug, Clone, Copy, PartialEq, Eq)]
309pub struct RuntimeMemoryReservation {
310 memory_bytes: u64,
311 result_bytes: u64,
312}
313
314impl RuntimeMemoryReservation {
315 pub const fn new(memory_bytes: u64, result_bytes: u64) -> Self {
316 Self {
317 memory_bytes,
318 result_bytes,
319 }
320 }
321
322 pub const fn memory_bytes(self) -> u64 {
323 self.memory_bytes
324 }
325
326 pub const fn result_bytes(self) -> u64 {
327 self.result_bytes
328 }
329
330 pub const fn intersect(self, other: Self) -> Self {
331 Self {
332 memory_bytes: if self.memory_bytes < other.memory_bytes {
333 self.memory_bytes
334 } else {
335 other.memory_bytes
336 },
337 result_bytes: if self.result_bytes < other.result_bytes {
338 self.result_bytes
339 } else {
340 other.result_bytes
341 },
342 }
343 }
344}
345
346impl RuntimeTaskContext {
347 pub fn new(cancellation: RuntimeCancellationToken, deadline: Option<Instant>) -> Self {
348 Self {
349 cancellation,
350 deadline,
351 admitted_parallelism: NonZeroUsize::MIN,
352 executor_thread_limit: None,
353 memory_reservation: None,
354 memory_controller: None,
355 memory_target: None,
356 io_wave_controller: None,
357 work_budget: None,
358 }
359 }
360
361 pub fn without_deadline(cancellation: RuntimeCancellationToken) -> Self {
362 Self::new(cancellation, None)
363 }
364
365 pub fn with_timeout(timeout: Duration) -> Self {
366 Self::new(
367 RuntimeCancellationToken::new(),
368 Instant::now().checked_add(timeout),
369 )
370 }
371
372 pub fn cancellation(&self) -> &RuntimeCancellationToken {
373 &self.cancellation
374 }
375
376 pub fn child(&self) -> Self {
377 Self {
378 cancellation: self.cancellation.child(),
379 deadline: self.deadline,
380 admitted_parallelism: self.admitted_parallelism,
381 executor_thread_limit: self.executor_thread_limit,
382 memory_reservation: self.memory_reservation,
383 memory_controller: self.memory_controller.clone(),
384 memory_target: self.memory_target.clone(),
385 io_wave_controller: self.io_wave_controller.clone(),
386 work_budget: self.work_budget.clone(),
387 }
388 }
389
390 pub fn constrained_by(&self, request: &Self) -> crate::Result<Self> {
395 fn controller<T: ?Sized>(
396 parent: &Option<Arc<T>>,
397 request: &Option<Arc<T>>,
398 resource: &str,
399 ) -> crate::Result<Option<Arc<T>>> {
400 match (parent, request) {
401 (Some(parent), Some(request)) if !Arc::ptr_eq(parent, request) => {
402 Err(crate::HawDBError::Execution(format!(
403 "request task context carries a different {resource} admission"
404 )))
405 }
406 (Some(parent), _) => Ok(Some(parent.clone())),
407 (_, request) => Ok(request.clone()),
408 }
409 }
410 fn minimum<T: Ord + Copy>(left: Option<T>, right: Option<T>) -> Option<T> {
411 match (left, right) {
412 (Some(left), Some(right)) => Some(left.min(right)),
413 (left, right) => left.or(right),
414 }
415 }
416 Ok(Self {
417 cancellation: self.cancellation.linked(&request.cancellation),
418 deadline: minimum(self.deadline, request.deadline),
419 admitted_parallelism: self.admitted_parallelism.min(request.admitted_parallelism),
420 executor_thread_limit: minimum(
421 self.executor_thread_limit,
422 request.executor_thread_limit,
423 ),
424 memory_reservation: match (self.memory_reservation, request.memory_reservation) {
425 (Some(parent), Some(request)) => Some(parent.intersect(request)),
426 (parent, request) => parent.or(request),
427 },
428 memory_controller: controller(
429 &self.memory_controller,
430 &request.memory_controller,
431 "memory",
432 )?,
433 memory_target: controller(
434 &self.memory_target,
435 &request.memory_target,
436 "memory target",
437 )?,
438 io_wave_controller: controller(
439 &self.io_wave_controller,
440 &request.io_wave_controller,
441 "I/O",
442 )?,
443 work_budget: work::RuntimeWorkBudget::intersect(
444 &self.work_budget,
445 &request.work_budget,
446 ),
447 })
448 }
449
450 pub fn with_admitted_parallelism(mut self, admitted_parallelism: NonZeroUsize) -> Self {
455 self.admitted_parallelism = admitted_parallelism;
456 self
457 }
458
459 pub fn admitted_parallelism(&self) -> NonZeroUsize {
460 self.admitted_parallelism
461 }
462
463 pub fn with_executor_thread_limit(mut self, limit: NonZeroUsize) -> Self {
468 self.executor_thread_limit = Some(
469 self.executor_thread_limit
470 .map_or(limit, |current| current.min(limit)),
471 );
472 self
473 }
474
475 pub fn executor_thread_limit(&self) -> Option<NonZeroUsize> {
476 self.executor_thread_limit
477 }
478
479 pub fn with_memory_reservation(mut self, reservation: RuntimeMemoryReservation) -> Self {
485 self.memory_reservation = Some(reservation);
486 self
487 }
488
489 pub fn memory_reservation(&self) -> Option<RuntimeMemoryReservation> {
490 self.memory_reservation
491 }
492
493 pub fn has_resource_controllers(&self) -> bool {
497 self.memory_controller.is_some()
498 || self.memory_target.is_some()
499 || self.io_wave_controller.is_some()
500 }
501
502 pub fn with_memory_target(mut self, target: Arc<dyn RuntimeMemoryTarget>) -> Self {
504 self.memory_target = Some(target);
505 self
506 }
507
508 pub fn working_memory_target(&self, hard_limit: u64) -> u64 {
511 self.memory_target.as_ref().map_or(hard_limit, |target| {
512 target.target_bytes(hard_limit).min(hard_limit)
513 })
514 }
515
516 pub fn with_memory_controller(mut self, controller: Arc<dyn RuntimeMemoryController>) -> Self {
518 self.memory_controller = Some(controller);
519 self
520 }
521
522 pub fn reserve_working_memory(
526 &self,
527 bytes: u64,
528 ) -> Result<Option<Box<dyn RuntimeMemoryPermit>>, RuntimeMemoryError> {
529 self.checkpoint().map_err(RuntimeMemoryError::Stopped)?;
530 if let Some(reservation) = self.memory_reservation
531 && bytes > reservation.memory_bytes()
532 {
533 return Err(RuntimeMemoryError::ReservationExceeded {
534 requested_bytes: bytes,
535 available_bytes: reservation.memory_bytes(),
536 });
537 }
538 self.memory_controller
539 .as_ref()
540 .map(|controller| {
541 controller.reserve(
542 bytes,
543 self.memory_reservation
544 .map_or(u64::MAX, RuntimeMemoryReservation::memory_bytes),
545 )
546 })
547 .transpose()
548 }
549
550 pub fn with_io_wave_controller(mut self, controller: Arc<dyn RuntimeIoWaveController>) -> Self {
552 self.io_wave_controller = Some(controller);
553 self
554 }
555
556 pub fn acquire_io_wave(
561 &self,
562 slots: NonZeroUsize,
563 ) -> Result<Option<Box<dyn RuntimeIoWavePermit>>, RuntimeIoWaveError> {
564 self.checkpoint().map_err(RuntimeIoWaveError::Stopped)?;
565 self.io_wave_controller
566 .as_ref()
567 .map(|controller| controller.acquire(slots, self))
568 .transpose()
569 }
570
571 pub fn try_acquire_io_wave(
576 &self,
577 slots: NonZeroUsize,
578 ) -> Result<RuntimeIoWaveTryAcquire, RuntimeIoWaveError> {
579 self.checkpoint().map_err(RuntimeIoWaveError::Stopped)?;
580 let Some(controller) = &self.io_wave_controller else {
581 return Ok(RuntimeIoWaveTryAcquire::Acquired(None));
582 };
583 controller.try_acquire(slots, self).map(|permit| {
584 permit.map_or(RuntimeIoWaveTryAcquire::Pending, |permit| {
585 RuntimeIoWaveTryAcquire::Acquired(Some(permit))
586 })
587 })
588 }
589
590 pub fn with_work_limit(mut self, limit: usize) -> Self {
593 self.work_budget = Some(Arc::new(work::RuntimeWorkBudget::new(
594 limit,
595 self.work_budget.take(),
596 )));
597 self
598 }
599
600 pub fn has_work_limit(&self) -> bool {
601 self.work_budget.is_some()
602 }
603
604 pub fn work_units(&self) -> usize {
605 self.work_budget.as_ref().map_or(0, |budget| budget.used())
606 }
607
608 pub fn check_work_limit(&self) -> crate::Result<()> {
610 match self
611 .work_budget
612 .as_ref()
613 .and_then(|budget| budget.refusal())
614 {
615 Some(error) => Err(error.into()),
616 None => Ok(()),
617 }
618 }
619
620 pub fn charge_work(&self, units: usize) -> crate::Result<()> {
623 let Some(budget) = &self.work_budget else {
624 return Ok(());
625 };
626 self.checkpoint().map_err(|reason| {
627 crate::HawDBError::Execution(format!("runtime task stopped: {reason}"))
628 })?;
629 budget.charge(units)
630 }
631
632 pub fn deadline(&self) -> Option<Instant> {
633 self.deadline
634 }
635
636 pub fn remaining(&self) -> Option<Duration> {
637 self.deadline
638 .map(|deadline| deadline.saturating_duration_since(Instant::now()))
639 }
640
641 pub fn checkpoint(&self) -> Result<(), RuntimeCancellationReason> {
642 if let Some(reason) = self.cancellation.reason() {
643 return Err(reason);
644 }
645 if self
646 .deadline
647 .is_some_and(|deadline| Instant::now() >= deadline)
648 {
649 return Err(RuntimeCancellationReason::DeadlineExceeded);
650 }
651 Ok(())
652 }
653}
654
655impl Default for RuntimeTaskContext {
656 fn default() -> Self {
657 Self::without_deadline(RuntimeCancellationToken::new())
658 }
659}
660
661#[derive(Debug, Clone, Copy, PartialEq, Eq)]
662pub enum RuntimeCancellationReason {
663 Cancelled,
664 DeadlineExceeded,
665 ResourceExhausted,
666}
667
668impl RuntimeCancellationReason {
669 pub const fn as_str(self) -> &'static str {
670 match self {
671 Self::Cancelled => "cancelled",
672 Self::DeadlineExceeded => "deadline_exceeded",
673 Self::ResourceExhausted => "resource_exhausted",
674 }
675 }
676}
677
678impl Display for RuntimeCancellationReason {
679 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
680 formatter.write_str(self.as_str())
681 }
682}
683
684impl Error for RuntimeCancellationReason {}
685
686#[cfg(test)]
687mod tests {
688 use super::*;
689 use std::sync::atomic::AtomicUsize;
690 use std::sync::atomic::Ordering;
691 use std::task::Wake;
692
693 #[derive(Debug)]
694 struct RecordingIoController {
695 active_slots: Arc<AtomicUsize>,
696 }
697
698 #[derive(Debug)]
699 struct RecordingIoPermit {
700 active_slots: Arc<AtomicUsize>,
701 slots: usize,
702 }
703
704 impl Drop for RecordingIoPermit {
705 fn drop(&mut self) {
706 self.active_slots.fetch_sub(self.slots, Ordering::AcqRel);
707 }
708 }
709
710 impl RuntimeIoWaveController for RecordingIoController {
711 fn try_acquire(
712 &self,
713 slots: NonZeroUsize,
714 context: &RuntimeTaskContext,
715 ) -> Result<Option<Box<dyn RuntimeIoWavePermit>>, RuntimeIoWaveError> {
716 self.acquire(slots, context).map(Some)
717 }
718
719 fn acquire(
720 &self,
721 slots: NonZeroUsize,
722 context: &RuntimeTaskContext,
723 ) -> Result<Box<dyn RuntimeIoWavePermit>, RuntimeIoWaveError> {
724 context.checkpoint().map_err(RuntimeIoWaveError::Stopped)?;
725 self.active_slots.fetch_add(slots.get(), Ordering::AcqRel);
726 Ok(Box::new(RecordingIoPermit {
727 active_slots: Arc::clone(&self.active_slots),
728 slots: slots.get(),
729 }))
730 }
731 }
732
733 #[test]
734 fn cancellation_is_shared_across_context_clones() {
735 let token = RuntimeCancellationToken::new();
736 let context = RuntimeTaskContext::without_deadline(token.clone());
737 assert!(context.checkpoint().is_ok());
738 assert!(token.cancel());
739 assert!(!token.cancel());
740 assert_eq!(
741 context.checkpoint(),
742 Err(RuntimeCancellationReason::Cancelled)
743 );
744 }
745
746 #[test]
747 fn expired_deadline_fails_at_a_cooperative_checkpoint() {
748 let context = RuntimeTaskContext::new(
749 RuntimeCancellationToken::new(),
750 Some(Instant::now() - Duration::from_millis(1)),
751 );
752 assert_eq!(
753 context.checkpoint(),
754 Err(RuntimeCancellationReason::DeadlineExceeded)
755 );
756 }
757
758 #[test]
759 fn child_cancellation_is_local_and_parent_cancellation_propagates() {
760 let parent_token = RuntimeCancellationToken::new();
761 let parent = RuntimeTaskContext::without_deadline(parent_token.clone());
762 let child = parent.child();
763
764 assert!(child.cancellation().cancel());
765 assert!(child.checkpoint().is_err());
766 assert!(parent.checkpoint().is_ok());
767
768 let sibling = parent.child();
769 assert!(parent_token.cancel());
770 assert!(parent.checkpoint().is_err());
771 assert!(sibling.checkpoint().is_err());
772 }
773
774 #[test]
775 fn resource_exhaustion_keeps_its_cause_and_is_local_to_the_child() {
776 let parent = RuntimeTaskContext::default();
777 let child = parent.child();
778 let clone = child.clone();
779 let grandchild = child.child();
780 assert!(child.cancellation().cancel_for_resource_exhaustion());
781 assert!(!clone.cancellation().cancel());
782 assert!(!clone.cancellation().cancel_for_resource_exhaustion());
783 for context in [&child, &clone, &grandchild] {
784 assert_eq!(
785 context.checkpoint(),
786 Err(RuntimeCancellationReason::ResourceExhausted)
787 );
788 }
789 assert_eq!(parent.checkpoint(), Ok(()));
790 assert_eq!(parent.child().checkpoint(), Ok(()));
791
792 let cancelled = parent.child();
793 assert!(cancelled.cancellation().cancel());
794 assert!(!cancelled.cancellation().cancel_for_resource_exhaustion());
795 assert_eq!(
796 cancelled.checkpoint(),
797 Err(RuntimeCancellationReason::Cancelled)
798 );
799 }
800
801 #[derive(Default)]
802 struct WakeCounter(AtomicUsize);
803
804 impl Wake for WakeCounter {
805 fn wake(self: Arc<Self>) {
806 self.0.fetch_add(1, Ordering::AcqRel);
807 }
808 }
809
810 #[test]
811 fn constrained_context_observes_both_cancellations_without_cancelling_parents() {
812 for cancel_parent in [false, true] {
813 let parent = RuntimeTaskContext::default();
814 let request = RuntimeTaskContext::default();
815 let combined = parent.constrained_by(&request).unwrap();
816 let counter = Arc::new(WakeCounter::default());
817 let waker = Waker::from(counter.clone());
818 let mut context = Context::from_waker(&waker);
819 let mut cancelled = Box::pin(combined.cancellation().cancelled());
820 assert!(cancelled.as_mut().poll(&mut context).is_pending());
821 if cancel_parent {
822 parent.cancellation().cancel();
823 } else {
824 request.cancellation().cancel();
825 }
826 assert_eq!(counter.0.load(Ordering::Acquire), 1);
827 assert!(cancelled.as_mut().poll(&mut context).is_ready());
828 assert_eq!(
829 combined.checkpoint(),
830 Err(RuntimeCancellationReason::Cancelled)
831 );
832 }
833 let parent = RuntimeTaskContext::default();
834 let request = RuntimeTaskContext::default();
835 let combined = parent.constrained_by(&request).unwrap();
836 combined.cancellation().cancel();
837 assert!(parent.checkpoint().is_ok());
838 assert!(request.checkpoint().is_ok());
839 }
840
841 #[test]
842 fn constrained_descendants_register_shared_ancestors_once_and_unregister_on_drop() {
843 let parent = RuntimeTaskContext::default();
844 let request = parent.child();
845 let combined = parent.constrained_by(&request).unwrap();
846 let waker = Waker::from(Arc::new(WakeCounter::default()));
847 let mut context = Context::from_waker(&waker);
848 let mut cancelled = Box::pin(combined.cancellation().cancelled());
849 assert!(cancelled.as_mut().poll(&mut context).is_pending());
850 assert_eq!(parent.cancellation.state.waiters.lock().unwrap().len(), 1);
851 assert_eq!(request.cancellation.state.waiters.lock().unwrap().len(), 1);
852 drop(cancelled);
853 assert!(parent.cancellation.state.waiters.lock().unwrap().is_empty());
854 assert!(request
855 .cancellation
856 .state
857 .waiters
858 .lock()
859 .unwrap()
860 .is_empty());
861 }
862
863 #[test]
864 fn constrained_context_tightens_limits_and_keeps_the_original_io_controller() {
865 let active_slots = Arc::new(AtomicUsize::new(0));
866 let parent = RuntimeTaskContext::with_timeout(Duration::from_secs(5))
867 .with_memory_reservation(RuntimeMemoryReservation::new(100, 20))
868 .with_admitted_parallelism(NonZeroUsize::new(4).unwrap())
869 .with_io_wave_controller(Arc::new(RecordingIoController {
870 active_slots: active_slots.clone(),
871 }));
872 let request = RuntimeTaskContext::with_timeout(Duration::from_secs(10))
873 .with_memory_reservation(RuntimeMemoryReservation::new(30, 50))
874 .with_admitted_parallelism(NonZeroUsize::new(2).unwrap());
875 let combined = parent.constrained_by(&request).unwrap();
876 assert_eq!(combined.deadline(), parent.deadline());
877 assert_eq!(
878 combined.memory_reservation(),
879 Some(RuntimeMemoryReservation::new(30, 20))
880 );
881 assert_eq!(combined.admitted_parallelism().get(), 2);
882 let io = combined.acquire_io_wave(NonZeroUsize::MIN).unwrap();
883 assert_eq!(active_slots.load(Ordering::Acquire), 1);
884 drop(io);
885 assert_eq!(active_slots.load(Ordering::Acquire), 0);
886 assert!(parent.constrained_by(&parent.child()).is_ok());
887 let independent =
888 request.with_io_wave_controller(Arc::new(RecordingIoController { active_slots }));
889 assert!(parent.constrained_by(&independent).is_err());
890 }
891
892 #[test]
893 fn cancellation_future_is_woken_by_parent_cancellation() {
894 let parent = RuntimeCancellationToken::new();
895 let child = parent.child();
896 let counter = Arc::new(WakeCounter::default());
897 let waker = Waker::from(Arc::clone(&counter));
898 let mut context = Context::from_waker(&waker);
899 let mut cancelled = Box::pin(child.cancelled());
900
901 assert!(cancelled.as_mut().poll(&mut context).is_pending());
902 assert!(parent.cancel());
903 assert_eq!(counter.0.load(Ordering::Acquire), 1);
904 assert!(cancelled.as_mut().poll(&mut context).is_ready());
905 }
906
907 #[test]
908 fn resource_exhaustion_wakes_waiters_on_descendants() {
909 let parent = RuntimeTaskContext::default();
910 let child = parent.child();
911 let counter = Arc::new(WakeCounter::default());
912 let waker = Waker::from(Arc::clone(&counter));
913 let mut context = Context::from_waker(&waker);
914 let mut cancelled = Box::pin(child.cancellation().cancelled());
915 assert!(cancelled.as_mut().poll(&mut context).is_pending());
916 assert!(parent.cancellation().cancel_for_resource_exhaustion());
917 assert_eq!(counter.0.load(Ordering::Acquire), 1);
918 assert!(cancelled.as_mut().poll(&mut context).is_ready());
919 assert_eq!(
920 child.checkpoint(),
921 Err(RuntimeCancellationReason::ResourceExhausted)
922 );
923 }
924
925 #[test]
926 fn dropped_cancellation_future_unregisters_from_its_parent_chain() {
927 let parent = RuntimeCancellationToken::new();
928 let child = parent.child();
929 let counter = Arc::new(WakeCounter::default());
930 let waker = Waker::from(Arc::clone(&counter));
931 let mut context = Context::from_waker(&waker);
932 let mut cancelled = Box::pin(child.cancelled());
933
934 assert!(cancelled.as_mut().poll(&mut context).is_pending());
935 drop(cancelled);
936 assert!(parent.cancel());
937 assert_eq!(counter.0.load(Ordering::Acquire), 0);
938 }
939
940 #[test]
941 fn child_preserves_admitted_parallelism() {
942 let context = RuntimeTaskContext::default()
943 .with_admitted_parallelism(NonZeroUsize::new(4).expect("test parallelism is non-zero"))
944 .with_executor_thread_limit(
945 NonZeroUsize::new(2).expect("test thread limit is non-zero"),
946 );
947
948 assert_eq!(context.admitted_parallelism().get(), 4);
949 assert_eq!(context.child().admitted_parallelism().get(), 4);
950 assert_eq!(context.child().executor_thread_limit().unwrap().get(), 2);
951 }
952
953 #[test]
954 fn child_preserves_memory_reservation() {
955 let reservation = RuntimeMemoryReservation::new(128, 32);
956 let context = RuntimeTaskContext::default().with_memory_reservation(reservation);
957
958 assert_eq!(context.memory_reservation(), Some(reservation));
959 assert_eq!(context.child().memory_reservation(), Some(reservation));
960 }
961
962 #[test]
963 fn child_preserves_io_controller_and_permit_releases_slots() {
964 let active_slots = Arc::new(AtomicUsize::new(0));
965 let context = RuntimeTaskContext::default().with_io_wave_controller(Arc::new(
966 RecordingIoController {
967 active_slots: Arc::clone(&active_slots),
968 },
969 ));
970
971 let permit = context
972 .child()
973 .acquire_io_wave(NonZeroUsize::new(2).unwrap())
974 .unwrap()
975 .unwrap();
976 assert_eq!(active_slots.load(Ordering::Acquire), 2);
977 drop(permit);
978 assert_eq!(active_slots.load(Ordering::Acquire), 0);
979 }
980
981 #[test]
982 fn ungoverned_try_acquire_is_immediately_ready() {
983 let acquired = RuntimeTaskContext::default()
984 .try_acquire_io_wave(NonZeroUsize::new(2).unwrap())
985 .unwrap();
986
987 assert!(matches!(acquired, RuntimeIoWaveTryAcquire::Acquired(None)));
988 }
989}