1use asupersync::types::CancelReason;
7use asupersync::{Budget, Cx, Outcome, RegionId, TaskId, Time};
8use std::sync::Arc;
9
10use crate::dependency::{CleanupStack, DependencyCache, DependencyOverrides, ResolutionStack};
11
12pub const DEFAULT_MAX_BODY_SIZE: usize = 1024 * 1024;
14
15#[derive(Debug, Clone, Copy)]
21pub struct BodyLimitConfig {
22 max_size: usize,
24}
25
26impl Default for BodyLimitConfig {
27 fn default() -> Self {
28 Self {
29 max_size: DEFAULT_MAX_BODY_SIZE,
30 }
31 }
32}
33
34impl BodyLimitConfig {
35 #[must_use]
37 pub fn new(max_size: usize) -> Self {
38 Self { max_size }
39 }
40
41 #[must_use]
43 pub fn max_size(self) -> usize {
44 self.max_size
45 }
46}
47
48#[derive(Debug, Clone)]
75pub struct RequestContext {
76 cx: Cx,
78 request_id: u64,
80 dependency_cache: Arc<DependencyCache>,
82 dependency_overrides: Arc<DependencyOverrides>,
84 resolution_stack: Arc<ResolutionStack>,
86 cleanup_stack: Arc<CleanupStack>,
88 body_limit: BodyLimitConfig,
90 deadline: Option<Time>,
99}
100
101impl RequestContext {
102 #[must_use]
108 pub fn new(cx: Cx, request_id: u64) -> Self {
109 Self {
110 cx,
111 request_id,
112 dependency_cache: Arc::new(DependencyCache::new()),
113 dependency_overrides: Arc::new(DependencyOverrides::new()),
114 resolution_stack: Arc::new(ResolutionStack::new()),
115 cleanup_stack: Arc::new(CleanupStack::new()),
116 body_limit: BodyLimitConfig::default(),
117 deadline: None,
118 }
119 }
120
121 #[must_use]
126 pub fn with_body_limit(cx: Cx, request_id: u64, max_body_size: usize) -> Self {
127 Self {
128 cx,
129 request_id,
130 dependency_cache: Arc::new(DependencyCache::new()),
131 dependency_overrides: Arc::new(DependencyOverrides::new()),
132 resolution_stack: Arc::new(ResolutionStack::new()),
133 cleanup_stack: Arc::new(CleanupStack::new()),
134 body_limit: BodyLimitConfig::new(max_body_size),
135 deadline: None,
136 }
137 }
138
139 #[must_use]
141 pub fn with_overrides(cx: Cx, request_id: u64, overrides: Arc<DependencyOverrides>) -> Self {
142 Self {
143 cx,
144 request_id,
145 dependency_cache: Arc::new(DependencyCache::new()),
146 dependency_overrides: overrides,
147 resolution_stack: Arc::new(ResolutionStack::new()),
148 cleanup_stack: Arc::new(CleanupStack::new()),
149 body_limit: BodyLimitConfig::default(),
150 deadline: None,
151 }
152 }
153
154 #[must_use]
156 pub fn with_overrides_and_body_limit(
157 cx: Cx,
158 request_id: u64,
159 overrides: Arc<DependencyOverrides>,
160 max_body_size: usize,
161 ) -> Self {
162 Self {
163 cx,
164 request_id,
165 dependency_cache: Arc::new(DependencyCache::new()),
166 dependency_overrides: overrides,
167 resolution_stack: Arc::new(ResolutionStack::new()),
168 cleanup_stack: Arc::new(CleanupStack::new()),
169 body_limit: BodyLimitConfig::new(max_body_size),
170 deadline: None,
171 }
172 }
173
174 #[must_use]
181 pub fn with_deadline(mut self, deadline: Time) -> Self {
182 self.deadline = Some(deadline);
183 self
184 }
185
186 #[must_use]
188 pub fn deadline(&self) -> Option<Time> {
189 self.deadline
190 }
191
192 #[must_use]
202 pub fn deadline_exceeded(&self) -> bool {
203 self.deadline
204 .is_some_and(|deadline| self.cx.now() >= deadline)
205 }
206
207 #[must_use]
211 pub fn request_id(&self) -> u64 {
212 self.request_id
213 }
214
215 #[must_use]
217 pub fn dependency_cache(&self) -> &DependencyCache {
218 &self.dependency_cache
219 }
220
221 #[must_use]
223 pub fn dependency_overrides(&self) -> &DependencyOverrides {
224 &self.dependency_overrides
225 }
226
227 #[must_use]
229 pub fn resolution_stack(&self) -> &ResolutionStack {
230 &self.resolution_stack
231 }
232
233 #[must_use]
237 pub fn cleanup_stack(&self) -> &CleanupStack {
238 &self.cleanup_stack
239 }
240
241 #[must_use]
246 pub fn body_limit(&self) -> &BodyLimitConfig {
247 &self.body_limit
248 }
249
250 #[must_use]
254 pub fn max_body_size(&self) -> usize {
255 self.body_limit.max_size()
256 }
257
258 #[must_use]
264 pub fn region_id(&self) -> RegionId {
265 self.cx.region_id()
266 }
267
268 #[must_use]
270 pub fn task_id(&self) -> TaskId {
271 self.cx.task_id()
272 }
273
274 #[must_use]
280 pub fn budget(&self) -> Budget {
281 self.cx.budget()
282 }
283
284 #[must_use]
289 pub fn is_cancelled(&self) -> bool {
290 self.cx.is_cancel_requested()
291 }
292
293 pub fn checkpoint(&self) -> Result<(), CancelledError> {
315 self.cx.checkpoint().map_err(|_| CancelledError)
316 }
317
318 pub fn masked<F, R>(&self, f: F) -> R
334 where
335 F: FnOnce() -> R,
336 {
337 self.cx.masked(f)
338 }
339
340 pub fn trace(&self, message: &str) {
345 self.cx.trace(message);
346 }
347
348 #[must_use]
353 pub fn cx(&self) -> &Cx {
354 &self.cx
355 }
356}
357
358#[derive(Debug, Clone, Copy)]
364pub struct CancelledError;
365
366impl std::fmt::Display for CancelledError {
367 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
368 write!(f, "request cancelled")
369 }
370}
371
372impl std::error::Error for CancelledError {}
373
374pub trait IntoOutcome<T, E> {
379 fn into_outcome(self) -> Outcome<T, E>;
381}
382
383impl<T, E> IntoOutcome<T, E> for Result<T, E> {
384 fn into_outcome(self) -> Outcome<T, E> {
385 match self {
386 Ok(v) => Outcome::Ok(v),
387 Err(e) => Outcome::Err(e),
388 }
389 }
390}
391
392impl<T, E> IntoOutcome<T, E> for Result<T, CancelledError>
393where
394 E: Default,
395{
396 fn into_outcome(self) -> Outcome<T, E> {
397 match self {
398 Ok(v) => Outcome::Ok(v),
399 Err(CancelledError) => Outcome::Cancelled(CancelReason::user("request cancelled")),
400 }
401 }
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407
408 #[test]
409 fn cancelled_error_display() {
410 let err = CancelledError;
411 assert_eq!(format!("{err}"), "request cancelled");
412 }
413
414 #[test]
415 fn checkpoint_returns_error_when_cancel_requested() {
416 let cx = Cx::for_testing();
417 let ctx = RequestContext::new(cx, 1);
418 ctx.cx().set_cancel_requested(true);
419 assert!(ctx.checkpoint().is_err());
420 }
421
422 #[test]
423 fn deadline_defaults_to_none_and_is_never_exceeded() {
424 let ctx = RequestContext::new(Cx::for_testing(), 1);
425 assert_eq!(ctx.deadline(), None);
426 assert!(!ctx.deadline_exceeded());
427 }
428
429 #[test]
430 fn with_deadline_exposes_the_server_deadline() {
431 let deadline = Time::from_secs(5);
432 let ctx = RequestContext::new(Cx::for_testing(), 1).with_deadline(deadline);
433 assert_eq!(ctx.deadline(), Some(deadline));
434 }
435
436 #[test]
437 fn deadline_exceeded_reflects_the_runtime_clock() {
438 let past = RequestContext::new(Cx::for_testing(), 1).with_deadline(Time::ZERO);
440 assert!(past.deadline_exceeded());
441
442 let future =
444 RequestContext::new(Cx::for_testing(), 1).with_deadline(Time::from_nanos(u64::MAX));
445 assert!(!future.deadline_exceeded());
446 }
447
448 #[test]
449 fn masked_defers_cancellation_at_checkpoint() {
450 let cx = Cx::for_testing();
451 let ctx = RequestContext::new(cx, 1);
452 ctx.cx().set_cancel_requested(true);
453
454 let result = ctx.masked(|| ctx.checkpoint());
455 assert!(result.is_ok());
456 assert!(ctx.checkpoint().is_err());
457 }
458
459 #[test]
464 fn body_limit_config_default() {
465 let config = BodyLimitConfig::default();
466 assert_eq!(config.max_size(), DEFAULT_MAX_BODY_SIZE);
467 assert_eq!(config.max_size(), 1024 * 1024); }
469
470 #[test]
471 fn body_limit_config_custom() {
472 let config = BodyLimitConfig::new(512 * 1024);
473 assert_eq!(config.max_size(), 512 * 1024); }
475
476 #[test]
477 fn request_context_default_body_limit() {
478 let cx = Cx::for_testing();
479 let ctx = RequestContext::new(cx, 1);
480 assert_eq!(ctx.max_body_size(), DEFAULT_MAX_BODY_SIZE);
481 assert_eq!(ctx.body_limit().max_size(), DEFAULT_MAX_BODY_SIZE);
482 }
483
484 #[test]
485 fn request_context_custom_body_limit() {
486 let cx = Cx::for_testing();
487 let ctx = RequestContext::with_body_limit(cx, 1, 2 * 1024 * 1024);
488 assert_eq!(ctx.max_body_size(), 2 * 1024 * 1024); }
490
491 #[test]
492 fn request_context_with_overrides_has_default_limit() {
493 let cx = Cx::for_testing();
494 let overrides = Arc::new(DependencyOverrides::new());
495 let ctx = RequestContext::with_overrides(cx, 1, overrides);
496 assert_eq!(ctx.max_body_size(), DEFAULT_MAX_BODY_SIZE);
497 }
498
499 #[test]
500 fn request_context_with_overrides_and_custom_limit() {
501 let cx = Cx::for_testing();
502 let overrides = Arc::new(DependencyOverrides::new());
503 let ctx = RequestContext::with_overrides_and_body_limit(cx, 1, overrides, 4 * 1024 * 1024);
504 assert_eq!(ctx.max_body_size(), 4 * 1024 * 1024); }
506
507 #[test]
512 #[allow(clippy::similar_names)]
513 fn request_id_isolation_unique_per_context() {
514 let cx1 = Cx::for_testing();
516 let cx2 = Cx::for_testing();
517 let cx3 = Cx::for_testing();
518
519 let ctx1 = RequestContext::new(cx1, 100);
520 let ctx2 = RequestContext::new(cx2, 200);
521 let ctx3 = RequestContext::new(cx3, 300);
522
523 assert_eq!(ctx1.request_id(), 100);
525 assert_eq!(ctx2.request_id(), 200);
526 assert_eq!(ctx3.request_id(), 300);
527
528 assert_ne!(ctx1.request_id(), ctx2.request_id());
530 assert_ne!(ctx2.request_id(), ctx3.request_id());
531 }
532
533 #[test]
534 #[allow(clippy::similar_names)]
535 fn dependency_cache_isolation_per_request() {
536 let cx1 = Cx::for_testing();
538 let cx2 = Cx::for_testing();
539
540 let ctx1 = RequestContext::new(cx1, 1);
541 let ctx2 = RequestContext::new(cx2, 2);
542
543 ctx1.dependency_cache().insert::<i32>(42);
545
546 let value1 = ctx1.dependency_cache().get::<i32>();
548 let value2 = ctx2.dependency_cache().get::<i32>();
549
550 assert!(value1.is_some(), "ctx1 should have cached value");
551 assert_eq!(value1.unwrap(), 42);
552 assert!(value2.is_none(), "ctx2 should NOT have ctx1's cached value");
553 }
554
555 #[test]
556 #[allow(clippy::similar_names)]
557 fn cleanup_stack_isolation_per_request() {
558 use std::sync::atomic::{AtomicUsize, Ordering};
560
561 let cleanup_counter1 = Arc::new(AtomicUsize::new(0));
562 let cleanup_counter2 = Arc::new(AtomicUsize::new(0));
563
564 let cx1 = Cx::for_testing();
565 let cx2 = Cx::for_testing();
566
567 let ctx1 = RequestContext::new(cx1, 1);
568 let ctx2 = RequestContext::new(cx2, 2);
569
570 {
572 let counter = cleanup_counter1.clone();
573 ctx1.cleanup_stack().push(Box::new(move || {
574 Box::pin(async move {
575 counter.fetch_add(1, Ordering::SeqCst);
576 })
577 }));
578 }
579
580 {
582 let counter = cleanup_counter2.clone();
583 ctx2.cleanup_stack().push(Box::new(move || {
584 Box::pin(async move {
585 counter.fetch_add(1, Ordering::SeqCst);
586 })
587 }));
588 }
589
590 futures_executor::block_on(ctx1.cleanup_stack().run_cleanups());
592
593 assert_eq!(
595 cleanup_counter1.load(Ordering::SeqCst),
596 1,
597 "ctx1 cleanup should have run"
598 );
599 assert_eq!(
600 cleanup_counter2.load(Ordering::SeqCst),
601 0,
602 "ctx2 cleanup should NOT have run"
603 );
604
605 futures_executor::block_on(ctx2.cleanup_stack().run_cleanups());
607 assert_eq!(
608 cleanup_counter2.load(Ordering::SeqCst),
609 1,
610 "ctx2 cleanup should have run"
611 );
612 }
613
614 #[test]
615 #[allow(clippy::similar_names)]
616 fn cx_cancellation_isolation_per_request() {
617 let cx1 = Cx::for_testing();
619 let cx2 = Cx::for_testing();
620 let cx3 = Cx::for_testing();
621
622 let ctx1 = RequestContext::new(cx1, 1);
623 let ctx2 = RequestContext::new(cx2, 2);
624 let ctx3 = RequestContext::new(cx3, 3);
625
626 assert!(ctx1.checkpoint().is_ok(), "ctx1 should not be cancelled");
628 assert!(ctx2.checkpoint().is_ok(), "ctx2 should not be cancelled");
629 assert!(ctx3.checkpoint().is_ok(), "ctx3 should not be cancelled");
630
631 ctx2.cx().set_cancel_requested(true);
633
634 assert!(
636 ctx1.checkpoint().is_ok(),
637 "ctx1 should still not be cancelled"
638 );
639 assert!(ctx2.checkpoint().is_err(), "ctx2 should be cancelled");
640 assert!(
641 ctx3.checkpoint().is_ok(),
642 "ctx3 should still not be cancelled"
643 );
644 }
645
646 #[test]
647 #[allow(clippy::similar_names)]
648 fn body_limit_isolation_per_request() {
649 let cx1 = Cx::for_testing();
651 let cx2 = Cx::for_testing();
652
653 let ctx1 = RequestContext::with_body_limit(cx1, 1, 1024); let ctx2 = RequestContext::with_body_limit(cx2, 2, 1024 * 1024); assert_eq!(ctx1.max_body_size(), 1024);
659 assert_eq!(ctx2.max_body_size(), 1024 * 1024);
660
661 assert_ne!(ctx1.max_body_size(), ctx2.max_body_size());
663 }
664
665 #[test]
666 fn concurrent_requests_fully_isolated() {
667 use std::thread;
669
670 const NUM_REQUESTS: usize = 100;
671 let results = Arc::new(parking_lot::Mutex::new(Vec::with_capacity(NUM_REQUESTS)));
672
673 let handles: Vec<_> = (0..NUM_REQUESTS)
674 .map(|i| {
675 let results = results.clone();
676 thread::spawn(move || {
677 let cx = Cx::for_testing();
678 let request_id = (i + 1) as u64 * 1000; let ctx = RequestContext::new(cx, request_id);
680
681 ctx.dependency_cache().insert::<u64>(request_id);
683
684 let cached = ctx.dependency_cache().get::<u64>();
686 let retrieved = cached.unwrap_or(0);
687
688 results.lock().push((request_id, retrieved));
689 })
690 })
691 .collect();
692
693 for handle in handles {
695 handle.join().expect("Thread panicked");
696 }
697
698 let results = results.lock();
700 assert_eq!(results.len(), NUM_REQUESTS);
701
702 for (request_id, retrieved) in results.iter() {
703 assert_eq!(
704 request_id, retrieved,
705 "Request {request_id} should retrieve its own cached value, not another request's"
706 );
707 }
708 }
709
710 #[test]
711 #[allow(clippy::similar_names)]
712 fn resolution_stack_isolation_per_request() {
713 use crate::dependency::DependencyScope;
715
716 let cx1 = Cx::for_testing();
717 let cx2 = Cx::for_testing();
718
719 let ctx1 = RequestContext::new(cx1, 1);
720 let ctx2 = RequestContext::new(cx2, 2);
721
722 ctx1.resolution_stack()
724 .push::<i32>("i32", DependencyScope::Request);
725
726 let cycle1 = ctx1.resolution_stack().check_cycle::<i32>("i32");
728 assert!(cycle1.is_some(), "ctx1 should detect cycle for i32");
729
730 let cycle2 = ctx2.resolution_stack().check_cycle::<i32>("i32");
732 assert!(
733 cycle2.is_none(),
734 "ctx2 should NOT see ctx1's resolution stack"
735 );
736
737 ctx2.resolution_stack()
739 .push::<i32>("i32", DependencyScope::Request);
740 assert_eq!(ctx2.resolution_stack().depth(), 1);
741
742 assert_eq!(ctx1.resolution_stack().depth(), 1);
744 assert_eq!(ctx2.resolution_stack().depth(), 1);
745
746 ctx1.resolution_stack().pop();
748 ctx2.resolution_stack().pop();
749 assert!(ctx1.resolution_stack().is_empty());
750 assert!(ctx2.resolution_stack().is_empty());
751 }
752}