mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-26 05:32:17 -04:00
93 lines
3.8 KiB
Python
93 lines
3.8 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/** (the shared bodies — engine_core.h until Phase 35 T4, the per-function headers after — and
|
|
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)
|