fix(task-worktree): build/bench 分锁 TTL + done 自动接管陈旧锁
A:LOCK_TTL_HOURS 由全局 6h 改为按锁分设(build 1h / bench 6h)—— 兜底窗口应与操作真实时长同量级:编译是秒~分钟级,6h 太钝。 并支持 lock acquire --ttl 自报窗口(keil_build 按其 --timeout 自报, timeout/3600+15min,下限 0.5h),使 build 锁窗口自动跟随构建上限。 B:done 收口前置把陈旧锁视为空闲并自动释放(原为硬失败), 仅当锁仍被有效占用时才拒绝。 新增 lock_ttl_hours / lock_is_stale / release_stale_locks 三个辅助; lock status 显示各锁 TTL;cmd_done 改调 release_stale_locks。 验证:20 项 TTL 探针 + 5 项 lock_cli 接线探针全绿; 假 UV4 端到端(exit 0 自持自释、payload ttl=0.5)与 busy 路径(exit 4)。
This commit is contained in:
@@ -29,7 +29,14 @@ EXIT_OK, EXIT_FAIL, EXIT_ENV, EXIT_USAGE = 0, 1, 2, 3
|
||||
BRANCH_PREFIX = "task/"
|
||||
MAX_CONCURRENT = 3
|
||||
LOCK_NAMES = ("build", "bench")
|
||||
LOCK_TTL_HOURS = 6.0 # 超过此持有时间即视为陈旧锁,下次 acquire 自动接管
|
||||
# 按锁分设陈旧窗口(小时)——兜底窗口应与「操作真实时长」同量级:
|
||||
# build:编译是秒~分钟级(keil_build.py 自持自释,持有窗口 ≈ UV4 运行时长,
|
||||
# 上限 = --timeout,默认 600s);崩溃泄漏时应在数十分钟内即可接管 → 1h。
|
||||
# bench:会话级(flash → @test → 探针,天然几十分钟~数小时)→ 保留 6h。
|
||||
# 另:锁 payload 里持有者可用 `ttl` 字段自报窗口(见 lock_acquire(ttl=...)),
|
||||
# 优先于这里的默认值——这样 build 锁的窗口能自动跟随其 --timeout。
|
||||
LOCK_TTL_HOURS = {"build": 1.0, "bench": 6.0}
|
||||
DEFAULT_LOCK_TTL_HOURS = 6.0
|
||||
JUNCTION_SPECS = (
|
||||
(".venv", "Python 环境"),
|
||||
("simulator/SDL2", "模拟器 SDL2 SDK"),
|
||||
@@ -212,6 +219,26 @@ def lock_age_hours(info) -> float:
|
||||
return float("inf")
|
||||
|
||||
|
||||
def lock_ttl_hours(name: str, info=None) -> float:
|
||||
"""锁 `name` 的陈旧判定窗口(小时)。
|
||||
|
||||
优先用锁 payload 里持有者自报的 `ttl`(例如 keil_build.py 按其 --timeout 声明),
|
||||
否则回落到该锁的默认窗口(build 1h / bench 6h)。
|
||||
"""
|
||||
if info:
|
||||
try:
|
||||
v = info.get("ttl")
|
||||
if v:
|
||||
return max(0.0, float(v))
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
return LOCK_TTL_HOURS.get(name, DEFAULT_LOCK_TTL_HOURS)
|
||||
|
||||
|
||||
def lock_is_stale(name: str, info) -> bool:
|
||||
return lock_age_hours(info) > lock_ttl_hours(name, info)
|
||||
|
||||
|
||||
def lock_path(name: str) -> Path:
|
||||
return LOCKS_DIR / (name + ".lock")
|
||||
|
||||
@@ -226,12 +253,14 @@ def lock_read(name: str):
|
||||
return {"task": "?", "pid": 0, "epoch": 0, "at": "?"}
|
||||
|
||||
|
||||
def lock_acquire(name: str, task: str, wait: int = 0, force: bool = False, note: str = ""):
|
||||
def lock_acquire(name: str, task: str, wait: int = 0, force: bool = False, note: str = "",
|
||||
ttl: float = None):
|
||||
"""抢占独占资源。
|
||||
|
||||
⚠ 锁**不**绑定进程存活:`lock acquire` 本身是短命进程,而用锁的烧录/构建
|
||||
在另一个进程里跑。因此判「陈旧」只看**时效**(超过 LOCK_TTL_HOURS),
|
||||
外加显式 `--force`;pid 仅作记录便于排障。
|
||||
在另一个进程里跑。因此判「陈旧」只看**时效**(超过该锁的 TTL),外加显式
|
||||
`--force`;pid 仅作记录便于排障。TTL 按锁分设(build 1h / bench 6h),
|
||||
持有者也可用 `ttl`(小时)自报更精确的窗口(如 build 按 --timeout 声明)。
|
||||
"""
|
||||
LOCKS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
p = lock_path(name)
|
||||
@@ -244,9 +273,10 @@ def lock_acquire(name: str, task: str, wait: int = 0, force: bool = False, note:
|
||||
if info.get("task") == task:
|
||||
return True, info
|
||||
age = lock_age_hours(info)
|
||||
if force or age > LOCK_TTL_HOURS:
|
||||
print("[lock] 接管 {} 锁(原持有者 {},已持有 {:.1f} 小时{})".format(
|
||||
name, info.get("task"), age, ",--force" if force else ",视为陈旧"))
|
||||
ttl_h = lock_ttl_hours(name, info)
|
||||
if force or age > ttl_h:
|
||||
print("[lock] 接管 {} 锁(原持有者 {},已持有 {:.1f} 小时 / TTL {:.1f} 小时{})".format(
|
||||
name, info.get("task"), age, ttl_h, ",--force" if force else ",视为陈旧"))
|
||||
try:
|
||||
p.unlink()
|
||||
except OSError:
|
||||
@@ -254,8 +284,8 @@ def lock_acquire(name: str, task: str, wait: int = 0, force: bool = False, note:
|
||||
continue
|
||||
if time.time() >= deadline:
|
||||
return False, info
|
||||
print("[lock] {} 被 {} 占用(已持有 {:.1f} 小时),等待…".format(
|
||||
name, info.get("task"), age))
|
||||
print("[lock] {} 被 {} 占用(已持有 {:.1f} / TTL {:.1f} 小时),等待…".format(
|
||||
name, info.get("task"), age, ttl_h))
|
||||
time.sleep(2)
|
||||
else:
|
||||
now = time.time()
|
||||
@@ -265,6 +295,7 @@ def lock_acquire(name: str, task: str, wait: int = 0, force: bool = False, note:
|
||||
"epoch": now,
|
||||
"at": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(now)),
|
||||
"note": note,
|
||||
"ttl": ttl,
|
||||
}
|
||||
os.write(fd, (json.dumps(payload, ensure_ascii=False) + "\n").encode("utf-8"))
|
||||
os.close(fd)
|
||||
@@ -631,13 +662,14 @@ def cmd_lock(a) -> int:
|
||||
for n in LOCK_NAMES:
|
||||
info = lock_read(n)
|
||||
if not info:
|
||||
print("{:<6} 空闲".format(n))
|
||||
print("{:<6} 空闲(TTL {:.1f}h)".format(n, lock_ttl_hours(n)))
|
||||
else:
|
||||
age = lock_age_hours(info)
|
||||
mark = " [陈旧,可被自动接管]" if age > LOCK_TTL_HOURS else ""
|
||||
print("{:<6} {} 持有(pid={},于 {},已 {:.1f} 小时,备注 {}){}".format(
|
||||
ttl_h = lock_ttl_hours(n, info)
|
||||
mark = " [陈旧,可被自动接管]" if age > ttl_h else ""
|
||||
print("{:<6} {} 持有(pid={},于 {},已 {:.1f} / TTL {:.1f} 小时,备注 {}){}".format(
|
||||
n, info.get("task"), info.get("pid"), info.get("at"), age,
|
||||
info.get("note") or "-", mark))
|
||||
ttl_h, info.get("note") or "-", mark))
|
||||
return EXIT_OK
|
||||
|
||||
if a.name not in LOCK_NAMES:
|
||||
@@ -646,12 +678,15 @@ def cmd_lock(a) -> int:
|
||||
if a.action == "acquire":
|
||||
if not a.task:
|
||||
raise EnvError("acquire 需要 --task <ID>")
|
||||
got, info = lock_acquire(a.name, a.task, wait=a.wait, force=a.force, note=a.note or "")
|
||||
got, info = lock_acquire(a.name, a.task, wait=a.wait, force=a.force,
|
||||
note=a.note or "", ttl=a.ttl)
|
||||
if not got:
|
||||
print("获取失败:{} 被 {} 占用(pid={},于 {})".format(
|
||||
a.name, info.get("task"), info.get("pid"), info.get("at")))
|
||||
return EXIT_FAIL
|
||||
print("已获取 {} 锁(task={} pid={})".format(a.name, a.task, os.getpid()))
|
||||
print("已获取 {} 锁(task={} pid={} ttl={})".format(
|
||||
a.name, a.task, os.getpid(),
|
||||
"{:.2f}h".format(a.ttl) if a.ttl else "默认"))
|
||||
return EXIT_OK
|
||||
|
||||
if a.bench_pass:
|
||||
@@ -666,6 +701,27 @@ def cmd_lock(a) -> int:
|
||||
return EXIT_OK if ok else EXIT_FAIL
|
||||
|
||||
|
||||
def release_stale_locks() -> None:
|
||||
"""`done` 前置:陈旧锁自动接管;仍被有效占用的则拒绝收口。
|
||||
|
||||
「陈旧」= 持有时间 > 该锁 TTL(build 1h / bench 6h,或持有者自报的 ttl)。
|
||||
崩溃/遗忘留下的锁不该阻塞收口——故自动释放而非硬失败(2026-10-12)。
|
||||
"""
|
||||
for n in LOCK_NAMES:
|
||||
info = lock_read(n)
|
||||
if not info:
|
||||
continue
|
||||
age = lock_age_hours(info)
|
||||
ttl_h = lock_ttl_hours(n, info)
|
||||
if age > ttl_h:
|
||||
ok, msg = lock_release(n, force=True)
|
||||
print("[i] {} 锁已陈旧({} 持有 {:.1f}h > TTL {:.1f}h),自动接管:{}".format(
|
||||
n, info.get("task"), age, ttl_h, msg))
|
||||
continue
|
||||
raise Fail("独占资源 {} 仍被 {} 占用(已 {:.1f} / TTL {:.1f} 小时),先 release".format(
|
||||
n, info.get("task"), age, ttl_h))
|
||||
|
||||
|
||||
def cmd_done(a) -> int:
|
||||
st = load_state(a.task_id)
|
||||
wt = Path(st["path"])
|
||||
@@ -683,11 +739,7 @@ def cmd_done(a) -> int:
|
||||
cur = git_out("rev-parse", "--abbrev-ref", "HEAD", cwd=main)
|
||||
if cur != base:
|
||||
raise Fail("主工作树当前在 {},不是集成分支 {};先切过去再 done".format(cur, base))
|
||||
for n in LOCK_NAMES:
|
||||
info = lock_read(n)
|
||||
if info:
|
||||
raise Fail("独占资源 {} 仍被 {} 占用(已 {:.1f} 小时),先 release".format(
|
||||
n, info.get("task"), lock_age_hours(info)))
|
||||
release_stale_locks()
|
||||
|
||||
ahead = int(git_out("rev-list", "--count", "{}..{}".format(base, branch), cwd=wt) or "0")
|
||||
already = git("merge-base", "--is-ancestor", branch, base, cwd=main, check=False).returncode == 0
|
||||
@@ -816,6 +868,8 @@ def build_parser():
|
||||
p.add_argument("--wait", type=int, default=0, help="等待秒数(0=不等待)")
|
||||
p.add_argument("--force", action="store_true")
|
||||
p.add_argument("--note", default="")
|
||||
p.add_argument("--ttl", type=float, default=None,
|
||||
help="本次持有自报的陈旧窗口(小时);缺省用该锁默认值(build 1h / bench 6h)")
|
||||
p.add_argument("--bench-pass", action="store_true",
|
||||
help="release bench 时顺带为该任务记录台架 PASS")
|
||||
p.set_defaults(func=cmd_lock)
|
||||
|
||||
Reference in New Issue
Block a user