mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-29 04:46:50 -06:00
M3: re-read a library file whose stamp is as recent as the read
A library file is cached by its length and modification time. File systems keep the time in coarse ticks, so a same-length rewrite right after a read can keep the stamp, and library.updates then reports no change. A stamp now proves the file unchanged only once its modification time lies at least two seconds before the read; otherwise the file is read again.
This commit is contained in:
@@ -449,9 +449,25 @@ struct Stamp {
|
||||
#[derive(Clone, Debug)]
|
||||
struct Entry {
|
||||
stamp: Option<Stamp>,
|
||||
/// When the file was read: a stamp only proves the file unchanged once its modification time
|
||||
/// is safely older than that, since file systems keep the time in coarse ticks.
|
||||
read_at: Option<std::time::SystemTime>,
|
||||
result: std::result::Result<Arc<PublishedLibrary>, String>,
|
||||
}
|
||||
|
||||
/// How far a file's modification time must lie before its read for the stamp to be trusted.
|
||||
const RACY_STAMP: std::time::Duration = std::time::Duration::from_secs(2);
|
||||
|
||||
impl Entry {
|
||||
fn still_valid(&self, now: &Stamp) -> bool {
|
||||
let settled = match (now.modified, self.read_at) {
|
||||
(Some(modified), Some(read_at)) => modified.checked_add(RACY_STAMP).is_some_and(|t| t < read_at),
|
||||
_ => false,
|
||||
};
|
||||
settled && self.stamp.as_ref() == Some(now)
|
||||
}
|
||||
}
|
||||
|
||||
/// Library files read by a session, by path (re-read when they change on disk).
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Cache {
|
||||
@@ -481,10 +497,11 @@ impl Cache {
|
||||
return Err(EngineError::Other(format!("{path} is too large to use as a library (the limit is {} MB)", max_bytes / 1024 / 1024)));
|
||||
}
|
||||
if let (Some(e), Ok(st)) = (self.entries.get(path), &stamp)
|
||||
&& e.stamp.as_ref() == Some(st)
|
||||
&& e.still_valid(st)
|
||||
{
|
||||
return e.result.clone().map_err(EngineError::Other);
|
||||
}
|
||||
let read_at = now();
|
||||
let result = match &stamp {
|
||||
Ok(_) => read_file(path).map(Arc::new),
|
||||
Err(e) => Err(e.clone()),
|
||||
@@ -492,7 +509,7 @@ impl Cache {
|
||||
if self.entries.len() >= MAX_CACHED && !self.entries.contains_key(path) {
|
||||
self.entries.clear();
|
||||
}
|
||||
self.entries.insert(path.to_string(), Entry { stamp: stamp.ok(), result: result.clone() });
|
||||
self.entries.insert(path.to_string(), Entry { stamp: stamp.ok(), read_at, result: result.clone() });
|
||||
result.map_err(EngineError::Other)
|
||||
}
|
||||
|
||||
@@ -506,10 +523,20 @@ impl Cache {
|
||||
if self.entries.len() >= MAX_CACHED && !self.entries.contains_key(path) {
|
||||
self.entries.clear();
|
||||
}
|
||||
self.entries.insert(path.to_string(), Entry { stamp: None, result });
|
||||
self.entries.insert(path.to_string(), Entry { stamp: None, read_at: None, result });
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn now() -> Option<std::time::SystemTime> {
|
||||
Some(std::time::SystemTime::now())
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn now() -> Option<std::time::SystemTime> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn stat(path: &str) -> std::result::Result<Stamp, String> {
|
||||
let m = std::fs::metadata(path).map_err(|e| format!("{path}: {e}"))?;
|
||||
|
||||
@@ -399,3 +399,21 @@ fn library_survives_saving_with_versions() {
|
||||
healthy(&user);
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_stamp_from_the_moment_it_was_read_does_not_prove_it_unchanged() {
|
||||
use std::time::{Duration, SystemTime};
|
||||
let read_at = SystemTime::now();
|
||||
let entry = |modified: SystemTime| Entry {
|
||||
stamp: Some(Stamp { len: 10, modified: Some(modified) }),
|
||||
read_at: Some(read_at),
|
||||
result: Err("unused".into()),
|
||||
};
|
||||
// Written in the same coarse tick as the read: a same-length rewrite would keep this stamp.
|
||||
let fresh = entry(read_at);
|
||||
assert!(!fresh.still_valid(&Stamp { len: 10, modified: Some(read_at) }));
|
||||
// Written well before the read: an unchanged stamp means an unchanged file.
|
||||
let old = read_at - Duration::from_secs(10);
|
||||
assert!(entry(old).still_valid(&Stamp { len: 10, modified: Some(old) }));
|
||||
assert!(!entry(old).still_valid(&Stamp { len: 11, modified: Some(old) }));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user