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
4 files VERSION+1 −1src/snapshot/snapshot_test.v+14 −6src/snapshot/tree.v+11 −4v.mod+1 −1
VERSION+1 −1
@@ -1 +1 @@1-3.0.371+3.0.38

view file

src/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() {

view file

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 have

view file

v.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 }

view file