a34d063b3d6f
judge racy cache rows by the newer of mtime and ctime, closing 3.0.29's codex review (3.0.38)
- time
- 2026-09-24 04:00 UTC · 2 weeks ago
- machine
- koh2
- agent
- claude-opus-5-5
- session
- koh 3 reliability campaign
- parent
- 702789575895
↓ koh-a34d063b.tar.gzatlas at this save
files changed4 files
VERSION+1 −1src/snapshot/snapshot_test.v+14 −6src/snapshot/tree.v+11 −4v.mod+1 −1src/snapshot/snapshot_test.v+14 −6
@@ -134,9 +134,17 @@134134 135135 // Linux stamps files from a clock that ticks every few milliseconds, so a136136 // same-size rewrite in the tick the file was hashed in leaves size, mtime and137-// ctime unchanged. A hash taken that close to the last write is never cached;138-// the next walk hashes again.139-fn test_cache_skips_a_file_written_moments_ago() {137+// ctime unchanged. A hash taken that close to the last change is never cached;138+// the next walk hashes again. Tools such as `cp -p` keep an old mtime while139+// rewriting, so the newer of the two times decides.140+fn test_cache_skips_a_file_changed_moments_ago() {141+ second := i64(1_000_000_000)142+ now := 100 * second143+ old := now - 10 * second144+ assert cacheable(Stat{ mtime_ns: old, ctime_ns: old }, now)145+ assert !cacheable(Stat{ mtime_ns: now - second, ctime_ns: now - second }, now)146+ assert !cacheable(Stat{ mtime_ns: old, ctime_ns: now - second }, now)147+140148 mut store := storage.create(temp_root('racy')!)!141149 root := store.root142150 defer {@@ -144,11 +152,11 @@144152 os.rmdir_all(root) or {}145153 }146154 put(root, 'fresh.txt', 'new\n')!147- put(root, 'old.txt', 'old\n')!155+ put(root, 'copied.txt', 'kept mtime\n')!148156 hour_ago := time.now().unix() - 3600149- os.utime(os.join_path(root, 'old.txt'), hour_ago, hour_ago)!157+ os.utime(os.join_path(root, 'copied.txt'), hour_ago, hour_ago)!150158 walk(store, root, Ignore{})!151- assert store.load_cache()!.keys() == ['old.txt']159+ assert store.load_cache()!.len == 0152160 }153161 154162 fn test_restore_writes_removes_and_leaves_ignored_files_alone() {src/snapshot/tree.v+11 −4
@@ -78,10 +78,7 @@7878 }7979 data := os.read_bytes(os.join_path(root, rel))!8080 hash := storage.hash_hex(data)81- // Linux stamps files from a clock that ticks every few milliseconds, so82- // a same-size rewrite in this tick would look unchanged to the cache.83- // A file written this recently is hashed again next time instead.84- if time.now().unix_nano() - st.mtime_ns < racy_window_ns {81+ if !cacheable(st, time.now().unix_nano()) {8582 return hash8683 }8784 s.save_cache(storage.CacheRow{@@ -94,6 +91,16 @@9491 return hash9592 }9693 return storage.hash_hex(os.read_bytes(os.join_path(root, rel))!)94+}95+96+// cacheable reports whether a hash may be cached. Linux stamps files from a97+// clock that ticks every few milliseconds, so a same-size rewrite in the tick98+// of the hash would look unchanged; a file changed that recently is hashed99+// again next time instead. `cp -p` and the like keep an old mtime while100+// rewriting, so the newer of mtime and ctime decides.101+fn cacheable(st Stat, now_ns i64) bool {102+ changed := if st.ctime_ns > st.mtime_ns { st.ctime_ns } else { st.mtime_ns }103+ return now_ns - changed >= racy_window_ns97104 }98105 99106 // store_objects writes the bytes of every entry the database does not havev.mod+1 −1
@@ -1,6 +1,6 @@11 Module {22 name: 'koh3'3- version: '3.0.37'3+ version: '3.0.38'44 description: 'One SQLite file per repo, one line of history.'55 license: 'MIT'66 }