services_hardening_runtime_supervision.rs (5758B)
1 #![forbid(unsafe_code)] 2 3 use std::{ 4 error::Error, 5 sync::{ 6 Arc, 7 atomic::{AtomicBool, AtomicUsize, Ordering}, 8 }, 9 }; 10 11 use myc::{ 12 MYC_CRITICAL_TASK_MAX_COUNT, MycCriticalTask, MycCriticalTaskError, MycLogRecord, 13 MycProcessResult, MycRuntimeSupervisionErrorKind, MycSupervisedRuntime, 14 }; 15 16 fn waiting_peer(joined: Arc<AtomicBool>) -> MycCriticalTask { 17 MycCriticalTask::new(move |cancellation| async move { 18 cancellation.cancelled().await; 19 assert!(cancellation.is_cancelled()); 20 joined.store(true, Ordering::SeqCst); 21 Ok(()) 22 }) 23 } 24 25 #[test] 26 fn task_inventory_is_nonempty_bounded_and_stops_at_maximum_plus_one() { 27 let empty = MycSupervisedRuntime::new([]).expect_err("empty graph must fail closed"); 28 assert_eq!(empty.kind(), MycRuntimeSupervisionErrorKind::EmptyTaskSet); 29 30 let maximum = 31 (0..MYC_CRITICAL_TASK_MAX_COUNT).map(|_| MycCriticalTask::new(|_| async { Ok(()) })); 32 assert_eq!( 33 MycSupervisedRuntime::new(maximum) 34 .expect("exact maximum") 35 .task_count(), 36 MYC_CRITICAL_TASK_MAX_COUNT 37 ); 38 39 let generated = Arc::new(AtomicUsize::new(0)); 40 let count = Arc::clone(&generated); 41 let unbounded = std::iter::repeat_with(move || { 42 count.fetch_add(1, Ordering::SeqCst); 43 MycCriticalTask::new(|_| async { Ok(()) }) 44 }); 45 let too_many = MycSupervisedRuntime::new(unbounded).expect_err("maximum plus one"); 46 assert_eq!( 47 too_many.kind(), 48 MycRuntimeSupervisionErrorKind::TooManyTasks 49 ); 50 assert_eq!( 51 generated.load(Ordering::SeqCst), 52 MYC_CRITICAL_TASK_MAX_COUNT + 1 53 ); 54 } 55 56 #[test] 57 fn registration_without_a_tokio_runtime_fails_before_any_task_can_detach() { 58 let runtime = MycSupervisedRuntime::new([MycCriticalTask::new(|_| async { Ok(()) })]) 59 .expect("bounded graph"); 60 let error = futures_executor::block_on(runtime.run()).expect_err("Tokio runtime required"); 61 assert_eq!( 62 error.kind(), 63 MycRuntimeSupervisionErrorKind::TaskRegistration 64 ); 65 } 66 67 #[tokio::test] 68 async fn task_error_coordinates_peer_cancellation_and_observes_every_join() { 69 let peer_joined = Arc::new(AtomicBool::new(false)); 70 let runtime = MycSupervisedRuntime::new([ 71 waiting_peer(Arc::clone(&peer_joined)), 72 MycCriticalTask::new(|_| async { Err(MycCriticalTaskError::failed()) }), 73 ]) 74 .expect("bounded graph"); 75 76 let error = runtime.run().await.expect_err("critical task failed"); 77 assert_eq!( 78 error.kind(), 79 MycRuntimeSupervisionErrorKind::TaskReturnedError 80 ); 81 assert_eq!(error.process_result(), MycProcessResult::UnexpectedInternal); 82 assert_eq!(error.diagnostic(), MycLogRecord::critical_task_failed()); 83 assert!(peer_joined.load(Ordering::SeqCst)); 84 assert!(Error::source(&error).is_none()); 85 } 86 87 #[tokio::test] 88 async fn early_success_and_panic_are_fatal_and_join_their_cancelled_peer() { 89 let early_peer = Arc::new(AtomicBool::new(false)); 90 let early = MycSupervisedRuntime::new([ 91 waiting_peer(Arc::clone(&early_peer)), 92 MycCriticalTask::new(|_| async { Ok(()) }), 93 ]) 94 .expect("bounded graph") 95 .run() 96 .await 97 .expect_err("critical task returned before cancellation"); 98 assert_eq!( 99 early.kind(), 100 MycRuntimeSupervisionErrorKind::UnexpectedCompletion 101 ); 102 assert!(early_peer.load(Ordering::SeqCst)); 103 104 let panic_peer = Arc::new(AtomicBool::new(false)); 105 let panicked = MycSupervisedRuntime::new([ 106 waiting_peer(Arc::clone(&panic_peer)), 107 MycCriticalTask::new(|_| async { 108 panic!("fixed critical-task test panic"); 109 #[allow(unreachable_code)] 110 Ok(()) 111 }), 112 ]) 113 .expect("bounded graph") 114 .run() 115 .await 116 .expect_err("critical task panicked"); 117 assert_eq!( 118 panicked.kind(), 119 MycRuntimeSupervisionErrorKind::TaskPanicked 120 ); 121 assert!(panic_peer.load(Ordering::SeqCst)); 122 } 123 124 #[test] 125 fn task_and_error_diagnostics_are_source_free_and_redacted() { 126 let secret = "caller-task-secret"; 127 let task = MycCriticalTask::new(move |_| async move { 128 let _ = secret; 129 Ok(()) 130 }); 131 let runtime = MycSupervisedRuntime::new([task]).expect("bounded graph"); 132 assert_eq!( 133 format!("{runtime:?}"), 134 "MycSupervisedRuntime { task_count: 1, tasks: \"[sealed]\" }" 135 ); 136 assert!(!format!("{runtime:?}").contains(secret)); 137 assert_eq!( 138 format!("{:?}", MycCriticalTaskError::failed()), 139 "MycCriticalTaskError" 140 ); 141 assert!(Error::source(&MycCriticalTaskError::failed()).is_none()); 142 143 let codes = [ 144 ( 145 MycRuntimeSupervisionErrorKind::EmptyTaskSet, 146 "runtime_task_set_empty", 147 ), 148 ( 149 MycRuntimeSupervisionErrorKind::TooManyTasks, 150 "runtime_task_set_too_large", 151 ), 152 ( 153 MycRuntimeSupervisionErrorKind::TaskRegistration, 154 "runtime_task_registration_failed", 155 ), 156 ( 157 MycRuntimeSupervisionErrorKind::TaskReturnedError, 158 "runtime_task_returned_error", 159 ), 160 ( 161 MycRuntimeSupervisionErrorKind::TaskPanicked, 162 "runtime_task_panicked", 163 ), 164 ( 165 MycRuntimeSupervisionErrorKind::UnexpectedCompletion, 166 "runtime_task_completed_early", 167 ), 168 ( 169 MycRuntimeSupervisionErrorKind::UnexpectedCancellation, 170 "runtime_task_cancelled_unexpectedly", 171 ), 172 ( 173 MycRuntimeSupervisionErrorKind::JoinFailed, 174 "runtime_task_join_failed", 175 ), 176 ]; 177 for (kind, code) in codes { 178 assert_eq!(kind.code(), code); 179 } 180 }