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:
227
test/wcc/989_pkgcache_concurrent_run.c
Normal file
227
test/wcc/989_pkgcache_concurrent_run.c
Normal file
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
* 989_pkgcache_concurrent_run — concurrent shared-cache build-correctness
|
||||
* smoke (#104). N distinct root programs that all `import shared` are built
|
||||
* CONCURRENTLY into ONE shared WW_PKGCACHE; every resulting binary must be
|
||||
* byte-IDENTICAL to a reference built in ISOLATION (private cache) and run to
|
||||
* its expected exit. This guards that concurrent `ww build --sep` sharing one
|
||||
* out/.pkgcache produces correct, deterministic binaries — a regression guard
|
||||
* for the cache subsystem under contention (store crash, lock bug, wrong-key
|
||||
* copy, etc.).
|
||||
*
|
||||
* It does NOT prove the temp+rename store is atomic against torn reads: with
|
||||
* content-keying every concurrent cold build MISSES at lookup and STORES (it
|
||||
* never HIT-reads a mid-store entry), so the torn-read window is not forced
|
||||
* here. That race is closed by construction at the cache_store fix site; the
|
||||
* deferred white-box guard is TASK #105.
|
||||
*
|
||||
* Both driver stages (rule 10): the cs and ww references are byte-identical.
|
||||
* Light wwstage-driver test (CLAUDE.md rule 14): all outputs + caches live
|
||||
* under a private /tmp tree, so it is parallel-safe and off every byte-id /
|
||||
* bootstrap gate. Models 989_pkgcache_run conventions.
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#define NPROG 4
|
||||
#define BASE_EXIT 7 /* shared.v() == 7; rootI returns 7 + I */
|
||||
|
||||
static int
|
||||
runwait(const char *cmd)
|
||||
{
|
||||
int rc = system(cmd);
|
||||
if (rc == -1) return -1;
|
||||
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static const char *
|
||||
absbin(void)
|
||||
{
|
||||
const char *b = getenv("BIN");
|
||||
if (!b) b = "out/bin";
|
||||
if (b[0] == '/') return b;
|
||||
static char buf[2048];
|
||||
char cwd[1024];
|
||||
if (getcwd(cwd, sizeof cwd) == NULL) return NULL;
|
||||
snprintf(buf, sizeof buf, "%s/%s", cwd, b);
|
||||
return buf;
|
||||
}
|
||||
|
||||
static int
|
||||
write_file(const char *path, const char *body)
|
||||
{
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) return -1;
|
||||
fputs(body, f);
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
slurp(const char *path, char **outbuf, size_t *outlen)
|
||||
{
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) return -1;
|
||||
fseek(f, 0, SEEK_END);
|
||||
long n = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
if (n < 0) { fclose(f); return -1; }
|
||||
char *b = malloc((size_t)n + 1);
|
||||
if (!b) { fclose(f); return -1; }
|
||||
if (fread(b, 1, (size_t)n, f) != (size_t)n) { free(b); fclose(f); return -1; }
|
||||
b[n] = '\0';
|
||||
fclose(f);
|
||||
*outbuf = b;
|
||||
*outlen = (size_t)n;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
files_eq(const char *a, const char *b)
|
||||
{
|
||||
char *ba = NULL, *bb = NULL;
|
||||
size_t na = 0, nb = 0;
|
||||
if (slurp(a, &ba, &na) < 0 || slurp(b, &bb, &nb) < 0) {
|
||||
free(ba); free(bb);
|
||||
return -1;
|
||||
}
|
||||
int eq = (na == nb && memcmp(ba, bb, na) == 0);
|
||||
free(ba); free(bb);
|
||||
return eq ? 0 : 1;
|
||||
}
|
||||
|
||||
static const char *shared_src =
|
||||
"package shared;\n"
|
||||
"export fn v() i32 = { return 7; };\n";
|
||||
|
||||
int
|
||||
main(void)
|
||||
{
|
||||
const char *bin = absbin();
|
||||
if (!bin) return 1;
|
||||
int fail = 0;
|
||||
char td[64], cmd[8192], batch[32768];
|
||||
char sharedww[1024];
|
||||
|
||||
snprintf(td, sizeof td, "/tmp/wwconc_%d", getpid());
|
||||
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
|
||||
runwait(cmd);
|
||||
snprintf(cmd, sizeof cmd, "mkdir -p %s/shared", td);
|
||||
runwait(cmd);
|
||||
|
||||
snprintf(sharedww, sizeof sharedww, "%s/shared/shared.ww", td);
|
||||
if (write_file(sharedww, shared_src)) { fail++; goto out; }
|
||||
|
||||
/* N distinct roots, each importing the one shared pkg. Distinct return
|
||||
* (7 + I) so each program — and so each clean binary — is distinguishable,
|
||||
* while the contended cache entry (shared) is common to all. */
|
||||
for (int i = 0; i < NPROG; i++) {
|
||||
char rootww[1024], src[256];
|
||||
snprintf(rootww, sizeof rootww, "%s/root%d.ww", td, i);
|
||||
snprintf(src, sizeof src,
|
||||
"package main;\nimport shared;\n"
|
||||
"fn main() i32 = { return shared.v() + %d; };\n", i);
|
||||
if (write_file(rootww, src)) { fail++; goto out; }
|
||||
}
|
||||
|
||||
struct { const char *drv, *tag; } stg[] = {
|
||||
{ "ww", "cs" },
|
||||
{ "ww_ww", "ww" },
|
||||
};
|
||||
|
||||
for (int s = 0; s < 2; s++) {
|
||||
const char *drv = stg[s].drv, *tag = stg[s].tag;
|
||||
char shcache[1024];
|
||||
snprintf(shcache, sizeof shcache, "%s/cache.%s", td, tag);
|
||||
|
||||
/* References: each rootI built ISOLATED (private cache, no contention)
|
||||
* = the clean baseline bytes a concurrent build must reproduce. */
|
||||
for (int i = 0; i < NPROG; i++) {
|
||||
char refcache[1024], refprog[1024], refscr[1024], rootww[1024];
|
||||
snprintf(refcache, sizeof refcache, "%s/refc.%s.%d", td, tag, i);
|
||||
snprintf(refprog, sizeof refprog, "%s/ref.%s.%d", td, tag, i);
|
||||
snprintf(refscr, sizeof refscr, "%s/ref.%s.%d.sepwork", td, tag, i);
|
||||
snprintf(rootww, sizeof rootww, "%s/root%d.ww", td, i);
|
||||
snprintf(cmd, sizeof cmd,
|
||||
"rm -rf %s %s; WW_PKGCACHE='%s' timeout 240 %s/%s build --sep "
|
||||
"-o %s %s >/dev/null 2>&1",
|
||||
refscr, refcache, refcache, bin, drv, refprog, rootww);
|
||||
if (runwait(cmd) != 0) {
|
||||
fprintf(stderr, "concur FAIL[%s]: reference build %d failed\n",
|
||||
drv, i);
|
||||
fail++;
|
||||
}
|
||||
if (runwait(refprog) != BASE_EXIT + i) {
|
||||
fprintf(stderr, "concur FAIL[%s]: reference %d wrong exit\n",
|
||||
drv, i);
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
|
||||
/* Fresh shared cache so every build in the batch MISS-stores `shared`
|
||||
* concurrently → write contention on shared/P.{wwi,o,key}. Distinct
|
||||
* per-prog output so every binary can be verified, not just one. */
|
||||
snprintf(cmd, sizeof cmd, "rm -rf %s", shcache);
|
||||
runwait(cmd);
|
||||
|
||||
size_t off = 0;
|
||||
off += (size_t)snprintf(batch + off, sizeof batch - off,
|
||||
"export WW_PKGCACHE='%s'; ", shcache);
|
||||
for (int i = 0; i < NPROG; i++) {
|
||||
off += (size_t)snprintf(batch + off, sizeof batch - off,
|
||||
"( rm -rf %s/c.%s.%d.sepwork; timeout 240 %s/%s build "
|
||||
"--sep -o %s/c.%s.%d %s/root%d.ww >/dev/null 2>&1 ) & ",
|
||||
td, tag, i, bin, drv,
|
||||
td, tag, i, td, i);
|
||||
}
|
||||
off += (size_t)snprintf(batch + off, sizeof batch - off, "wait");
|
||||
if (off >= sizeof batch) {
|
||||
fprintf(stderr, "concur FAIL: batch cmd truncated\n");
|
||||
fail++; goto out;
|
||||
}
|
||||
runwait(batch);
|
||||
|
||||
for (int i = 0; i < NPROG; i++) {
|
||||
char prog[1024], refprog[1024];
|
||||
snprintf(prog, sizeof prog, "%s/c.%s.%d", td, tag, i);
|
||||
snprintf(refprog, sizeof refprog, "%s/ref.%s.%d", td, tag, i);
|
||||
if (runwait(prog) != BASE_EXIT + i) {
|
||||
fprintf(stderr, "concur FAIL[%s]: concurrent prog i=%d "
|
||||
"wrong/failed exit\n", drv, i);
|
||||
fail++;
|
||||
}
|
||||
if (files_eq(prog, refprog) != 0) {
|
||||
fprintf(stderr, "concur FAIL[%s]: concurrent binary i=%d "
|
||||
"!= isolated reference\n", drv, i);
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* rule 10: the cs and ww isolated references are byte-identical. */
|
||||
for (int i = 0; i < NPROG; i++) {
|
||||
char a[1024], b[1024];
|
||||
snprintf(a, sizeof a, "%s/ref.cs.%d", td, i);
|
||||
snprintf(b, sizeof b, "%s/ref.ww.%d", td, i);
|
||||
if (files_eq(a, b) != 0) {
|
||||
fprintf(stderr, "concur FAIL: cs != ww reference %d (rule 10)\n", i);
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
|
||||
out:
|
||||
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
|
||||
runwait(cmd);
|
||||
if (fail) {
|
||||
fprintf(stderr, "concur: %d check(s) failed\n", fail);
|
||||
return 1;
|
||||
}
|
||||
printf("concur: %d concurrent --sep builds sharing one cache, both stages "
|
||||
"— every binary byte-identical to its isolated reference + correct run; "
|
||||
"cs==ww references (rule 10)\n", NPROG);
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user