mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-26 13:33:34 -04:00
e0eaa16741
STAGE 1 of docs/concurrency-design.md, landed and negative-control proven.
tools/shared_lock.py (NEW) — one reader/writer flock over the FLEET-SHARED state
(src/shared/*, config/overlays.mk, config/dedup.us.yaml, the overlay .c files
propagation rewrites). Per-binary resources keep gate_stage's existing per-binary flock.
- gate_stage takes it SHARED when the gate writes no shared state, EXCLUSIVE when it
does (propagate, or the arity pre-pass enabled) -- so distinct-binary gates still run
concurrently but can never overlap a writer.
- dedup_propagate and fix_arity_callers --apply take it EXCLUSIVE.
- NESTING-AWARE: gate_stage SPAWNS both writers, so a naive child lock would deadlock
against the parent. The holder exports BFM_SHARED_LOCK_HELD and children inherit.
NEGATIVE CONTROLS (all pass):
NC1 a held SHARED lock refuses a non-blocking exclusive writer, loudly, naming the lock
NC2 parent-holds/child-inherits does NOT deadlock (the real risk in this design)
NC3 two readers acquire concurrently (0.00s) -- phase-B parallelism preserved
bulk_harvest docstring CORRECTED: its "propagation is the ONLY writer of the shared
engine_core.h" claim was FALSE as written and had been asserted for phases (F1 -- the
arity pre-pass writes it from inside every worker). Now states what is actually true,
under which two conditions, plus the one-line assertion that detects a violation.
BANKS: wave-3's drafts re-gated on a CLEAN tree -> 15 of 20 banked. The same drafts
previously reported 0 banked / 20 near -- that verdict was 100% an artifact of the
broken tree, which is why they were held as UNJUDGED rather than accepted as failures.
check-all 213 passed / 0 failed. F1 bracketing assertion CLEAN.
Session total banked: 44 functions + func_8015C030 propagated x7.
92 lines
3.7 KiB
Python
92 lines
3.7 KiB
Python
#!/usr/bin/env python3
|
|
"""shared_lock.py — one reader/writer lock over the FLEET-SHARED source state.
|
|
|
|
Stage 1 of docs/concurrency-design.md.
|
|
|
|
WHAT IT PROTECTS
|
|
src/shared/engine_core.h, src/shared/engine_types.h, config/overlays.mk,
|
|
config/dedup.us.yaml, and the per-overlay .c files that propagation rewrites.
|
|
These are the only files whose writers span binaries; every per-binary resource
|
|
(build/<bin>/**, src/<bin>/*.c, asm/<bin>/**) is already isolated and is guarded by
|
|
gate_stage's existing per-binary flock.
|
|
|
|
WHY
|
|
A gate that merely READS the shared header can safely run beside other such gates —
|
|
that is the parallel lane bulk_harvest wants. But a writer (dedup_propagate,
|
|
fix_arity_callers --apply) racing a reader can let a gate compute a verdict against a
|
|
TRANSIENT tree state that never exists again ("verdict decay", design §2.2b). That
|
|
cannot false-bank — the gate compares against config/check.<bin>.sha, which no stage
|
|
writes, and INCLUDE_ASM pastes the original bytes — but it mints banks whose C is not
|
|
byte-correct against the state actually committed, and the R22 then fails on a batch
|
|
that "all gated green".
|
|
|
|
MODES
|
|
shared() readers — parallel gates that do not write shared state
|
|
exclusive() writers — dedup_propagate, fix_arity_callers --apply, serial-lane gates
|
|
|
|
NESTING
|
|
gate_stage SPAWNS both writers as subprocesses. A child re-acquiring exclusively while
|
|
the parent holds the lock would deadlock, so the parent exports BFM_SHARED_LOCK_HELD=1
|
|
and children treat the lock as already held. The parent is responsible for taking the
|
|
correct mode for what it is about to do (EX if it will propagate or run the arity
|
|
pre-pass, SH otherwise) — see gate_stage.run_gate.
|
|
"""
|
|
import contextlib
|
|
import fcntl
|
|
import os
|
|
import sys
|
|
|
|
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
LOCK = os.path.join(REPO, ".run/auto/shared_state.lock")
|
|
ENV = "BFM_SHARED_LOCK_HELD"
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def hold(exclusive, nonblocking=False, announce=None):
|
|
"""Hold the shared-state lock. No-op if an ancestor already holds it (see NESTING)."""
|
|
if os.environ.get(ENV):
|
|
yield "inherited"
|
|
return
|
|
os.makedirs(os.path.dirname(LOCK), exist_ok=True)
|
|
fh = open(LOCK, "w")
|
|
mode = fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH
|
|
if nonblocking:
|
|
mode |= fcntl.LOCK_NB
|
|
try:
|
|
fcntl.flock(fh, mode)
|
|
except OSError:
|
|
fh.close()
|
|
raise SystemExit(
|
|
f"[shared-lock] REFUSED: another process holds {os.path.relpath(LOCK, REPO)}"
|
|
+ (f" ({announce})" if announce else "")
|
|
+ ".\n A fleet-shared writer may not run while gates are reading shared state"
|
|
" (docs/concurrency-design.md §2.2b). Wait for it, or re-run without --nb.")
|
|
prev = os.environ.get(ENV)
|
|
os.environ[ENV] = "1"
|
|
try:
|
|
yield "exclusive" if exclusive else "shared"
|
|
finally:
|
|
if prev is None:
|
|
os.environ.pop(ENV, None)
|
|
else:
|
|
os.environ[ENV] = prev
|
|
fcntl.flock(fh, fcntl.LOCK_UN)
|
|
fh.close()
|
|
|
|
|
|
def held_by_ancestor():
|
|
return bool(os.environ.get(ENV))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Test helper: hold the lock for N seconds so a negative control can observe a refusal.
|
|
# python3 tools/shared_lock.py shared 30
|
|
import time
|
|
mode = (sys.argv[1] if len(sys.argv) > 1 else "shared").lower()
|
|
secs = float(sys.argv[2]) if len(sys.argv) > 2 else 5.0
|
|
os.environ.pop(ENV, None)
|
|
with hold(exclusive=(mode == "exclusive")) as m:
|
|
print(f"[shared-lock] holding {m} for {secs}s (pid {os.getpid()})", flush=True)
|
|
time.sleep(secs)
|
|
print("[shared-lock] released", flush=True)
|