Skip to main content

eredu_runtime/cache/
persistence.rs

1//! Backend-neutral prompt-cache catalog validation and durable publication.
2
3use eredu_core::cache::{
4    CacheBlockId, CacheRepresentation, PromptCacheBlock, PromptCacheError, PromptCacheManifest,
5    PromptCacheStateTensor, PromptCacheTopology, PROMPT_CACHE_SCHEMA_VERSION,
6};
7use sha2::{Digest, Sha256};
8use std::{
9    fs::{self, File},
10    io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write},
11    path::{Component, Path, PathBuf},
12    sync::{
13        atomic::{AtomicU64, Ordering},
14        OnceLock,
15    },
16    time::{SystemTime, UNIX_EPOCH},
17};
18
19static NEXT_LIVE_CACHE_PUBLICATION_ID: AtomicU64 = AtomicU64::new(1);
20static NEXT_REVERSIBLE_CACHE_PUBLICATION_ID: AtomicU64 = AtomicU64::new(1);
21static LIVE_CACHE_PROCESS_NAMESPACE: OnceLock<String> = OnceLock::new();
22
23/// Maximum accepted safetensors metadata header size for a prompt-cache shard.
24pub const MAX_PROMPT_CACHE_SHARD_HEADER_BYTES: u64 = 1024 * 1024;
25/// Directory containing immutable replacement generations.
26pub const PROMPT_CACHE_GENERATIONS_DIRECTORY: &str = ".generations";
27/// Atomic pointer to the active immutable generation.
28pub const PROMPT_CACHE_CURRENT_FILE: &str = "CURRENT";
29
30/// Resolves the canonical rank-local prompt-cache storage directory.
31///
32/// Replicated cache state remains at `root`; distributed state is isolated by
33/// all three semantic rank coordinates, using `x` for inactive axes.
34pub fn prompt_cache_rank_path(root: &Path, topology: &PromptCacheTopology) -> PathBuf {
35    let coordinate = |axis: Option<(usize, usize)>| {
36        axis.map_or_else(|| "x".to_owned(), |(_, rank)| rank.to_string())
37    };
38    if topology.cache_rank_identity().is_none() {
39        root.to_path_buf()
40    } else {
41        root.join(format!(
42            "rank-p{}-t{}-e{}",
43            coordinate(topology.stage()),
44            coordinate(topology.shard()),
45            coordinate(topology.addressable())
46        ))
47    }
48}
49
50/// Filesystem, catalog, and publication failures for a reusable prompt cache.
51#[derive(Debug, thiserror::Error)]
52pub enum PromptCachePersistenceError {
53    /// Backend-neutral manifest geometry or identity is invalid.
54    #[error(transparent)]
55    PromptCache(#[from] PromptCacheError),
56    /// A filesystem operation failed.
57    #[error("failed to {action} at {path}: {source}")]
58    Io {
59        /// Filesystem action that failed.
60        action: &'static str,
61        /// Path involved in the failed action.
62        path: PathBuf,
63        /// Underlying filesystem failure.
64        #[source]
65        source: std::io::Error,
66    },
67    /// A manifest could not be encoded or decoded.
68    #[error("invalid prompt cache manifest JSON: {0}")]
69    ManifestJson(#[source] serde_json::Error),
70    /// Filesystem publication metadata is malformed.
71    #[error("malformed prompt cache storage: {0}")]
72    MalformedStorage(String),
73    /// A shard path could escape the prompt-cache directory.
74    #[error("unsafe prompt cache shard path {0:?}")]
75    UnsafeShardPath(String),
76    /// A manifest referenced a missing shard.
77    #[error("missing prompt cache shard {0}")]
78    MissingShard(PathBuf),
79    /// A safetensors shard had missing, extra, or corrupt arrays.
80    #[error("malformed prompt cache shard {path}: {reason}")]
81    MalformedShard {
82        /// Invalid shard path.
83        path: PathBuf,
84        /// Structural or data validation failure.
85        reason: String,
86    },
87    /// The target path cannot be published atomically.
88    #[error("invalid prompt cache path {0}")]
89    InvalidPromptCachePath(PathBuf),
90    /// The destination exists and explicit replacement was not requested.
91    #[error("prompt cache destination already exists: {0}")]
92    PromptCacheExists(PathBuf),
93    /// A reversible publication method was invoked in the wrong lifecycle state.
94    #[error("invalid reversible prompt cache publication state: {0}")]
95    InvalidReversiblePublication(&'static str),
96}
97
98/// Filesystem failure while publishing one ephemeral live-cache block.
99#[derive(Debug, thiserror::Error)]
100pub enum LiveCachePublicationError {
101    /// A filesystem operation failed.
102    #[error("failed to {action} at {path}: {source}")]
103    Io {
104        /// Filesystem action that failed.
105        action: &'static str,
106        /// Path involved in the failed action.
107        path: PathBuf,
108        /// Underlying filesystem failure.
109        #[source]
110        source: std::io::Error,
111    },
112}
113
114/// Runtime-owned unique staging and atomic publication for one live-cache block.
115#[derive(Debug)]
116pub struct LiveCacheBlockPublication {
117    destination: PathBuf,
118    staging: PathBuf,
119    committed: bool,
120}
121
122impl LiveCacheBlockPublication {
123    /// Reserves unique paths derived from the complete block and rank identity.
124    pub fn begin(directory: &Path, id: &CacheBlockId) -> Self {
125        let process_namespace = LIVE_CACHE_PROCESS_NAMESPACE.get_or_init(|| {
126            let started = SystemTime::now()
127                .duration_since(UNIX_EPOCH)
128                .unwrap_or_default()
129                .as_nanos();
130            format!("p{:08x}-t{started:032x}", std::process::id())
131        });
132        let publication_id = NEXT_LIVE_CACHE_PUBLICATION_ID.fetch_add(1, Ordering::Relaxed);
133        let representation = match id.representation {
134            CacheRepresentation::KeyValue => "kv",
135            CacheRepresentation::CompressedLatentRotary => "mla",
136        };
137        let rank_component =
138            |rank: Option<usize>| rank.map_or_else(|| "x".to_string(), |rank| rank.to_string());
139        let rank = id.rank.map_or_else(
140            || "rank-px-tx-ex".to_string(),
141            |rank| {
142                format!(
143                    "rank-p{}-t{}-e{}",
144                    rank_component(rank.stage_rank()),
145                    rank_component(rank.shard_rank()),
146                    rank_component(rank.addressable_rank())
147                )
148            },
149        );
150        let base = format!(
151            "live-{process_namespace}-w{publication_id:016x}-s{:016x}-layer-{:05}-{representation}-{rank}-{}-{}",
152            id.session_id, id.global_layer, id.start, id.end
153        );
154        Self {
155            destination: directory.join(format!("{base}.safetensors")),
156            staging: directory.join(format!(".{base}.tmp.safetensors")),
157            committed: false,
158        }
159    }
160
161    /// Unique temporary path into which the backend serializes native storage.
162    pub fn staging_path(&self) -> &Path {
163        &self.staging
164    }
165
166    /// Final unique path used by the live-cache catalog.
167    pub fn destination_path(&self) -> &Path {
168        &self.destination
169    }
170
171    /// Atomically publishes the staged file without replacing an existing path.
172    pub fn commit(mut self) -> Result<PathBuf, LiveCachePublicationError> {
173        fs::hard_link(&self.staging, &self.destination).map_err(|source| {
174            LiveCachePublicationError::Io {
175                action: "publish uniquely named live cache block",
176                path: self.destination.clone(),
177                source,
178            }
179        })?;
180        if let Err(source) = fs::remove_file(&self.staging) {
181            let _ = fs::remove_file(&self.destination);
182            return Err(LiveCachePublicationError::Io {
183                action: "remove published live cache temporary file",
184                path: self.staging.clone(),
185                source,
186            });
187        }
188        self.committed = true;
189        Ok(self.destination.clone())
190    }
191}
192
193impl Drop for LiveCacheBlockPublication {
194    fn drop(&mut self) {
195        if !self.committed {
196            let _ = fs::remove_file(&self.staging);
197        }
198    }
199}
200
201/// A runtime-owned staging directory that publishes one immutable cache atomically.
202#[derive(Debug)]
203pub struct PromptCachePublication {
204    destination: PathBuf,
205    parent: PathBuf,
206    generations: PathBuf,
207    generation_name: String,
208    publication_root: Option<PathBuf>,
209    staging: PathBuf,
210    replacing: bool,
211    nonce: u128,
212    committed: bool,
213}
214
215impl PromptCachePublication {
216    /// Creates an isolated staging directory for a new cache or replacement generation.
217    pub fn begin(
218        destination: impl AsRef<Path>,
219        replace_existing: bool,
220    ) -> Result<Self, PromptCachePersistenceError> {
221        let destination = destination.as_ref().to_path_buf();
222        let parent = destination
223            .parent()
224            .ok_or_else(|| {
225                PromptCachePersistenceError::InvalidPromptCachePath(destination.clone())
226            })?
227            .to_path_buf();
228        fs::create_dir_all(&parent).map_err(|source| PromptCachePersistenceError::Io {
229            action: "create prompt cache parent",
230            path: parent.clone(),
231            source,
232        })?;
233        let replacing = destination.exists();
234        if replacing && !replace_existing {
235            return Err(PromptCachePersistenceError::PromptCacheExists(destination));
236        }
237        if replacing && !destination.is_dir() {
238            return Err(PromptCachePersistenceError::InvalidPromptCachePath(
239                destination,
240            ));
241        }
242        let file_name = destination
243            .file_name()
244            .and_then(|name| name.to_str())
245            .ok_or_else(|| {
246                PromptCachePersistenceError::InvalidPromptCachePath(destination.clone())
247            })?;
248        let nonce = SystemTime::now()
249            .duration_since(UNIX_EPOCH)
250            .unwrap_or_default()
251            .as_nanos();
252        let generation_name = format!("generation-{nonce}");
253        let (generations, staging, publication_root) = if replacing {
254            let generations = destination.join(PROMPT_CACHE_GENERATIONS_DIRECTORY);
255            fs::create_dir_all(&generations).map_err(|source| PromptCachePersistenceError::Io {
256                action: "create prompt cache generation directory",
257                path: generations.clone(),
258                source,
259            })?;
260            let staging = generations.join(format!(".tmp-{nonce}"));
261            fs::create_dir(&staging).map_err(|source| PromptCachePersistenceError::Io {
262                action: "create temporary prompt cache",
263                path: staging.clone(),
264                source,
265            })?;
266            (generations, staging, None)
267        } else {
268            let publication_root = parent.join(format!(".{file_name}.tmp-{nonce}"));
269            fs::create_dir(&publication_root).map_err(|source| {
270                PromptCachePersistenceError::Io {
271                    action: "create temporary prompt cache root",
272                    path: publication_root.clone(),
273                    source,
274                }
275            })?;
276            let generations = publication_root.join(PROMPT_CACHE_GENERATIONS_DIRECTORY);
277            if let Err(source) = fs::create_dir(&generations) {
278                let _ = fs::remove_dir_all(&publication_root);
279                return Err(PromptCachePersistenceError::Io {
280                    action: "create prompt cache generation directory",
281                    path: generations,
282                    source,
283                });
284            }
285            let staging = generations.join(&generation_name);
286            if let Err(source) = fs::create_dir(&staging) {
287                let _ = fs::remove_dir_all(&publication_root);
288                return Err(PromptCachePersistenceError::Io {
289                    action: "create temporary prompt cache",
290                    path: staging,
291                    source,
292                });
293            }
294            (generations, staging, Some(publication_root))
295        };
296        Ok(Self {
297            destination,
298            parent,
299            generations,
300            generation_name,
301            publication_root,
302            staging,
303            replacing,
304            nonce,
305            committed: false,
306        })
307    }
308
309    /// Directory into which the backend writes its native tensor shards.
310    pub fn staging_directory(&self) -> &Path {
311        &self.staging
312    }
313
314    /// Writes the manifest, validates every shard, and atomically publishes the cache.
315    pub fn commit(
316        mut self,
317        manifest: &PromptCacheManifest,
318    ) -> Result<(), PromptCachePersistenceError> {
319        let manifest_path = self.staging.join("manifest.json");
320        let file =
321            File::create(&manifest_path).map_err(|source| PromptCachePersistenceError::Io {
322                action: "create prompt cache manifest",
323                path: manifest_path.clone(),
324                source,
325            })?;
326        let mut writer = BufWriter::new(file);
327        serde_json::to_writer_pretty(&mut writer, manifest)
328            .map_err(PromptCachePersistenceError::ManifestJson)?;
329        writer
330            .write_all(b"\n")
331            .map_err(|source| PromptCachePersistenceError::Io {
332                action: "write prompt cache manifest",
333                path: manifest_path.clone(),
334                source,
335            })?;
336        writer
337            .flush()
338            .map_err(|source| PromptCachePersistenceError::Io {
339                action: "flush prompt cache manifest",
340                path: manifest_path.clone(),
341                source,
342            })?;
343        sync_file(&manifest_path)?;
344        validate_prompt_cache_manifest(&self.staging, manifest)?;
345        sync_directory(&self.staging)?;
346
347        if self.replacing {
348            let generation = self.generations.join(&self.generation_name);
349            durable_rename(&self.staging, &generation, false).map_err(|source| {
350                PromptCachePersistenceError::Io {
351                    action: "publish prompt cache generation",
352                    path: generation,
353                    source,
354                }
355            })?;
356            sync_directory(&self.generations)?;
357            publish_generation_pointer(&self.destination, &self.generation_name, self.nonce)?;
358        } else {
359            sync_directory(&self.generations)?;
360            let publication_root = self
361                .publication_root
362                .as_ref()
363                .expect("new prompt-cache publication owns a staging root");
364            publish_generation_pointer(publication_root, &self.generation_name, self.nonce)?;
365            durable_rename(publication_root, &self.destination, false).map_err(|source| {
366                PromptCachePersistenceError::Io {
367                    action: "publish prompt cache",
368                    path: self.destination.clone(),
369                    source,
370                }
371            })?;
372        }
373        sync_directory(&self.parent)?;
374        self.committed = true;
375        Ok(())
376    }
377}
378
379impl Drop for PromptCachePublication {
380    fn drop(&mut self) {
381        if !self.committed {
382            let staging = self.publication_root.as_ref().unwrap_or(&self.staging);
383            if staging.exists() {
384                let _ = fs::remove_dir_all(staging);
385            }
386        }
387    }
388}
389
390/// A prepared cache directory whose visibility can be rolled back exactly.
391///
392/// The backend writes a complete ordinary cache to [`Self::staging_destination`].
393/// Publication of a new destination is one directory rename. Replacement moves
394/// only the immutable generation into the existing destination and atomically
395/// switches `CURRENT`, retaining the previous generation identity until commit.
396#[derive(Debug)]
397pub struct ReversiblePromptCachePublication {
398    destination: PathBuf,
399    parent: PathBuf,
400    staging: PathBuf,
401    replace_existing: bool,
402    previous_generation: Option<String>,
403    moved_generation: Option<PathBuf>,
404    published: bool,
405    committed: bool,
406    nonce: u128,
407}
408
409impl ReversiblePromptCachePublication {
410    /// Reserves a unique sibling destination without creating or publishing it.
411    pub fn begin(
412        destination: impl AsRef<Path>,
413        replace_existing: bool,
414    ) -> Result<Self, PromptCachePersistenceError> {
415        let destination = destination.as_ref().to_path_buf();
416        let parent = destination
417            .parent()
418            .ok_or_else(|| {
419                PromptCachePersistenceError::InvalidPromptCachePath(destination.clone())
420            })?
421            .to_path_buf();
422        fs::create_dir_all(&parent).map_err(|source| PromptCachePersistenceError::Io {
423            action: "create reversible prompt cache parent",
424            path: parent.clone(),
425            source,
426        })?;
427        if destination.exists() && !replace_existing {
428            return Err(PromptCachePersistenceError::PromptCacheExists(destination));
429        }
430        if destination.exists() && !destination.is_dir() {
431            return Err(PromptCachePersistenceError::InvalidPromptCachePath(
432                destination,
433            ));
434        }
435        let file_name = destination
436            .file_name()
437            .and_then(|name| name.to_str())
438            .ok_or_else(|| {
439                PromptCachePersistenceError::InvalidPromptCachePath(destination.clone())
440            })?;
441        let publication_id = NEXT_REVERSIBLE_CACHE_PUBLICATION_ID.fetch_add(1, Ordering::Relaxed);
442        let nonce = SystemTime::now()
443            .duration_since(UNIX_EPOCH)
444            .unwrap_or_default()
445            .as_nanos()
446            ^ u128::from(publication_id);
447        let staging = parent.join(format!(
448            ".{file_name}.transaction-p{:08x}-{nonce:032x}",
449            std::process::id()
450        ));
451        if staging.exists() {
452            return Err(PromptCachePersistenceError::InvalidPromptCachePath(staging));
453        }
454        Ok(Self {
455            destination,
456            parent,
457            staging,
458            replace_existing,
459            previous_generation: None,
460            moved_generation: None,
461            published: false,
462            committed: false,
463            nonce,
464        })
465    }
466
467    /// Hidden destination into which an ordinary cache save must be completed.
468    pub fn staging_destination(&self) -> &Path {
469        &self.staging
470    }
471
472    /// Makes the prepared cache visible while retaining an exact rollback path.
473    pub fn publish(&mut self) -> Result<(), PromptCachePersistenceError> {
474        if self.published || self.moved_generation.is_some() {
475            return Err(PromptCachePersistenceError::InvalidReversiblePublication(
476                "publication was already attempted",
477            ));
478        }
479        inspect_prompt_cache(&self.staging)?;
480        if !self.destination.exists() {
481            durable_rename(&self.staging, &self.destination, false).map_err(|source| {
482                PromptCachePersistenceError::Io {
483                    action: "publish prepared prompt cache",
484                    path: self.destination.clone(),
485                    source,
486                }
487            })?;
488            sync_directory(&self.parent)?;
489            self.published = true;
490            return Ok(());
491        }
492        if !self.replace_existing {
493            return Err(PromptCachePersistenceError::PromptCacheExists(
494                self.destination.clone(),
495            ));
496        }
497
498        let previous = resolve_prompt_cache_root(&self.destination)?;
499        let previous_generation = previous
500            .file_name()
501            .and_then(|name| name.to_str())
502            .ok_or_else(|| {
503                PromptCachePersistenceError::MalformedStorage(
504                    "active prompt-cache generation has no safe name".into(),
505                )
506            })?
507            .to_owned();
508        let staged = resolve_prompt_cache_root(&self.staging)?;
509        let generation_name = staged
510            .file_name()
511            .and_then(|name| name.to_str())
512            .ok_or_else(|| {
513                PromptCachePersistenceError::MalformedStorage(
514                    "prepared prompt-cache generation has no safe name".into(),
515                )
516            })?
517            .to_owned();
518        let generations = self.destination.join(PROMPT_CACHE_GENERATIONS_DIRECTORY);
519        let target = generations.join(&generation_name);
520        if target.exists() {
521            return Err(PromptCachePersistenceError::InvalidPromptCachePath(target));
522        }
523        durable_rename(&staged, &target, false).map_err(|source| {
524            PromptCachePersistenceError::Io {
525                action: "install prepared prompt cache generation",
526                path: target.clone(),
527                source,
528            }
529        })?;
530        self.previous_generation = Some(previous_generation);
531        self.moved_generation = Some(target);
532        sync_directory(&generations)?;
533        publish_generation_pointer(&self.destination, &generation_name, self.nonce)?;
534        self.published = true;
535        Ok(())
536    }
537
538    /// Accepts the visible cache and releases rollback metadata.
539    pub fn commit(mut self) -> Result<(), PromptCachePersistenceError> {
540        if !self.published {
541            return Err(PromptCachePersistenceError::InvalidReversiblePublication(
542                "an unpublished cache cannot commit",
543            ));
544        }
545        if self.staging.exists() {
546            fs::remove_dir_all(&self.staging).map_err(|source| {
547                PromptCachePersistenceError::Io {
548                    action: "remove committed prompt cache staging directory",
549                    path: self.staging.clone(),
550                    source,
551                }
552            })?;
553        }
554        self.committed = true;
555        Ok(())
556    }
557
558    /// Restores the prior visible cache, or removes a newly published cache.
559    pub fn rollback(mut self) -> Result<(), PromptCachePersistenceError> {
560        self.rollback_inner()?;
561        self.committed = true;
562        Ok(())
563    }
564
565    fn rollback_inner(&mut self) -> Result<(), PromptCachePersistenceError> {
566        if self.published {
567            match self.previous_generation.as_deref() {
568                Some(previous) => {
569                    publish_generation_pointer(&self.destination, previous, self.nonce ^ 1)?;
570                }
571                None if self.destination.exists() => {
572                    fs::remove_dir_all(&self.destination).map_err(|source| {
573                        PromptCachePersistenceError::Io {
574                            action: "remove rolled-back prompt cache",
575                            path: self.destination.clone(),
576                            source,
577                        }
578                    })?;
579                    sync_directory(&self.parent)?;
580                }
581                None => {}
582            }
583        }
584        if let Some(generation) = self.moved_generation.take() {
585            if generation.exists() {
586                fs::remove_dir_all(&generation).map_err(|source| {
587                    PromptCachePersistenceError::Io {
588                        action: "remove rolled-back prompt cache generation",
589                        path: generation.clone(),
590                        source,
591                    }
592                })?;
593                if let Some(parent) = generation.parent() {
594                    sync_directory(parent)?;
595                }
596            }
597        }
598        if self.staging.exists() {
599            fs::remove_dir_all(&self.staging).map_err(|source| {
600                PromptCachePersistenceError::Io {
601                    action: "remove rolled-back prompt cache staging directory",
602                    path: self.staging.clone(),
603                    source,
604                }
605            })?;
606        }
607        Ok(())
608    }
609}
610
611impl Drop for ReversiblePromptCachePublication {
612    fn drop(&mut self) {
613        if !self.committed {
614            let _ = self.rollback_inner();
615        }
616    }
617}
618
619/// Reads and validates a prompt-cache manifest without loading tensor arrays.
620pub fn inspect_prompt_cache(
621    directory: impl AsRef<Path>,
622) -> Result<PromptCacheManifest, PromptCachePersistenceError> {
623    let directory = resolve_prompt_cache_root(directory.as_ref())?;
624    let manifest_path = directory.join("manifest.json");
625    let reader = BufReader::new(File::open(&manifest_path).map_err(|source| {
626        PromptCachePersistenceError::Io {
627            action: "open prompt cache manifest",
628            path: manifest_path.clone(),
629            source,
630        }
631    })?);
632    let value: serde_json::Value =
633        serde_json::from_reader(reader).map_err(PromptCachePersistenceError::ManifestJson)?;
634    let schema_version = value
635        .get("schema_version")
636        .and_then(serde_json::Value::as_u64)
637        .and_then(|version| u32::try_from(version).ok())
638        .ok_or_else(|| {
639            PromptCachePersistenceError::PromptCache(PromptCacheError::Malformed(
640                "prompt-cache schema_version is missing or is not a u32".into(),
641            ))
642        })?;
643    if schema_version != PROMPT_CACHE_SCHEMA_VERSION {
644        return Err(PromptCacheError::UnsupportedSchema(schema_version).into());
645    }
646    let manifest =
647        serde_json::from_value(value).map_err(PromptCachePersistenceError::ManifestJson)?;
648    validate_prompt_cache_manifest(&directory, &manifest)?;
649    Ok(manifest)
650}
651
652/// Resolves the active immutable generation selected by the durable pointer.
653pub fn resolve_prompt_cache_root(directory: &Path) -> Result<PathBuf, PromptCachePersistenceError> {
654    let current_path = directory.join(PROMPT_CACHE_CURRENT_FILE);
655    let metadata = current_path.metadata().map_err(|source| {
656        if source.kind() == std::io::ErrorKind::NotFound {
657            PromptCachePersistenceError::MalformedStorage(
658                "prompt-cache generation pointer CURRENT is missing".into(),
659            )
660        } else {
661            PromptCachePersistenceError::Io {
662                action: "stat prompt cache generation pointer",
663                path: current_path.clone(),
664                source,
665            }
666        }
667    })?;
668    let length = metadata.len();
669    if length == 0 || length > 256 {
670        return Err(PromptCachePersistenceError::MalformedStorage(
671            "prompt-cache generation pointer has an invalid length".into(),
672        ));
673    }
674    let generation =
675        fs::read_to_string(&current_path).map_err(|source| PromptCachePersistenceError::Io {
676            action: "read prompt cache generation pointer",
677            path: current_path.clone(),
678            source,
679        })?;
680    let generation = generation.trim();
681    let generation_path = Path::new(generation);
682    if generation.is_empty()
683        || generation_path
684            .components()
685            .any(|component| !matches!(component, Component::Normal(_)))
686        || generation_path.components().count() != 1
687    {
688        return Err(PromptCachePersistenceError::MalformedStorage(
689            "prompt-cache generation pointer is unsafe".into(),
690        ));
691    }
692    let root = directory
693        .join(PROMPT_CACHE_GENERATIONS_DIRECTORY)
694        .join(generation_path);
695    if !root.is_dir() {
696        return Err(PromptCachePersistenceError::MalformedStorage(format!(
697            "prompt-cache generation {generation:?} is missing"
698        )));
699    }
700    Ok(root)
701}
702
703/// Validates manifest structure and the bounded metadata of every referenced shard.
704pub fn validate_prompt_cache_manifest(
705    directory: &Path,
706    manifest: &PromptCacheManifest,
707) -> Result<(), PromptCachePersistenceError> {
708    manifest.validate()?;
709    for block in &manifest.blocks {
710        let shard = safe_prompt_cache_shard_path(directory, &block.shard)?;
711        if !shard.is_file() {
712            return Err(PromptCachePersistenceError::MissingShard(shard));
713        }
714        validate_block_shard(&shard, block)?;
715    }
716    for state in &manifest.state_tensors {
717        let shard = safe_prompt_cache_shard_path(directory, &state.shard)?;
718        if !shard.is_file() {
719            return Err(PromptCachePersistenceError::MissingShard(shard));
720        }
721        validate_state_shard(&shard, state)?;
722    }
723    Ok(())
724}
725
726/// Resolves a manifest shard path while rejecting traversal and symlink escapes.
727pub fn safe_prompt_cache_shard_path(
728    directory: &Path,
729    relative: &str,
730) -> Result<PathBuf, PromptCachePersistenceError> {
731    let path = Path::new(relative);
732    if path.is_absolute()
733        || path
734            .components()
735            .any(|component| !matches!(component, Component::Normal(_)))
736    {
737        return Err(PromptCachePersistenceError::UnsafeShardPath(
738            relative.into(),
739        ));
740    }
741    let joined = directory.join(path);
742    if joined.exists() {
743        let root =
744            fs::canonicalize(directory).map_err(|source| PromptCachePersistenceError::Io {
745                action: "canonicalize prompt cache directory",
746                path: directory.to_path_buf(),
747                source,
748            })?;
749        let canonical =
750            fs::canonicalize(&joined).map_err(|source| PromptCachePersistenceError::Io {
751                action: "canonicalize prompt cache shard",
752                path: joined.clone(),
753                source,
754            })?;
755        if !canonical.starts_with(&root) {
756            return Err(PromptCachePersistenceError::UnsafeShardPath(
757                relative.into(),
758            ));
759        }
760    }
761    Ok(joined)
762}
763
764/// Synchronizes a newly written shard and returns its exact payload SHA-256.
765pub fn finalize_prompt_cache_shard(path: &Path) -> Result<String, PromptCachePersistenceError> {
766    sync_file(path)?;
767    hash_prompt_cache_shard_payload(path)
768}
769
770/// Hashes the safetensors payload bytes, excluding its bounded metadata header.
771pub fn hash_prompt_cache_shard_payload(path: &Path) -> Result<String, PromptCachePersistenceError> {
772    let (_, _, data_start) = read_shard_metadata(path)?;
773    let mut file = File::open(path).map_err(|source| PromptCachePersistenceError::Io {
774        action: "open prompt cache shard payload",
775        path: path.to_path_buf(),
776        source,
777    })?;
778    file.seek(SeekFrom::Start(data_start))
779        .map_err(|source| PromptCachePersistenceError::Io {
780            action: "seek prompt cache shard payload",
781            path: path.to_path_buf(),
782            source,
783        })?;
784    let mut hasher = Sha256::new();
785    let mut buffer = [0u8; 64 * 1024];
786    loop {
787        let read = file
788            .read(&mut buffer)
789            .map_err(|source| PromptCachePersistenceError::Io {
790                action: "hash prompt cache shard payload",
791                path: path.to_path_buf(),
792                source,
793            })?;
794        if read == 0 {
795            break;
796        }
797        hasher.update(&buffer[..read]);
798    }
799    Ok(hex(hasher.finalize()))
800}
801
802fn validate_block_shard(
803    path: &Path,
804    block: &PromptCacheBlock,
805) -> Result<(), PromptCachePersistenceError> {
806    let (metadata, file_len, data_start) = read_shard_metadata(path)?;
807    let entries = metadata.tensors();
808    if entries.len() != 2 {
809        return Err(malformed(
810            path,
811            format!("expected two arrays, found {}", entries.len()),
812        ));
813    }
814    let mut logical_bytes = 0u64;
815    for (name, expected_shape, expected_dtype) in [
816        (&block.first_array, &block.first_shape, &block.first_dtype),
817        (
818            &block.second_array,
819            &block.second_shape,
820            &block.second_dtype,
821        ),
822    ] {
823        let tensor = metadata
824            .info(name)
825            .ok_or_else(|| malformed(path, format!("missing array {name}")))?;
826        let shape = tensor
827            .shape
828            .iter()
829            .map(|dimension| i32::try_from(*dimension))
830            .collect::<Result<Vec<_>, _>>()
831            .map_err(|_| malformed(path, "array dimension exceeds runtime range"))?;
832        if &shape != expected_shape || stored_dtype_name(tensor.dtype) != *expected_dtype {
833            return Err(malformed(
834                path,
835                format!("array {name} shape or dtype does not match the manifest"),
836            ));
837        }
838        logical_bytes = logical_bytes.saturating_add(
839            u64::try_from(tensor.data_offsets.1.saturating_sub(tensor.data_offsets.0))
840                .unwrap_or(u64::MAX),
841        );
842    }
843    if logical_bytes != block.logical_bytes {
844        return Err(malformed(
845            path,
846            format!(
847                "logical byte count {logical_bytes} does not match manifest value {}",
848                block.logical_bytes
849            ),
850        ));
851    }
852    validate_file_boundary(path, &metadata, file_len, data_start)
853}
854
855fn validate_state_shard(
856    path: &Path,
857    state: &PromptCacheStateTensor,
858) -> Result<(), PromptCachePersistenceError> {
859    let (metadata, file_len, data_start) = read_shard_metadata(path)?;
860    let entries = metadata.tensors();
861    if entries.len() != 1 {
862        return Err(malformed(
863            path,
864            format!("expected one state array, found {}", entries.len()),
865        ));
866    }
867    let tensor = metadata
868        .info(&state.array)
869        .ok_or_else(|| malformed(path, format!("missing state array {}", state.array)))?;
870    let shape = tensor
871        .shape
872        .iter()
873        .map(|dimension| i32::try_from(*dimension))
874        .collect::<Result<Vec<_>, _>>()
875        .map_err(|_| malformed(path, "state array dimension exceeds runtime range"))?;
876    let logical_bytes = u64::try_from(tensor.data_offsets.1.saturating_sub(tensor.data_offsets.0))
877        .unwrap_or(u64::MAX);
878    if shape != state.shape
879        || stored_dtype_name(tensor.dtype) != state.dtype
880        || logical_bytes != state.logical_bytes
881    {
882        return Err(malformed(
883            path,
884            "state array shape, dtype, or byte count does not match the manifest",
885        ));
886    }
887    validate_file_boundary(path, &metadata, file_len, data_start)
888}
889
890fn validate_file_boundary(
891    path: &Path,
892    metadata: &safetensors::tensor::Metadata,
893    file_len: u64,
894    data_start: u64,
895) -> Result<(), PromptCachePersistenceError> {
896    let expected_file_len = data_start
897        .checked_add(metadata.data_len() as u64)
898        .ok_or_else(|| malformed(path, "safetensors file length overflow"))?;
899    if expected_file_len != file_len {
900        return Err(malformed(
901            path,
902            format!(
903                "safetensors payload boundary {expected_file_len} does not match file length {file_len}"
904            ),
905        ));
906    }
907    Ok(())
908}
909
910fn read_shard_metadata(
911    path: &Path,
912) -> Result<(safetensors::tensor::Metadata, u64, u64), PromptCachePersistenceError> {
913    let mut file = File::open(path).map_err(|source| PromptCachePersistenceError::Io {
914        action: "open prompt cache shard metadata",
915        path: path.to_path_buf(),
916        source,
917    })?;
918    let file_len = file
919        .metadata()
920        .map_err(|source| PromptCachePersistenceError::Io {
921            action: "stat prompt cache shard",
922            path: path.to_path_buf(),
923            source,
924        })?
925        .len();
926    let mut length_bytes = [0u8; 8];
927    file.read_exact(&mut length_bytes)
928        .map_err(|source| PromptCachePersistenceError::Io {
929            action: "read prompt cache shard header length",
930            path: path.to_path_buf(),
931            source,
932        })?;
933    let header_len = u64::from_le_bytes(length_bytes);
934    if header_len == 0 || header_len > MAX_PROMPT_CACHE_SHARD_HEADER_BYTES {
935        return Err(malformed(
936            path,
937            format!("safetensors header length {header_len} exceeds the prompt-cache bound"),
938        ));
939    }
940    let data_start = 8u64
941        .checked_add(header_len)
942        .ok_or_else(|| malformed(path, "safetensors header length overflow"))?;
943    if data_start > file_len {
944        return Err(malformed(
945            path,
946            "safetensors header extends beyond the file",
947        ));
948    }
949    let mut header = vec![0u8; header_len as usize];
950    file.read_exact(&mut header)
951        .map_err(|source| PromptCachePersistenceError::Io {
952            action: "read prompt cache shard header",
953            path: path.to_path_buf(),
954            source,
955        })?;
956    let metadata =
957        serde_json::from_slice(&header).map_err(|error| malformed(path, error.to_string()))?;
958    Ok((metadata, file_len, data_start))
959}
960
961fn malformed(path: &Path, reason: impl Into<String>) -> PromptCachePersistenceError {
962    PromptCachePersistenceError::MalformedShard {
963        path: path.to_path_buf(),
964        reason: reason.into(),
965    }
966}
967
968fn stored_dtype_name(dtype: safetensors::Dtype) -> String {
969    use safetensors::Dtype as Stored;
970    match dtype {
971        Stored::BOOL => "Bool",
972        Stored::U8 => "Uint8",
973        Stored::U16 => "Uint16",
974        Stored::U32 => "Uint32",
975        Stored::U64 => "Uint64",
976        Stored::I8 => "Int8",
977        Stored::I16 => "Int16",
978        Stored::I32 => "Int32",
979        Stored::I64 => "Int64",
980        Stored::F16 => "Float16",
981        Stored::BF16 => "Bfloat16",
982        Stored::F32 => "Float32",
983        Stored::F64 => "Float64",
984        dtype => return format!("{dtype:?}"),
985    }
986    .into()
987}
988
989fn publish_generation_pointer(
990    destination: &Path,
991    generation_name: &str,
992    nonce: u128,
993) -> Result<(), PromptCachePersistenceError> {
994    let temporary = destination.join(format!(".{PROMPT_CACHE_CURRENT_FILE}.tmp-{nonce}"));
995    let current = destination.join(PROMPT_CACHE_CURRENT_FILE);
996    let mut file = File::create(&temporary).map_err(|source| PromptCachePersistenceError::Io {
997        action: "create prompt cache generation pointer",
998        path: temporary.clone(),
999        source,
1000    })?;
1001    writeln!(file, "{generation_name}").map_err(|source| PromptCachePersistenceError::Io {
1002        action: "write prompt cache generation pointer",
1003        path: temporary.clone(),
1004        source,
1005    })?;
1006    file.sync_all()
1007        .map_err(|source| PromptCachePersistenceError::Io {
1008            action: "sync prompt cache generation pointer",
1009            path: temporary.clone(),
1010            source,
1011        })?;
1012    durable_rename(&temporary, &current, true).map_err(|source| {
1013        PromptCachePersistenceError::Io {
1014            action: "switch prompt cache generation",
1015            path: current,
1016            source,
1017        }
1018    })?;
1019    sync_directory(destination)
1020}
1021
1022fn sync_file(path: &Path) -> Result<(), PromptCachePersistenceError> {
1023    File::open(path)
1024        .and_then(|file| file.sync_all())
1025        .map_err(|source| PromptCachePersistenceError::Io {
1026            action: "synchronize cache file",
1027            path: path.to_path_buf(),
1028            source,
1029        })
1030}
1031
1032#[cfg(unix)]
1033fn sync_directory(path: &Path) -> Result<(), PromptCachePersistenceError> {
1034    File::open(path)
1035        .and_then(|file| file.sync_all())
1036        .map_err(|source| PromptCachePersistenceError::Io {
1037            action: "synchronize cache directory",
1038            path: path.to_path_buf(),
1039            source,
1040        })
1041}
1042
1043#[cfg(windows)]
1044fn sync_directory(path: &Path) -> Result<(), PromptCachePersistenceError> {
1045    if path.is_dir() {
1046        Ok(())
1047    } else {
1048        Err(PromptCachePersistenceError::Io {
1049            action: "validate cache directory before durable publication",
1050            path: path.to_path_buf(),
1051            source: std::io::Error::new(
1052                std::io::ErrorKind::NotADirectory,
1053                "cache publication path is not a directory",
1054            ),
1055        })
1056    }
1057}
1058
1059#[cfg(not(any(unix, windows)))]
1060fn sync_directory(path: &Path) -> Result<(), PromptCachePersistenceError> {
1061    if path.is_dir() {
1062        Ok(())
1063    } else {
1064        Err(PromptCachePersistenceError::Io {
1065            action: "validate cache directory before publication",
1066            path: path.to_path_buf(),
1067            source: std::io::Error::new(
1068                std::io::ErrorKind::NotADirectory,
1069                "cache publication path is not a directory",
1070            ),
1071        })
1072    }
1073}
1074
1075#[cfg(not(windows))]
1076fn durable_rename(source: &Path, destination: &Path, _replace: bool) -> std::io::Result<()> {
1077    fs::rename(source, destination)
1078}
1079
1080#[cfg(windows)]
1081fn durable_rename(source: &Path, destination: &Path, _replace: bool) -> std::io::Result<()> {
1082    fs::rename(source, destination)
1083}
1084
1085fn hex(digest: impl AsRef<[u8]>) -> String {
1086    const HEX: &[u8; 16] = b"0123456789abcdef";
1087    let digest = digest.as_ref();
1088    let mut encoded = String::with_capacity(digest.len() * 2);
1089    for &byte in digest {
1090        encoded.push(HEX[usize::from(byte >> 4)] as char);
1091        encoded.push(HEX[usize::from(byte & 0x0f)] as char);
1092    }
1093    encoded
1094}
1095
1096#[cfg(test)]
1097mod tests {
1098    use super::*;
1099    use eredu_core::cache::{
1100        CacheRepresentation, LayerCachePolicy, PromptCacheDescriptor, PromptCacheStateSegment,
1101        PromptCacheTopology,
1102    };
1103    use eredu_core::{AttentionPolicy, LayerSchedule};
1104    use safetensors::tensor::{serialize_to_file, Dtype, TensorView};
1105    use std::collections::HashMap;
1106
1107    fn manifest(shard: &Path) -> PromptCacheManifest {
1108        let bytes = [0u8; 16];
1109        let tensor = TensorView::new(Dtype::F32, vec![1, 1, 2, 2], &bytes).unwrap();
1110        serialize_to_file(
1111            HashMap::from([("keys", tensor.clone()), ("values", tensor)]),
1112            None,
1113            shard,
1114        )
1115        .unwrap();
1116        let hash = finalize_prompt_cache_shard(shard).unwrap();
1117        let descriptor = PromptCacheDescriptor::new(
1118            "test",
1119            "test",
1120            "checkpoint",
1121            "content",
1122            "architecture",
1123            1,
1124            0,
1125            1,
1126            1,
1127            LayerSchedule::new(
1128                1,
1129                vec![LayerCachePolicy::key_value(AttentionPolicy::Full, 1, 2).unwrap()],
1130            )
1131            .unwrap(),
1132            vec![0],
1133            vec![PromptCacheStateSegment::new("state", 0..1).unwrap()],
1134            0,
1135            PromptCacheTopology::default(),
1136        )
1137        .unwrap();
1138        PromptCacheManifest {
1139            schema_version: PROMPT_CACHE_SCHEMA_VERSION,
1140            model_family: descriptor.model_family().into(),
1141            effective_model_type: descriptor.effective_model_type().into(),
1142            checkpoint_fingerprint: descriptor.checkpoint_fingerprint().into(),
1143            prefix_content_fingerprint: descriptor.prefix_content_fingerprint().into(),
1144            architecture_fingerprint: descriptor.architecture_fingerprint().into(),
1145            layer_count: 1,
1146            global_layer_start: 0,
1147            global_layer_end: 1,
1148            block_size_tokens: 2,
1149            batch_size: 1,
1150            total_prefix_tokens: 2,
1151            prefix_sha256: "00".repeat(32),
1152            layer_layout: descriptor.layer_layout().clone(),
1153            layer_prefix_offsets: vec![0],
1154            state_segments: descriptor.state_segments().to_vec(),
1155            sink_tokens: 0,
1156            topology: descriptor.topology().clone(),
1157            distributed_commit: None,
1158            application_namespace: None,
1159            blocks: vec![PromptCacheBlock {
1160                global_layer: 0,
1161                representation: CacheRepresentation::KeyValue,
1162                start: 0,
1163                end: 2,
1164                rank: None,
1165                shard: shard.file_name().unwrap().to_str().unwrap().into(),
1166                first_array: "keys".into(),
1167                second_array: "values".into(),
1168                first_shape: vec![1, 1, 2, 2],
1169                second_shape: vec![1, 1, 2, 2],
1170                first_dtype: "Float32".into(),
1171                second_dtype: "Float32".into(),
1172                logical_bytes: 32,
1173                payload_sha256: hash,
1174            }],
1175            state_tensors: vec![],
1176        }
1177    }
1178
1179    #[test]
1180    fn publication_validates_and_atomically_replaces_generations() {
1181        let root = tempfile::tempdir().unwrap();
1182        let destination = root.path().join("cache");
1183        let publication = PromptCachePublication::begin(&destination, false).unwrap();
1184        let first = manifest(&publication.staging_directory().join("block.safetensors"));
1185        publication.commit(&first).unwrap();
1186        assert_eq!(inspect_prompt_cache(&destination).unwrap(), first);
1187        assert!(destination.join(PROMPT_CACHE_CURRENT_FILE).is_file());
1188        let first_root = resolve_prompt_cache_root(&destination).unwrap();
1189        assert_eq!(
1190            first_root.parent().unwrap(),
1191            destination.join(PROMPT_CACHE_GENERATIONS_DIRECTORY)
1192        );
1193        assert!(first_root.join("manifest.json").is_file());
1194
1195        let publication = PromptCachePublication::begin(&destination, true).unwrap();
1196        let second = manifest(&publication.staging_directory().join("block.safetensors"));
1197        publication.commit(&second).unwrap();
1198        assert_eq!(inspect_prompt_cache(&destination).unwrap(), second);
1199        assert!(destination.join(PROMPT_CACHE_CURRENT_FILE).is_file());
1200    }
1201
1202    #[test]
1203    fn reversible_publication_restores_replacement_and_removes_new_destination() {
1204        fn prepare_cache(destination: &Path, label: &str) -> PromptCacheManifest {
1205            let publication = PromptCachePublication::begin(destination, false).unwrap();
1206            let mut manifest = manifest(&publication.staging_directory().join("block.safetensors"));
1207            manifest.application_namespace = Some(label.into());
1208            publication.commit(&manifest).unwrap();
1209            manifest
1210        }
1211
1212        let root = tempfile::tempdir().unwrap();
1213        let destination = root.path().join("cache");
1214        let mut fresh = ReversiblePromptCachePublication::begin(&destination, false).unwrap();
1215        let fresh_manifest = prepare_cache(fresh.staging_destination(), "fresh");
1216        fresh.publish().unwrap();
1217        assert_eq!(inspect_prompt_cache(&destination).unwrap(), fresh_manifest);
1218        fresh.rollback().unwrap();
1219        assert!(!destination.exists());
1220
1221        let first = prepare_cache(&destination, "first");
1222        let first_root = resolve_prompt_cache_root(&destination).unwrap();
1223        let mut replacement = ReversiblePromptCachePublication::begin(&destination, true).unwrap();
1224        let second = prepare_cache(replacement.staging_destination(), "second");
1225        replacement.publish().unwrap();
1226        assert_eq!(inspect_prompt_cache(&destination).unwrap(), second);
1227        replacement.rollback().unwrap();
1228        assert_eq!(inspect_prompt_cache(&destination).unwrap(), first);
1229        assert_eq!(resolve_prompt_cache_root(&destination).unwrap(), first_root);
1230
1231        let mut committed = ReversiblePromptCachePublication::begin(&destination, true).unwrap();
1232        let third = prepare_cache(committed.staging_destination(), "third");
1233        committed.publish().unwrap();
1234        committed.commit().unwrap();
1235        assert_eq!(inspect_prompt_cache(&destination).unwrap(), third);
1236    }
1237
1238    #[test]
1239    fn pointerless_legacy_prompt_cache_layout_is_rejected() {
1240        let root = tempfile::tempdir().unwrap();
1241        let destination = root.path().join("cache");
1242        fs::create_dir(&destination).unwrap();
1243        let legacy = manifest(&destination.join("block.safetensors"));
1244        serde_json::to_writer(
1245            File::create(destination.join("manifest.json")).unwrap(),
1246            &legacy,
1247        )
1248        .unwrap();
1249
1250        for result in [
1251            resolve_prompt_cache_root(&destination).map(|_| ()),
1252            inspect_prompt_cache(&destination).map(|_| ()),
1253        ] {
1254            assert!(matches!(
1255                result,
1256                Err(PromptCachePersistenceError::MalformedStorage(reason))
1257                    if reason == "prompt-cache generation pointer CURRENT is missing"
1258            ));
1259        }
1260    }
1261
1262    #[test]
1263    fn failed_publication_removes_staging_directory() {
1264        let root = tempfile::tempdir().unwrap();
1265        let destination = root.path().join("cache");
1266        let staging = {
1267            let publication = PromptCachePublication::begin(&destination, false).unwrap();
1268            publication.staging_directory().to_path_buf()
1269        };
1270        assert!(!staging.exists());
1271    }
1272
1273    #[test]
1274    fn shard_paths_reject_traversal() {
1275        let root = Path::new("/tmp/cache");
1276        assert_eq!(
1277            safe_prompt_cache_shard_path(root, "block.safetensors").unwrap(),
1278            root.join("block.safetensors")
1279        );
1280        assert!(matches!(
1281            safe_prompt_cache_shard_path(root, "../outside.safetensors"),
1282            Err(PromptCachePersistenceError::UnsafeShardPath(_))
1283        ));
1284        assert!(safe_prompt_cache_shard_path(root, "/outside.safetensors").is_err());
1285    }
1286
1287    #[test]
1288    fn malformed_manifest_is_rejected_before_tensor_loading() {
1289        let directory = tempfile::tempdir().unwrap();
1290        let generation = directory
1291            .path()
1292            .join(PROMPT_CACHE_GENERATIONS_DIRECTORY)
1293            .join("generation-test");
1294        fs::create_dir_all(&generation).unwrap();
1295        fs::write(generation.join("manifest.json"), b"{not-json").unwrap();
1296        fs::write(
1297            directory.path().join(PROMPT_CACHE_CURRENT_FILE),
1298            b"generation-test\n",
1299        )
1300        .unwrap();
1301        assert!(matches!(
1302            inspect_prompt_cache(directory.path()),
1303            Err(PromptCachePersistenceError::ManifestJson(_))
1304        ));
1305    }
1306
1307    #[test]
1308    fn shard_metadata_reads_are_bounded() {
1309        let directory = tempfile::tempdir().unwrap();
1310        let path = directory.path().join("oversized.safetensors");
1311        fs::write(
1312            &path,
1313            (MAX_PROMPT_CACHE_SHARD_HEADER_BYTES + 1).to_le_bytes(),
1314        )
1315        .unwrap();
1316        assert!(matches!(
1317            hash_prompt_cache_shard_payload(&path),
1318            Err(PromptCachePersistenceError::MalformedShard { .. })
1319        ));
1320    }
1321
1322    #[test]
1323    fn live_cache_publication_is_unique_rank_aware_and_atomic() {
1324        let directory = tempfile::tempdir().unwrap();
1325        let id = CacheBlockId {
1326            session_id: 7,
1327            global_layer: 3,
1328            representation: CacheRepresentation::KeyValue,
1329            start: 4,
1330            end: 8,
1331            rank: Some(eredu_core::cache::CacheRankIdentity::new(
1332                Some(1),
1333                Some(2),
1334                None,
1335            )),
1336        };
1337        let first = LiveCacheBlockPublication::begin(directory.path(), &id);
1338        let second = LiveCacheBlockPublication::begin(directory.path(), &id);
1339        assert_ne!(first.destination_path(), second.destination_path());
1340        assert!(first
1341            .destination_path()
1342            .to_string_lossy()
1343            .contains("layer-00003-kv-rank-p1-t2-ex-4-8"));
1344
1345        fs::write(first.staging_path(), b"block").unwrap();
1346        let destination = first.commit().unwrap();
1347        assert_eq!(fs::read(destination).unwrap(), b"block");
1348    }
1349
1350    #[test]
1351    fn live_cache_publication_cleans_staging_and_never_replaces() {
1352        let directory = tempfile::tempdir().unwrap();
1353        let id = CacheBlockId {
1354            session_id: 1,
1355            global_layer: 0,
1356            representation: CacheRepresentation::CompressedLatentRotary,
1357            start: 0,
1358            end: 1,
1359            rank: None,
1360        };
1361        let abandoned = LiveCacheBlockPublication::begin(directory.path(), &id);
1362        let abandoned_path = abandoned.staging_path().to_path_buf();
1363        fs::write(&abandoned_path, b"temporary").unwrap();
1364        drop(abandoned);
1365        assert!(!abandoned_path.exists());
1366
1367        let colliding = LiveCacheBlockPublication::begin(directory.path(), &id);
1368        fs::write(colliding.staging_path(), b"new").unwrap();
1369        fs::write(colliding.destination_path(), b"existing").unwrap();
1370        let destination = colliding.destination_path().to_path_buf();
1371        assert!(colliding.commit().is_err());
1372        assert_eq!(fs::read(destination).unwrap(), b"existing");
1373    }
1374}