1use std::collections::BTreeMap;
37use std::path::Path;
38use std::path::PathBuf;
39use std::sync::OnceLock;
40use std::sync::atomic::AtomicU64;
41use std::sync::atomic::Ordering;
42use std::time::Duration;
43
44use crate::random::splitmix64;
45
46pub const RACE_PROBE_ENV: &str = "HERMIT_RACE_PROBE";
48
49pub const RACE_PROBE_SEED_ENV: &str = "HERMIT_RACE_PROBE_SEED";
51
52pub const RACE_PROBE_HOLD_DIR_ENV: &str = "HERMIT_RACE_PROBE_HOLD_DIR";
54
55const HOLD_POLL: Duration = Duration::from_millis(10);
57
58pub(crate) const BG_PRE_CALL: &str = "bg_pre_call";
60pub(crate) const BG_POST_CALL: &str = "bg_post_call";
62pub(crate) const BG_HARVEST_SKIP: &str = "bg_harvest_skip";
65pub(crate) const FIFO_STAGING_MAPPED: &str = "fifo_staging_mapped";
69pub(crate) const FIFO_STAGING_RECLAIM: &str = "fifo_staging_reclaim";
72pub(crate) const PROCFS_SPELLING_RESOLVED: &str = "procfs_spelling_resolved";
77pub(crate) const EXIT_HOOK_TAIL: &str = "exit_hook_tail";
86pub(crate) const RECEIPT_DECISION: &str = "receipt_decision";
91
92const POINTS: [&str; 8] = [
94 BG_PRE_CALL,
95 BG_POST_CALL,
96 BG_HARVEST_SKIP,
97 FIFO_STAGING_MAPPED,
98 FIFO_STAGING_RECLAIM,
99 PROCFS_SPELLING_RESOLVED,
100 EXIT_HOOK_TAIL,
101 RECEIPT_DECISION,
102];
103
104#[derive(Clone, Copy, Debug, PartialEq)]
105struct Spec {
106 lo_ms: u64,
107 hi_ms: u64,
108 probability: f64,
109 hold: bool,
111}
112
113#[derive(Debug)]
114struct Probes {
115 seed: u64,
116 counter: AtomicU64,
117 points: BTreeMap<String, Spec>,
118 hold_dir: Option<PathBuf>,
119}
120
121#[derive(Clone, Debug, PartialEq)]
123enum Perturbation {
124 Wait(Duration),
126 Hold(PathBuf),
128}
129
130fn parse_item(item: &str) -> Result<(String, Spec), String> {
131 let (name, rest) = item
132 .split_once('=')
133 .ok_or_else(|| format!("{item:?} is not point=LO[-HI][@P]"))?;
134 if !POINTS.contains(&name) {
135 return Err(format!("unknown point {name:?}; points are {POINTS:?}"));
136 }
137 if rest == "hold" {
138 return Ok((
139 name.to_owned(),
140 Spec {
141 lo_ms: 0,
142 hi_ms: 0,
143 probability: 1.0,
144 hold: true,
145 },
146 ));
147 }
148 let (range, probability) = match rest.split_once('@') {
149 Some((range, p)) => (
150 range,
151 p.parse::<f64>()
152 .ok()
153 .filter(|p| (0.0..=1.0).contains(p))
154 .ok_or_else(|| format!("{item:?}: probability must be in [0, 1]"))?,
155 ),
156 None => (rest, 1.0),
157 };
158 let number = |text: &str| {
159 text.parse::<u64>()
160 .map_err(|_| format!("{item:?}: {text:?} is not a number of milliseconds"))
161 };
162 let (lo_ms, hi_ms) = match range.split_once('-') {
163 Some((lo, hi)) => (number(lo)?, number(hi)?),
164 None => {
165 let ms = number(range)?;
166 (ms, ms)
167 }
168 };
169 if hi_ms < lo_ms {
170 return Err(format!("{item:?}: the range is empty"));
171 }
172 Ok((
173 name.to_owned(),
174 Spec {
175 lo_ms,
176 hi_ms,
177 probability,
178 hold: false,
179 },
180 ))
181}
182
183impl Probes {
184 fn from_settings(
188 config: Option<&str>,
189 seed: Option<&str>,
190 hold_dir: Option<&str>,
191 ) -> Option<Self> {
192 let config = config?;
193 let hold_dir = hold_dir.filter(|dir| !dir.is_empty()).map(PathBuf::from);
194 let mut points = BTreeMap::new();
199 for item in config.split(',').map(str::trim).filter(|s| !s.is_empty()) {
200 match parse_item(item) {
201 Ok((_, spec)) if spec.hold && hold_dir.is_none() => {
202 eprintln!(
203 "race_probe: ignoring {item:?}: {RACE_PROBE_HOLD_DIR_ENV} is not set"
204 );
205 }
206 Ok((name, spec)) => {
207 points.insert(name, spec);
208 }
209 Err(error) => eprintln!("race_probe: ignoring {error}"),
210 }
211 }
212 if points.is_empty() {
213 return None;
214 }
215 let seed = match seed.map(str::parse::<u64>) {
216 Some(Ok(seed)) => seed,
217 _ => {
218 std::time::SystemTime::now()
219 .duration_since(std::time::UNIX_EPOCH)
220 .map_or(0, |d| d.as_nanos() as u64)
221 ^ u64::from(std::process::id())
222 }
223 };
224 Some(Probes {
225 seed,
226 counter: AtomicU64::new(0),
227 points,
228 hold_dir,
229 })
230 }
231
232 fn choose(&self, point: &str) -> Option<Perturbation> {
235 let spec = self.points.get(point)?;
236 if spec.hold {
237 return Some(Perturbation::Hold(self.hold_dir.as_ref()?.join(point)));
239 }
240 let visit = self.counter.fetch_add(1, Ordering::Relaxed);
241 let r = splitmix64(self.seed ^ splitmix64(visit));
242 let unit = (r >> 11) as f64 / (1u64 << 53) as f64;
243 if unit >= spec.probability {
244 return None;
245 }
246 let span = spec.hi_ms - spec.lo_ms;
247 let ms = spec.lo_ms
248 + if span == 0 {
249 0
250 } else {
251 splitmix64(r) % (span + 1)
252 };
253 Some(Perturbation::Wait(Duration::from_millis(ms)))
254 }
255}
256
257fn probes() -> Option<&'static Probes> {
258 static PROBES: OnceLock<Option<Probes>> = OnceLock::new();
259 PROBES
260 .get_or_init(|| {
261 let config = std::env::var(RACE_PROBE_ENV).ok()?;
262 let seed = std::env::var(RACE_PROBE_SEED_ENV).ok();
263 let hold_dir = std::env::var(RACE_PROBE_HOLD_DIR_ENV).ok();
264 let probes =
265 Probes::from_settings(Some(&config), seed.as_deref(), hold_dir.as_deref())?;
266 eprintln!(
267 "race_probe: {RACE_PROBE_ENV}={config} {RACE_PROBE_SEED_ENV}={}",
268 probes.seed
269 );
270 Some(probes)
271 })
272 .as_ref()
273}
274
275fn choose(point: &str) -> Option<Perturbation> {
276 let perturbation = probes()?.choose(point)?;
277 tracing::debug!(target: "race_probe", "race_probe: {point} perturbed ({perturbation:?})");
278 Some(perturbation)
279}
280
281pub(crate) async fn delay(point: &str) {
283 match choose(point) {
284 Some(Perturbation::Wait(wait)) if !wait.is_zero() => tokio::time::sleep(wait).await,
285 Some(Perturbation::Hold(release)) => hold(point, &release).await,
286 _ => {}
287 }
288}
289
290pub(crate) async fn delay_on_any_backend(point: &str, in_guest: bool) {
302 let Some(perturbation) = choose(point) else {
303 return;
304 };
305 let on_tokio = !in_guest && tokio::runtime::Handle::try_current().is_ok();
306 match perturbation {
307 Perturbation::Wait(wait) if wait.is_zero() => {}
308 Perturbation::Wait(wait) if on_tokio => tokio::time::sleep(wait).await,
309 Perturbation::Wait(wait) => std::thread::sleep(wait),
310 Perturbation::Hold(release) if on_tokio => hold(point, &release).await,
311 Perturbation::Hold(release) => hold_blocking(point, &release),
312 }
313}
314
315async fn hold(point: &str, release: &Path) {
317 if release.exists() {
318 return;
319 }
320 eprintln!(
321 "race_probe: {point} holding until {} exists",
322 release.display()
323 );
324 while !release.exists() {
325 tokio::time::sleep(HOLD_POLL).await;
326 }
327}
328
329fn hold_blocking(point: &str, release: &Path) {
331 if release.exists() {
332 return;
333 }
334 eprintln!(
335 "race_probe: {point} holding until {} exists",
336 release.display()
337 );
338 while !release.exists() {
339 std::thread::sleep(HOLD_POLL);
340 }
341}
342
343pub(crate) fn perturb(point: &str) -> bool {
345 choose(point).is_some()
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 #[test]
353 fn race_probes_are_off_unless_configured() {
354 assert!(Probes::from_settings(None, None, None).is_none());
357 assert!(Probes::from_settings(None, Some("7"), None).is_none());
358 assert!(Probes::from_settings(Some(""), Some("7"), None).is_none());
359 assert!(Probes::from_settings(Some(" , "), None, None).is_none());
360 assert!(Probes::from_settings(Some("no_such_point=5"), None, None).is_none());
361 assert!(std::env::var_os(RACE_PROBE_ENV).is_none());
364 assert!(probes().is_none());
365 for point in POINTS {
366 assert!(!perturb(point));
367 assert_eq!(choose(point), None);
368 }
369 }
370
371 #[test]
372 fn race_probe_settings_parse_as_documented() {
373 assert_eq!(
374 parse_item("bg_pre_call=300"),
375 Ok((
376 BG_PRE_CALL.to_owned(),
377 Spec {
378 lo_ms: 300,
379 hi_ms: 300,
380 probability: 1.0,
381 hold: false,
382 }
383 ))
384 );
385 assert_eq!(
386 parse_item("bg_post_call=0-50@0.25"),
387 Ok((
388 BG_POST_CALL.to_owned(),
389 Spec {
390 lo_ms: 0,
391 hi_ms: 50,
392 probability: 0.25,
393 hold: false,
394 }
395 ))
396 );
397 for bad in [
398 "bg_pre_call",
399 "bg_pre_call=x",
400 "bg_pre_call=5-2",
401 "bg_pre_call=1@2",
402 "bg_harvest_skip=0@-0.1",
403 "unknown=1",
404 ] {
405 assert!(parse_item(bad).is_err(), "{bad} parsed");
406 }
407 let probes = Probes::from_settings(
409 Some("bg_pre_call=x, bg_harvest_skip=0@0.5"),
410 Some("1"),
411 None,
412 )
413 .unwrap();
414 assert_eq!(probes.points.len(), 1);
415 assert!(probes.points.contains_key(BG_HARVEST_SKIP));
416
417 let probes = Probes::from_settings(
420 Some("bg_pre_call=10@0,bg_pre_call=20@1,bg_pre_call=x,bg_harvest_skip=0@0.5"),
421 Some("7"),
422 None,
423 )
424 .unwrap();
425 assert_eq!(probes.points.len(), 2);
426 assert_eq!(
427 probes.points.get(BG_PRE_CALL),
428 Some(&Spec {
429 lo_ms: 20,
430 hi_ms: 20,
431 probability: 1.0,
432 hold: false,
433 })
434 );
435 assert_eq!(
436 probes.choose(BG_PRE_CALL),
437 Some(Perturbation::Wait(Duration::from_millis(20)))
438 );
439 }
440
441 #[test]
442 fn race_probe_choices_follow_probability_range_and_seed() {
443 let draw = |config: &str, seed: &str, point: &str| {
444 let probes = Probes::from_settings(Some(config), Some(seed), None).unwrap();
445 (0..1000).map(|_| probes.choose(point)).collect::<Vec<_>>()
446 };
447 assert!(
448 draw("bg_harvest_skip=0@0", "1", BG_HARVEST_SKIP)
449 .iter()
450 .all(Option::is_none)
451 );
452 assert!(
453 draw("bg_harvest_skip=0@1", "1", BG_HARVEST_SKIP)
454 .iter()
455 .all(|wait| *wait == Some(Perturbation::Wait(Duration::ZERO)))
456 );
457 assert!(
459 draw("bg_harvest_skip=0@1", "1", BG_PRE_CALL)
460 .iter()
461 .all(Option::is_none)
462 );
463 let half = draw("bg_pre_call=10-20@0.5", "3", BG_PRE_CALL);
464 let taken = half.iter().flatten().count();
465 assert!((400..600).contains(&taken), "{taken} of 1000 at 0.5");
466 assert!(half.iter().flatten().all(|wait| {
467 matches!(wait, Perturbation::Wait(wait)
468 if (Duration::from_millis(10)..=Duration::from_millis(20)).contains(wait))
469 }));
470 assert_eq!(half, draw("bg_pre_call=10-20@0.5", "3", BG_PRE_CALL));
472 assert_ne!(half, draw("bg_pre_call=10-20@0.5", "4", BG_PRE_CALL));
473
474 let config = Some("bg_pre_call=10-20@0.5,bg_harvest_skip=0@0.5");
477 let with_missing = Probes::from_settings(config, Some("3"), None).unwrap();
478 let direct = Probes::from_settings(config, Some("3"), None).unwrap();
479 for _ in 0..1000 {
480 assert_eq!(with_missing.choose("no_such_point"), None);
481 assert_eq!(with_missing.choose(BG_POST_CALL), None);
482 assert_eq!(with_missing.choose(BG_PRE_CALL), direct.choose(BG_PRE_CALL));
483 assert_eq!(
484 with_missing.choose(BG_HARVEST_SKIP),
485 direct.choose(BG_HARVEST_SKIP)
486 );
487 }
488 }
489
490 #[test]
491 fn a_hold_needs_a_hold_directory_and_names_its_release_file() {
492 assert_eq!(
493 parse_item("fifo_staging_mapped=hold"),
494 Ok((
495 FIFO_STAGING_MAPPED.to_owned(),
496 Spec {
497 lo_ms: 0,
498 hi_ms: 0,
499 probability: 1.0,
500 hold: true,
501 }
502 ))
503 );
504 for bad in ["fifo_staging_mapped=hold@0.5", "fifo_staging_mapped=holds"] {
505 assert!(parse_item(bad).is_err(), "{bad} parsed");
506 }
507 let probes = Probes::from_settings(
509 Some("fifo_staging_mapped=hold,bg_harvest_skip=0"),
510 Some("1"),
511 None,
512 )
513 .unwrap();
514 assert!(!probes.points.contains_key(FIFO_STAGING_MAPPED));
515 assert!(Probes::from_settings(Some("fifo_staging_mapped=hold"), None, Some("")).is_none());
516 let config = Some("fifo_staging_mapped=hold,fifo_staging_reclaim=hold,bg_pre_call=1-9@0.5");
519 let holding = Probes::from_settings(config, Some("5"), Some("/hold")).unwrap();
520 let direct = Probes::from_settings(Some("bg_pre_call=1-9@0.5"), Some("5"), None).unwrap();
521 for _ in 0..100 {
522 assert_eq!(
523 holding.choose(FIFO_STAGING_MAPPED),
524 Some(Perturbation::Hold(PathBuf::from(
525 "/hold/fifo_staging_mapped"
526 )))
527 );
528 assert_eq!(
529 holding.choose(FIFO_STAGING_RECLAIM),
530 Some(Perturbation::Hold(PathBuf::from(
531 "/hold/fifo_staging_reclaim"
532 )))
533 );
534 assert_eq!(holding.choose(BG_PRE_CALL), direct.choose(BG_PRE_CALL));
535 }
536 }
537
538 #[tokio::test(start_paused = true)]
541 async fn a_held_visit_waits_for_its_release_file() {
542 let dir = tempfile::tempdir().unwrap();
543 let release = dir.path().join(FIFO_STAGING_MAPPED);
544 let mut held = Box::pin(hold(FIFO_STAGING_MAPPED, &release));
545 tokio::select! {
547 _ = &mut held => panic!("released before its file existed"),
548 _ = tokio::time::sleep(Duration::from_secs(60)) => {}
549 }
550 std::fs::write(&release, b"").unwrap();
551 tokio::time::timeout(Duration::from_secs(1), held)
552 .await
553 .expect("released once its file exists");
554 tokio::time::timeout(Duration::ZERO, hold(FIFO_STAGING_MAPPED, &release))
555 .await
556 .expect("an existing file releases at once");
557 }
558}