1use crate::data::{DataCatalogError, DistributionSource, ProductIdentity};
13use serde::{Deserialize, Serialize};
14use sha2::{Digest, Sha256};
15use std::fmt::Write as _;
16use std::time::Duration;
17
18pub const EXACT_CACHE_SCHEMA_VERSION: u8 = 3;
20
21pub const EXACT_CACHE_CONTROL_DIRECTORY: &str = ".sidereon-cache-v3";
23
24pub const EXACT_CACHE_MARKER_FILENAME: &str = "current.json";
26
27#[derive(Debug, thiserror::Error)]
29pub enum ExactCacheError {
30 #[error("invalid exact product identity: {0}")]
32 Identity(#[from] DataCatalogError),
33 #[error("invalid exact-cache entry identifier")]
35 InvalidEntryId,
36 #[error("invalid or mismatched exact-cache commit: {0}")]
38 InvalidCommit(&'static str),
39 #[cfg(not(target_arch = "wasm32"))]
41 #[error("exact-cache {operation} failed: {source}")]
42 Io {
43 operation: &'static str,
45 #[source]
47 source: std::io::Error,
48 },
49 #[cfg(not(target_arch = "wasm32"))]
51 #[error("timed out waiting for the exact-cache lock")]
52 LockTimeout,
53 #[error("timed out waiting for the exact-cache in-flight owner")]
55 SingleFlightTimeout,
56 #[error("exact-cache single-flight ownership was lost")]
58 SingleFlightOwnershipLost,
59 #[error("invalid exact-cache single-flight options")]
61 InvalidSingleFlightOptions,
62 #[cfg(not(target_arch = "wasm32"))]
64 #[error("durable exact-cache publication is unsupported on this platform")]
65 UnsupportedPlatform,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct ExactCacheDigests {
71 pub identity_sha256: String,
73 pub distribution_source: String,
75 pub product_sha256: String,
77 pub product_byte_length: u64,
79 pub archive_sha256: String,
81 pub archive_byte_length: u64,
83 pub provenance_sha256: String,
85 pub provenance_byte_length: u64,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct VerifiedExactCacheCommit {
92 pub entry_id: String,
94 pub digests: ExactCacheDigests,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub struct ExactCacheSingleFlightOptions {
101 pub poll_interval: Duration,
103 pub heartbeat_interval: Duration,
105 pub liveness_timeout: Duration,
107 pub wait_timeout: Duration,
109}
110
111impl Default for ExactCacheSingleFlightOptions {
112 fn default() -> Self {
113 Self {
114 poll_interval: Duration::from_millis(50),
115 heartbeat_interval: Duration::from_secs(5),
116 liveness_timeout: Duration::from_secs(30),
117 wait_timeout: Duration::from_secs(30 * 60),
118 }
119 }
120}
121
122impl ExactCacheSingleFlightOptions {
123 fn validate(self) -> Result<Self, ExactCacheError> {
124 if self.poll_interval.is_zero()
125 || self.heartbeat_interval.is_zero()
126 || self.liveness_timeout.is_zero()
127 || self.wait_timeout.is_zero()
128 || self.heartbeat_interval >= self.liveness_timeout
129 {
130 return Err(ExactCacheError::InvalidSingleFlightOptions);
131 }
132 Ok(self)
133 }
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum ExactCacheSingleFlightDecision {
139 Wait(Duration),
141 Takeover,
143 Timeout,
145}
146
147#[derive(Debug, Clone)]
149pub struct ExactCacheSingleFlightWait {
150 started: Duration,
151 unchanged_since: Duration,
152 observation_sha256: Option<[u8; 32]>,
153}
154
155impl ExactCacheSingleFlightWait {
156 #[must_use]
158 pub fn new(now: Duration) -> Self {
159 Self {
160 started: now,
161 unchanged_since: now,
162 observation_sha256: None,
163 }
164 }
165
166 pub fn observe(
171 &mut self,
172 now: Duration,
173 revision: &[u8],
174 options: ExactCacheSingleFlightOptions,
175 ) -> Result<ExactCacheSingleFlightDecision, ExactCacheError> {
176 let options = options.validate()?;
177 let revision_sha256: [u8; 32] = Sha256::digest(revision).into();
178 if self.observation_sha256 != Some(revision_sha256) {
179 self.observation_sha256 = Some(revision_sha256);
180 self.unchanged_since = now;
181 }
182
183 let no_progress = now.saturating_sub(self.unchanged_since);
184 if no_progress >= options.liveness_timeout {
185 return Ok(ExactCacheSingleFlightDecision::Takeover);
186 }
187 let elapsed = now.saturating_sub(self.started);
188 if elapsed >= options.wait_timeout {
189 return Ok(ExactCacheSingleFlightDecision::Timeout);
190 }
191 Ok(ExactCacheSingleFlightDecision::Wait(
192 options
193 .poll_interval
194 .min(options.wait_timeout - elapsed)
195 .min(options.liveness_timeout - no_progress),
196 ))
197 }
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
201#[serde(deny_unknown_fields)]
202struct CommitRecord {
203 schema_version: u8,
204 entry: String,
205 identity_sha256: String,
206 distribution_source: String,
207 product_sha256: String,
208 product_byte_length: u64,
209 archive_sha256: String,
210 archive_byte_length: u64,
211 provenance_sha256: String,
212 provenance_byte_length: u64,
213}
214
215pub fn identity_sha256(identity: &ProductIdentity) -> Result<String, ExactCacheError> {
217 Ok(sha256_hex(&identity.canonical_bytes()?))
218}
219
220pub fn build_commit_record(
228 identity: &ProductIdentity,
229 source: DistributionSource,
230 entry_id: &str,
231 product: &[u8],
232 archive: &[u8],
233 provenance: &[u8],
234) -> Result<Vec<u8>, ExactCacheError> {
235 validate_entry_id(entry_id)?;
236 let record = CommitRecord {
237 schema_version: EXACT_CACHE_SCHEMA_VERSION,
238 entry: entry_id.to_owned(),
239 identity_sha256: identity_sha256(identity)?,
240 distribution_source: source.code().to_owned(),
241 product_sha256: sha256_hex(product),
242 product_byte_length: byte_length(product)?,
243 archive_sha256: sha256_hex(archive),
244 archive_byte_length: byte_length(archive)?,
245 provenance_sha256: sha256_hex(provenance),
246 provenance_byte_length: byte_length(provenance)?,
247 };
248 serde_json::to_vec(&record).map_err(|_| ExactCacheError::InvalidCommit("serialization"))
249}
250
251pub fn verify_commit_record(
258 identity: &ProductIdentity,
259 source: DistributionSource,
260 marker: &[u8],
261 product: &[u8],
262 archive: &[u8],
263 provenance: &[u8],
264) -> Result<VerifiedExactCacheCommit, ExactCacheError> {
265 let record: CommitRecord = serde_json::from_slice(marker)
266 .map_err(|_| ExactCacheError::InvalidCommit("malformed JSON"))?;
267 if record.schema_version != EXACT_CACHE_SCHEMA_VERSION {
268 return Err(ExactCacheError::InvalidCommit("schema version"));
269 }
270 validate_entry_id(&record.entry)?;
271 let expected_identity = identity_sha256(identity)?;
272 let expected = ExactCacheDigests {
273 identity_sha256: expected_identity,
274 distribution_source: source.code().to_owned(),
275 product_sha256: sha256_hex(product),
276 product_byte_length: byte_length(product)?,
277 archive_sha256: sha256_hex(archive),
278 archive_byte_length: byte_length(archive)?,
279 provenance_sha256: sha256_hex(provenance),
280 provenance_byte_length: byte_length(provenance)?,
281 };
282 let actual = ExactCacheDigests {
283 identity_sha256: record.identity_sha256,
284 distribution_source: record.distribution_source,
285 product_sha256: record.product_sha256,
286 product_byte_length: record.product_byte_length,
287 archive_sha256: record.archive_sha256,
288 archive_byte_length: record.archive_byte_length,
289 provenance_sha256: record.provenance_sha256,
290 provenance_byte_length: record.provenance_byte_length,
291 };
292 if actual != expected {
293 return Err(ExactCacheError::InvalidCommit("identity, source, or bytes"));
294 }
295 Ok(VerifiedExactCacheCommit {
296 entry_id: record.entry,
297 digests: actual,
298 })
299}
300
301fn validate_entry_id(entry_id: &str) -> Result<(), ExactCacheError> {
302 if entry_id.len() == 32
303 && entry_id
304 .bytes()
305 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
306 {
307 Ok(())
308 } else {
309 Err(ExactCacheError::InvalidEntryId)
310 }
311}
312
313fn byte_length(bytes: &[u8]) -> Result<u64, ExactCacheError> {
314 u64::try_from(bytes.len()).map_err(|_| ExactCacheError::InvalidCommit("byte length overflow"))
315}
316
317fn sha256_hex(bytes: &[u8]) -> String {
318 let digest = Sha256::digest(bytes);
319 let mut encoded = String::with_capacity(64);
320 for byte in digest {
321 write!(&mut encoded, "{byte:02x}").expect("writing to String cannot fail");
322 }
323 encoded
324}
325
326#[cfg(not(target_arch = "wasm32"))]
327mod native {
328 use super::*;
329 use fs2::FileExt;
330 use std::fs::{self, File, OpenOptions};
331 use std::io::{ErrorKind, Write};
332 use std::path::{Path, PathBuf};
333 use std::sync::atomic::{AtomicBool, Ordering};
334 use std::sync::{Arc, Condvar, Mutex, OnceLock};
335 use std::thread::{self, JoinHandle};
336 use std::time::{Instant, SystemTime, UNIX_EPOCH};
337
338 const LOCK_FILENAME: &str = ".sidereon-cache.lock";
339 const INFLIGHT_FILENAME: &str = "in-flight.json";
340 const INFLIGHT_HEARTBEAT_DIRECTORY: &str = "in-flight-heartbeats";
341 const INFLIGHT_PROTOCOL_VERSION: u8 = 1;
342
343 #[derive(Debug, Clone)]
345 pub struct CommittedExactCacheEntry {
346 pub entry_id: String,
348 pub product_path: PathBuf,
350 pub archive_path: PathBuf,
352 pub provenance_path: PathBuf,
354 pub product: Vec<u8>,
356 pub archive: Vec<u8>,
358 pub provenance: Vec<u8>,
360 }
361
362 #[derive(Debug)]
364 pub enum ExactCacheOpen {
365 Hit(CommittedExactCacheEntry),
368 Owner(ExactCacheOwner),
370 }
371
372 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
373 #[serde(deny_unknown_fields)]
374 struct InflightRecord {
375 protocol_version: u8,
376 owner_token: String,
377 process_id: u32,
378 process_nonce: String,
379 created_unix_ms: u64,
380 identity_sha256: String,
381 distribution_source: String,
382 }
383
384 #[derive(Debug, Clone, PartialEq, Eq)]
385 struct InflightSnapshot {
386 marker: Vec<u8>,
387 record: Option<InflightRecord>,
388 heartbeat: Option<HeartbeatFingerprint>,
389 }
390
391 impl InflightSnapshot {
392 fn revision(&self) -> [u8; 32] {
393 let mut digest = Sha256::new();
394 digest.update(self.marker.len().to_be_bytes());
395 digest.update(&self.marker);
396 match &self.heartbeat {
397 Some(heartbeat) => {
398 digest.update([1]);
399 digest.update(heartbeat.byte_length.to_be_bytes());
400 match heartbeat.modified {
401 Some(modified) => match modified.duration_since(UNIX_EPOCH) {
402 Ok(duration) => {
403 digest.update([1]);
404 digest.update(duration.as_secs().to_be_bytes());
405 digest.update(duration.subsec_nanos().to_be_bytes());
406 }
407 Err(error) => {
408 digest.update([2]);
409 digest.update(error.duration().as_secs().to_be_bytes());
410 digest.update(error.duration().subsec_nanos().to_be_bytes());
411 }
412 },
413 None => digest.update([0]),
414 }
415 }
416 None => digest.update([0]),
417 }
418 digest.finalize().into()
419 }
420 }
421
422 #[derive(Debug, Clone, PartialEq, Eq)]
423 struct HeartbeatFingerprint {
424 byte_length: u64,
425 modified: Option<SystemTime>,
426 }
427
428 #[derive(Debug)]
429 struct HeartbeatControl {
430 stopped: Mutex<bool>,
431 wake: Condvar,
432 }
433
434 impl HeartbeatControl {
435 fn wait(&self, interval: Duration) -> bool {
436 let stopped = self.stopped.lock().expect("heartbeat mutex poisoned");
437 let (stopped, _) = self
438 .wake
439 .wait_timeout_while(stopped, interval, |stopped| !*stopped)
440 .expect("heartbeat condition variable poisoned");
441 *stopped
442 }
443
444 fn stop(&self) {
445 *self.stopped.lock().expect("heartbeat mutex poisoned") = true;
446 self.wake.notify_all();
447 }
448 }
449
450 pub struct ExactCacheOwner {
452 cache: ExactProductCache,
453 token: String,
454 options: ExactCacheSingleFlightOptions,
455 heartbeat_control: Arc<HeartbeatControl>,
456 heartbeat_failed: Arc<AtomicBool>,
457 heartbeat_thread: Option<JoinHandle<()>>,
458 released: bool,
459 }
460
461 impl std::fmt::Debug for ExactCacheOwner {
462 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
463 formatter
464 .debug_struct("ExactCacheOwner")
465 .field("stable_path", &self.cache.stable_path)
466 .field("token", &self.token)
467 .field("options", &self.options)
468 .field("heartbeat_failed", &self.heartbeat_failed)
469 .field("released", &self.released)
470 .finish_non_exhaustive()
471 }
472 }
473
474 trait MonotonicClock {
475 fn now(&self) -> Duration;
476 fn sleep(&self, duration: Duration);
477 }
478
479 struct SystemMonotonicClock {
480 origin: Instant,
481 }
482
483 impl SystemMonotonicClock {
484 fn new() -> Self {
485 Self {
486 origin: Instant::now(),
487 }
488 }
489 }
490
491 impl MonotonicClock for SystemMonotonicClock {
492 fn now(&self) -> Duration {
493 self.origin.elapsed()
494 }
495
496 fn sleep(&self, duration: Duration) {
497 thread::sleep(duration);
498 }
499 }
500
501 #[cfg(feature = "exact-cache-test-failpoints")]
502 #[derive(Debug, Clone)]
503 #[doc(hidden)]
504 pub struct ExactCacheTestClock {
505 state: Arc<(Mutex<TestClockState>, Condvar)>,
506 }
507
508 #[cfg(feature = "exact-cache-test-failpoints")]
509 #[derive(Debug)]
510 struct TestClockState {
511 now: Duration,
512 sleepers: usize,
513 }
514
515 #[cfg(feature = "exact-cache-test-failpoints")]
516 impl ExactCacheTestClock {
517 #[must_use]
519 pub fn new() -> Self {
520 Self {
521 state: Arc::new((
522 Mutex::new(TestClockState {
523 now: Duration::ZERO,
524 sleepers: 0,
525 }),
526 Condvar::new(),
527 )),
528 }
529 }
530
531 pub fn advance(&self, duration: Duration) {
533 let (state, wake) = &*self.state;
534 let mut state = state.lock().expect("test clock mutex poisoned");
535 state.now = state.now.saturating_add(duration);
536 wake.notify_all();
537 }
538
539 pub fn wait_for_sleepers(&self, count: usize) {
541 let deadline = Instant::now() + Duration::from_secs(10);
542 let (state, wake) = &*self.state;
543 let mut state = state.lock().expect("test clock mutex poisoned");
544 while state.sleepers < count {
545 let remaining = deadline
546 .checked_duration_since(Instant::now())
547 .expect("timed out waiting for test-clock sleeper");
548 let (next, timeout) = wake
549 .wait_timeout(state, remaining)
550 .expect("test clock condition variable poisoned");
551 state = next;
552 assert!(
553 !timeout.timed_out() || state.sleepers >= count,
554 "timed out waiting for test-clock sleeper"
555 );
556 }
557 }
558 }
559
560 #[cfg(feature = "exact-cache-test-failpoints")]
561 impl Default for ExactCacheTestClock {
562 fn default() -> Self {
563 Self::new()
564 }
565 }
566
567 #[cfg(feature = "exact-cache-test-failpoints")]
568 impl MonotonicClock for ExactCacheTestClock {
569 fn now(&self) -> Duration {
570 self.state.0.lock().expect("test clock mutex poisoned").now
571 }
572
573 fn sleep(&self, duration: Duration) {
574 let (state, wake) = &*self.state;
575 let mut state = state.lock().expect("test clock mutex poisoned");
576 let deadline = state.now.saturating_add(duration);
577 state.sleepers += 1;
578 wake.notify_all();
579 while state.now < deadline {
580 state = wake
581 .wait(state)
582 .expect("test clock condition variable poisoned");
583 }
584 }
585 }
586
587 #[derive(Debug, Clone)]
589 pub struct ExactProductCache {
590 stable_path: PathBuf,
591 identity: ProductIdentity,
592 source: DistributionSource,
593 }
594
595 pub struct ExactCacheGuard {
597 lock_file: File,
598 stable_path: PathBuf,
599 }
600
601 impl Drop for ExactCacheGuard {
602 fn drop(&mut self) {
603 let _ = FileExt::unlock(&self.lock_file);
604 }
605 }
606
607 impl ExactCacheOwner {
608 fn new(
609 cache: ExactProductCache,
610 token: String,
611 options: ExactCacheSingleFlightOptions,
612 ) -> Self {
613 let heartbeat_control = Arc::new(HeartbeatControl {
614 stopped: Mutex::new(false),
615 wake: Condvar::new(),
616 });
617 let heartbeat_failed = Arc::new(AtomicBool::new(false));
618 let thread_cache = cache.clone();
619 let thread_token = token.clone();
620 let thread_control = Arc::clone(&heartbeat_control);
621 let thread_failed = Arc::clone(&heartbeat_failed);
622 let heartbeat_thread = thread::Builder::new()
623 .name("exact-cache-heartbeat".to_owned())
624 .spawn(move || {
625 while !thread_control.wait(options.heartbeat_interval) {
626 if thread_cache
627 .refresh_inflight_heartbeat(&thread_token)
628 .is_err()
629 {
630 thread_failed.store(true, Ordering::Release);
631 break;
632 }
633 }
634 })
635 .ok();
636 if heartbeat_thread.is_none() {
637 heartbeat_failed.store(true, Ordering::Release);
638 }
639 Self {
640 cache,
641 token,
642 options,
643 heartbeat_control,
644 heartbeat_failed,
645 heartbeat_thread,
646 released: false,
647 }
648 }
649
650 pub fn heartbeat(&self) -> Result<(), ExactCacheError> {
652 if self.heartbeat_failed.load(Ordering::Acquire) {
653 return Err(ExactCacheError::SingleFlightOwnershipLost);
654 }
655 let result = self.cache.refresh_inflight_heartbeat(&self.token);
656 if result.is_err() {
657 self.heartbeat_failed.store(true, Ordering::Release);
658 }
659 result
660 }
661
662 pub fn publish(
664 mut self,
665 product: &[u8],
666 archive: &[u8],
667 provenance: &[u8],
668 ) -> Result<CommittedExactCacheEntry, ExactCacheError> {
669 self.stop_heartbeat();
670 if self.heartbeat_failed.load(Ordering::Acquire) {
671 return Err(ExactCacheError::SingleFlightOwnershipLost);
672 }
673 let guard = self.cache.lock(self.options.wait_timeout)?;
674 if !self.cache.inflight_token_is_current(&self.token)? {
675 return Err(ExactCacheError::SingleFlightOwnershipLost);
676 }
677 let entry = self.cache.publish(&guard, product, archive, provenance)?;
678 self.cache.release_inflight(&guard, &self.token)?;
679 self.released = true;
680 Ok(entry)
681 }
682
683 fn stop_heartbeat(&mut self) {
684 self.heartbeat_control.stop();
685 if let Some(heartbeat_thread) = self.heartbeat_thread.take() {
686 if heartbeat_thread.join().is_err() {
687 self.heartbeat_failed.store(true, Ordering::Release);
688 }
689 }
690 }
691 }
692
693 impl Drop for ExactCacheOwner {
694 fn drop(&mut self) {
695 self.stop_heartbeat();
696 if self.released {
697 return;
698 }
699 if let Ok(guard) = self.cache.lock(Duration::ZERO) {
700 let _ = self.cache.release_inflight(&guard, &self.token);
701 }
702 }
703 }
704
705 impl ExactProductCache {
706 pub fn new(
708 stable_path: impl Into<PathBuf>,
709 identity: ProductIdentity,
710 source: DistributionSource,
711 ) -> Result<Self, ExactCacheError> {
712 identity.validate()?;
713 let stable_path = stable_path.into();
714 if stable_path.file_name().is_none() || stable_path.parent().is_none() {
715 return Err(ExactCacheError::InvalidCommit("stable product path"));
716 }
717 Ok(Self {
718 stable_path,
719 identity,
720 source,
721 })
722 }
723
724 #[must_use]
726 pub fn stable_path(&self) -> &Path {
727 &self.stable_path
728 }
729
730 pub fn open_single_flight(
735 &self,
736 options: ExactCacheSingleFlightOptions,
737 ) -> Result<ExactCacheOpen, ExactCacheError> {
738 let clock = SystemMonotonicClock::new();
739 self.open_single_flight_with_clock(options, &clock)
740 }
741
742 #[cfg(feature = "exact-cache-test-failpoints")]
744 #[doc(hidden)]
745 pub fn open_single_flight_with_test_clock(
746 &self,
747 options: ExactCacheSingleFlightOptions,
748 clock: &ExactCacheTestClock,
749 ) -> Result<ExactCacheOpen, ExactCacheError> {
750 self.open_single_flight_with_clock(options, clock)
751 }
752
753 pub fn lock(&self, timeout: Duration) -> Result<ExactCacheGuard, ExactCacheError> {
755 ensure_supported_platform()?;
756 let parent = self
757 .stable_path
758 .parent()
759 .ok_or(ExactCacheError::InvalidCommit("stable product parent"))?;
760 durable_create_dir_all(parent)?;
761 let lock_path = parent.join(LOCK_FILENAME);
762 let lock_file = OpenOptions::new()
763 .create(true)
764 .truncate(false)
765 .read(true)
766 .write(true)
767 .open(lock_path)
768 .map_err(|source| io("open lock", source))?;
769 lock_file
770 .sync_all()
771 .map_err(|source| io("sync lock", source))?;
772 sync_directory(parent)?;
773 let deadline = Instant::now()
774 .checked_add(timeout)
775 .ok_or(ExactCacheError::LockTimeout)?;
776 loop {
777 match lock_file.try_lock_exclusive() {
778 Ok(()) => {
779 return Ok(ExactCacheGuard {
780 lock_file,
781 stable_path: self.stable_path.clone(),
782 });
783 }
784 Err(error) if error.kind() == ErrorKind::WouldBlock => {
785 let now = Instant::now();
786 if now >= deadline {
787 return Err(ExactCacheError::LockTimeout);
788 }
789 thread::sleep((deadline - now).min(Duration::from_millis(10)));
790 }
791 Err(source) => return Err(io("lock", source)),
792 }
793 }
794 }
795
796 pub fn read(&self) -> Result<Option<CommittedExactCacheEntry>, ExactCacheError> {
801 let marker_path = self.marker_path();
802 for _ in 0..16 {
803 let marker = match fs::read(&marker_path) {
804 Ok(bytes) => bytes,
805 Err(error) if error.kind() == ErrorKind::NotFound => return Ok(None),
806 Err(source) => return Err(io("read marker", source)),
807 };
808 test_read_barrier();
809 match self.read_committed_entry(&marker) {
810 Ok(entry) => return Ok(Some(entry)),
811 Err(error) => match fs::read(&marker_path) {
812 Ok(current) if current != marker => continue,
813 Err(current_error) if current_error.kind() == ErrorKind::NotFound => {
814 continue;
815 }
816 _ => return Err(error),
817 },
818 }
819 }
820 Err(ExactCacheError::InvalidCommit(
821 "commit changed repeatedly during read",
822 ))
823 }
824
825 fn read_committed_entry(
826 &self,
827 marker: &[u8],
828 ) -> Result<CommittedExactCacheEntry, ExactCacheError> {
829 let record: CommitRecord = serde_json::from_slice(marker)
830 .map_err(|_| ExactCacheError::InvalidCommit("malformed JSON"))?;
831 validate_entry_id(&record.entry)?;
832 let paths = self.entry_paths(&record.entry)?;
833 let product = fs::read(&paths.product).map_err(|source| io("read product", source))?;
834 let archive = fs::read(&paths.archive).map_err(|source| io("read archive", source))?;
835 let provenance =
836 fs::read(&paths.provenance).map_err(|source| io("read provenance", source))?;
837 let verified = verify_commit_record(
838 &self.identity,
839 self.source,
840 marker,
841 &product,
842 &archive,
843 &provenance,
844 )?;
845 Ok(CommittedExactCacheEntry {
846 entry_id: verified.entry_id,
847 product_path: paths.product,
848 archive_path: paths.archive,
849 provenance_path: paths.provenance,
850 product,
851 archive,
852 provenance,
853 })
854 }
855
856 pub fn publish(
858 &self,
859 guard: &ExactCacheGuard,
860 product: &[u8],
861 archive: &[u8],
862 provenance: &[u8],
863 ) -> Result<CommittedExactCacheEntry, ExactCacheError> {
864 self.require_guard(guard)?;
865 ensure_supported_platform()?;
866 let control = self.control_directory();
867 let entries = control.join("entries");
868 durable_create_dir_all(&entries)?;
869 let entry_id = self.allocate_entry_id(&entries)?;
870 let paths = self.entry_paths(&entry_id)?;
871 let entry_directory = paths
872 .product
873 .parent()
874 .ok_or(ExactCacheError::InvalidCommit("entry parent"))?;
875 sync_directory(&entries)?;
876 let marker_temp =
877 control.join(format!(".{EXACT_CACHE_MARKER_FILENAME}.{entry_id}.tmp"));
878 let marker = build_commit_record(
879 &self.identity,
880 self.source,
881 &entry_id,
882 product,
883 archive,
884 provenance,
885 )?;
886 let result: Result<(), ExactCacheError> = (|| {
887 write_exclusive(&paths.product, product)?;
888 test_failpoint("after_payload");
889 write_exclusive(&paths.archive, archive)?;
890 test_failpoint("after_archive");
891 write_exclusive(&paths.provenance, provenance)?;
892 test_failpoint("after_metadata");
893 sync_directory(entry_directory)?;
894 sync_directory(&entries)?;
895 test_failpoint("after_entry_sync");
896 write_exclusive(&marker_temp, &marker)?;
897 test_failpoint("after_marker_write");
898 fs::rename(&marker_temp, self.marker_path())
899 .map_err(|source| io("rename marker", source))?;
900 test_failpoint("after_marker_rename");
901 sync_directory(&control)?;
902 test_failpoint("after_commit_sync");
903 Ok(())
904 })();
905 if result.is_err() {
906 let _ = fs::remove_file(&marker_temp);
907 if self.current_entry_id().as_deref() != Some(entry_id.as_str()) {
908 let _ = fs::remove_dir_all(entry_directory);
909 }
910 }
911 result?;
912 Ok(CommittedExactCacheEntry {
913 entry_id,
914 product_path: paths.product,
915 archive_path: paths.archive,
916 provenance_path: paths.provenance,
917 product: product.to_vec(),
918 archive: archive.to_vec(),
919 provenance: provenance.to_vec(),
920 })
921 }
922
923 pub fn cleanup_abandoned(&self, guard: &ExactCacheGuard) -> Result<(), ExactCacheError> {
925 self.require_guard(guard)?;
926 let control = self.control_directory();
927 let entries = control.join("entries");
928 let current = match fs::read(self.marker_path()) {
929 Ok(marker) => {
930 let record: CommitRecord = match serde_json::from_slice(&marker) {
931 Ok(record) => record,
932 Err(_) => return Ok(()),
933 };
934 if validate_entry_id(&record.entry).is_err() {
935 return Ok(());
936 }
937 Some(record.entry)
938 }
939 Err(error) if error.kind() == ErrorKind::NotFound => None,
940 Err(_) => return Ok(()),
941 };
942 if let Ok(children) = fs::read_dir(&entries) {
943 for child in children.flatten() {
944 let name = child.file_name();
945 if current.as_deref() != name.to_str() {
946 let _ = fs::remove_dir_all(child.path());
947 }
948 }
949 }
950 if let Ok(children) = fs::read_dir(&control) {
951 let prefix = format!(".{EXACT_CACHE_MARKER_FILENAME}.");
952 for child in children.flatten() {
953 let name = child.file_name();
954 let Some(name) = name.to_str() else {
955 continue;
956 };
957 if name.starts_with(&prefix) && name.ends_with(".tmp") {
958 let _ = fs::remove_file(child.path());
959 }
960 }
961 }
962 Ok(())
963 }
964
965 fn open_single_flight_with_clock<C: MonotonicClock>(
966 &self,
967 options: ExactCacheSingleFlightOptions,
968 clock: &C,
969 ) -> Result<ExactCacheOpen, ExactCacheError> {
970 let options = options.validate()?;
971 ensure_supported_platform()?;
972 let started = clock.now();
973 let mut wait_state = ExactCacheSingleFlightWait::new(started);
974
975 loop {
976 if let Some(entry) = self.read()? {
977 return Ok(ExactCacheOpen::Hit(entry));
978 }
979
980 let now = clock.now();
981 let snapshot = self.read_inflight_snapshot()?;
982 match snapshot {
983 None => {
984 if let Some(opened) = self.try_claim_transition(options)? {
985 return Ok(opened);
986 }
987 let elapsed = now.saturating_sub(started);
988 if elapsed >= options.wait_timeout {
989 return Err(ExactCacheError::SingleFlightTimeout);
990 }
991 clock.sleep(options.poll_interval.min(options.wait_timeout - elapsed));
992 }
993 Some(snapshot) => {
994 let decision = wait_state.observe(now, &snapshot.revision(), options)?;
995 test_failpoint("after_inflight_wait_observation");
996 match decision {
997 ExactCacheSingleFlightDecision::Wait(duration) => {
998 clock.sleep(duration);
999 }
1000 ExactCacheSingleFlightDecision::Takeover => {
1001 if let Some(opened) =
1002 self.try_takeover_transition(&snapshot, options)?
1003 {
1004 return Ok(opened);
1005 }
1006 let elapsed = clock.now().saturating_sub(started);
1007 if elapsed >= options.wait_timeout {
1008 return Err(ExactCacheError::SingleFlightTimeout);
1009 }
1010 clock.sleep(
1011 options.poll_interval.min(options.wait_timeout - elapsed),
1012 );
1013 }
1014 ExactCacheSingleFlightDecision::Timeout => {
1015 return Err(ExactCacheError::SingleFlightTimeout);
1016 }
1017 }
1018 }
1019 }
1020 }
1021 }
1022
1023 fn try_claim_transition(
1024 &self,
1025 options: ExactCacheSingleFlightOptions,
1026 ) -> Result<Option<ExactCacheOpen>, ExactCacheError> {
1027 let Some(guard) = self.try_transition_guard()? else {
1028 return Ok(None);
1029 };
1030 if let Some(entry) = self.read()? {
1031 return Ok(Some(ExactCacheOpen::Hit(entry)));
1032 }
1033 if self.read_inflight_snapshot()?.is_some() {
1034 return Ok(None);
1035 }
1036 let Some(token) = self.claim_inflight(&guard)? else {
1037 return Ok(None);
1038 };
1039 Ok(Some(ExactCacheOpen::Owner(ExactCacheOwner::new(
1040 self.clone(),
1041 token,
1042 options,
1043 ))))
1044 }
1045
1046 fn try_takeover_transition(
1047 &self,
1048 observed: &InflightSnapshot,
1049 options: ExactCacheSingleFlightOptions,
1050 ) -> Result<Option<ExactCacheOpen>, ExactCacheError> {
1051 let Some(guard) = self.try_transition_guard()? else {
1052 return Ok(None);
1053 };
1054 if let Some(entry) = self.read()? {
1055 return Ok(Some(ExactCacheOpen::Hit(entry)));
1056 }
1057 if self.read_inflight_snapshot()?.as_ref() != Some(observed) {
1058 return Ok(None);
1059 }
1060 if !self.retire_inflight(&guard, observed)? {
1061 return Ok(None);
1062 }
1063 let Some(token) = self.claim_inflight(&guard)? else {
1064 return Ok(None);
1065 };
1066 Ok(Some(ExactCacheOpen::Owner(ExactCacheOwner::new(
1067 self.clone(),
1068 token,
1069 options,
1070 ))))
1071 }
1072
1073 fn try_transition_guard(&self) -> Result<Option<ExactCacheGuard>, ExactCacheError> {
1074 match self.lock(Duration::ZERO) {
1075 Ok(guard) => Ok(Some(guard)),
1076 Err(ExactCacheError::LockTimeout) => Ok(None),
1077 Err(error) => Err(error),
1078 }
1079 }
1080
1081 fn claim_inflight(
1082 &self,
1083 guard: &ExactCacheGuard,
1084 ) -> Result<Option<String>, ExactCacheError> {
1085 self.require_guard(guard)?;
1086 let control = self.control_directory();
1087 let heartbeats = control.join(INFLIGHT_HEARTBEAT_DIRECTORY);
1088 durable_create_dir_all(&heartbeats)?;
1089 if self.inflight_path().exists() {
1090 return Ok(None);
1091 }
1092
1093 let token = random_identifier("random in-flight owner token")?;
1094 let heartbeat_path = self.inflight_heartbeat_path(&token)?;
1095 write_exclusive(&heartbeat_path, b"\0")?;
1096 sync_directory(&heartbeats)?;
1097 test_failpoint("after_inflight_heartbeat");
1098
1099 let record = InflightRecord {
1100 protocol_version: INFLIGHT_PROTOCOL_VERSION,
1101 owner_token: token.clone(),
1102 process_id: std::process::id(),
1103 process_nonce: process_nonce()?,
1104 created_unix_ms: unix_milliseconds(),
1105 identity_sha256: identity_sha256(&self.identity)?,
1106 distribution_source: self.source.code().to_owned(),
1107 };
1108 let marker = serde_json::to_vec(&record)
1109 .map_err(|_| ExactCacheError::InvalidCommit("in-flight serialization"))?;
1110 if !write_exclusive_if_absent(&self.inflight_path(), &marker)? {
1111 let _ = fs::remove_file(heartbeat_path);
1112 return Ok(None);
1113 }
1114 test_failpoint("after_inflight_marker");
1115 sync_directory(&control)?;
1116 test_failpoint("after_inflight_sync");
1117 Ok(Some(token))
1118 }
1119
1120 fn read_inflight_snapshot(&self) -> Result<Option<InflightSnapshot>, ExactCacheError> {
1121 let marker = match fs::read(self.inflight_path()) {
1122 Ok(marker) => marker,
1123 Err(error) if error.kind() == ErrorKind::NotFound => return Ok(None),
1124 Err(source) => return Err(io("read in-flight marker", source)),
1125 };
1126 let record = serde_json::from_slice::<InflightRecord>(&marker)
1127 .ok()
1128 .filter(|record| {
1129 record.protocol_version == INFLIGHT_PROTOCOL_VERSION
1130 && validate_entry_id(&record.owner_token).is_ok()
1131 && validate_entry_id(&record.process_nonce).is_ok()
1132 });
1133 if let Some(record) = &record {
1134 if record.identity_sha256 != identity_sha256(&self.identity)?
1135 || record.distribution_source != self.source.code()
1136 {
1137 return Err(ExactCacheError::InvalidCommit(
1138 "in-flight identity or source",
1139 ));
1140 }
1141 }
1142 let heartbeat = match &record {
1143 Some(record) => {
1144 match fs::metadata(self.inflight_heartbeat_path(&record.owner_token)?) {
1145 Ok(metadata) => Some(HeartbeatFingerprint {
1146 byte_length: metadata.len(),
1147 modified: metadata.modified().ok(),
1148 }),
1149 Err(error) if error.kind() == ErrorKind::NotFound => None,
1150 Err(source) => return Err(io("read in-flight heartbeat", source)),
1151 }
1152 }
1153 None => None,
1154 };
1155 Ok(Some(InflightSnapshot {
1156 marker,
1157 record,
1158 heartbeat,
1159 }))
1160 }
1161
1162 fn refresh_inflight_heartbeat(&self, token: &str) -> Result<(), ExactCacheError> {
1163 if !self.inflight_token_is_current(token)? {
1164 return Err(ExactCacheError::SingleFlightOwnershipLost);
1165 }
1166 let heartbeat_path = self.inflight_heartbeat_path(token)?;
1167 let mut heartbeat = OpenOptions::new()
1168 .append(true)
1169 .open(heartbeat_path)
1170 .map_err(|source| io("open in-flight heartbeat", source))?;
1171 heartbeat
1172 .write_all(b"\0")
1173 .map_err(|source| io("write in-flight heartbeat", source))?;
1174 heartbeat
1175 .sync_all()
1176 .map_err(|source| io("sync in-flight heartbeat", source))?;
1177 test_failpoint("after_inflight_heartbeat_refresh");
1178 Ok(())
1179 }
1180
1181 fn inflight_token_is_current(&self, token: &str) -> Result<bool, ExactCacheError> {
1182 Ok(self
1183 .read_inflight_snapshot()?
1184 .and_then(|snapshot| snapshot.record)
1185 .is_some_and(|record| record.owner_token == token))
1186 }
1187
1188 fn release_inflight(
1189 &self,
1190 guard: &ExactCacheGuard,
1191 token: &str,
1192 ) -> Result<(), ExactCacheError> {
1193 self.require_guard(guard)?;
1194 let Some(snapshot) = self.read_inflight_snapshot()? else {
1195 return Ok(());
1196 };
1197 if snapshot
1198 .record
1199 .as_ref()
1200 .map(|record| record.owner_token.as_str())
1201 != Some(token)
1202 {
1203 return Ok(());
1204 }
1205 let _ = self.retire_inflight(guard, &snapshot)?;
1206 Ok(())
1207 }
1208
1209 fn retire_inflight(
1210 &self,
1211 guard: &ExactCacheGuard,
1212 expected: &InflightSnapshot,
1213 ) -> Result<bool, ExactCacheError> {
1214 self.require_guard(guard)?;
1215 if self.read_inflight_snapshot()?.as_ref() != Some(expected) {
1216 return Ok(false);
1217 }
1218 let nonce = random_identifier("random retired marker id")?;
1219 let token = expected
1220 .record
1221 .as_ref()
1222 .map_or("malformed", |record| record.owner_token.as_str());
1223 let retired = self
1224 .control_directory()
1225 .join(format!(".in-flight.{token}.{nonce}.retired"));
1226 match fs::rename(self.inflight_path(), &retired) {
1227 Ok(()) => {}
1228 Err(error) if error.kind() == ErrorKind::NotFound => return Ok(false),
1229 Err(source) => return Err(io("retire in-flight marker", source)),
1230 }
1231 test_failpoint("after_inflight_retire");
1232
1233 let retired_marker =
1234 fs::read(&retired).map_err(|source| io("read retired in-flight marker", source))?;
1235 if retired_marker != expected.marker {
1236 let _ = write_exclusive_if_absent(&self.inflight_path(), &retired_marker)?;
1237 let _ = fs::remove_file(&retired);
1238 sync_directory(&self.control_directory())?;
1239 return Ok(false);
1240 }
1241
1242 sync_directory(&self.control_directory())?;
1243 test_failpoint("after_inflight_retire_sync");
1244 fs::remove_file(&retired)
1245 .map_err(|source| io("remove retired in-flight marker", source))?;
1246 if let Some(record) = &expected.record {
1247 match fs::remove_file(self.inflight_heartbeat_path(&record.owner_token)?) {
1248 Ok(()) => {}
1249 Err(error) if error.kind() == ErrorKind::NotFound => {}
1250 Err(source) => return Err(io("remove in-flight heartbeat", source)),
1251 }
1252 }
1253 test_failpoint("after_inflight_reap");
1254 sync_directory(&self.control_directory())?;
1255 let heartbeats = self.control_directory().join(INFLIGHT_HEARTBEAT_DIRECTORY);
1256 if heartbeats.is_dir() {
1257 sync_directory(&heartbeats)?;
1258 }
1259 test_failpoint("after_inflight_reap_sync");
1260 Ok(true)
1261 }
1262
1263 fn require_guard(&self, guard: &ExactCacheGuard) -> Result<(), ExactCacheError> {
1264 if guard.stable_path == self.stable_path {
1265 Ok(())
1266 } else {
1267 Err(ExactCacheError::InvalidCommit("cache lock scope"))
1268 }
1269 }
1270
1271 fn control_directory(&self) -> PathBuf {
1272 self.stable_path
1273 .parent()
1274 .expect("validated cache path has parent")
1275 .join(EXACT_CACHE_CONTROL_DIRECTORY)
1276 }
1277
1278 fn marker_path(&self) -> PathBuf {
1279 self.control_directory().join(EXACT_CACHE_MARKER_FILENAME)
1280 }
1281
1282 fn inflight_path(&self) -> PathBuf {
1283 self.control_directory().join(INFLIGHT_FILENAME)
1284 }
1285
1286 fn inflight_heartbeat_path(&self, token: &str) -> Result<PathBuf, ExactCacheError> {
1287 validate_entry_id(token)?;
1288 Ok(self
1289 .control_directory()
1290 .join(INFLIGHT_HEARTBEAT_DIRECTORY)
1291 .join(format!("{token}.heartbeat")))
1292 }
1293
1294 fn entry_paths(&self, entry_id: &str) -> Result<EntryPaths, ExactCacheError> {
1295 validate_entry_id(entry_id)?;
1296 let filename = self
1297 .stable_path
1298 .file_name()
1299 .ok_or(ExactCacheError::InvalidCommit("stable product filename"))?;
1300 let entry = self.control_directory().join("entries").join(entry_id);
1301 let product = entry.join(filename);
1302 let archive = entry.join(format!("{}.archive", filename.to_string_lossy()));
1303 let provenance = entry.join(format!("{}.provenance.json", filename.to_string_lossy()));
1304 Ok(EntryPaths {
1305 product,
1306 archive,
1307 provenance,
1308 })
1309 }
1310
1311 fn allocate_entry_id(&self, entries: &Path) -> Result<String, ExactCacheError> {
1312 for _ in 0..128 {
1313 let entry_id = random_identifier("random entry id")?;
1314 match fs::create_dir(entries.join(&entry_id)) {
1315 Ok(()) => return Ok(entry_id),
1316 Err(error) if error.kind() == ErrorKind::AlreadyExists => continue,
1317 Err(source) => return Err(io("create entry", source)),
1318 }
1319 }
1320 Err(ExactCacheError::InvalidCommit("entry identifier collision"))
1321 }
1322
1323 fn current_entry_id(&self) -> Option<String> {
1324 let marker = fs::read(self.marker_path()).ok()?;
1325 let record: CommitRecord = serde_json::from_slice(&marker).ok()?;
1326 validate_entry_id(&record.entry).ok()?;
1327 Some(record.entry)
1328 }
1329 }
1330
1331 struct EntryPaths {
1332 product: PathBuf,
1333 archive: PathBuf,
1334 provenance: PathBuf,
1335 }
1336
1337 fn io(operation: &'static str, source: std::io::Error) -> ExactCacheError {
1338 ExactCacheError::Io { operation, source }
1339 }
1340
1341 fn ensure_supported_platform() -> Result<(), ExactCacheError> {
1342 if cfg!(any(target_os = "linux", target_os = "macos")) {
1343 Ok(())
1344 } else {
1345 Err(ExactCacheError::UnsupportedPlatform)
1346 }
1347 }
1348
1349 fn durable_create_dir_all(path: &Path) -> Result<(), ExactCacheError> {
1350 if path.is_dir() {
1351 return Ok(());
1352 }
1353 let mut missing = Vec::new();
1354 let mut cursor = path;
1355 while !cursor.exists() {
1356 missing.push(cursor.to_path_buf());
1357 cursor = cursor
1358 .parent()
1359 .ok_or(ExactCacheError::InvalidCommit("cache directory parent"))?;
1360 }
1361 for directory in missing.iter().rev() {
1362 match fs::create_dir(directory) {
1363 Ok(()) => {}
1364 Err(error) if error.kind() == ErrorKind::AlreadyExists && directory.is_dir() => {}
1365 Err(source) => return Err(io("create directory", source)),
1366 }
1367 let parent = directory
1368 .parent()
1369 .ok_or(ExactCacheError::InvalidCommit("cache directory parent"))?;
1370 sync_directory(parent)?;
1371 }
1372 Ok(())
1373 }
1374
1375 fn write_exclusive(path: &Path, bytes: &[u8]) -> Result<(), ExactCacheError> {
1376 let mut file = OpenOptions::new()
1377 .create_new(true)
1378 .write(true)
1379 .open(path)
1380 .map_err(|source| io("create immutable file", source))?;
1381 file.write_all(bytes)
1382 .map_err(|source| io("write immutable file", source))?;
1383 file.sync_all()
1384 .map_err(|source| io("sync immutable file", source))
1385 }
1386
1387 fn write_exclusive_if_absent(path: &Path, bytes: &[u8]) -> Result<bool, ExactCacheError> {
1388 let mut file = match OpenOptions::new().create_new(true).write(true).open(path) {
1389 Ok(file) => file,
1390 Err(error) if error.kind() == ErrorKind::AlreadyExists => return Ok(false),
1391 Err(source) => return Err(io("create in-flight marker", source)),
1392 };
1393 let result = file
1394 .write_all(bytes)
1395 .map_err(|source| io("write in-flight marker", source))
1396 .and_then(|()| {
1397 file.sync_all()
1398 .map_err(|source| io("sync in-flight marker", source))
1399 });
1400 if result.is_err() {
1401 let _ = fs::remove_file(path);
1402 }
1403 result.map(|()| true)
1404 }
1405
1406 fn random_identifier(operation: &'static str) -> Result<String, ExactCacheError> {
1407 let mut random = [0_u8; 16];
1408 getrandom::getrandom(&mut random)
1409 .map_err(|error| io(operation, std::io::Error::other(error.to_string())))?;
1410 let mut identifier = String::with_capacity(32);
1411 for byte in random {
1412 write!(&mut identifier, "{byte:02x}").expect("writing to String cannot fail");
1413 }
1414 Ok(identifier)
1415 }
1416
1417 fn process_nonce() -> Result<String, ExactCacheError> {
1418 static PROCESS_NONCE: OnceLock<String> = OnceLock::new();
1419 if let Some(nonce) = PROCESS_NONCE.get() {
1420 return Ok(nonce.clone());
1421 }
1422 let nonce = random_identifier("random in-flight process nonce")?;
1423 let _ = PROCESS_NONCE.set(nonce);
1424 Ok(PROCESS_NONCE
1425 .get()
1426 .expect("process nonce initialized")
1427 .clone())
1428 }
1429
1430 fn unix_milliseconds() -> u64 {
1431 let milliseconds = SystemTime::now()
1432 .duration_since(UNIX_EPOCH)
1433 .unwrap_or_default()
1434 .as_millis();
1435 u64::try_from(milliseconds).unwrap_or(u64::MAX)
1436 }
1437
1438 fn sync_directory(path: &Path) -> Result<(), ExactCacheError> {
1439 File::open(path)
1440 .and_then(|directory| directory.sync_all())
1441 .map_err(|source| io("sync directory", source))
1442 }
1443
1444 #[cfg(feature = "exact-cache-test-failpoints")]
1445 fn test_failpoint(name: &str) {
1446 if std::env::var_os("SIDEREON_TEST_EXACT_CACHE_PAUSE_FAILPOINT").as_deref()
1447 == Some(std::ffi::OsStr::new(name))
1448 {
1449 let barrier = PathBuf::from(
1450 std::env::var_os("SIDEREON_TEST_EXACT_CACHE_FAILPOINT_BARRIER")
1451 .expect("exact-cache pause failpoint barrier"),
1452 );
1453 fs::write(barrier.with_extension("ready"), b"ready")
1454 .expect("write exact-cache failpoint barrier");
1455 let release = barrier.with_extension("release");
1456 let deadline = Instant::now() + Duration::from_secs(30);
1457 while !release.exists() {
1458 assert!(
1459 Instant::now() < deadline,
1460 "timed out waiting for exact-cache failpoint release"
1461 );
1462 thread::sleep(Duration::from_millis(5));
1463 }
1464 }
1465 if std::env::var_os("SIDEREON_TEST_EXACT_CACHE_FAILPOINT").as_deref()
1466 == Some(std::ffi::OsStr::new(name))
1467 {
1468 std::process::exit(86);
1469 }
1470 }
1471
1472 #[cfg(not(feature = "exact-cache-test-failpoints"))]
1473 fn test_failpoint(_name: &str) {}
1474
1475 #[cfg(feature = "exact-cache-test-failpoints")]
1476 fn test_read_barrier() {
1477 static ONCE: std::sync::Once = std::sync::Once::new();
1478 ONCE.call_once(|| {
1479 let Some(barrier) = std::env::var_os("SIDEREON_TEST_EXACT_CACHE_READ_BARRIER") else {
1480 return;
1481 };
1482 let barrier = PathBuf::from(barrier);
1483 fs::write(barrier.with_extension("ready"), b"ready")
1484 .expect("write exact-cache read barrier");
1485 let release = barrier.with_extension("release");
1486 let deadline = Instant::now() + Duration::from_secs(10);
1487 while !release.exists() {
1488 assert!(
1489 Instant::now() < deadline,
1490 "timed out waiting for exact-cache read barrier"
1491 );
1492 thread::sleep(Duration::from_millis(5));
1493 }
1494 });
1495 }
1496
1497 #[cfg(not(feature = "exact-cache-test-failpoints"))]
1498 fn test_read_barrier() {}
1499}
1500
1501#[cfg(not(target_arch = "wasm32"))]
1502pub use native::{
1503 CommittedExactCacheEntry, ExactCacheGuard, ExactCacheOpen, ExactCacheOwner, ExactProductCache,
1504};
1505
1506#[cfg(all(not(target_arch = "wasm32"), feature = "exact-cache-test-failpoints"))]
1507#[doc(hidden)]
1508pub use native::ExactCacheTestClock;
1509
1510#[cfg(test)]
1511mod tests {
1512 use super::*;
1513 use crate::data::{product, AnalysisCenter, ProductDate, ProductType};
1514
1515 fn identity() -> ProductIdentity {
1516 product(
1517 AnalysisCenter::CodUlt,
1518 ProductType::Sp3,
1519 ProductDate::new(2026, 7, 16).expect("date"),
1520 Some("05M"),
1521 Some("0000"),
1522 )
1523 .expect("product")
1524 .identity()
1525 .expect("identity")
1526 }
1527
1528 #[test]
1529 fn commit_binds_every_byte_group_identity_and_source() {
1530 let identity = identity();
1531 let marker = build_commit_record(
1532 &identity,
1533 DistributionSource::Direct,
1534 "0123456789abcdef0123456789abcdef",
1535 b"product",
1536 b"archive",
1537 b"provenance",
1538 )
1539 .expect("commit");
1540 let verified = verify_commit_record(
1541 &identity,
1542 DistributionSource::Direct,
1543 &marker,
1544 b"product",
1545 b"archive",
1546 b"provenance",
1547 )
1548 .expect("verify");
1549 assert_eq!(verified.entry_id, "0123456789abcdef0123456789abcdef");
1550
1551 for (product, archive, provenance) in [
1552 (&b"changed"[..], &b"archive"[..], &b"provenance"[..]),
1553 (&b"product"[..], &b"changed"[..], &b"provenance"[..]),
1554 (&b"product"[..], &b"archive"[..], &b"changed"[..]),
1555 ] {
1556 assert!(verify_commit_record(
1557 &identity,
1558 DistributionSource::Direct,
1559 &marker,
1560 product,
1561 archive,
1562 provenance,
1563 )
1564 .is_err());
1565 }
1566 assert!(verify_commit_record(
1567 &identity,
1568 DistributionSource::NasaCddis,
1569 &marker,
1570 b"product",
1571 b"archive",
1572 b"provenance",
1573 )
1574 .is_err());
1575 }
1576
1577 #[test]
1578 fn malformed_entry_ids_are_rejected() {
1579 for entry in [
1580 "",
1581 "ABCDEF0123456789ABCDEF0123456789",
1582 "0123456789abcdef0123456789abcdeg",
1583 "0123456789abcdef",
1584 ] {
1585 assert!(build_commit_record(
1586 &identity(),
1587 DistributionSource::Direct,
1588 entry,
1589 b"product",
1590 b"archive",
1591 b"provenance",
1592 )
1593 .is_err());
1594 }
1595 }
1596
1597 #[test]
1598 fn single_flight_wait_state_resets_on_progress_and_prefers_stale_takeover() {
1599 let options = ExactCacheSingleFlightOptions {
1600 poll_interval: Duration::from_secs(2),
1601 heartbeat_interval: Duration::from_secs(1),
1602 liveness_timeout: Duration::from_secs(5),
1603 wait_timeout: Duration::from_secs(20),
1604 };
1605 let mut wait = ExactCacheSingleFlightWait::new(Duration::ZERO);
1606 assert_eq!(
1607 wait.observe(Duration::ZERO, b"owner-a:1", options)
1608 .expect("first observation"),
1609 ExactCacheSingleFlightDecision::Wait(Duration::from_secs(2))
1610 );
1611 assert_eq!(
1612 wait.observe(Duration::from_secs(4), b"owner-a:1", options)
1613 .expect("unchanged observation"),
1614 ExactCacheSingleFlightDecision::Wait(Duration::from_secs(1))
1615 );
1616 assert_eq!(
1617 wait.observe(Duration::from_secs(4), b"owner-a:2", options)
1618 .expect("heartbeat progress"),
1619 ExactCacheSingleFlightDecision::Wait(Duration::from_secs(2))
1620 );
1621 assert_eq!(
1622 wait.observe(Duration::from_secs(9), b"owner-a:2", options)
1623 .expect("stale observation"),
1624 ExactCacheSingleFlightDecision::Takeover
1625 );
1626
1627 let options = ExactCacheSingleFlightOptions {
1628 wait_timeout: Duration::from_secs(5),
1629 ..options
1630 };
1631 let mut equal_deadlines = ExactCacheSingleFlightWait::new(Duration::ZERO);
1632 equal_deadlines
1633 .observe(Duration::ZERO, b"owner", options)
1634 .expect("initial observation");
1635 assert_eq!(
1636 equal_deadlines
1637 .observe(Duration::from_secs(5), b"owner", options)
1638 .expect("equal deadlines"),
1639 ExactCacheSingleFlightDecision::Takeover
1640 );
1641 }
1642
1643 #[test]
1644 fn single_flight_wait_state_times_out_while_owner_is_live() {
1645 let options = ExactCacheSingleFlightOptions {
1646 poll_interval: Duration::from_secs(1),
1647 heartbeat_interval: Duration::from_secs(2),
1648 liveness_timeout: Duration::from_secs(10),
1649 wait_timeout: Duration::from_secs(3),
1650 };
1651 let mut wait = ExactCacheSingleFlightWait::new(Duration::ZERO);
1652 wait.observe(Duration::ZERO, b"owner:1", options)
1653 .expect("initial observation");
1654 assert_eq!(
1655 wait.observe(Duration::from_secs(3), b"owner:2", options)
1656 .expect("live owner at wait bound"),
1657 ExactCacheSingleFlightDecision::Timeout
1658 );
1659 }
1660}