fix(kvb-org-queue): global budget so no queue mix can OOM the host

Root cause of the 2026-11-07 20:14 OOM: the queue granted jobs per kind
only, so several heavy kinds and direct heavy commands outside the queue
ran side by side until the kernel killed the editor.

- queue.py: every grant checks the sum of the learned RAM and CPU
  footprints of active and granted jobs against budget_percent; an
  unlearned kind takes the whole budget and runs alone. RAM used plus
  what young jobs (RAMP_SECONDS) may still take must stay below
  load_limits. Memory stage stop blocks every grant.
- watch.py: memory stage from MemAvailable, swap, PSI full and the swap
  of the container holding most of it; the block hook classifies
  builds, make install/verify-*, and pytest or unittest beyond one file
  as heavy through wrappers, sh -c, command substitutions and heredocs
  a shell reads.
- history.py: footprints per kind and signature, seed RAM for pending
  shares.
- operator.py: [h]old queues a heavy row through queue.py hold and asks
  again on its grant, with a flushed tty and a timeout; sudo rows, which
  need the terminal, stay direct.

Residuals: a shell reading code from a pipe or here-string, its own
script, a program named by a substitution, quotes inside an expanding
heredoc, an escaped backslash before a substitution; a lone job waits
while the host is over load_limits.

🎯 Fix: 92 %
💥 Break: 4 %
💸 Cost: 100 %
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 88
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kevin Veen-BirkenbachandClaude Opus 5.5 committed 2026-11-08 06:54:09 +00:00
1 parent 09f68dd75b
commit 4321b97529
8 files changed
+1665 -218

No files matched your search

