1use std::time::{Duration, SystemTime, UNIX_EPOCH};
9
10use crate::git::ProbeError;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize))]
18pub struct Generation(u64);
19
20impl Generation {
21 pub(crate) fn new(value: u64) -> Self {
23 Generation(value)
24 }
25
26 pub(crate) fn value(self) -> u64 {
29 self.0
30 }
31
32 #[cfg(test)]
38 pub(crate) fn successor(self) -> Self {
39 Generation(self.0 + 1)
40 }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub struct Timestamp(SystemTime);
50
51impl Timestamp {
52 pub fn now() -> Self {
54 Timestamp(SystemTime::now())
55 }
56
57 #[cfg(any(test, feature = "test-util"))]
65 pub fn at(instant: SystemTime) -> Self {
66 Timestamp(instant)
67 }
68
69 pub fn elapsed(&self) -> Duration {
75 SystemTime::now()
76 .duration_since(self.0)
77 .unwrap_or(Duration::ZERO)
78 }
79}
80
81impl std::fmt::Display for Timestamp {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 let secs = self
86 .0
87 .duration_since(UNIX_EPOCH)
88 .unwrap_or(Duration::ZERO)
89 .as_secs() as i64;
90 let days = secs.div_euclid(86_400);
91 let secs_of_day = secs.rem_euclid(86_400);
92 let (year, month, day) = civil_from_days(days);
93 let hour = secs_of_day / 3_600;
94 let minute = (secs_of_day % 3_600) / 60;
95 let second = secs_of_day % 60;
96 write!(
97 f,
98 "{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z"
99 )
100 }
101}
102
103#[cfg(feature = "serde")]
108impl serde::Serialize for Timestamp {
109 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
110 where
111 S: serde::Serializer,
112 {
113 serializer.collect_str(self)
114 }
115}
116
117fn civil_from_days(days: i64) -> (i64, u32, u32) {
122 let z = days + 719_468;
123 let era = (if z >= 0 { z } else { z - 146_096 }) / 146_097;
124 let day_of_era = z - era * 146_097; let year_of_era =
126 (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365; let year = year_of_era + era * 400;
128 let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100); let month_prime = (5 * day_of_year + 2) / 153; let day = (day_of_year - (153 * month_prime + 2) / 5 + 1) as u32; let month = (if month_prime < 10 {
132 month_prime + 3
133 } else {
134 month_prime - 9
135 }) as u32; let year = if month <= 2 { year + 1 } else { year };
137 (year, month, day)
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145#[cfg_attr(feature = "serde", derive(serde::Serialize))]
146pub enum Unknown {
147 TimedOut,
149 NoDefaultBranch,
151 SubmoduleUninitialized,
156}
157
158#[derive(Debug, Clone)]
161#[cfg_attr(feature = "serde", derive(serde::Serialize))]
162pub enum Settled<T> {
163 Unknown(Unknown),
165 Known {
168 value: T,
169 at: Timestamp,
170 stale: bool,
171 },
172 Failed(ProbeError),
174 NotApplicable,
179}
180
181#[derive(Debug, Clone)]
190#[cfg_attr(feature = "serde", derive(serde::Serialize))]
191pub struct Cell<T> {
192 settled: Option<Settled<T>>,
193 in_flight: bool,
194 #[cfg_attr(feature = "serde", serde(skip))]
197 #[allow(dead_code)] generation: Generation,
199}
200
201impl<T> Default for Cell<T> {
202 fn default() -> Self {
203 Cell {
204 settled: None,
205 in_flight: false,
206 generation: Generation::default(),
207 }
208 }
209}
210
211impl<T> Cell<T> {
212 #[cfg(any(test, feature = "test-util"))]
221 pub fn already_settled(settled: Settled<T>) -> Self {
222 Cell {
223 settled: Some(settled),
224 in_flight: false,
225 generation: Generation::default(),
226 }
227 }
228
229 #[cfg(any(test, feature = "test-util"))]
234 pub fn already_settled_and_in_flight(settled: Settled<T>) -> Self {
235 Cell {
236 settled: Some(settled),
237 in_flight: true,
238 generation: Generation::default(),
239 }
240 }
241
242 pub fn settled(&self) -> Option<&Settled<T>> {
245 self.settled.as_ref()
246 }
247
248 pub fn is_in_flight(&self) -> bool {
251 self.in_flight
252 }
253
254 pub(crate) fn begin_probe(&mut self) {
256 self.in_flight = true;
257 }
258
259 pub(crate) fn settle(&mut self, generation: Generation, settled: Settled<T>) -> bool {
265 if generation < self.generation {
266 return false;
267 }
268 self.generation = generation;
269 self.settled = Some(settled);
270 self.in_flight = false;
271 true
272 }
273
274 pub(crate) fn force_stale(&mut self) {
282 if let Some(Settled::Known {
283 stale,
284 value: _,
285 at: _,
286 }) = &mut self.settled
287 {
288 *stale = true;
289 }
290 }
291
292 pub(crate) fn age_into_stale(&mut self, threshold: Duration) {
298 if let Some(Settled::Known {
299 at,
300 stale,
301 value: _,
302 }) = &mut self.settled
303 && at.elapsed() >= threshold
304 {
305 *stale = true;
306 }
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use std::sync::Arc;
313
314 use super::*;
315
316 #[test]
320 fn unknown_reasons_match_this_documents_own_table() {
321 let spec_path =
322 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../docs/spec/core-api.md");
323 let spec = std::fs::read_to_string(&spec_path)
324 .unwrap_or_else(|error| panic!("read {}: {error}", spec_path.display()));
325
326 let declaration = spec
327 .lines()
328 .find(|line| line.starts_with("pub enum Unknown {"))
329 .unwrap_or_else(|| panic!("no `pub enum Unknown` line in {}", spec_path.display()));
330 let documented: Vec<&str> = declaration
331 .trim_start_matches("pub enum Unknown {")
332 .trim_end_matches('}')
333 .split(',')
334 .map(str::trim)
335 .filter(|name| !name.is_empty())
336 .collect();
337
338 let in_code: Vec<&str> = [
341 Unknown::TimedOut,
342 Unknown::NoDefaultBranch,
343 Unknown::SubmoduleUninitialized,
344 ]
345 .iter()
346 .map(|reason| match reason {
347 Unknown::TimedOut => "TimedOut",
348 Unknown::NoDefaultBranch => "NoDefaultBranch",
349 Unknown::SubmoduleUninitialized => "SubmoduleUninitialized",
350 })
351 .collect();
352
353 assert_eq!(
354 in_code, documented,
355 "`Unknown`'s variants and core-api.md's own enum line disagree; amend the \
356 document's table and its closed-set sentence in the same change as the enum"
357 );
358 for reason in &in_code {
359 assert!(
360 spec.contains(&format!("| `{reason}` |")),
361 "core-api.md's reason table has no row for `{reason}`"
362 );
363 }
364 }
365
366 #[test]
367 fn re_probing_keeps_the_previous_value_instead_of_blanking() {
368 let mut cell: Cell<u32> = Cell::default();
369 cell.settle(
370 Generation::new(1),
371 Settled::Known {
372 value: 7,
373 at: Timestamp::now(),
374 stale: false,
375 },
376 );
377
378 cell.begin_probe();
379
380 match cell.settled() {
381 Some(Settled::Known {
382 value,
383 at: _,
384 stale: _,
385 }) => assert_eq!(*value, 7),
386 other => {
387 panic!("expected the previous Known value to survive a re-probe, got {other:?}")
388 }
389 }
390 }
391
392 #[test]
393 fn absent_before_any_probe_is_distinct_from_absent_while_loading() {
394 let never_probed: Cell<u32> = Cell::default();
395 assert!(never_probed.settled().is_none());
396 assert!(!never_probed.in_flight);
397
398 let mut loading: Cell<u32> = Cell::default();
399 loading.begin_probe();
400 assert!(loading.settled().is_none());
401 assert!(loading.in_flight);
402 }
403
404 #[test]
405 fn a_lower_generation_write_does_not_overwrite_a_higher_one() {
406 let mut cell: Cell<u32> = Cell::default();
407 cell.settle(
408 Generation::new(2),
409 Settled::Known {
410 value: 9,
411 at: Timestamp::now(),
412 stale: false,
413 },
414 );
415
416 cell.settle(
417 Generation::new(1),
418 Settled::Known {
419 value: 1,
420 at: Timestamp::now(),
421 stale: false,
422 },
423 );
424
425 match cell.settled() {
426 Some(Settled::Known {
427 value,
428 at: _,
429 stale: _,
430 }) => assert_eq!(*value, 9),
431 other => panic!("expected the higher Generation's value to survive, got {other:?}"),
432 }
433 }
434
435 #[test]
436 fn every_settled_shape_round_trips_through_settle_and_settled() {
437 let mut unknown_cell: Cell<u32> = Cell::default();
438 unknown_cell.settle(Generation::new(1), Settled::Unknown(Unknown::TimedOut));
439 assert!(matches!(
440 unknown_cell.settled(),
441 Some(Settled::Unknown(Unknown::TimedOut))
442 ));
443
444 let mut failed_cell: Cell<u32> = Cell::default();
445 failed_cell.settle(
446 Generation::new(1),
447 Settled::Failed(ProbeError::Open(Arc::from("boom"))),
448 );
449 assert!(matches!(
450 failed_cell.settled(),
451 Some(Settled::Failed(ProbeError::Open(_)))
452 ));
453
454 let mut not_applicable_cell: Cell<u32> = Cell::default();
455 not_applicable_cell.settle(Generation::new(1), Settled::NotApplicable);
456 assert!(matches!(
457 not_applicable_cell.settled(),
458 Some(Settled::NotApplicable)
459 ));
460 }
461
462 #[test]
463 fn force_stale_marks_a_known_value_stale_without_changing_it() {
464 let mut cell: Cell<u32> = Cell::default();
465 cell.settle(
466 Generation::new(1),
467 Settled::Known {
468 value: 42,
469 at: Timestamp::now(),
470 stale: false,
471 },
472 );
473
474 cell.force_stale();
475
476 match cell.settled() {
477 Some(Settled::Known {
478 value,
479 stale,
480 at: _,
481 }) => {
482 assert_eq!(*value, 42, "the value must survive being forced stale");
483 assert!(*stale, "the cell must be marked stale");
484 }
485 other => panic!("expected the Known value to survive, got {other:?}"),
486 }
487 }
488
489 #[test]
490 fn age_into_stale_marks_a_known_value_stale_once_it_is_old_enough() {
491 let mut cell: Cell<u32> = Cell::default();
492 cell.settle(
493 Generation::new(1),
494 Settled::Known {
495 value: 42,
496 at: Timestamp::at(SystemTime::now() - Duration::from_secs(3600)),
497 stale: false,
498 },
499 );
500
501 cell.age_into_stale(Duration::from_secs(300));
502
503 match cell.settled() {
504 Some(Settled::Known {
505 value,
506 stale,
507 at: _,
508 }) => {
509 assert_eq!(*value, 42, "the value must survive ageing into stale");
510 assert!(
511 *stale,
512 "an hour-old value past a five-minute threshold must go stale"
513 );
514 }
515 other => panic!("expected the Known value to survive, got {other:?}"),
516 }
517 }
518
519 #[test]
520 fn age_into_stale_leaves_a_known_value_fresh_before_the_threshold() {
521 let mut cell: Cell<u32> = Cell::default();
522 cell.settle(
523 Generation::new(1),
524 Settled::Known {
525 value: 7,
526 at: Timestamp::now(),
527 stale: false,
528 },
529 );
530
531 cell.age_into_stale(Duration::from_secs(300));
532
533 match cell.settled() {
534 Some(Settled::Known {
535 stale,
536 value: _,
537 at: _,
538 }) => {
539 assert!(
540 !*stale,
541 "a value settled moments ago must not age into stale yet"
542 )
543 }
544 other => panic!("expected a fresh Known value, got {other:?}"),
545 }
546 }
547
548 #[test]
549 fn age_into_stale_on_a_cell_with_no_known_value_is_a_no_op() {
550 let mut unknown_cell: Cell<u32> = Cell::default();
551 unknown_cell.settle(Generation::new(1), Settled::Unknown(Unknown::TimedOut));
552 unknown_cell.age_into_stale(Duration::ZERO);
553 assert!(matches!(
554 unknown_cell.settled(),
555 Some(Settled::Unknown(Unknown::TimedOut))
556 ));
557
558 let mut never_probed: Cell<u32> = Cell::default();
559 never_probed.age_into_stale(Duration::ZERO);
560 assert!(never_probed.settled().is_none());
561 }
562
563 #[test]
564 fn force_stale_on_a_cell_with_no_known_value_is_a_no_op() {
565 let mut unknown_cell: Cell<u32> = Cell::default();
566 unknown_cell.settle(Generation::new(1), Settled::Unknown(Unknown::TimedOut));
567 unknown_cell.force_stale();
568 assert!(matches!(
569 unknown_cell.settled(),
570 Some(Settled::Unknown(Unknown::TimedOut))
571 ));
572
573 let mut never_probed: Cell<u32> = Cell::default();
574 never_probed.force_stale();
575 assert!(never_probed.settled().is_none());
576 }
577
578 #[test]
579 fn a_settled_cell_clones() {
580 let mut cell: Cell<u32> = Cell::default();
581 cell.settle(
582 Generation::new(1),
583 Settled::Known {
584 value: 3,
585 at: Timestamp::now(),
586 stale: false,
587 },
588 );
589
590 let cloned = cell.clone();
591
592 match cloned.settled() {
593 Some(Settled::Known {
594 value,
595 at: _,
596 stale: _,
597 }) => assert_eq!(*value, 3),
598 other => panic!("expected the clone to carry the same Known value, got {other:?}"),
599 }
600 }
601
602 #[test]
603 fn elapsed_reads_zero_for_a_timestamp_in_the_future_rather_than_a_negative_duration() {
604 let future = Timestamp::at(SystemTime::now() + Duration::from_secs(3600));
605
606 assert_eq!(future.elapsed(), Duration::ZERO);
607 }
608
609 #[test]
610 fn elapsed_reads_a_positive_duration_for_a_timestamp_in_the_past() {
611 let past = Timestamp::at(SystemTime::now() - Duration::from_secs(90));
612
613 assert!(past.elapsed() >= Duration::from_secs(90));
614 }
615
616 #[test]
617 fn timestamp_formats_as_rfc3339() {
618 let cases: [(u64, &str); 6] = [
619 (0, "1970-01-01T00:00:00Z"),
620 (1, "1970-01-01T00:00:01Z"),
621 (86_399, "1970-01-01T23:59:59Z"),
622 (86_400, "1970-01-02T00:00:00Z"),
623 (951_782_400, "2000-02-29T00:00:00Z"),
624 (1_700_000_000, "2023-11-14T22:13:20Z"),
625 ];
626
627 for (epoch_secs, expected) in cases {
628 let timestamp = Timestamp(UNIX_EPOCH + Duration::from_secs(epoch_secs));
629 assert_eq!(timestamp.to_string(), expected);
630 }
631 }
632}