Skip to main content

sidereon_core/
exact_cache.rs

1//! Exact-product cache identity binding and atomic publication.
2//!
3//! The pure functions in this module define one commit record for every
4//! Sidereon interface, including WebAssembly hosts. On native targets,
5//! [`ExactProductCache`] adds bounded cross-process locking, immutable entry
6//! staging, durable writes, and an atomic reader-visible commit.
7//!
8//! Network transport and product parsing remain outside this module. Callers
9//! must validate product semantics before publication and repeat that semantic
10//! validation on bytes returned from a cache hit.
11
12use crate::data::{DataCatalogError, DistributionSource, ProductIdentity};
13use serde::{Deserialize, Serialize};
14use sha2::{Digest, Sha256};
15use std::fmt::Write as _;
16use std::time::Duration;
17
18/// Commit-record version shared by all Sidereon interfaces.
19pub const EXACT_CACHE_SCHEMA_VERSION: u8 = 3;
20
21/// Directory below one exact identity/source cache directory.
22pub const EXACT_CACHE_CONTROL_DIRECTORY: &str = ".sidereon-cache-v3";
23
24/// Name of the single reader-visible commit record.
25pub const EXACT_CACHE_MARKER_FILENAME: &str = "current.json";
26
27/// Error produced by the shared exact-product cache protocol.
28#[derive(Debug, thiserror::Error)]
29pub enum ExactCacheError {
30    /// The product identity is internally inconsistent.
31    #[error("invalid exact product identity: {0}")]
32    Identity(#[from] DataCatalogError),
33    /// An immutable transaction identifier is not 32 lower-case hexadecimal characters.
34    #[error("invalid exact-cache entry identifier")]
35    InvalidEntryId,
36    /// A commit record is malformed or does not bind the supplied entry.
37    #[error("invalid or mismatched exact-cache commit: {0}")]
38    InvalidCommit(&'static str),
39    /// A native filesystem operation failed.
40    #[cfg(not(target_arch = "wasm32"))]
41    #[error("exact-cache {operation} failed: {source}")]
42    Io {
43        /// Operation that failed.
44        operation: &'static str,
45        /// Underlying filesystem error.
46        #[source]
47        source: std::io::Error,
48    },
49    /// The per-entry cross-process lock was not acquired in time.
50    #[cfg(not(target_arch = "wasm32"))]
51    #[error("timed out waiting for the exact-cache lock")]
52    LockTimeout,
53    /// A live single-flight owner did not commit within the configured wait.
54    #[error("timed out waiting for the exact-cache in-flight owner")]
55    SingleFlightTimeout,
56    /// The single-flight owner token is no longer current or its heartbeat failed.
57    #[error("exact-cache single-flight ownership was lost")]
58    SingleFlightOwnershipLost,
59    /// Single-flight duration options are zero or internally inconsistent.
60    #[error("invalid exact-cache single-flight options")]
61    InvalidSingleFlightOptions,
62    /// Cross-process durable cache publication is unsupported on this platform.
63    #[cfg(not(target_arch = "wasm32"))]
64    #[error("durable exact-cache publication is unsupported on this platform")]
65    UnsupportedPlatform,
66}
67
68/// Digests and lengths bound by one exact-cache commit.
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct ExactCacheDigests {
71    /// SHA-256 of canonical full [`ProductIdentity`] bytes.
72    pub identity_sha256: String,
73    /// Explicit distribution source.
74    pub distribution_source: String,
75    /// SHA-256 of decompressed, validated product bytes.
76    pub product_sha256: String,
77    /// Product byte length.
78    pub product_byte_length: u64,
79    /// SHA-256 of distributor archive bytes.
80    pub archive_sha256: String,
81    /// Archive byte length.
82    pub archive_byte_length: u64,
83    /// SHA-256 of the exact provenance bytes.
84    pub provenance_sha256: String,
85    /// Provenance byte length.
86    pub provenance_byte_length: u64,
87}
88
89/// Parsed, verified commit record.
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct VerifiedExactCacheCommit {
92    /// Immutable transaction identifier referenced by the marker.
93    pub entry_id: String,
94    /// Verified identity, source, and byte bindings.
95    pub digests: ExactCacheDigests,
96}
97
98/// Bounded timing policy for exact-cache single-flight coordination.
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub struct ExactCacheSingleFlightOptions {
101    /// Interval between committed-entry and heartbeat observations.
102    pub poll_interval: Duration,
103    /// Interval between automatic owner heartbeat writes.
104    pub heartbeat_interval: Duration,
105    /// Required continuous no-progress interval before owner retirement.
106    pub liveness_timeout: Duration,
107    /// Maximum total time spent waiting for another owner.
108    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/// Target-neutral next action for an exact-cache single-flight waiter.
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum ExactCacheSingleFlightDecision {
139    /// Observe the committed entry and owner revision again after this delay.
140    Wait(Duration),
141    /// Recheck the exact owner revision atomically and take over if unchanged.
142    Takeover,
143    /// Stop without downloading because the bounded total wait expired.
144    Timeout,
145}
146
147/// Target-neutral liveness state shared by filesystem and browser substrates.
148#[derive(Debug, Clone)]
149pub struct ExactCacheSingleFlightWait {
150    started: Duration,
151    unchanged_since: Duration,
152    observation_sha256: Option<[u8; 32]>,
153}
154
155impl ExactCacheSingleFlightWait {
156    /// Begin observing an owner at one monotonic timestamp.
157    #[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    /// Observe an opaque owner/heartbeat revision and select the next action.
167    ///
168    /// `now` and the returned delay use a caller-local monotonic clock. The
169    /// revision must change whenever the owner token or heartbeat changes.
170    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
215/// Return the SHA-256 binding for every field of an exact product identity.
216pub fn identity_sha256(identity: &ProductIdentity) -> Result<String, ExactCacheError> {
217    Ok(sha256_hex(&identity.canonical_bytes()?))
218}
219
220/// Build a canonical commit record for an immutable cache transaction.
221///
222/// `entry_id` must be a freshly allocated, immutable transaction directory.
223/// Native callers normally use [`ExactProductCache::publish`], which allocates
224/// it. WebAssembly hosts can generate 16 random bytes, encode them as lower-case
225/// hexadecimal, store the three byte objects under that identifier, and then
226/// atomically replace their commit marker with the returned bytes.
227pub 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
251/// Verify that one marker and immutable byte triple belong to the requested
252/// full identity and explicit distribution source.
253///
254/// The returned provenance bytes are authenticated by the marker but remain
255/// application data. The acquisition interface must parse them and confirm
256/// requested/resolved identities and product semantics before accepting a hit.
257pub 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    /// Paths and exact bytes from one verified immutable cache entry.
344    #[derive(Debug, Clone)]
345    pub struct CommittedExactCacheEntry {
346        /// Immutable transaction identifier.
347        pub entry_id: String,
348        /// Validated product path.
349        pub product_path: PathBuf,
350        /// Distributor archive path.
351        pub archive_path: PathBuf,
352        /// Provenance path.
353        pub provenance_path: PathBuf,
354        /// Exact product bytes authenticated by the commit record.
355        pub product: Vec<u8>,
356        /// Exact archive bytes authenticated by the commit record.
357        pub archive: Vec<u8>,
358        /// Exact provenance bytes authenticated by the commit record.
359        pub provenance: Vec<u8>,
360    }
361
362    /// Result of opening an exact cache with single-flight coordination.
363    #[derive(Debug)]
364    pub enum ExactCacheOpen {
365        /// A complete committed entry was already available or was published
366        /// by the owner observed while waiting.
367        Hit(CommittedExactCacheEntry),
368        /// This process owns acquisition and is the only caller that should fetch.
369        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    /// Exclusive right to fetch and publish one single-flight cache miss.
451    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        /// Create a stopped monotonic clock for deterministic integration tests.
518        #[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        /// Advance the clock and wake every cache waiter.
532        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        /// Block until at least `count` cache sleeps have begun.
540        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    /// One exact identity/source cache rooted at the caller's stable product path.
588    #[derive(Debug, Clone)]
589    pub struct ExactProductCache {
590        stable_path: PathBuf,
591        identity: ProductIdentity,
592        source: DistributionSource,
593    }
594
595    /// Held cross-process lock for one [`ExactProductCache`].
596    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        /// Refresh this owner's liveness heartbeat immediately.
651        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        /// Publish validated bytes and release single-flight ownership.
663        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        /// Create a cache handle after validating the complete identity.
707        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        /// Stable caller-facing path used to locate this cache entry.
725        #[must_use]
726        pub fn stable_path(&self) -> &Path {
727            &self.stable_path
728        }
729
730        /// Open this cache with bounded single-flight miss coalescing.
731        ///
732        /// A hit contains bytes verified by the unchanged schema-v3 commit
733        /// protocol. Only the returned owner should perform acquisition.
734        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        /// Open using an injectable monotonic clock for deterministic tests.
743        #[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        /// Acquire the per-entry cross-process lock with bounded waiting.
754        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        /// Read and digest-verify the currently committed immutable entry.
797        ///
798        /// Returns `Ok(None)` only when no commit marker exists. A malformed,
799        /// incomplete, or mismatched entry is an error, never a cache miss.
800        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        /// Publish a complete validated candidate under the held entry lock.
857        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        /// Remove unreferenced transactions while holding the entry lock.
924        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}