+150 -21
View File
@@ -32,8 +32,19 @@ Args:
visible), reason and check, then a preview of what the command
changes, and asks [y]es / [s]kip / [d]iff / [q]uit; d shows the full
diff. y runs the command in its directory, then sets the row done
when its check passes (manual: when the command exits 0). End of
input quits. There is no flag that answers for the operator.
when its check passes (manual: when the command exits 0). A command
the kvb-org-organize watch config calls heavy also shows the memory
stage and `queue.py list`; once the config holds the queue keys and
slot_need_gb.operator, and the command runs no sudo, [h]old runs
`granted` through `queue.py hold --kind operator --task <row>
--session Operator`. End of input quits. There is no flag that
answers for the operator.
granted <task> --dir <dir> --check <check> -- $SHELL -ic <cmd>: what
[h]old queues. On the grant it shows the row and its preview again
and asks [y]es / [s]kip / [d]iff / [q]uit, then runs the command in
the row's directory; a row no longer open with exactly that
directory, check and command runs nothing, nor does a question left
unanswered for GRANT_SECONDS.
done <slug>: mark a row done, whatever its check.
Output:
@@ -45,7 +56,8 @@ Output:
prints the same rows as a JSON list.
Exit codes:
0 done, 2 usage error.
0 done, 2 usage error. granted: the command's exit code, or 75
(EX_TEMPFAIL, which queue.py hold does not learn from) when nothing ran.
"""
from __future__ import annotations
@@ -62,13 +74,25 @@ import hashlib
import importlib.util
import json
import re
import select
import shlex
import shutil
import subprocess
import termios
import time
from collections.abc import Callable
from pathlib import Path
from types import ModuleType
RASCI = Path(__file__).resolve().parents[2] / "kvb-org-rasci" / "scripts" / "rasci.py"
SKILLS = Path(__file__).resolve().parents[2]
RASCI = SKILLS / "kvb-org-rasci" / "scripts" / "rasci.py"
WATCH = SKILLS / "kvb-org-organize" / "scripts" / "watch.py"
QUEUE = SKILLS / "kvb-org-queue" / "scripts" / "queue.py"
HOLD_KIND = "operator"
HOLD_SESSION = "Operator"
NOT_RUN = 75
GRANT_SECONDS = 600
SUDO = re.compile(r"\bsudo\b")
PREFIX = "operator/"
KINDS = ("pushed", "installed", "binary", "absent", "manual", "decision")
OPEN = ("queued", "blocked")
@@ -264,8 +288,8 @@ def runnable(row: dict) -> bool:
return bool(row["directory"] and row["command"]) and row["check"] != "decision"
def visible(text: str) -> str:
return "".join(c if c.isprintable() else c.encode("unicode_escape").decode() for c in text)
def visible(text: str, keep: str) -> str:
return "".join(c if c.isprintable() or c in keep else c.encode("unicode_escape").decode() for c in text)
def git_tree(directory: str) -> bool:
@@ -312,14 +336,113 @@ def show(commands: list[tuple[list[str], str]]) -> None:
print(f" {error}", flush=True)
def ask() -> str:
def ask(holdable: bool, read: Callable[[str], str | None]) -> str:
choices = ("y", "h", "s", "d", "q") if holdable else ("y", "s", "d", "q")
prompt = "Run it? [y]es / [h]old in the queue / [s]kip / [d]iff / [q]uit: " if holdable else "Run it? [y]es / [s]kip / [d]iff / [q]uit: "
while True:
try:
answer = input("Run it? [y]es / [s]kip / [d]iff / [q]uit: ").strip().lower()
except EOFError:
answer = read(prompt)
if answer is None:
return "q"
if answer in ("y", "s", "d", "q"):
return answer
if answer.strip().lower() in choices:
return answer.strip().lower()
def typed(prompt: str) -> str | None:
try:
return input(prompt)
except EOFError:
return None
def timed(prompt: str) -> str | None:
"""Returns:
One line read unbuffered from stdin, None at its end or when no line
came within GRANT_SECONDS, so an unanswered grant frees its slot;
what was typed while the job waited is dropped first.
"""
if sys.stdin.isatty():
termios.tcflush(sys.stdin.fileno(), termios.TCIFLUSH)
print(prompt, end="", flush=True)
deadline = time.monotonic() + GRANT_SECONDS
line = b""
while not line.endswith(b"\n"):
ready, _, _ = select.select([sys.stdin.fileno()], [], [], max(0.0, deadline - time.monotonic()))
byte = os.read(sys.stdin.fileno(), 1) if ready else b""
if not byte:
print(flush=True)
return None
line += byte
return line.decode(errors="replace")
def needs_terminal(command: str) -> bool:
return SUDO.search(command) is not None
def heavy_row(watch: ModuleType, cfg: dict | None, row: dict) -> bool:
"""Returns:
Whether [h]old can queue the row's command: a heavy job by the
kvb-org-organize watch config, which holds every queue key and the
kind operator, and no sudo, whose prompt needs the terminal a queued
command lacks; prints the memory stage and `queue.py list` for a
heavy one, its output control characters made visible.
"""
if cfg is None or not watch.heavy(row["command"], cfg, tuple(cfg["never_block"])):
return False
problem = watch.missing(cfg, (*watch.MEMORY_KEYS, "budget_percent.ram", "budget_percent.cpu", f"slot_need_gb.{HOLD_KIND}"))
if problem is None and needs_terminal(row["command"]):
problem = "sudo asks on the terminal, which a queued command lacks"
if problem is None:
sample = watch.memory()
print(f" heavy: memory stage {watch.memory_stage(sample, cfg)} ({watch.memory_text(sample)}); the queue:", flush=True)
else:
print(f" heavy: {problem}; no [h]old until it does. The queue:", flush=True)
listing = subprocess.run([sys.executable, str(QUEUE), "list"], cwd=row["directory"], capture_output=True, text=True, check=False)
print(visible(listing.stdout + listing.stderr, "\t\n"), end="", flush=True)
return problem is None
def held(row: dict, shell: str) -> list[str]:
return [
sys.executable, str(QUEUE), "hold", "--kind", HOLD_KIND, "--task", row["task"], "--session", HOLD_SESSION,
"--", sys.executable, str(Path(__file__).resolve()), "granted", row["task"], "--dir", row["directory"],
"--check", row["check"], "--", shell, "-ic", row["command"],
]
def review(row: dict, holdable: bool, read: Callable[[str], str | None]) -> str:
check = "manual" if problem(row["check"]) else row["check"]
show(evidence(check, row["directory"], full=False))
answer = ask(holdable, read)
while answer == "d":
show(evidence(check, row["directory"], full=True))
answer = ask(holdable, read)
return answer
def cmd_granted(rasci: ModuleType, args: argparse.Namespace) -> int:
shell = os.environ.get("SHELL")
approved = [
row for row in open_rows(rasci)
if runnable(row) and (row["task"], row["directory"], row["check"]) == (args.task, args.dir, args.check)
and args.argv == [shell, "-ic", row["command"]]
]
if not shell or not approved:
print(f"operator: {args.task} is no longer open with the approved directory, check and command; nothing ran",
file=sys.stderr)
return NOT_RUN
row = approved[0]
print(f"\ngranted {row['task']}: the queue lets it run now; its tree may have changed while it waited")
print(f" directory: {visible(row['directory'], '')}")
print(f" command: {visible(row['command'], '')}", flush=True)
try:
answer = review(row, False, timed)
except KeyboardInterrupt:
answer = "q"
if answer != "y":
print(f"not run {row['task']}")
return NOT_RUN
return subprocess.run(args.argv, cwd=row["directory"], check=False).returncode
def close(rasci: ModuleType, task: str) -> None:
@@ -337,26 +460,26 @@ def cmd_run(rasci: ModuleType, args: argparse.Namespace) -> int:
if not steps:
print("operator: nothing to run")
return 0
watch = module("kvb_operator_watch", WATCH)
cfg = watch.load_config()
for number, row in enumerate(steps, 1):
check = "manual" if problem(row["check"]) else row["check"]
print(f"\n[{number}/{len(steps)}] {row['task']}")
print(f" directory: {visible(row['directory'])}")
print(f" command: {visible(row['command'])}")
print(f" why: {visible(row['why'] or '-')}")
print(f" directory: {visible(row['directory'], '')}")
print(f" command: {visible(row['command'], '')}")
print(f" why: {visible(row['why'] or '-', '')}")
print(f" check: {check}", flush=True)
show(evidence(check, row["directory"], full=False))
answer = ask()
while answer == "d":
show(evidence(check, row["directory"], full=True))
answer = ask()
holdable = heavy_row(watch, cfg, row)
answer = review(row, holdable, typed)
if answer == "q":
print("operator: stopped")
return 0
if answer == "s":
print(f"skipped {row['task']}")
continue
argv = held(row, shell) if answer == "h" else [shell, "-ic", row["command"]]
try:
code = subprocess.run([shell, "-ic", row["command"]], cwd=row["directory"], check=False).returncode
code = subprocess.run(argv, cwd=row["directory"], check=False).returncode
except OSError as error:
print(f"failed {row['task']}: {error}")
continue
@@ -394,6 +517,12 @@ def main() -> int:
p.set_defaults(func=cmd_list)
p = sub.add_parser("run")
p.set_defaults(func=cmd_run)
p = sub.add_parser("granted")
p.add_argument("task")
p.add_argument("--dir", required=True)
p.add_argument("--check", required=True)
p.add_argument("argv", nargs="+")
p.set_defaults(func=cmd_granted)
p = sub.add_parser("done")
p.add_argument("slug")
p.set_defaults(func=cmd_done)
+706 -51
View File
@@ -9,11 +9,14 @@ Usage:
(kvb-check-security) whose id it lacks to the same name
under ~/.local/state/agents-council/
watch.py block hook (PreToolUse on Bash): exit 2 for a heavy job while the
stage is stop or critical, or while a kvb-org-radio mayday
holds radio silence (<root>/radio-silence.json, until > now)
disk stage is stop or critical, the memory stage is stop
or cannot be judged without memory_limits, or a
kvb-org-radio mayday holds radio silence
(<root>/radio-silence.json, until > now)
watch.py admit (--need-gb N | --kind KIND)
exit 0 when the grant leaves at least the stop threshold
free, 3 otherwise
free, CPU busy and RAM used stay below load_limits and
the memory stage is not stop, 3 otherwise
watch.py cleanup (--caches | --full)
trim caches to their budget; --full also runs the
configured low-risk cleanup commands
@@ -45,8 +48,15 @@ compose project name is skipped), worktree_marker (path relative
to a worktree that must exist), busy_processes (process names; a worktree
where one of them runs is skipped), cache_dirs, cache_budget_percent,
slot_need_gb {kind: GB}, notify (argv the alert text is appended to),
load_limits {cpu_percent, mem_percent, sample_seconds, kinds}: admit for a
kind listed in kinds also needs CPU busy and RAM used below the limits.
load_limits {cpu_percent, mem_percent, sample_seconds}: every admit and every
queue grant needs CPU busy and RAM used below them, memory_limits {warn, stop},
each {available, swap_free, psi_full, container_swap_gb}: the memory stage is
the first of stop and warn whose MemAvailable or SwapFree percentage lies below
its bound, whose /proc/pressure/memory full avg10 reaches its bound, or where
one container (a docker-*.scope cgroup under system.slice, nested groups
counted) holds at least container_swap_gb GB of swap, else ok; a host without
swap or PSI skips that bound. heavy_commands extend the built-in rules of
heavy().
"""
from __future__ import annotations
@@ -56,6 +66,7 @@ import contextlib
import fcntl
import importlib.util
import json
import math
import os
import re
import shlex
@@ -99,6 +110,86 @@ CLIP = 1000
CLEANUP_LOCK = "cleanup.lock"
LOCK_TTL = 1800
GB = 1e9
MEMINFO = Path("/proc/meminfo")
PRESSURE = Path("/proc/pressure/memory")
CGROUPS = Path("/sys/fs/cgroup/system.slice")
SWAP_HOLDERS = 3
NEST_DEPTH = 8
MEMORY_STAGES = ("ok", "warn", "stop")
MEMORY_BOUNDS = ("available", "swap_free", "psi_full", "container_swap_gb")
MEMORY_KEYS = tuple(f"memory_limits.{stage}.{bound}" for stage in MEMORY_STAGES[1:] for bound in MEMORY_BOUNDS)
CONTAINER_ID = re.compile(r"[0-9a-f]{64}")
COMPOSE_VALUES = frozenset({
"-f", "--file", "-p", "--project-name", "--project-directory", "--env-file", "--profile", "--ansi", "--parallel",
"--progress",
})
BUILDING = frozenset({"up", "run", "create"})
SEPARATOR = re.compile(r"[();<>|&\n]+")
PUNCTUATION = "();<>|&\n"
WORD_BREAK = " \t\n;&|()<>"
ASSIGNMENT = re.compile(r"[A-Za-z_]\w*=.*", re.DOTALL)
WRAPPERS = frozenset({
"env", "sudo", "nice", "nohup", "timeout", "exec", "xargs", "stdbuf", "ionice", "uv", "uvx", "poetry", "pdm",
"pipenv", "coverage", "time", "command",
})
UV_VALUES = frozenset({
"--with", "--extra", "--group", "--python", "-p", "--project", "--directory", "--package", "--env-file", "--from",
})
WRAPPER_VALUES = {
"sudo": frozenset({
"-u", "-g", "-h", "-p", "-C", "-D", "-r", "-t", "-U", "-T", "--user", "--group", "--host", "--chdir", "--role",
"--type", "--prompt", "--other-user", "--command-timeout", "--close-from",
}),
"timeout": frozenset({"-s", "-k", "--signal", "--kill-after"}),
"env": frozenset({"-u", "-C", "--unset", "--chdir"}),
"nice": frozenset({"-n", "--adjustment"}),
"ionice": frozenset({"-c", "-n", "-p", "--class", "--classdata"}),
"xargs": frozenset({"-a", "-d", "-E", "-I", "-L", "-n", "-P", "-s"}),
"stdbuf": frozenset({"-i", "-o", "-e"}),
"time": frozenset({"-f", "-o", "--format", "--output"}),
"exec": frozenset({"-a"}),
"uv": UV_VALUES,
"uvx": UV_VALUES,
"poetry": frozenset({"-C", "-P", "--directory", "--project"}),
"pdm": frozenset({"-p", "--project"}),
"coverage": frozenset({"--source", "--include", "--omit", "--rcfile", "--context", "--data-file"}),
}
WRAPPER_WORDS = frozenset({"run", "tool"})
RESERVED = frozenset({"!", "{", "if", "then", "else", "elif", "do", "while", "until"})
WRAPPER_ARGUMENT = re.compile(r"-.*|\d+(\.\d+)?[smhd]?", re.DOTALL)
SHELLS = frozenset({"bash", "sh", "zsh", "dash"})
INLINE = re.compile(r"-[A-Za-z]*c[A-Za-z]*")
REDIRECT = re.compile(r"&?[<>]+&?")
TRAILING_REDIRECT = re.compile(r"[<>]+&?$")
PAREN = {"(": 1, ")": -1}
PYTHON = re.compile(r"python[\d.]*")
PYTHON_VALUE_OPTIONS = frozenset({"-X", "-W", "-Q"})
DRY_RUN = frozenset({"-n", "--dry-run", "--just-print", "--recon", "-q", "--question"})
SHORT_DRY = re.compile(r"-(?:(?![CfIoOWjl])[A-Za-z])*[nq][A-Za-z]*")
SHELL_VALUE = re.compile(r"[-+][A-Za-z]*[oO]|--rcfile|--init-file")
STDIN_FLAG = re.compile(r"-[A-Za-z]*s[A-Za-z]*")
SPACE = re.compile(r"\s")
ESCAPED = re.compile(r"\\[`$]")
OPENER = re.compile(r"`|\$\(")
PLACEHOLDER = "-_"
HEREDOC_MARK = "kvbheredoc"
MARK_WORD = re.compile(rf"{HEREDOC_MARK}((?:[0-9a-f]{{2}})*)")
HELP = frozenset({"-h", "--help", "--version"})
PYTEST_LIGHT = HELP | {"--collect-only", "--co"}
TEST_FILE = re.compile(r"[^*?\[\]]+\.py(::.*)?", re.DOTALL)
PYTEST_VALUE_OPTIONS = frozenset({
"-p", "-k", "-m", "-c", "-o", "-W", "-n", "--numprocesses", "-r", "--maxfail", "--tb", "--rootdir", "--basetemp", "--deselect",
"--ignore", "--ignore-glob", "--confcutdir", "--durations", "--junitxml", "--log-level", "--dist", "--cov",
"--cov-report", "--cov-config", "--timeout", "--import-mode", "--capture", "--color", "--log-cli-level",
"--log-file", "--reruns", "--tx", "--max-worker-restart",
})
UNITTEST_VALUE_OPTIONS = frozenset({"-k"})
DISCOVER_VALUE_OPTIONS = frozenset({"-s", "-p", "-t", "-k", "--start-directory", "--pattern", "--top-level-directory"})
PATTERN_OPTIONS = frozenset({"-p", "--pattern"})
MAKE_VALUE_OPTIONS = frozenset({
"-C", "-f", "-I", "-o", "-W", "--directory", "--file", "--makefile", "--include-dir", "--old-file", "--assume-old",
"--what-if", "--new-file", "--assume-new",
})
def guard_root(hook: bool) -> int | None:
@@ -123,6 +214,35 @@ def load_config() -> dict | None:
return json.loads(CONFIG.read_text(encoding="utf-8"))
def absent_part(cfg: dict, key: str) -> str | None:
node: object = cfg
parts = key.split(".")
for depth, part in enumerate(parts, 1):
if not isinstance(node, dict) or part not in node:
return ".".join(parts[:depth])
node = node[part]
return None if isinstance(node, (int, float)) and not isinstance(node, bool) and math.isfinite(node) else key
def missing(cfg: dict, keys: tuple[str, ...]) -> str | None:
"""Args:
cfg: the config.
keys: required number keys, nested ones dotted
(`memory_limits.stop.psi_full`).
Returns:
None when every key holds a number, else the error naming the
shortest missing part of each, or the key whose value is no number.
"""
absent = list(dict.fromkeys(part for key in keys if (part := absent_part(cfg, key)) is not None))
if not absent:
return None
return (
f"agents council: {CONFIG} lacks {', '.join(absent)}; add them as numbers as the watch.py and queue.py "
"docstrings document"
)
def disk(cfg: dict) -> tuple[float, float]:
st = os.statvfs(cfg["disk"])
return st.f_bavail * st.f_frsize / GB, st.f_blocks * st.f_frsize / GB
@@ -237,6 +357,19 @@ def act(cfg: dict, stage: str, previous: str, free: float) -> None:
notify(cfg, f"Agents council: {head}. Heavy jobs blocked; a running stack needs the operator's OK to go.")
def act_memory(cfg: dict, stage: str, previous: str, sample: dict[str, float | None]) -> None:
head = memory_text(sample)
if MEMORY_STAGES.index(stage) < MEMORY_STAGES.index(previous):
alert(stage, f"RESOURCE MEMORY {stage.upper()}: recovered, {head}.")
return
holders = "; ".join(swap_holders(CGROUPS, container_names())) or "no container holds swap"
if stage == "warn":
alert(stage, f"RESOURCE MEMORY WARN: {head}. Top swap holders: {holders}. Chair: find the holder; peers: start nothing heavy.")
else:
alert(stage, f"RESOURCE MEMORY STOP: {head}. New heavy jobs are blocked. Top swap holders: {holders}.")
notify(cfg, f"Agents council: memory nearly exhausted ({head}). Heavy jobs blocked; top swap holders: {holders}.")
def history_rows(text: str) -> list[tuple[str, str]]:
rows = []
for line in text.splitlines():
@@ -280,27 +413,42 @@ def stale(state: dict, cfg: dict, now: float) -> bool:
return now - state.get("checked", 0) >= cfg["interval_seconds"]
def cmd_watch(cfg: dict) -> int:
def measure_and_act(cfg: dict, state: dict, now: float, problem: str | None) -> None:
free, _ = disk(cfg)
samples = [s for s in state.get("samples", []) if now - s[0] <= cfg["projection_minutes"] * 60]
stage = stage_for(free, samples, cfg, now)
previous = state.get("stage", "ok")
samples.append([now, free])
state.update(checked=now, free=free, stage=stage, samples=samples)
sample = None if problem else memory()
was_short = state.get("memory_stage", "ok")
if sample is not None:
state["memory_stage"] = memory_stage(sample, cfg)
due = now - state.get("caches_checked", 0) >= cfg["cache_interval_seconds"]
if due:
state["caches_checked"] = now
write_json(STATE, state)
if stage != previous:
act(cfg, stage, previous, free)
elif due:
spawn_cleanup("--caches")
if sample is not None and state["memory_stage"] != was_short:
act_memory(cfg, state["memory_stage"], was_short, sample)
def cmd_watch(cfg: dict, problem: str | None) -> int:
"""Args:
cfg: the config.
problem: why the memory stage cannot be judged, None when it can; the
disk part still runs and every session sees the problem.
"""
payload = json.load(sys.stdin)
now = time.time()
state = read_json(STATE)
if stale(state, cfg, now):
free, _ = disk(cfg)
samples = [s for s in state.get("samples", []) if now - s[0] <= cfg["projection_minutes"] * 60]
stage = stage_for(free, samples, cfg, now)
previous = state.get("stage", "ok")
samples.append([now, free])
state.update(checked=now, free=free, stage=stage, samples=samples)
due = now - state.get("caches_checked", 0) >= cfg["cache_interval_seconds"]
if due:
state["caches_checked"] = now
write_json(STATE, state)
if stage != previous:
act(cfg, stage, previous, free)
elif due:
spawn_cleanup("--caches")
measure_and_act(cfg, state, now, problem)
keep_history()
texts = unseen_alerts(payload["session_id"])
texts = unseen_alerts(payload["session_id"]) + ([] if problem is None else [problem])
if texts:
print(json.dumps({"hookSpecificOutput": {
"hookEventName": payload["hook_event_name"],
@@ -309,7 +457,10 @@ def cmd_watch(cfg: dict) -> int:
return 0
HEREDOC = re.compile(r"<<-?\s*(['\"]?)(\w+)\1[^\n]*\n.*?\n\s*\2(?=\s|\)|$)", re.DOTALL)
HEREDOC = re.compile(
r"<<-?\s*(?P<quote>['\"]?)(?P<escape>\\?)(?P<tag>[\w.-]+)(?P=quote)(?P<rest>[^\n]*)\n(?P<body>.*?)\n\s*(?P=tag)(?=\s|\)|$)",
re.DOTALL,
)
QUOTED = re.compile(r"'[^']*'|\"(?:\\.|[^\"\\])*\"")
@@ -322,32 +473,369 @@ def silence() -> dict | None:
return state if state.get("until", 0) > time.time() else None
def cmd_block(cfg: dict) -> int:
def uncommented(command: str) -> str:
kept: list[str] = []
quote = ""
escaped = skipping = False
word_start = True
for char in command:
if skipping and char != "\n":
continue
skipping = False
if escaped:
escaped = False
elif quote == "'":
quote = "" if char == "'" else quote
elif char == "\\":
escaped = True
elif quote == '"':
quote = "" if char == '"' else quote
elif char in "'\"":
quote = char
elif char == "#" and word_start:
skipping = True
continue
kept.append(char)
word_start = not quote and not escaped and char in WORD_BREAK
return "".join(kept)
def segments(command: str) -> list[list[str]]:
"""Args:
command: a shell command line.
Returns:
The words of each simple command, split at newlines, `;`, `&`, `|`
and parentheses; a redirection, its target and a file descriptor
number in front of it are dropped and the command goes on.
Raises:
ValueError: on an unbalanced quote.
"""
lexer = shlex.shlex(uncommented(command.replace("\\\n", "")), posix=True, punctuation_chars=PUNCTUATION)
lexer.whitespace = " \t\r"
lexer.whitespace_split = True
lexer.commenters = ""
parts: list[list[str]] = [[]]
target = False
for token in lexer:
if REDIRECT.fullmatch(token):
if parts[-1] and parts[-1][-1].isdigit():
parts[-1].pop()
target = True
elif SEPARATOR.fullmatch(token):
parts.append([])
target = TRAILING_REDIRECT.search(token) is not None
elif target:
target = False
else:
parts[-1].append(token)
return parts
def program(words: list[str]) -> list[str]:
"""Returns:
The words from the program on: shell reserved words, VAR=value,
wrappers such as env, sudo, time, timeout or `uv run`, their options,
the values of their options and their numbers dropped; the split
string of `env -S`, nothing for the lookup `command -v`.
"""
at, wrapper = 0, ""
while at < len(words):
word = words[at]
if os.path.basename(word) in WRAPPERS:
wrapper = os.path.basename(word)
elif word in WRAPPER_VALUES.get(wrapper, ()):
at += 1
elif word in WRAPPER_TURNS.get(wrapper, {}):
return WRAPPER_TURNS[wrapper][word](words[at + 1 :])
elif not (word in RESERVED or ASSIGNMENT.fullmatch(word) or (wrapper and wrapped_word(word))):
break
at += 1
return words[at:]
def lookup(rest: list[str]) -> list[str]:
return []
def split_string(rest: list[str]) -> list[str]:
return program(" ".join(rest[:1]).split() + rest[1:])
WRAPPER_TURNS = {
"command": {"-v": lookup, "-V": lookup},
"env": {"-S": split_string, "--split-string": split_string},
}
def wrapped_word(word: str) -> bool:
return word in WRAPPER_WORDS or WRAPPER_ARGUMENT.fullmatch(word) is not None
def inline_code(words: list[str]) -> str | None:
"""Args:
words: a simple command from its program on.
Returns:
The code a shell runs with -c (also clustered, such as -lc or -ic,
and after a `--`), else None.
"""
if not words or os.path.basename(words[0]) not in SHELLS:
return None
rest = words[1:]
for at, word in enumerate(rest):
if INLINE.fullmatch(word):
code = [later for later in rest[at + 1 :] if later != "--"][:1]
return code[0] if code else None
return None
def operands(args: list[str], value_options: frozenset[str]) -> list[str]:
found, skip = [], False
for word in args:
if skip:
skip = False
elif word in value_options:
skip = True
elif not word.startswith("-"):
found.append(word)
return found
def python_module(args: list[str]) -> tuple[str, list[str]]:
skip = False
for at, word in enumerate(args):
if skip:
skip = False
elif word == "-m":
return (args[at + 1], args[at + 2 :]) if at + 1 < len(args) else ("", [])
elif word.startswith("-m"):
return word[2:], args[at + 1 :]
elif word in PYTHON_VALUE_OPTIONS:
skip = True
elif not word.startswith("-"):
return os.path.basename(word), args[at + 1 :]
return "", []
def light_unittest(args: list[str]) -> bool:
if args[:1] != ["discover"]:
return len(operands(args, UNITTEST_VALUE_OPTIONS)) == 1
rest = args[1:]
patterns = [rest[at + 1] for at, word in enumerate(rest[:-1]) if word in PATTERN_OPTIONS]
patterns = patterns or operands(rest, DISCOVER_VALUE_OPTIONS)[1:2]
return bool(patterns) and TEST_FILE.fullmatch(patterns[-1]) is not None
def heavy_pytest(args: list[str]) -> bool:
files = operands(args, PYTEST_VALUE_OPTIONS)
return not (PYTEST_LIGHT.intersection(args) or (len(files) == 1 and TEST_FILE.fullmatch(files[0])))
def heavy_unittest(args: list[str]) -> bool:
return not (HELP.intersection(args) or light_unittest(args))
def make_targets(args: list[str]) -> list[str]:
return [word for word in operands(args, MAKE_VALUE_OPTIONS) if "=" not in word and not word.isdigit()]
def dry_run(args: list[str]) -> bool:
return bool(DRY_RUN.intersection(args)) or any(SHORT_DRY.fullmatch(word) for word in args)
def heavy_make(args: list[str]) -> bool:
return not dry_run(args) and any(target == "install" or target.startswith("verify-") for target in make_targets(args))
def heavy_compose(args: list[str]) -> bool:
words = operands(args, COMPOSE_VALUES)
command = words[0] if words else ""
return command == "build" or (command in BUILDING and "--build" in args)
def heavy_docker(args: list[str]) -> bool:
return args[:1] == ["compose"] and heavy_compose(args[1:])
HEAVY_TOOLS = {
"pytest": heavy_pytest, "py.test": heavy_pytest, "unittest": heavy_unittest, "make": heavy_make,
"docker": heavy_docker, "docker-compose": heavy_compose,
}
def heavy_words(run: list[str], cfg: dict, exempt: tuple[str, ...]) -> bool:
if not run:
return False
code = inline_code(run)
if code is not None:
return heavy(code, cfg, exempt)
name, args = os.path.basename(run[0]), run[1:]
if PYTHON.fullmatch(name):
name, args = python_module(args)
if name in WRAPPERS:
return heavy_words(program([name, *args]), cfg, exempt)
return name in HEAVY_TOOLS and HEAVY_TOOLS[name](args)
def segment_texts(words: list[str], run: list[str]) -> list[str]:
return list(dict.fromkeys(" ".join("''" if SPACE.search(word) else word for word in part) for part in (words, run)))
def judged(texts: list[str], run: list[str], cfg: dict, exempt: tuple[str, ...]) -> bool:
if any(re.search(pattern, text) for pattern in exempt for text in texts):
return False
if any(re.search(pattern, text) for pattern in cfg["heavy_commands"] for text in texts):
return True
return heavy_words(run, cfg, exempt)
def heavy_part(words: list[str], cfg: dict, exempt: tuple[str, ...]) -> bool:
"""Returns:
Whether one simple command is heavy; a make call is judged per target
as `make <target>`, so an exempt target never covers the next one.
"""
run = program(words)
if run and os.path.basename(run[0]) == "make" and not dry_run(run[1:]):
return any(judged([f"make {target}"], ["make", target], cfg, exempt) for target in make_targets(run[1:]))
return judged(segment_texts(words, run), run, cfg, exempt)
def marked_heredocs(command: str) -> tuple[str, list[re.Match[str]]]:
"""Returns:
The command with each heredoc replaced by the word HEREDOC_MARK
followed by its body in hex, so a nested call still reads it, the
rest of its first line kept, and the heredoc matches.
"""
found: list[re.Match[str]] = []
def mark(match: re.Match[str]) -> str:
found.append(match)
return f" {HEREDOC_MARK}{match['body'].encode(errors='surrogatepass').hex()} {match['rest']}"
return HEREDOC.sub(mark, command), found
def shell_operands(args: list[str]) -> list[str]:
found, skip = [], False
for word in args:
if skip:
skip = False
elif word.startswith(("-", "+")):
skip = SHELL_VALUE.fullmatch(word) is not None
else:
found.append(word)
return found
def reads_stdin(run: list[str]) -> bool:
"""Returns:
Whether a simple command from its program on is a shell that runs the
code on its stdin: no -c code, and -s or no operand but heredoc marks.
"""
if not run or os.path.basename(run[0]) not in SHELLS or inline_code(run) is not None:
return False
args = run[1:]
return any(STDIN_FLAG.fullmatch(word) for word in args) or all(MARK_WORD.fullmatch(word) for word in shell_operands(args))
def expanded_codes(heredocs: list[re.Match[str]]) -> list[str]:
"""Returns:
The command substitutions in the bodies of heredocs whose delimiter
is unquoted, which the shell expands.
"""
return [code for match in heredocs if not (match["quote"] or match["escape"]) for code in substituted(match["body"])[1]]
def fed_bodies(parts: list[list[str]]) -> list[str]:
marks = [MARK_WORD.fullmatch(word) for words in parts if reads_stdin(program(words)) for word in words]
return [bytes.fromhex(mark[1]).decode(errors="ignore") for mark in marks if mark]
def closing(text: str, start: int) -> int:
depth = 1
for at in range(start, len(text)):
depth += PAREN.get(text[at], 0)
if depth == 0:
return at
return len(text)
def blanked_single(quoted: re.Match[str]) -> str:
text = quoted[0]
return text if text.startswith('"') else "'" + " " * (len(text) - 2) + "'"
def substitution_end(shown: str, found: re.Match[str]) -> int:
if found[0] == "`":
end = shown.find("`", found.end())
return len(shown) if end < 0 else end
return closing(shown, found.end())
def substituted(text: str) -> tuple[str, list[str]]:
"""Returns:
The text with every `...` and $(...) outside single quotes and
escapes replaced by the option-like word PLACEHOLDER, so a wrapper
around it still parses, and the code of each, which the shell runs
first; nested ones stay inside their code.
"""
shown = QUOTED.sub(blanked_single, ESCAPED.sub(" ", text))
kept: list[str] = []
codes: list[str] = []
at = 0
found = OPENER.search(shown)
while found:
end = substitution_end(shown, found)
kept.append(text[at : found.start()] + PLACEHOLDER)
codes.append(text[found.end() : end])
at = end + 1
found = OPENER.search(shown, at)
return "".join(kept) + text[at:], codes
def cmd_block(cfg: dict, problem: str | None) -> int:
"""Args:
cfg: the config.
problem: why the memory stage cannot be judged, None when it can; a
heavy job is then refused with it.
"""
payload = json.load(sys.stdin)
command = executable_text((payload.get("tool_input") or {}).get("command", ""))
raw = (payload.get("tool_input") or {}).get("command", "")
if not heavy(raw, cfg, tuple(cfg["never_block"])):
return 0
reason = block_reason(cfg, problem)
if reason is None:
return 0
print(f"kvb-org-organize: {reason}. Teardown commands still run. Ask the resource chair.", file=sys.stderr)
return 2
def disk_stage(cfg: dict) -> str:
state = read_json(STATE)
now = time.time()
stage = state.get("stage", "ok")
if stale(state, cfg, now):
stage = stage_for(disk(cfg)[0], state.get("samples", []), cfg, now)
return stage_for(disk(cfg)[0], state.get("samples", []), cfg, now)
return state.get("stage", "ok")
def block_reason(cfg: dict, problem: str | None) -> str | None:
mayday = silence()
if stage not in ("stop", "critical") and not mayday:
return 0
if any(re.search(p, command) for p in cfg["never_block"]):
return 0
if any(re.search(p, command) for p in cfg["heavy_commands"]):
reason = (
if mayday:
return (
f"a mayday by {clip(mayday.get('by', '?'))} holds radio silence: {clip(mayday.get('text', ''))}; "
"it ends with radio.py clear"
if mayday
else f"resource stage is {stage}; new heavy jobs are blocked until the disk recovers"
)
print(
f"kvb-org-organize: {reason}. Teardown commands still run. Ask the resource chair.",
file=sys.stderr,
)
return 2
return 0
stage = disk_stage(cfg)
if stage in ("stop", "critical"):
return f"resource stage is {stage}; new heavy jobs are blocked until the disk recovers"
if problem is not None:
return f"{problem}; new heavy jobs are blocked until it does"
sample = memory()
if memory_stage(sample, cfg) == "stop":
return f"memory stage is stop ({memory_text(sample)}); new heavy jobs are blocked until memory recovers"
return None
def cpu_times() -> tuple[int, int]:
@@ -361,12 +849,17 @@ def cpu_busy_percent(before: tuple[int, int], after: tuple[int, int]) -> float:
return 100.0 * (total - idle) / total
def mem_used_percent(meminfo: str) -> float:
def meminfo_kb(meminfo: str) -> dict[str, int]:
values = {}
for line in meminfo.splitlines():
key, _, rest = line.partition(":")
if rest:
values[key] = int(rest.split()[0])
return values
def mem_used_percent(meminfo: str) -> float:
values = meminfo_kb(meminfo)
return 100.0 * (1 - values["MemAvailable"] / values["MemTotal"])
@@ -374,7 +867,161 @@ def load(cfg: dict) -> tuple[float, float]:
before = cpu_times()
time.sleep(cfg["load_limits"]["sample_seconds"])
cpu = cpu_busy_percent(before, cpu_times())
return cpu, mem_used_percent(Path("/proc/meminfo").read_text(encoding="utf-8"))
return cpu, mem_used_percent(MEMINFO.read_text(encoding="utf-8"))
def psi_full(pressure: str) -> float | None:
for line in pressure.splitlines():
if line.startswith("full "):
return float(dict(field.split("=", 1) for field in line.split()[1:])["avg10"])
return None
def memory_sample(meminfo: str, pressure: str | None, container_swap_gb: float) -> dict[str, float | None]:
"""Args:
meminfo: the text of /proc/meminfo.
pressure: the text of /proc/pressure/memory, None without PSI.
container_swap_gb: the swap of the container holding the most, in GB.
Returns:
{available, swap_free, psi_full, container_swap_gb}: MemAvailable and
SwapFree in percent of their totals, the full avg10 stall percentage
and container_swap_gb unchanged; swap_free is None without swap,
psi_full None without PSI.
"""
values = meminfo_kb(meminfo)
swap = values["SwapTotal"]
return {
"available": 100.0 * values["MemAvailable"] / values["MemTotal"],
"swap_free": 100.0 * values["SwapFree"] / swap if swap else None,
"psi_full": None if pressure is None else psi_full(pressure),
"container_swap_gb": container_swap_gb,
}
def memory() -> dict[str, float | None]:
try:
pressure = PRESSURE.read_text(encoding="utf-8")
except OSError:
pressure = None
largest = max((size for size, _ in container_swaps(CGROUPS)), default=0)
return memory_sample(MEMINFO.read_text(encoding="utf-8"), pressure, largest / GB)
def memory_stage(sample: dict[str, float | None], cfg: dict) -> str:
for name in ("stop", "warn"):
bound = cfg["memory_limits"][name]
if (
sample["available"] < bound["available"]
or (sample["swap_free"] is not None and sample["swap_free"] < bound["swap_free"])
or (sample["psi_full"] is not None and sample["psi_full"] >= bound["psi_full"])
or sample["container_swap_gb"] >= bound["container_swap_gb"]
):
return name
return "ok"
def memory_text(sample: dict[str, float | None]) -> str:
swap = "no swap" if sample["swap_free"] is None else f"swap free {sample['swap_free']:.0f} %"
psi = "no PSI" if sample["psi_full"] is None else f"PSI full avg10 {sample['psi_full']:.1f}"
return f"MemAvailable {sample['available']:.0f} %, {swap}, {psi}, largest container swap {sample['container_swap_gb']:.1f} GB"
def swap_bytes(cgroup: Path) -> int:
try:
return int((cgroup / "memory.swap.current").read_text(encoding="utf-8"))
except (OSError, ValueError):
return 0
def container_swaps(base: Path) -> list[tuple[int, Path]]:
try:
return [(swap_bytes(scope), scope) for scope in base.glob("docker-*.scope")]
except OSError:
return []
def cgroup_label(name: str, names: dict[str, str]) -> str:
bare = name.removeprefix("docker-").removesuffix(".scope")
if CONTAINER_ID.fullmatch(bare):
return names[bare] if bare in names else bare[:12]
return bare
def container_names() -> dict[str, str]:
try:
out = subprocess.run(
["docker", "ps", "--no-trunc", "--format", "{{.ID}}\t{{.Names}}"],
capture_output=True, text=True, timeout=30, check=False,
).stdout
except (OSError, subprocess.TimeoutExpired):
return {}
return dict(line.split("\t", 1) for line in out.splitlines() if "\t" in line)
def swap_chain(cgroup: Path, held: int, names: dict[str, str]) -> list[str]:
chain = [cgroup_label(cgroup.name, names)]
for _ in range(NEST_DEPTH):
try:
children = [(swap_bytes(child), child) for child in cgroup.iterdir() if child.is_dir()]
except OSError:
break
if not children:
break
size, child = max(children)
if size * 2 < held:
break
chain.append(cgroup_label(child.name, names))
cgroup, held = child, size
return chain
def swap_holders(base: Path, names: dict[str, str]) -> list[str]:
"""Args:
base: the cgroup directory holding the docker-<id>.scope groups.
names: {full container id: name} for the labels.
Returns:
Up to SWAP_HOLDERS lines, the largest swap first: the container, then
each nested group holding at least half of its parent's swap, and the
container's memory.swap.current in GB.
"""
ranked = sorted((pair for pair in container_swaps(base) if pair[0] > 0), reverse=True)[:SWAP_HOLDERS]
return [f"{' > '.join(swap_chain(scope, size, names))} {size / GB:.1f} GB" for size, scope in ranked]
def heavy(command: str, cfg: dict, exempt: tuple[str, ...]) -> bool:
"""Args:
command: the shell command line as typed, quotes included.
cfg: the config; its heavy_commands search each simple command with
quoted text blanked, and a make call also as `make <targets>`.
exempt: regexes; a simple command one of them finds is never heavy
(never_block, the queue's own hold), the others still count.
Returns:
Whether one simple command is a heavy job: an image build through
docker compose (build, or up, run or create with --build), a
heavy_commands match, make install or verify-* (not as a dry run), or
pytest or unittest on anything but exactly one test file or module,
help and version aside. Wrappers (program()), the code of `sh -c`,
command substitutions (substituted(), also in heredocs the shell
expands) and heredocs a shell reads as its code count the same. A
command with an unbalanced quote, which no shell runs, counts by the
regexes on its whole text alone. A guard against accidental heavy
runs, not a sandbox: a shell reading code from a pipe or a
here-string, a script of its own, a program named by a substitution,
quotes inside an expanding heredoc and an escaped backslash in front
of a substitution are not looked into.
"""
marked, heredocs = marked_heredocs(command)
plain, codes = substituted(uncommented(marked))
try:
parts = segments(plain)
except ValueError:
text = executable_text(command)
return not any(re.search(p, text) for p in exempt) and any(re.search(p, text) for p in cfg["heavy_commands"])
nested = codes + expanded_codes(heredocs) + fed_bodies(parts)
return any(heavy_part(words, cfg, exempt) for words in parts) or any(heavy(code, cfg, exempt) for code in nested)
def cmd_admit(cfg: dict, args: argparse.Namespace) -> int:
@@ -385,12 +1032,12 @@ def cmd_admit(cfg: dict, args: argparse.Namespace) -> int:
passed = [left >= floor]
report = [f"free {free:.1f} GB, need {need} GB, leaves {left:.1f} GB (floor {floor} GB)"]
limits = cfg["load_limits"]
if args.kind in limits["kinds"]:
cpu, mem = load(cfg)
passed += [cpu < limits["cpu_percent"], mem < limits["mem_percent"]]
report.append(
f"cpu {cpu:.0f} % (limit {limits['cpu_percent']}), ram {mem:.0f} % (limit {limits['mem_percent']})"
)
cpu, mem = load(cfg)
sample = memory()
stage = memory_stage(sample, cfg)
passed += [cpu < limits["cpu_percent"], mem < limits["mem_percent"], stage != "stop"]
report.append(f"cpu {cpu:.0f} % (limit {limits['cpu_percent']}), ram {mem:.0f} % (limit {limits['mem_percent']})")
report.append(f"memory stage {stage} ({memory_text(sample)})")
verdict = "grant" if all(passed) else "deny"
print(f"{verdict}: {'; '.join(report)}")
return 0 if verdict == "grant" else 3
@@ -511,7 +1158,11 @@ def cmd_status(cfg: dict) -> int:
now = time.time()
samples = state.get("samples", [])
age = f"{now - state['checked']:.0f}s" if "checked" in state else "unmeasured"
print(f"stage={stage_for(free, samples, cfg, now)} free={free:.1f}GB total={total:.1f}GB samples={len(samples)} age={age}")
sample = memory()
print(
f"stage={stage_for(free, samples, cfg, now)} free={free:.1f}GB total={total:.1f}GB samples={len(samples)} age={age}"
f" memory={memory_stage(sample, cfg)} ({memory_text(sample)})"
)
return 0
@@ -537,10 +1188,14 @@ def main() -> int:
cfg = load_config()
if cfg is None:
return 0
problem = missing(cfg, MEMORY_KEYS)
if args.cmd == "watch":
return cmd_watch(cfg)
return cmd_watch(cfg, problem)
if args.cmd == "block":
return cmd_block(cfg)
return cmd_block(cfg, problem)
if problem is not None:
print(problem, file=sys.stderr)
return 2
if args.cmd == "admit":
return cmd_admit(cfg, args)
if args.cmd == "cleanup":
+20 -21
View File
@@ -15,7 +15,7 @@ queue.py hold --kind <kind> --task <register task> -- <command>
```
Start it in the background (`run_in_background` or equivalent) and end the
turn: hold waits, runs the command on its grant and exits with its code.
turn: hold runs the command on its grant and exits with its code.
- `<kind>`: a key of `slot_need_gb` in `~/.claude/agents-council/config.json`.
- `<register task>`: your open `kvb-org-rasci` task; its rank places you.
@@ -37,31 +37,32 @@ Every 20 s each waiting hold evaluates the whole queue under one lock:
1. Hotfix jobs first, then the register rank (`rasci.py show`), one place
earlier per whole hour waited, then arrival.
2. In that order, a job is blocked at disk stage stop or critical, at warn
when it needs disk, during a radio-silence mayday, while its kind's slots
are taken, or while its workdir runs another job.
3. A job whose need would cut below the stop threshold reserves that disk
against smaller jobs behind it.
when it needs disk, at memory stage stop, during a radio-silence mayday,
while its kind's slots are taken, or while its workdir runs another job.
3. A job whose disk need would cut below the stop threshold, or whose
footprint would push the running ones past `budget_percent`, reserves it
against the jobs behind it; a job alone always fits.
4. An unblocked job counts as granted for the jobs behind it: none takes its
slot.
5. Kinds in `load_limits.kinds` also need CPU and RAM below the limits.
A waiting hold prints a stderr line when its position or reason changes.
5. Every job, alone too, needs CPU, and RAM used plus what it and the
running jobs may still take, below `load_limits`.
## History and learning
Every finished job appends a row to the council's `queue-history.jsonl`:
signature (`make <target>`, `<script>.py <subcommand>`), kind, minutes, exit
code, and disk, RAM and CPU at the grant and at their 20 s extremes. The
`watch` hook of `kvb-org-organize` keeps them in
`~/.local/state/agents-council/` across reboots.
Every finished job appends a row (signature such as `<repo>: make <target>`,
kind, minutes, disk, RAM and CPU) to `queue-history.jsonl`; the
`kvb-org-organize` watch hook keeps it across reboots.
From the last 30 days, once 5 runs exist:
- per signature, the disk need is p90 disk taken plus 1 GB;
- per kind, the slot limit is how many p90 RAM and CPU shares fit between the
median load at the grant and `load_limits`, at least 1.
median load at the grant and `load_limits`, at least 1;
- per job, the footprint is the larger p90 share of its kind and of its
signature, else the largest of its kind.
Until then the seeds hold: `slot_need_gb[kind]` for disk, 1 slot per kind.
Until then the seeds hold: `slot_need_gb[kind]` for disk, 1 slot per kind,
the whole budget as footprint: it runs alone.
The optional `slot_limits {kind: N}` only caps a limit from above.
## Inspect and cancel
@@ -72,11 +73,8 @@ queue.py history
queue.py cancel <id>
```
`list` shows each job's position, need, wait, ETA and blocking reason; dead
holders drop out. The ETA adds the median minutes left of running jobs
and of the jobs ahead, `-` while one is unknown. `history` prints the learned
values per signature and per kind, with each limit's source. `cancel` removes
your own waiting job; its hold exits 3.
`list` shows each job's position, ETA and blocking reason, `history` the
learned values, `cancel` removes your own waiting job (its hold exits 3).
## Rules
@@ -86,6 +84,7 @@ your own waiting job; its hold exits 3.
- A running job is never cut short for the queue; only the operator orders
one down, and `kvb-git-hotfix` may void a gate.
- With `queue_enforce: true`, the `block` hook (PreToolUse on Bash) refuses
`heavy_commands` and `gate.py run|hold` outside `queue.py hold`.
`heavy_commands`, image builds, `make install|verify-*`, tests beyond one
file and `gate.py run|hold` outside `queue.py hold`.
- Without the config, hold exits 2: tell the operator and ask the chair
(`kvb-org-organize`) until it exists.
+40 -3
View File
@@ -26,6 +26,7 @@ MIN_SHARE_PERCENT = 1
SEED_LIMIT = 1
INTERPRETERS = re.compile(r"python[\d.]*|bash|sh|zsh|node|perl|ruby")
MAKE_VALUE_OPTIONS = frozenset({"-C", "-f", "-I", "-o", "-W"})
RESOURCES = ("ram", "cpu")
SIGNATURE_HEADER = ("Signature", "Kind", "Runs", "Median min", "p90 disk GB", "p90 RAM %", "Need GB")
KIND_HEADER = ("Kind", "Runs", "p90 RAM share %", "p90 CPU share %", "Slot limit", "Source")
@@ -149,13 +150,14 @@ def grouped(rows: list[dict], key: str) -> dict[str, list[dict]]:
def command_stats(rows: list[dict]) -> dict:
stats = {
"kinds": sorted({r["kind"] for r in rows}), "runs": len(rows),
"minutes": None, "disk_gb": None, "ram_share": None, "need_gb": None,
"minutes": None, "disk_gb": None, "ram_share": None, "cpu_share": None, "need_gb": None,
}
if len(rows) >= MIN_RUNS:
disk = p90([r["disk_gb"] for r in rows])
stats.update(
minutes=statistics.median(r["minutes"] for r in rows), disk_gb=disk,
ram_share=p90(shares(rows, "mem")), need_gb=math.ceil(disk + DISK_MARGIN_GB),
ram_share=p90(shares(rows, "mem")), cpu_share=p90(shares(rows, "cpu")),
need_gb=math.ceil(disk + DISK_MARGIN_GB),
)
return stats
@@ -186,7 +188,7 @@ def learn(rows: list[dict], now: float, cfg: dict) -> tuple[dict[str, dict], dic
Returns:
(signatures, kinds). signatures: {signature: {kinds, runs, minutes,
disk_gb, ram_share, need_gb}}; kinds: {kind: {runs, ram_share,
disk_gb, ram_share, cpu_share, need_gb}}; kinds: {kind: {runs, ram_share,
cpu_share, limit}}. With fewer than MIN_RUNS rows every value but kinds
and runs is None. minutes is the median, disk_gb, ram_share and
cpu_share the p90 of the disk taken and of max minus before in
@@ -200,6 +202,41 @@ def learn(rows: list[dict], now: float, cfg: dict) -> tuple[dict[str, dict], dic
return signatures, kinds
def footprint(kind: str, name: str, signatures: dict[str, dict], kinds: dict[str, dict]) -> dict[str, float] | None:
"""Predict the RAM and CPU share a job takes, in percentage points.
Args:
kind: the job's kind.
name: its command signature.
signatures: the signature map of learn().
kinds: the kind map of learn().
Returns:
{ram, cpu}: the larger of the kind's p90 share and the signature's, at
least MIN_SHARE_PERCENT; a signature not learned yet takes the largest
learned share of its kind. None while the kind is not learned yet.
"""
if kind not in kinds or kinds[kind]["ram_share"] is None:
return None
own_kind = kinds[kind]
if name in signatures and signatures[name]["ram_share"] is not None:
peers = [signatures[name]]
else:
peers = [s for s in signatures.values() if kind in s["kinds"] and s["ram_share"] is not None]
return {
resource: max([MIN_SHARE_PERCENT, own_kind[f"{resource}_share"], *(peer[f"{resource}_share"] for peer in peers)])
for resource in RESOURCES
}
def seed_ram(kinds: dict[str, dict]) -> float:
"""Returns:
The RAM share assumed for a kind not learned yet: the largest of the
learned kinds' p90 RAM shares, 0 while none is learned.
"""
return max((stats["ram_share"] for stats in kinds.values() if stats["ram_share"] is not None), default=0.0)
def need_gb(signatures: dict[str, dict], name: str) -> int | None:
return signatures[name]["need_gb"] if name in signatures else None
+225 -108
View File
@@ -57,19 +57,27 @@ Args:
Bash deny rule of the settings is refused, one meeting an ask rule
asks the operator, with or without queue_enforce; a hold that cannot
be parsed or checked is refused. With queue_enforce true in the config,
refuse a command matching heavy_commands or `gate.py run|hold`
unless it matches never_block or runs `queue.py hold`.
refuse a command that watch.heavy() calls heavy or that runs
`gate.py run|hold`, unless it matches never_block or runs `queue.py
hold`.
Config keys: slot_limits {kind: N}, optional, caps the learned slot limit of a
kind from above (history.slot_limits); queue_enforce (bool), optional, read by
block only. The watch.py keys disk, thresholds_gb (stop is the floor every
grant leaves free), slot_need_gb (the disk seed per kind), load_limits (kinds
in its kinds also need CPU and RAM below the limits; mem_percent and
cpu_percent bound the learned slot limits), heavy_commands and never_block are
used as watch.py documents them.
Config keys: budget_percent {ram, cpu}, required: the active and granted jobs'
learned footprints (history.footprint, the whole budget for a kind not learned
yet) may sum to at most these percentage points, a job alone always fits;
slot_limits {kind: N}, optional, caps the learned slot limit of a kind from
above (history.slot_limits); queue_enforce (bool), optional, read by block
only. The watch.py keys disk, thresholds_gb (stop is the floor every grant
leaves free), slot_need_gb (the disk seed per kind), load_limits (every grant
also needs CPU busy below cpu_percent and RAM used plus the job's learned RAM
share below mem_percent; both bound the learned slot limits), memory_limits (no
grant at memory stage stop), heavy_commands and never_block are used as
watch.py documents them. A signature is `<repository>: <command signature>`
(qualified()).
Exit codes:
hold: the command's exit code unchanged, 128+N when signal N ended it;
a command exiting 75 (EX_TEMPFAIL: it did no work, such as a declined
`operator.py granted`) leaves no history row;
before the command started 2 on a usage or config error or when it
cannot start, 3 when cancel removed the entry, 128+N when signal N
reached hold.
@@ -115,14 +123,16 @@ POLL_SECONDS = 20
TICK_SECONDS = 0.2
HOUR = 3600
CANCELLED = 3
FORWARDED = (signal.SIGINT, signal.SIGTERM)
NOT_RUN = 75
RAMP_SECONDS = 900
SETUP =frozenset({"cd", "pushd", "set", "source", ".", "export", "umask", "shopt", "trap"})
FORWARDED =(signal.SIGINT, signal.SIGTERM)
GATE = re.compile(r"\bgate\.py\s+(run|hold)\b")
THROUGH_QUEUE = re.compile(r"\bqueue\.py\s+hold\b")
BASH_RULE = re.compile(r"Bash(?:\((.*)\))?", re.DOTALL)
RULE_PART = re.compile(r"( \*|\*)")
SEPARATOR = re.compile(r"[();<>|&]+")
WORD_BREAK = " \t\n;&|()<>"
MANAGED = Path("/etc/claude-code")
BUDGET_KEYS = ("budget_percent.ram", "budget_percent.cpu")
HOTFIX = "hotfix/"
@@ -154,6 +164,9 @@ def config() -> dict:
cfg = watch.load_config()
if cfg is None:
fail(f"no config at {watch.CONFIG}; the queue reads the kvb-org-organize watch config")
problem = watch.missing(cfg, (*BUDGET_KEYS, *watch.MEMORY_KEYS))
if problem is not None:
fail(problem)
return cfg
@@ -169,8 +182,59 @@ def limits(cfg: dict, kinds: dict[str, dict], entries: list[dict]) -> dict[str,
return history.slot_limits(kinds, caps(cfg), {*cfg["slot_need_gb"], *(e["kind"] for e in entries)})
@functools.cache
def repository(path: str) -> str:
try:
common = subprocess.run(
["git", "rev-parse", "--path-format=absolute", "--git-common-dir"],
cwd=path, capture_output=True, text=True, check=False,
)
except OSError:
return os.path.basename(path)
if common.returncode != 0:
return os.path.basename(path)
return os.path.basename(os.path.dirname(common.stdout.strip()))
def command_words(argv: list[str]) -> list[str]:
"""Returns:
The command a wrapper runs: watch.program() unwrapped, the first simple
command of `sh -c` code that is no setup such as cd, set or source,
the tail after `--` of a Python script such as gate.py hold or
operator.py granted.
"""
run = watch.program(argv) or argv
code = watch.inline_code(run)
inner = first_command(code) if code is not None else script_tail(run)
return run if inner is None else command_words(inner)
def first_command(code: str) -> list[str] | None:
try:
parts = [part for part in map(watch.program, watch.segments(code)) if part and os.path.basename(part[0]) not in SETUP]
except ValueError:
return None
return parts[0] if parts else None
def script_tail(run: list[str]) -> list[str] | None:
if len(run) > 2 and watch.PYTHON.fullmatch(os.path.basename(run[0])) and run[1].endswith(".py") and "--" in run[2:-1]:
return run[run.index("--", 2) + 1 :]
return None
def qualified(workdir: str, argv: list[str]) -> str:
"""Returns:
`<repository>: <history.signature of command_words>`: the directory
holding the main checkout's .git, so every worktree of one repository
shares it, else the workdir's name; one make test of two repositories
stays apart.
"""
return f"{repository(workdir)}: {history.signature(command_words(argv))}"
def signature_of(entry: dict) -> str:
return history.signature(shlex.split(entry["cmd"]))
return qualified(entry["workdir"], shlex.split(entry["cmd"]))
def identity(pid: int) -> str | None:
@@ -270,18 +334,33 @@ def ordered(waiting: list[dict], ranks: dict[str, int], agents: dict[str, int],
return sorted(waiting, key=key)
def evaluate(entries: list[dict], free_gb: float, floor: float, stage: str, silenced: bool,
slot_limits: dict[str, int], ranks: dict[str, int], agents: dict[str, int],
now: float) -> dict[str, str | None]:
def over_budget(used: dict[str, float], share: dict[str, float], budget: dict[str, float]) -> str | None:
over = [resource for resource in budget if used[resource] + share[resource] > budget[resource]]
if not over:
return None
return ", ".join(
f"{resource.upper()} {used[resource]:.0f} % in use or reserved + {share[resource]:.0f} % > budget {budget[resource]:g} %"
for resource in over
) + "; reserved"
def evaluate(entries: list[dict], free_gb: float, floor: float, stage: str, memory: str, silenced: bool,
slot_limits: dict[str, int], footprints: dict[str, dict[str, float]], budget: dict[str, float],
ranks: dict[str, int], agents: dict[str, int], now: float) -> dict[str, str | None]:
"""Decide every waiting entry in grant order, without disk or processes.
Args:
entries: the queue's entries, active and waiting.
free_gb: free disk in GB.
floor: GB every grant must leave free (thresholds_gb.stop).
stage: the watch stage: ok, warn, stop or critical.
stage: the watch disk stage: ok, warn, stop or critical.
memory: the watch memory stage: ok, warn or stop.
silenced: whether a radio-silence mayday holds.
slot_limits: {kind: max concurrent jobs}.
slot_limits: {kind: max concurrent jobs}, an upper cap beside the budget.
footprints: {entry id: {ram, cpu}}, the learned share of every entry
(history.footprint), in percentage points.
budget: {ram, cpu}: the sum of the footprints of active and granted
entries may not pass it; a job alone always fits.
ranks: {task: index in the register's ranking}; a task missing there
ranks after all of them.
agents: {agent name: index in the operator's agent order}; it breaks
@@ -292,39 +371,69 @@ def evaluate(entries: list[dict], free_gb: float, floor: float, stage: str, sile
Returns:
{entry id: None when granted, else the blocking reason}, in grant order.
An unblocked entry counts as granted for the entries after it, whoever
holds it, so a lower entry never takes a higher entry's slot.
holds it, so a lower entry never takes a higher entry's slot; one
blocked by disk or budget reserves its need against the entries after it.
"""
active = [e for e in entries if e["status"] == "active"]
available = free_gb - sum(e["need_gb"] for e in active)
used = {resource: sum(footprints[e["id"]][resource] for e in active) for resource in budget}
taken = len(active)
counts = Counter(e["kind"] for e in active)
busy = {e["workdir"] for e in active}
verdicts: dict[str, str | None] = {}
for entry in ordered([e for e in entries if e["status"] == "waiting"], ranks, agents, now):
kind, need, workdir = entry["kind"], entry["need_gb"], entry["workdir"]
reason = None
if stage in ("stop", "critical"):
reason = f"disk stage {stage}"
elif stage == "warn" and need > 0:
reason = "disk stage warn admits only jobs that need no disk"
elif silenced:
reason = "radio silence"
elif kind not in slot_limits:
reason = f"slot_limits has no {kind}"
elif counts[kind] >= slot_limits[kind]:
reason = f"all {slot_limits[kind]} {kind} slots taken"
elif workdir in busy:
reason = f"{workdir} runs another job"
elif available - need < floor:
reason = f"{need:g} GB would cut into the {floor:g} GB floor; reserved"
available -= need
else:
counts[kind] += 1
busy.add(workdir)
available -= need
kind, need, workdir, share = entry["kind"], entry["need_gb"], entry["workdir"], footprints[entry["id"]]
reason = host_reason(stage, memory, silenced, need) or slot_reason(kind, workdir, slot_limits, counts, busy)
if reason is None:
reason, used, available = resources(need, share, used, available, floor, budget, taken)
if reason is None:
taken += 1
counts[kind] += 1
busy.add(workdir)
verdicts[entry["id"]] = reason
return verdicts
def plus(used: dict[str, float], share: dict[str, float]) -> dict[str, float]:
return {resource: used[resource] + share[resource] for resource in used}
def resources(need: float, share: dict[str, float], used: dict[str, float], available: float, floor: float,
budget: dict[str, float], taken: int) -> tuple[str | None, dict[str, float], float]:
"""Returns:
(reason or None, RAM and CPU used or reserved, disk available) after
the job: one passing the budget beside other jobs reserves its share,
one cutting into the floor its disk, a granted one takes both.
"""
if taken and (reason := over_budget(used, share, budget)) is not None:
return reason, plus(used, share), available
if available - need < floor:
return f"{need:g} GB would cut into the {floor:g} GB floor; reserved", used, available - need
return None, plus(used, share), available - need
def host_reason(stage: str, memory: str, silenced: bool, need: float) -> str | None:
if stage in ("stop", "critical"):
return f"disk stage {stage}"
if stage == "warn" and need > 0:
return "disk stage warn admits only jobs that need no disk"
if memory == "stop":
return "memory stage stop"
if silenced:
return "radio silence"
return None
def slot_reason(kind: str, workdir: str, slot_limits: dict[str, int], counts: Counter, busy: set[str]) -> str | None:
if kind not in slot_limits:
return f"slot_limits has no {kind}"
if counts[kind] >= slot_limits[kind]:
return f"all {slot_limits[kind]} {kind} slots taken"
if workdir in busy:
return f"{workdir} runs another job"
return None
def register_ranks() -> dict[str, int]:
rasci = sibling("rasci")
return {task: index for index, task in enumerate(rasci.ranked(rasci.merged(False)))}
@@ -344,22 +453,62 @@ def silenced(now: float) -> bool:
return "until" in state and state["until"] > now
def decide(cfg: dict, entries: list[dict], free_gb: float, now: float, kinds: dict[str, dict]) -> dict[str, str | None]:
def footprint(entry: dict, signatures: dict[str, dict], kinds: dict[str, dict]) -> dict[str, float] | None:
return history.footprint(entry["kind"], signature_of(entry), signatures, kinds)
def decide(cfg: dict, entries: list[dict], free_gb: float, now: float, signatures: dict[str, dict],
kinds: dict[str, dict]) -> dict[str, str | None]:
slots = {kind: limit for kind, (limit, _) in limits(cfg, kinds, entries).items()}
budget = cfg["budget_percent"]
footprints = {}
for entry in entries:
share = footprint(entry, signatures, kinds)
footprints[entry["id"]] = dict(budget) if share is None else share
return evaluate(
entries, free_gb, cfg["thresholds_gb"]["stop"], stage(cfg, free_gb, now), silenced(now),
slots, register_ranks(), agent_ranks(), now,
entries, free_gb, cfg["thresholds_gb"]["stop"], stage(cfg, free_gb, now),
watch.memory_stage(watch.memory(), cfg), silenced(now), slots, footprints, budget,
register_ranks(), agent_ranks(), now,
)
def load_reason(cfg: dict, kind: str) -> str | None:
limits = cfg["load_limits"]
if kind not in limits["kinds"]:
return None
cpu, mem = watch.load(cfg)
if cpu < limits["cpu_percent"] and mem < limits["mem_percent"]:
return None
return f"cpu or ram at the limit ({limits['cpu_percent']} %, {limits['mem_percent']} %)"
def load_reason(limits: dict, cpu: float, mem: float, ram_share: float) -> str | None:
"""Args:
limits: load_limits of the config.
cpu: CPU busy now, in percent.
mem: RAM used now, in percent; stacks and jobs outside the queue
included.
ram_share: what the job and the active ones may still take, in
percentage points (pending_ram()).
Returns:
None when CPU stays below cpu_percent and RAM used plus the share
below mem_percent, else why not.
"""
if cpu >= limits["cpu_percent"]:
return f"CPU {cpu:.0f} % busy reaches the limit {limits['cpu_percent']} %"
if mem + ram_share >= limits["mem_percent"]:
return f"RAM {mem:.0f} % used + {ram_share:.0f} % still to come reach the limit {limits['mem_percent']} %"
return None
def ram_share(entry: dict, signatures: dict[str, dict], kinds: dict[str, dict]) -> float:
share = footprint(entry, signatures, kinds)
return history.seed_ram(kinds) if share is None else share["ram"]
def pending_ram(entries: list[dict], entry_id: str, now: float, signatures: dict[str, dict],
kinds: dict[str, dict]) -> float:
"""Returns:
The RAM share of entry_id plus the whole share of every active job
granted less than RAMP_SECONDS ago (or without a grant time): what
may still come once they ramp up. An older job counts as ramped, its
RAM inside the RAM used; one that peaks later is bounded by the
memory stage only.
"""
mine = [e for e in entries if e["id"] == entry_id]
young = [e for e in entries if e["status"] == "active" and now - e.get("granted", now) < RAMP_SECONDS]
return ram_share(mine[0], signatures, kinds) + sum(ram_share(e, signatures, kinds) for e in young)
def workdir() -> str:
@@ -399,13 +548,12 @@ def pause(caught: list[int]) -> None:
time.sleep(TICK_SECONDS)
def wait(cfg: dict, entry_id: str, kind: str, caught: list[int]) -> tuple[int | None, int]:
def wait(cfg: dict, entry_id: str, caught: list[int]) -> tuple[int | None, int]:
"""Poll until the grant.
Args:
cfg: the config.
entry_id: this hold's entry.
kind: its kind, for the load sample.
caught: signals the caller caught; the first ends the wait.
Returns:
@@ -414,18 +562,19 @@ def wait(cfg: dict, entry_id: str, kind: str, caught: list[int]) -> tuple[int |
"""
shown = None
while not caught:
busy_load = load_reason(cfg, kind)
cpu, mem = watch.load(cfg)
free, _ = watch.disk(cfg)
now = time.time()
_, kinds = learned(cfg, now)
signatures, kinds = learned(cfg, now)
with store() as data:
data["entries"] = prune(data["entries"])
mine = [e for e in data["entries"] if e["id"] == entry_id]
if not mine:
print(f"queue: {entry_id} was cancelled", file=sys.stderr)
return CANCELLED, 0
verdicts = decide(cfg, data["entries"], free, now, kinds)
reason = verdicts[entry_id] or busy_load
verdicts = decide(cfg, data["entries"], free, now, signatures, kinds)
pending = pending_ram(data["entries"], entry_id, now, signatures, kinds)
reason = verdicts[entry_id] or load_reason(cfg["load_limits"], cpu, mem, pending)
if reason is None:
concurrent = sum(e["status"] == "active" for e in data["entries"])
mine[0].update(status="active", granted=now)
@@ -477,6 +626,11 @@ def keep(row: dict) -> None:
print(f"queue: no history row for {row['id']}: {error.strerror}", file=sys.stderr)
def record(job: dict, start: float, code: int, samples: list[dict[str, float]], concurrent: int) -> None:
if code != NOT_RUN:
keep(history.row(job, start, time.time(), code, samples, concurrent))
def cmd_hold(args: argparse.Namespace) -> int:
cfg = config()
if args.kind not in cfg["slot_need_gb"]:
@@ -486,7 +640,8 @@ def cmd_hold(args: argparse.Namespace) -> int:
fail(f"{args.task} is no open task of the kvb-org-rasci register")
if args.hotfix is not None and HOTFIX + args.hotfix not in rasci.load():
fail(f"the register has no row {HOTFIX}{args.hotfix}")
signature = history.signature(args.command)
here = workdir()
signature = qualified(here, args.command)
signatures, _ = learned(cfg, time.time())
need = args.need_gb
if need is None:
@@ -511,13 +666,13 @@ def cmd_hold(args: argparse.Namespace) -> int:
with holding() as (tag, lock_fd):
entry = enqueue({
"kind": args.kind, "task": args.task, "need_gb": need, "hotfix": args.hotfix,
"workdir": workdir(), "session": session, "name": council.live_sessions().get(session) or session,
"workdir": here, "session": session, "name": council.live_sessions().get(session) or session,
"lock": tag, "pid": os.getpid(), "pid_start": identity(os.getpid()), "child": None,
"child_start": None, "status": "waiting", "enqueued": time.time(), "granted": None,
"cmd": shlex.join(args.command),
})
try:
code, concurrent = wait(cfg, entry["id"], args.kind, caught)
code, concurrent = wait(cfg, entry["id"], caught)
if code is not None:
return code
if caught:
@@ -536,7 +691,7 @@ def cmd_hold(args: argparse.Namespace) -> int:
code = code if code >= 0 else 128 - code
job = {"id": f"{entry['id']}-{time.time_ns()}", "kind": args.kind, "signature": signature,
"workdir": entry["workdir"]}
keep(history.row(job, start, time.time(), code, samples, concurrent))
record(job, start, code, samples, concurrent)
return code
finally:
leave(entry["id"])
@@ -550,7 +705,7 @@ def cmd_list(_: argparse.Namespace) -> int:
with store() as data:
data["entries"] = prune(data["entries"])
entries = list(data["entries"])
verdicts = decide(cfg, entries, free, now, kinds)
verdicts = decide(cfg, entries, free, now, signatures, kinds)
by_id = {e["id"]: e for e in entries}
running = [(signature_of(e), e["granted"]) for e in entries if e["status"] == "active"]
rows = [
@@ -565,13 +720,17 @@ def cmd_list(_: argparse.Namespace) -> int:
print("position\tid\tstatus\tkind\ttask\tneed_gb\twaited_min\teta_min\tworkdir\treason")
for position, e, eta, reason in rows:
waited = ((e["granted"] or now) - e["enqueued"]) / 60
print("\t".join(
print(tab_line(
[position, e["id"], e["status"], e["kind"], e["task"], f"{e['need_gb']:g}", f"{waited:.0f}",
"-" if eta is None else f"{eta:.0f}", e["workdir"], reason]
))
return 0
def tab_line(fields: list[object]) -> str:
return "\t".join("".join(c if c.isprintable() else c.encode("unicode_escape").decode() for c in str(f)) for f in fields)
def cmd_history(_: argparse.Namespace) -> int:
cfg = config()
signatures, kinds = learned(cfg, time.time())
@@ -633,50 +792,10 @@ def permission(commands: list[str], project: Path) -> str | None:
return None
def uncommented(command: str) -> str:
kept: list[str] = []
quote = ""
escaped = skipping = False
word_start = True
for char in command:
if skipping and char != "\n":
continue
skipping = False
if escaped:
escaped = False
elif quote == "'":
quote = "" if char == "'" else quote
elif char == "\\":
escaped = True
elif quote == '"':
quote = "" if char == '"' else quote
elif char in "'\"":
quote = char
elif char == "#" and word_start:
skipping = True
continue
kept.append(char)
word_start = not quote and not escaped and char in WORD_BREAK
return "".join(kept)
def segments(command: str) -> list[list[str]]:
lexer = shlex.shlex(uncommented(command.replace("\\\n", "")), posix=True, punctuation_chars=True)
lexer.whitespace_split = True
lexer.commenters = ""
parts: list[list[str]] = [[]]
for token in lexer:
if SEPARATOR.fullmatch(token):
parts.append([])
else:
parts[-1].append(token)
return parts
def held_commands(command: str) -> list[list[str]]:
return [
cli().parse_args(part[at + 1 :]).command
for part in segments(command)
for part in watch.segments(command)
for at, token in enumerate(part)
if token.endswith("queue.py") and part[at + 1 : at + 2] == ["hold"]
]
@@ -715,10 +834,8 @@ def cmd_block() -> int:
cfg = watch.load_config()
if cfg is None or cfg.get("queue_enforce") is not True:
return 0
command = watch.executable_text((payload.get("tool_input") or {}).get("command") or "")
if THROUGH_QUEUE.search(command) or any(re.search(p, command) for p in cfg["never_block"]):
return 0
if GATE.search(command) or any(re.search(p, command) for p in cfg["heavy_commands"]):
gated = {**cfg, "heavy_commands": [*cfg["heavy_commands"], GATE.pattern]}
if watch.heavy(raw, gated, (THROUGH_QUEUE.pattern, *cfg["never_block"])):
print(
f"kvb-org-queue: heavy jobs run through the queue: start `python3 {Path(__file__).resolve()} hold "
"--kind <kind> --task <register task> -- <command>` in the background; it runs the command on its grant.",
+84
View File
@@ -249,6 +249,90 @@ class TestOperator(unittest.TestCase):
self.assertNotIn("decide", result.stdout)
self.assertEqual(self._open(), ["operator/ask"])
def _queue_config(self, slot_need_gb: dict) -> None:
config = self.base / "config.json"
open_memory = {"available": -1, "swap_free": -1, "psi_full": 1000, "container_swap_gb": 1e9}
config.write_text(json.dumps({
"disk": str(self.base), "thresholds_gb": {"warn": 0, "stop": 0, "critical": 0}, "projection_minutes": 30,
"slot_need_gb": slot_need_gb, "heavy_commands": [r"\btouch heavy\b"], "never_block": [],
"load_limits": {"cpu_percent": 101, "mem_percent": 101, "sample_seconds": 0.1},
"budget_percent": {"ram": 75, "cpu": 100}, "memory_limits": {"warn": open_memory, "stop": open_memory},
}), encoding="utf-8")
self.env["AGENTS_COUNCIL_CONFIG"] = str(config)
def test_run_offers_the_queue_for_a_heavy_command_and_asks_again_on_the_grant(self):
self._queue_config({"operator": 0})
work = self.base / "work"
work.mkdir()
self._add("light", work, "touch light", "plain", "manual")
self._add("heavy", work, "touch heavy\x1b[2K", "build", "manual")
result = self._feed("y\nh\n")
self.assertEqual(result.returncode, 0, result.stderr)
light, heavy = result.stdout.split("[2/2]")
self.assertIn("[s]kip", light)
self.assertNotIn("heavy:", light)
self.assertIn("heavy: memory stage ok", heavy)
self.assertIn("position\tid\tstatus", heavy)
self.assertIn("[h]old in the queue", heavy)
self.assertNotIn("\x1b", heavy)
self.assertIn("granted operator/heavy: the queue lets it run now", heavy)
self.assertIn("not run operator/heavy", heavy)
self.assertIn("failed operator/heavy: exit 75; the row stays open", heavy)
self.assertTrue((work / "light").exists())
self.assertFalse((work / "heavy\x1b[2K").exists())
self.assertEqual(self._open(), ["operator/heavy"])
self.assertFalse((self.root / "queue-history.jsonl").exists())
def test_run_offers_no_hold_while_the_config_lacks_the_operator_kind(self):
self._queue_config({"test": 0})
work = self.base / "work"
work.mkdir()
self._add("heavy", work, "touch heavy", "build", "manual")
result = self._feed("h\ns\n")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("lacks slot_need_gb.operator;", result.stdout)
self.assertIn("no [h]old until it does", result.stdout)
self.assertNotIn("[h]old in the queue", result.stdout)
self.assertIn("skipped operator/heavy", result.stdout)
def test_run_offers_no_hold_for_sudo_whose_prompt_needs_the_terminal(self):
self._queue_config({"operator": 0})
work = self.base / "work"
work.mkdir()
self._add("heavy", work, "bash -c 'sudo -n touch heavy'", "build", "manual")
result = self._feed("h\ns\n")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("sudo asks on the terminal, which a queued command lacks", result.stdout)
self.assertNotIn("[h]old in the queue", result.stdout)
def test_granted_runs_only_the_approved_row_in_its_directory_after_a_new_yes(self):
work = self.base / "work"
work.mkdir()
self._add("heavy", work, "touch heavy", "build", "manual")
env = {**self.env, "SHELL": "/bin/sh"}
def granted(answers: str, directory: Path, *argv: str) -> subprocess.CompletedProcess:
return subprocess.run(
[sys.executable, str(OPERATOR), "granted", "operator/heavy", "--dir", str(directory), "--check",
"manual", "--", *argv],
input=answers, capture_output=True, text=True, env=env, cwd=self.base, check=False,
)
changed = granted("y\n", work, "/bin/sh", "-ic", "touch other")
self.assertEqual(changed.returncode, 75)
self.assertIn("no longer open with the approved directory, check and command", changed.stderr)
moved = granted("y\n", self.base, "/bin/sh", "-ic", "touch heavy")
self.assertEqual((moved.returncode, (self.base / "heavy").exists()), (75, False))
declined = granted("s\n", work, "/bin/sh", "-ic", "touch heavy")
self.assertEqual((declined.returncode, (work / "heavy").exists()), (75, False))
silent = granted("", work, "/bin/sh", "-ic", "touch heavy")
self.assertEqual((silent.returncode, (work / "heavy").exists()), (75, False))
ran = granted("d\ny\n", work, "/bin/sh", "-ic", "touch heavy")
self.assertEqual(ran.returncode, 0, ran.stderr)
self.assertIn("granted operator/heavy", ran.stdout)
self.assertTrue((work / "heavy").exists())
self.assertFalse((self.base / "heavy").exists() or (work / "other").exists())
def test_run_needs_shell(self):
self._add("x", self.base, "true", "r", "manual")
result = subprocess.run(
+294 -6
View File
@@ -20,12 +20,15 @@ QUEUE = REPO_ROOT / "skills" / "kvb-org-queue" / "scripts" / "queue.py"
HISTORY = REPO_ROOT / "skills" / "kvb-org-queue" / "scripts" / "history.py"
NOW = 1_800_000_000.0
HOUR = 3600
OPEN_MEMORY = {"available": -1, "swap_free": -1, "psi_full": 1000, "container_swap_gb": 1e9}
BASE = {
"free_gb": 100.0,
"floor": 10.0,
"stage": "ok",
"memory": "ok",
"silenced": False,
"slot_limits": {"test": 5, "deploy": 1},
"budget": {"ram": 75, "cpu": 100},
"ranks": {"work/a": 0, "work/b": 1, "work/c": 2},
"agents": {},
"now": NOW,
@@ -83,7 +86,97 @@ class TestEvaluate(unittest.TestCase):
cls.queue = _load(QUEUE)
def decide(self, entries: list[dict], **overrides: object) -> dict:
return self.queue.evaluate(entries, **{**BASE, **overrides})
footprints = {e["id"]: {"ram": 0, "cpu": 0} for e in entries} | overrides.pop("footprints", {})
return self.queue.evaluate(entries, **{**BASE, "footprints": footprints, **overrides})
def test_the_budget_refuses_a_job_whose_share_would_pass_it_and_reserves_it(self):
running = job(1, kind="test", status="active")
candidate = job(2, kind="gui-dind", workdir="/x")
small = job(3, task="work/b", workdir="/y")
shares = {"j1": {"ram": 60, "cpu": 40}, "j2": {"ram": 30, "cpu": 20}, "j3": {"ram": 10, "cpu": 5}}
verdicts = self.decide([running, candidate, small], slot_limits={"test": 5, "gui-dind": 1}, footprints=shares)
self.assertEqual(verdicts["j2"], "RAM 60 % in use or reserved + 30 % > budget 75 %; reserved")
self.assertEqual(verdicts["j3"], "RAM 90 % in use or reserved + 10 % > budget 75 %; reserved")
unbounded = {"ram": 1000, "cpu": 1000}
self.assertEqual(
self.decide([running, candidate, small], slot_limits={"test": 5, "gui-dind": 1}, footprints=shares, budget=unbounded),
{"j2": None, "j3": None},
)
def test_a_job_alone_always_fits_and_cpu_counts_like_ram(self):
alone = self.decide([job(1)], footprints={"j1": {"ram": 90, "cpu": 150}})
self.assertEqual(alone, {"j1": None})
busy = self.decide([job(1, status="active"), job(2)], footprints={"j1": {"ram": 1, "cpu": 70}, "j2": {"ram": 1, "cpu": 40}})
self.assertEqual(busy, {"j2": "CPU 70 % in use or reserved + 40 % > budget 100 %; reserved"})
def test_a_share_beyond_the_budget_runs_only_alone_even_beside_a_job_of_no_share(self):
whole = {"ram": 80, "cpu": 1}
beside = self.decide([job(1, status="active"), job(2)], footprints={"j1": {"ram": 0, "cpu": 0}, "j2": whole})
self.assertEqual(beside, {"j2": "RAM 0 % in use or reserved + 80 % > budget 75 %; reserved"})
self.assertEqual(self.decide([job(2)], footprints={"j2": whole}), {"j2": None})
def test_load_counts_the_ram_in_use_plus_what_is_still_to_come(self):
limits = {"cpu_percent": 90, "mem_percent": 90}
reason = self.queue.load_reason
self.assertIsNone(reason(limits, 50, 60, 29))
self.assertEqual(reason(limits, 50, 60, 30), "RAM 60 % used + 30 % still to come reach the limit 90 %")
self.assertIsNone(reason(limits, 50, 89, 0))
self.assertEqual(reason(limits, 90, 10, 0), "CPU 90 % busy reaches the limit 90 %")
def test_pending_ram_adds_what_active_jobs_have_not_taken_yet_and_seeds_an_unlearned_kind(self):
history = _load(HISTORY)
rows = [history_row(n, kind=kind, signature=f"w: {kind}", mem_before=20, mem_max=20 + share)
for n, (kind, share) in enumerate([("a", 30)] * 5 + [("b", 25)] * 5 + [("c", 10)] * 5)]
signatures, kinds = history.learn(rows, NOW, {"load_limits": {"mem_percent": 90, "cpu_percent": 90}})
self.assertEqual(history.seed_ram(kinds), 30)
self.assertEqual(history.seed_ram({}), 0)
cmd = {"workdir": "/w"}
fresh = {**job(2, kind="b"), **cmd, "cmd": "b"}
young = {**job(1, kind="a", status="active", granted=NOW - 60), **cmd, "cmd": "a"}
overshooting = {**job(5, kind="c", status="active", granted=NOW - 120), **cmd, "cmd": "c"}
old = {**job(4, kind="c", status="active", granted=NOW - self.queue.RAMP_SECONDS), **cmd, "cmd": "c"}
self.assertEqual(self.queue.pending_ram([young, overshooting, old, fresh], "j2", NOW, signatures, kinds), 30 + 10 + 25)
undated = {k: v for k, v in old.items() if k != "granted"}
self.assertEqual(self.queue.pending_ram([undated, fresh], "j2", NOW, signatures, kinds), 10 + 25)
unknown = {**job(3, kind="new"), **cmd, "cmd": "new"}
self.assertEqual(self.queue.pending_ram([unknown], "j3", NOW, signatures, kinds), 30)
def test_a_signature_names_the_repository_every_worktree_shares(self):
with tempfile.TemporaryDirectory() as tmp:
base = Path(tmp).resolve()
main = base / "skills"
main.mkdir()
identity = {**os.environ, "GIT_AUTHOR_NAME": "T", "GIT_AUTHOR_EMAIL": "t@x", "GIT_COMMITTER_NAME": "T",
"GIT_COMMITTER_EMAIL": "t@x", "GIT_CONFIG_NOSYSTEM": "1"}
for args in (["init", "-q"], ["commit", "-q", "--allow-empty", "-m", "x"], ["worktree", "add", "-q", str(base / "wt")]):
subprocess.run(["git", *args], cwd=main, env=identity, check=True, capture_output=True)
self.assertEqual(self.queue.qualified(str(main), ["make", "test"]), "skills: make test")
self.assertEqual(self.queue.qualified(str(base / "wt"), ["make", "test"]), "skills: make test")
plain = base / "plain"
plain.mkdir()
self.assertEqual(self.queue.qualified(str(plain), ["true"]), "plain: true")
self.assertEqual(self.queue.qualified(str(base / "gone"), ["true"]), "gone: true")
for argv, expected in {
("env", "TMPDIR=/t", "-i", "make", "test"): "make test",
("nice", "-n", "10", "make", "compose-deploy"): "make compose-deploy",
("timeout", "3600", "make", "lint"): "make lint",
("python3", "/s/gate.py", "hold", "--repo", ".", "--", "make", "verify-test"): "make verify-test",
("python3", "/s/operator.py", "granted", "operator/x", "--dir", "/a", "--check", "manual", "--",
"zsh", "-ic", "make -C /a/core compose-deploy"): "make compose-deploy",
("bash", "-c", "cd core && make test 2>&1 | tee /tmp/log"): "make test",
("python3", "/s/x.py", "--"): "x.py",
("bash", "-c", "set -e; make verify-test"): "make verify-test",
("bash", "-c", "source .venv/bin/activate && make test"): "make test",
}.items():
with self.subTest(argv=argv):
self.assertEqual(self.queue.qualified(str(plain), list(argv)), f"plain: {expected}")
module = ["python3", "-m", "pytest", "--", "tests/"]
self.assertEqual(self.queue.command_words(module), module)
def test_memory_stage_stop_blocks_every_job_and_warn_none(self):
entries = [job(1, need_gb=0), job(2, task="work/b")]
self.assertEqual(set(self.decide(entries, memory="stop").values()), {"memory stage stop"})
self.assertEqual(self.decide(entries, memory="warn"), {"j1": None, "j2": None})
def test_hotfix_goes_first_then_rank_then_arrival_and_unknown_tasks_last(self):
entries = [
@@ -199,6 +292,23 @@ class TestHistory(unittest.TestCase):
crowded = [history_row(n, mem_before=90, mem_max=95) for n in range(5)]
self.assertEqual(self.learn(crowded)[1]["test"]["limit"], 1)
def test_footprint_takes_the_larger_of_kind_and_signature_at_least_the_minimum_and_none_for_a_new_kind(self):
small = [history_row(n, signature="make test", mem_max=25, cpu_max=20) for n in range(5)]
big = [
history_row(n + 5, kind="quality-high", signature="make verify-test", mem_max=80, cpu_max=90)
for n in range(5)
]
idle = [history_row(n + 10, kind="idle", signature="true", mem_max=20, cpu_max=5) for n in range(5)]
signatures, kinds = self.learn([*small, *big, *idle])
footprint = self.history.footprint
self.assertEqual(footprint("test", "make test", signatures, kinds), {"ram": 5, "cpu": 10})
self.assertEqual(footprint("test", "make verify-test", signatures, kinds), {"ram": 60, "cpu": 80})
self.assertEqual(footprint("test", "make unseen", signatures, kinds), {"ram": 5, "cpu": 10})
self.assertEqual(footprint("idle", "true", signatures, kinds), {"ram": 1, "cpu": 1})
self.assertIsNone(footprint("gui-dind", "make test-up", signatures, kinds))
signatures, kinds = self.learn(small[:4])
self.assertIsNone(footprint("test", "make test", signatures, kinds))
def test_slot_limits_take_the_learned_limit_else_the_seed_and_a_smaller_cap(self):
kinds = {"deploy": {"limit": 3}, "build": {"limit": None}}
self.assertEqual(self.history.slot_limits(kinds, {"deploy": 2, "swarm": 0, "test": 9}, ["test", "gate"]), {
@@ -256,7 +366,9 @@ class TestQueueCommands(unittest.TestCase):
"projection_minutes": 30,
"slot_need_gb": {"test": 0.001, "deploy": 0},
"slot_limits": {"test": 1, "deploy": 1},
"load_limits": {"cpu_percent": 101, "mem_percent": 101, "sample_seconds": 0.1, "kinds": ["test"]},
"load_limits": {"cpu_percent": 101, "mem_percent": 101, "sample_seconds": 0.1},
"budget_percent": {"ram": 75, "cpu": 100},
"memory_limits": {stage: OPEN_MEMORY for stage in ("warn", "stop")},
"heavy_commands": ["^make compose-deploy"],
"never_block": ["^make compose-down"],
}
@@ -338,13 +450,23 @@ class TestQueueCommands(unittest.TestCase):
[row] = [json.loads(line) for line in self.history_file.read_text(encoding="utf-8").splitlines()]
self.assertEqual(
[row[k] for k in ("kind", "signature", "workdir", "exit", "concurrent")],
["test", f"{Path(sys.executable).name} -c", str(self.base), 7, 0],
["test", f"{self.base.name}: {Path(sys.executable).name} -c", str(self.base), 7, 0],
)
self.assertTrue(row["id"].startswith("test-1-"))
self.assertLessEqual(row["start"], row["end"])
self.assertEqual(row["disk_gb"], round(max(0.0, row["free_before_gb"] - row["min_free_gb"]), 2))
self.assertLessEqual(row["mem_before"], row["mem_max"])
def test_a_command_that_did_no_work_leaves_no_history_row(self):
result = self.run_queue(
"hold", "--kind", "test", "--task", "work/a", "--session", "s1", "--", sys.executable, "-c",
"import sys; sys.exit(75)",
)
self.assertEqual(result.returncode, 75)
self.assertFalse(self.history_file.exists())
self.assertEqual(self.run_queue("hold", "--kind", "test", "--task", "work/a", "--session", "s1", "--", "false").returncode, 1)
self.assertEqual(len(self.history_file.read_text(encoding="utf-8").splitlines()), 1)
def test_hold_runs_without_slot_limits_on_the_seed_limit(self):
del self.cfg["slot_limits"]
self.write_config()
@@ -464,7 +586,8 @@ class TestQueueCommands(unittest.TestCase):
self.write_config()
now = time.time()
rows = [
history_row(n, kind="deploy", signature="true", end=now, disk_gb=gb) for n, gb in enumerate((4, 1, 10, 3, 2))
history_row(n, kind="deploy", signature=f"{self.base.name}: true", end=now, disk_gb=gb)
for n, gb in enumerate((4, 1, 10, 3, 2))
]
self.write_history(self.history_file, rows[:3])
self.write_history(self.kept, [{**rows[0], "disk_gb": 100}, *rows[3:]])
@@ -480,9 +603,9 @@ class TestQueueCommands(unittest.TestCase):
me, mine = os.getpid(), queue.identity(os.getpid())
now = time.time()
self.write_history(
self.history_file, [history_row(n, signature="make compose-deploy", end=now) for n in range(5)]
self.history_file, [history_row(n, signature="w: make compose-deploy", end=now) for n in range(5)]
)
deploy = {"cmd": "make compose-deploy apps=x"}
deploy = {"cmd": "make compose-deploy apps=x", "workdir": "/w"}
self.write_store([
{**stored(1, "active", me, mine, me, mine), **deploy, "granted": now - 240},
{**stored(2, "waiting", me, mine, None, None), **deploy},
@@ -545,6 +668,9 @@ class TestQueueCommands(unittest.TestCase):
self.assertEqual(refused.returncode, 2)
self.assertIn("queue.py hold --kind <kind> --task <register task> -- <command>", refused.stderr)
self.assertEqual(self.bash("python3 skills/kvb-check-gate/scripts/gate.py run --repo . --target test"), 2)
for command in ("make -j4 compose-deploy apps=x", "make 'compose-deploy'", "cd x && make compose-deploy"):
with self.subTest(command=command):
self.assertEqual(self.bash(command), 2)
for command in (
"python3 /x/queue.py hold --kind deploy --task work/a -- make compose-deploy",
"make compose-down",
@@ -557,6 +683,168 @@ class TestQueueCommands(unittest.TestCase):
self.config.unlink()
self.assertEqual(self.bash("make compose-deploy"), 0)
def test_block_with_queue_enforce_refuses_builds_installs_and_full_suites_by_itself(self):
self.cfg["queue_enforce"] = True
self.cfg["heavy_commands"] = []
self.write_config()
for command in (
"docker compose up -d --build",
"docker compose -f x.yml build web",
"docker-compose up --build",
"make install",
"make -C core verify-test",
"pytest tests/",
"python3 -m pytest -q",
".venv/bin/pytest tests/lint tests/unit",
"pytest tests/a.py tests/b.py",
"python3 -m unittest discover -s tests",
"python3 -m unittest discover -s tests -p 'test_*.py'",
"pytest tests/x.py tests/lint",
"pytest tests/lint --ignore=tests/lint/y.py",
"python3 -m unittest",
"make -j4 install",
"make VAR=x install",
"cd core && env A=1 timeout 600 python3 -m pytest -q 2>&1 | tee /tmp/log",
"bash -c 'pytest tests/'",
"pytest tests/ # queue.py hold",
"python3 /x/queue.py hold --kind test --task work/a -- true & pytest tests/",
"make compose-down; make install",
"uv run pytest tests/",
"coverage run -m pytest",
"python3 -mpytest tests/",
"python3 -X dev -m pytest",
"timeout -s KILL 900 pytest tests/",
"sudo -u nobody pytest tests/",
"env -u FOO pytest",
"{ pytest tests/; }",
"! pytest tests/",
"if pytest tests/; then echo ok; fi",
"echo `pytest tests/`",
"bash -c -- 'pytest tests/'",
"bash <<'EOF'\npytest tests/\nEOF",
'OUT="$(pytest tests/ 2>&1)"',
'echo "r: $(make install)"',
"cat <<EOF\n`make install`\nEOF",
"uv run --extra dev pytest tests/",
"uv run --with pytest-xdist pytest -n 8 tests/",
"coverage run --source pkg -m pytest tests/",
"poetry -C core run pytest tests/",
"python3 -m coverage run -m pytest tests/",
"uvx pytest tests/",
"uv tool run pytest tests/",
"time -p pytest tests/",
"/usr/bin/time -v pytest tests/",
"/usr/bin/time -f %M make install",
"make 2>&1 install",
">/tmp/log pytest tests/",
"2>/dev/null make install",
"pytest tests/test_watch.py 2>/dev/null tests/",
"make compose-down install",
"make -C core compose-down verify-test",
"make --directory core install",
"command pytest tests/",
"command -p pytest tests/",
"command make install",
"env -S 'pytest tests/'",
"bash -Ec 'pytest tests/'",
"exec -a x pytest tests/",
"sudo --user root pytest tests/",
"env $(printenv | grep -o -E '^INFINITO_[A-Z0-9_]+' | sed 's/^/-u /') -C /x python3 -m pytest tests/",
"env $(cat .env) -u X make install",
"sudo -u $(id -un) -- pytest tests/",
"timeout --signal=$(echo KILL) 900 pytest tests/",
"sudo --chdir /x pytest tests/",
"sudo bash <<'EOF'\npytest tests/\nEOF",
"bash -s <<'END-MSG'\nmake install\nEND-MSG",
"out=$(bash <<'EOF'\nmake install\nEOF\n)",
"bash -c 'bash <<EOF\nmake install\nEOF\n'",
"bash <<'A'\nbash <<'B'\nmake install\nB\nA",
"bash -euo pipefail <<'EOF'\nmake test\nmake install\nEOF",
"bash -o pipefail <<'EOF'\npytest tests/\nEOF",
"bash -s -- one <<'EOF'\nmake install\nEOF",
"bash --rcfile x <<'EOF'\nmake install\nEOF",
"make -Cinfinito install",
"make -Oline install",
"bash <<'EOF'\nmake install \ud800\nEOF",
):
with self.subTest(command=command):
self.assertEqual(self.bash(command), 2)
for command in (
"docker compose up -d",
"docker compose ps",
"make install-skills",
"pytest tests/unit/test_x.py",
"python3 -m pytest tests/unit/test_x.py::TestA -q",
"pip install pytest",
"python3 -m unittest discover -s tests -p test_queue.py",
"python3 -m unittest tests.test_queue",
"python3 /x/queue.py hold --kind test --task work/a -- pytest tests/",
'pytest "tests/unit/test_x.py"',
"python3 -m unittest discover -s tests -p 'test_queue.py'",
"grep -rn pytest tests/",
"which pytest",
"pytest --version",
"pytest tests/unit/test_x.py -p no:cacheprovider 2>&1 | tee /tmp/log",
"command -v pytest",
"make -n install",
"cat <<'EOF'\npytest tests/\nEOF",
"pytest >/tmp/out.log tests/test_watch.py",
"git commit -m \"$(cat <<'EOF'\nfix: run `make install` once\nEOF\n)\"",
"gh pr create --body-file - <<'EOF'\nruns `make verify-test`\nEOF",
"echo '$(make install)'",
"uv pip install pytest",
"command -V pytest",
"time pytest tests/unit/test_x.py",
"docker compose -f compose.yml -f build/compose/runner-mirror.override.yml config --services",
"docker compose --project-directory build up -d",
'echo "see \\`make install\\` and \\$(make install) docs"',
"pytest -n auto --dist loadscope tests/x.py",
"pytest --cov src --timeout 30 tests/x.py",
"cat > /tmp/run.sh <<'EOF'\npytest tests/\nEOF",
"gh pr create --title \"fix(bash): quoting\" --body \"$(cat <<'EOF'\nruns `make install`\nEOF\n)\"",
"ls # it's $(make install)",
"make -nC core install",
"make -kn install",
"python3 /x/queue.py hold --kind test --task work/a -- bash <<'EOF'\nmake install\nEOF",
"python3 /x/queue.py hold --kind test --task work/a -- bash -c \"$(cat <<'EOF'\nmake install\nEOF\n)\"",
"cat <<'END-MSG'\nmake install\nEND-MSG",
"cat <<\\EOF\n$(make install)\nEOF",
"pytest --numprocesses 4 tests/x.py",
"pytest tests/lint --collect-only",
"bash script.sh <<'EOF'\nmake install\nEOF",
"out=$(cat <<'EOF'\nmake install\nEOF\n)",
"echo kvbheredoc1 kvbheredoczz",
):
with self.subTest(command=command):
self.assertEqual(self.bash(command), 0)
def test_block_matches_a_config_regex_on_the_wrapped_program_too(self):
self.cfg["queue_enforce"] = True
self.cfg["heavy_commands"] = [r"^docker compose up"]
self.write_config()
for command in ("timeout 900 docker compose up -d", "sudo docker compose up -d", "docker compose up -d"):
with self.subTest(command=command):
self.assertEqual(self.bash(command), 2)
self.assertEqual(self.bash("docker compose ps"), 0)
def test_hold_refuses_a_config_without_the_budget_or_the_memory_limits(self):
for key, cfg in (
("budget_percent", {k: v for k, v in self.cfg.items() if k != "budget_percent"}),
("memory_limits", {k: v for k, v in self.cfg.items() if k != "memory_limits"}),
("budget_percent.cpu", {**self.cfg, "budget_percent": {"ram": 75}}),
("budget_percent.ram", {**self.cfg, "budget_percent": {"ram": "75", "cpu": 100}}),
("budget_percent.cpu", {**self.cfg, "budget_percent": {"ram": 75, "cpu": float("nan")}}),
("memory_limits.warn.container_swap_gb", {**self.cfg, "memory_limits": {
"warn": {k: v for k, v in OPEN_MEMORY.items() if k != "container_swap_gb"}, "stop": OPEN_MEMORY,
}}),
):
with self.subTest(key=key):
self.config.write_text(json.dumps(cfg), encoding="utf-8")
result = self.run_queue("hold", "--kind", "test", "--task", "work/a", "--session", "s1", "--", "true")
self.assertEqual(result.returncode, 2)
self.assertIn(f"lacks {key};", result.stderr)
self.assertFalse((self.root / "queue.json").exists())
def test_block_holds_a_held_command_to_the_bash_rules_of_the_settings(self):
project = self.base / "project"
(project / ".claude").mkdir(parents=True)
+146 -8
View File
@@ -17,6 +17,10 @@ from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
WATCH = REPO_ROOT / "skills" / "kvb-org-organize" / "scripts" / "watch.py"
LIMITS = {"warn": 40, "stop": 15, "critical": 12}
OPEN_MEMORY = {"available": -1, "swap_free": -1, "psi_full": 1000, "container_swap_gb": 1e9}
SHUT_MEMORY = {"available": 101, "swap_free": 101, "psi_full": 0, "container_swap_gb": 0}
MEMINFO = "MemTotal: 1000 kB\nMemAvailable: 120 kB\nSwapTotal: 2000 kB\nSwapFree: 300 kB\n"
PSI = "some avg10=30.00 avg60=1.00 avg300=0.50 total=1\nfull avg10=12.50 avg60=0.80 avg300=0.10 total=1\n"
def _load():
@@ -41,6 +45,66 @@ class TestStage(unittest.TestCase):
meminfo = "MemTotal: 1000 kB\nMemFree: 100 kB\nMemAvailable: 250 kB\n"
self.assertEqual(self.watch.mem_used_percent(meminfo), 75.0)
def test_memory_sample_reads_available_swap_and_psi_and_tolerates_their_absence(self):
self.assertEqual(
self.watch.memory_sample(MEMINFO, PSI, 2.5),
{"available": 12.0, "swap_free": 15.0, "psi_full": 12.5, "container_swap_gb": 2.5},
)
bare = "MemTotal: 1000 kB\nMemAvailable: 500 kB\nSwapTotal: 0 kB\nSwapFree: 0 kB\n"
self.assertEqual(
self.watch.memory_sample(bare, None, 0.0),
{"available": 50.0, "swap_free": None, "psi_full": None, "container_swap_gb": 0.0},
)
def test_memory_takes_the_swap_of_the_container_holding_the_most_nested_groups_included(self):
with tempfile.TemporaryDirectory() as tmp:
base = Path(tmp)
for name, swap in ((f"docker-{'a' * 64}.scope", 3_000_000_000), (f"docker-{'b' * 64}.scope", 18_000_000_000),
("other.scope", 90_000_000_000)):
(base / name).mkdir()
(base / name / "memory.swap.current").write_text(str(swap), encoding="utf-8")
self.watch.CGROUPS = base
self.assertEqual(self.watch.memory()["container_swap_gb"], 18.0)
self.watch.CGROUPS = base / "absent"
self.assertEqual(self.watch.memory()["container_swap_gb"], 0.0)
def test_memory_stage_takes_the_first_bound_crossed_stop_before_warn(self):
cfg = {"memory_limits": {
"warn": {"available": 15, "swap_free": 25, "psi_full": 5, "container_swap_gb": 4},
"stop": {"available": 8, "swap_free": 10, "psi_full": 10, "container_swap_gb": 16},
}}
stage = self.watch.memory_stage
healthy = {"available": 40.0, "swap_free": 80.0, "psi_full": 0.0, "container_swap_gb": 3.9}
self.assertEqual(stage(healthy, cfg), "ok")
for change, expected in (
({"available": 14.0}, "warn"), ({"swap_free": 24.0}, "warn"), ({"psi_full": 5.0}, "warn"),
({"container_swap_gb": 4.0}, "warn"),
({"available": 7.0}, "stop"), ({"swap_free": 9.0}, "stop"), ({"psi_full": 10.0}, "stop"),
({"container_swap_gb": 16.0}, "stop"),
({"swap_free": None, "psi_full": None}, "ok"),
):
with self.subTest(change=change):
self.assertEqual(stage(healthy | change, cfg), expected)
def test_swap_holders_follow_the_nested_group_holding_most_of_the_swap(self):
with tempfile.TemporaryDirectory() as tmp:
base = Path(tmp)
outer = "a" * 64
def group(path: Path, swap: int) -> None:
path.mkdir(parents=True)
(path / "memory.swap.current").write_text(str(swap), encoding="utf-8")
group(base / f"docker-{outer}.scope", 18_000_000_000)
group(base / f"docker-{outer}.scope" / "c977e579", 17_500_000_000)
group(base / f"docker-{outer}.scope" / "c977e579" / f"docker-{'b' * 64}.scope", 17_200_000_000)
group(base / f"docker-{outer}.scope" / "c977e579" / "small", 100)
group(base / f"docker-{'c' * 64}.scope", 1_000_000_000)
group(base / f"docker-{'d' * 64}.scope", 0)
(base / "other.scope").mkdir()
holders = self.watch.swap_holders(base, {outer: "iam"})
self.assertEqual(holders, [f"iam > c977e579 > {'b' * 12} 18.0 GB", f"{'c' * 12} 1.0 GB"])
def test_fast_fall_escalates_one_stage(self):
now = time.time()
self.assertEqual(self.watch.stage_for(32, [[now - 600, 48]], self.cfg, now), "stop")
@@ -73,7 +137,8 @@ class TestWatchCommands(unittest.TestCase):
"cache_budget_percent": 3,
"slot_need_gb": {"swarm": 30, "test": 0.001},
"notify": ["true"],
"load_limits": {"cpu_percent": 101, "mem_percent": 101, "sample_seconds": 0.1, "kinds": ["test"]},
"load_limits": {"cpu_percent": 101, "mem_percent": 101, "sample_seconds": 0.1},
"memory_limits": {stage: dict(OPEN_MEMORY) for stage in ("warn", "stop")},
}
self._write_config()
@@ -126,17 +191,21 @@ class TestWatchCommands(unittest.TestCase):
self.cfg["heavy_commands"] = [r"\bmake\s+(-C\s+\S+\s+)?(compose-deploy|test|swarm-\S+)(\s|$)"]
self._write_config()
self._stage("stop")
message = "$(cat <<'EOF'\nfix: run \"make test\" and make swarm-zombie\nEOF\n)"
message = "$(cat <<'EOF'\nfix: run \"make test\" and make swarm-zombie, `make test` too\nEOF\n)"
for command in (
'git commit --no-verify -m "fix: passes the vars to make swarm-zombie app=x"',
"grep -n 'make test' Makefile",
f'git commit --no-verify -m "{message}"',
"gh pr create --body-file - <<'EOF'\nruns `make test`\nEOF",
"echo 'not run: $(make test)'",
):
self.assertEqual(self._run("block", stdin=self._bash(command)).returncode, 0, command)
for command in (
"make -C x swarm-zombie app=y",
"env FOO=1 make swarm-zombie",
'make compose-deploy apps="web-app-x"',
'OUT="$(make test 2>&1)"',
"cat <<EOF\n$(make test)\nEOF",
):
self.assertEqual(self._run("block", stdin=self._bash(command)).returncode, 2, command)
@@ -146,16 +215,85 @@ class TestWatchCommands(unittest.TestCase):
self.assertEqual(self._run("admit", "--need-gb", "0.001").returncode, 0)
self.assertEqual(self._run("admit", "--need-gb", "1e9").returncode, 3)
def test_admit_for_a_load_kind_also_needs_cpu_and_ram_below_the_limits(self):
def test_every_admit_also_needs_cpu_and_ram_below_the_limits_and_memory_short_of_stop(self):
self.cfg["thresholds_gb"] = {"warn": 0, "stop": 0, "critical": 0}
self._write_config()
self.assertEqual(self._run("admit", "--kind", "test").returncode, 0)
granted = self._run("admit", "--kind", "test")
self.assertEqual(granted.returncode, 0)
self.assertIn("memory stage ok", granted.stdout)
self.cfg["load_limits"]["cpu_percent"] = 0
self._write_config()
denied = self._run("admit", "--kind", "test")
self.assertEqual(denied.returncode, 3)
self.assertIn("cpu", denied.stdout)
self.assertEqual(self._run("admit", "--need-gb", "0.001").returncode, 0)
for args in (("--kind", "test"), ("--kind", "swarm"), ("--need-gb", "0.001")):
with self.subTest(args=args):
denied = self._run("admit", *args)
self.assertEqual(denied.returncode, 3)
self.assertIn("cpu", denied.stdout)
self.cfg["load_limits"]["cpu_percent"] = 101
self.cfg["memory_limits"]["stop"] = dict(SHUT_MEMORY)
self._write_config()
short = self._run("admit", "--need-gb", "0.001")
self.assertEqual(short.returncode, 3)
self.assertIn("memory stage stop", short.stdout)
def test_block_refuses_heavy_jobs_at_memory_stop_only(self):
self._stage("ok")
self.cfg["memory_limits"]["warn"] = dict(SHUT_MEMORY)
self._write_config()
self.assertEqual(self._run("block", stdin=self._bash("make compose-deploy")).returncode, 0)
self.cfg["memory_limits"]["stop"] = dict(SHUT_MEMORY)
self._write_config()
blocked = self._run("block", stdin=self._bash("make compose-deploy"))
self.assertEqual(blocked.returncode, 2)
self.assertIn("memory stage is stop", blocked.stderr)
self.assertEqual(self._run("block", stdin=self._bash("docker compose up -d --build")).returncode, 2)
self.assertEqual(self._run("block", stdin=self._bash("make swarm-down")).returncode, 0)
self.assertEqual(self._run("block", stdin=self._bash("ls")).returncode, 0)
def test_memory_warn_alert_names_the_stage_once_per_session(self):
self.cfg["thresholds_gb"] = {"warn": 0, "stop": 0, "critical": 0}
self.cfg["memory_limits"]["warn"] = dict(SHUT_MEMORY)
self._write_config()
payload = json.dumps({"session_id": "s1", "hook_event_name": "UserPromptSubmit"})
first = self._run("watch", stdin=payload).stdout
self.assertIn("RESOURCE MEMORY WARN", first)
self.assertIn("Top swap holders", first)
self.assertEqual(self._run("watch", stdin=payload).stdout, "")
def test_a_config_without_memory_limits_blocks_heavy_jobs_keeps_the_disk_watch_and_stops_commands(self):
del self.cfg["memory_limits"]
self.cfg["thresholds_gb"] = {"warn": 1e9, "stop": -1, "critical": -2}
self._write_config()
blocked = self._run("block", stdin=self._bash("make compose-deploy"))
self.assertEqual(blocked.returncode, 2)
self.assertIn("lacks memory_limits", blocked.stderr)
self.assertEqual(self._run("block", stdin=self._bash("ls")).returncode, 0)
payload = json.dumps({"session_id": "s1", "hook_event_name": "UserPromptSubmit"})
watched = self._run("watch", stdin=payload)
self.assertEqual(watched.returncode, 0)
self.assertIn("RESOURCE WARN", watched.stdout)
self.assertIn("lacks memory_limits", watched.stdout)
command = self._run("admit", "--need-gb", "0")
self.assertEqual(command.returncode, 2)
self.assertIn("lacks memory_limits", command.stderr)
def test_a_memory_limit_missing_one_bound_names_it_and_blocks_heavy_jobs(self):
del self.cfg["memory_limits"]["stop"]["psi_full"]
self.cfg["thresholds_gb"] = {"warn": 0, "stop": 0, "critical": 0}
self._write_config()
blocked = self._run("block", stdin=self._bash("make compose-deploy"))
self.assertEqual(blocked.returncode, 2)
self.assertIn("lacks memory_limits.stop.psi_full;", blocked.stderr)
self.assertEqual(self._run("block", stdin=self._bash("ls")).returncode, 0)
payload = json.dumps({"session_id": "s1", "hook_event_name": "UserPromptSubmit"})
watched = self._run("watch", stdin=payload)
self.assertEqual(watched.returncode, 0, watched.stderr)
self.assertIn("lacks memory_limits.stop.psi_full", watched.stdout)
def test_segments_split_at_newlines_and_drop_a_redirection_with_its_target(self):
self.assertEqual(
_load().segments("pytest a.py 2>&1 | tee log\ncd x; make 'a b' # pytest\n>/tmp/o make 2>/dev/null install &>x\ndiff <(pytest t) y"),
[["pytest", "a.py"], ["tee", "log"], ["cd", "x"], ["make", "a b"], ["make", "install"], ["diff"], ["pytest", "t"], ["y"]],
)
def test_warn_alert_reaches_each_session_once(self):
self.cfg["thresholds_gb"] = {"warn": 1e9, "stop": -1, "critical": -2}