wcc,ww,os: atomic pkgcache store via temp+rename, both stages (#104)
The out/.pkgcache content-keyed store copied each artifact IN-PLACE
(cp -f / copyfile) to the fixed paths P.wwi/P.o/P.key. Key-last gave
crash-consistency but NOT concurrent-read safety: two same-stage builds
of a shared lib pkg (rt/time/os) target one out/.pkgcache/<pkg>/P.{wwi,o};
once an early finisher writes P.key, a later build's cache_lookup copies
P.wwi/P.o while a mid-finisher is still mid-write -> torn read -> corrupt
link / cs!=ww. The key is content-only, so it is purely the non-atomic
write.
Fix (Go-build-cache pattern, both stages in lock-step, rule 10): write
each artifact to a per-pid same-dir temp (P.wwi.tmp.<pid> etc.) then
rename() into place. Same dir => rename is atomic (cross-fs is not);
per-pid temp => concurrent writers don't clobber each other mid-copy;
content-keyed => last-writer-wins is byte-identical. Key renamed LAST so
a reader that sees the new key always finds complete artifacts. On any
mid-store error the per-pid temps are unlinked so a failed store leaves
no litter (cstage goto cleanup; wwstage cachermtmp helper).
cstage cmd/ww/main.c cache_store: libc rename(2) + getpid().
wwstage selfhost/cmd/ww/main.ww cachestore: new os.rename + cachetmp.
lib/os/os.ww: add rename(2) (RENAME=82), ref/hare/os/os.ha:17 -- returns
raw i32 errno like sibling remove/mkdir/rmdir (ww's os is the flat
syscall floor, no fs:: layer); a second pathbuf2 slot holds newpath
since kpath's single pathbuf can't carry both paths.
cache_lookup is unchanged: it reads cache->private scratch, and an atomic
source is never torn.
The torn-read race is closed BY CONSTRUCTION; a deterministic behavioral
regression-guard isn't feasible through the product build path (content-
keying => concurrent COLD builds all MISS+STORE, never HIT-read a mid-store
entry; a warm cache is never re-stored). The deferred white-box guard is
TASK #105. A WHY-comment at both fix sites records this.
Tests: 989_sepbuild_run KEEPS its private per-pid WW_PKGCACHE -- the
comment is corrected: the pin is NOT a torn-read mask (closed by
construction) but cold-compile isolation for the test's INTERMEDIATE
(.s/.unit.ww) byte-id compare, which a cache HIT legitimately skips
producing. The former 989_pkgcache_atomic_run is renamed to
989_pkgcache_concurrent_run and HONESTLY relabeled: it is a concurrent
shared-cache build-correctness smoke (N concurrent --sep builds sharing
one cache -> every binary byte-identical to an isolated reference + correct
run, both stages), NOT a torn-read/atomicity proof (a review revert-
experiment proved the original claim vacuous). Shrunk to 4 concurrent
builds x 1 batch x both stages. COLD/dev-only, off every byte-id/bootstrap
gate.
selfhost/cmd/ww/main.combined.ww remains stale (its writer was deleted at
the M4 E3-C1 flip; #90 deletes the file) -- not regenerated.
make test: all 445 passed; make sizelint clean; 990-997 byte-id hold.
This commit is contained in:
@@ -1266,8 +1266,65 @@ fn cachelookup(cacheroot: *u8, g: *sepgraph, pi: i32, manifest: *u8,
|
||||
return 1;
|
||||
};
|
||||
|
||||
// cachestore — on MISS persist the artifacts then the key (key LAST: a crash
|
||||
// mid-store never leaves a key without its artifacts; the next run re-misses).
|
||||
// cachetmp — build "<dir>/<name>.tmp.<pid>" (NUL-terminated) into a fresh
|
||||
// buffer. #104: a per-pid unique same-dir temp for the atomic cachestore
|
||||
// rename. pid is folded in decimal manually (no strconv import here, twin
|
||||
// of makeruntmp).
|
||||
fn cachetmp(dir: *u8, name: str, pid: i32) *u8 = {
|
||||
let buf: []u8 = alloc([], (os.PATH_MAX: u64))!;
|
||||
buf.len = os.PATH_MAX;
|
||||
let off: u64 = cstrinto(buf.ptr, 0u64, dir);
|
||||
off = byteinto(buf.ptr, off, 47u8); // '/'
|
||||
off = strinto(buf.ptr, off, name);
|
||||
off = strinto(buf.ptr, off, ".tmp.");
|
||||
let dig: [16]u8;
|
||||
let n: i32 = 0;
|
||||
if (pid <= 0) {
|
||||
dig[n] = 48u8; // '0'
|
||||
n += 1;
|
||||
} else {
|
||||
let v: i32 = pid;
|
||||
for (v > 0) {
|
||||
dig[n] = ((v % 10) + 48): u8;
|
||||
n += 1;
|
||||
v = v / 10;
|
||||
};
|
||||
};
|
||||
let k: i32 = n - 1;
|
||||
for (k >= 0) {
|
||||
buf.ptr[off] = dig[k];
|
||||
off += 1u64;
|
||||
k -= 1;
|
||||
};
|
||||
cstrseal(buf.ptr, off);
|
||||
return buf.ptr;
|
||||
};
|
||||
|
||||
// cachermtmp — remove the per-pid cachestore temps on a mid-store error so a
|
||||
// failed store leaves no litter (#104). Twin of cstage cache_store's cleanup.
|
||||
fn cachermtmp(a: *u8, b: *u8, c: *u8) void = {
|
||||
os.remove(pathstr(a));
|
||||
os.remove(pathstr(b));
|
||||
os.remove(pathstr(c));
|
||||
};
|
||||
|
||||
// cachestore — on MISS persist the artifacts then the key. Each is copied/
|
||||
// written to a per-pid same-dir temp then os.rename'd into place: rename is
|
||||
// atomic within one filesystem (cross-fs is not), so a concurrent cachelookup
|
||||
// never observes a half-written P.wwi/P.o/P.key (#104). The per-pid temp name
|
||||
// keeps two concurrent writers from clobbering mid-copy; content-keyed ⇒
|
||||
// last-writer-wins is byte-identical. The key is renamed LAST so a reader that
|
||||
// sees the new key always finds complete artifacts, and a crash mid-store
|
||||
// never leaves a key without them. Twin of cstage cache_store.
|
||||
//
|
||||
// The torn-read race is thus closed BY CONSTRUCTION. A deterministic
|
||||
// behavioral regression-guard isn't feasible through the product build path:
|
||||
// content-keying means concurrent COLD builds all MISS at lookup and STORE —
|
||||
// none HIT-reads a mid-store entry — and a warm cache is never re-stored, so
|
||||
// "store concurrent with a HIT-read of the same entry" can't be forced. The
|
||||
// deferred white-box guard is TASK #105; 989_pkgcache_concurrent_run smokes
|
||||
// that concurrent shared-cache builds stay correct. On any mid-store error the
|
||||
// per-pid temps are removed so a failed store leaves no litter.
|
||||
fn cachestore(cacheroot: *u8, g: *sepgraph, pi: i32, manifest: *u8,
|
||||
mlen: u64, wwi: *u8, objf: *u8) void = {
|
||||
let dir: *u8 = pkgcachedir(cacheroot, g, pi);
|
||||
@@ -1278,13 +1335,26 @@ fn cachestore(cacheroot: *u8, g: *sepgraph, pi: i32, manifest: *u8,
|
||||
let cwwi: *u8 = joinpathlit(dir, "P.wwi");
|
||||
let cobj: *u8 = joinpathlit(dir, "P.o");
|
||||
let keyp: *u8 = joinpathlit(dir, "P.key");
|
||||
if (copyfile(wwi, cwwi) != 0) { return; };
|
||||
if (copyfile(objf, cobj) != 0) { return; };
|
||||
let fd: i32 = os.open(pathstr(keyp),
|
||||
let pid: i32 = os.getpid();
|
||||
let twwi: *u8 = cachetmp(dir, "P.wwi", pid);
|
||||
let tobj: *u8 = cachetmp(dir, "P.o", pid);
|
||||
let tkey: *u8 = cachetmp(dir, "P.key", pid);
|
||||
if (copyfile(wwi, twwi) != 0) { cachermtmp(twwi, tobj, tkey); return; };
|
||||
if (copyfile(objf, tobj) != 0) { cachermtmp(twwi, tobj, tkey); return; };
|
||||
let fd: i32 = os.open(pathstr(tkey),
|
||||
os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644
|
||||
if (fd < 0) { return; };
|
||||
if (fd < 0) { cachermtmp(twwi, tobj, tkey); return; };
|
||||
os.writeall(fd, manifest, mlen);
|
||||
os.close(fd);
|
||||
if (os.rename(pathstr(twwi), pathstr(cwwi)) != 0) {
|
||||
cachermtmp(twwi, tobj, tkey); return;
|
||||
};
|
||||
if (os.rename(pathstr(tobj), pathstr(cobj)) != 0) {
|
||||
cachermtmp(twwi, tobj, tkey); return;
|
||||
};
|
||||
if (os.rename(pathstr(tkey), pathstr(keyp)) != 0) {
|
||||
cachermtmp(twwi, tobj, tkey); return;
|
||||
};
|
||||
};
|
||||
|
||||
// buildonesep — the --sep orchestration: discover deps, reverse-topo,
|
||||
|
||||
Reference in New Issue
Block a user