feat(runners): cap runner memory by host fraction
All checks were successful
CICD Start / Sanity and Base Decision (push) Successful in 26s
CICD Start / Sanity and Base Decision (pull_request) Successful in 34s

This commit is contained in:
copilotcoder
2026-07-05 11:40:31 -04:00
parent 6611c20bbf
commit 6e2b2f5e3a

View File

@@ -4,6 +4,7 @@
import os
import subprocess
import sys
import tempfile
from datetime import datetime
@@ -17,6 +18,23 @@ HOSTS = {
"pi-desktop.darkhelm.lan": "/home/darkhelm/Projects/DarkHelm.org/gitea-runner",
}
DEFAULT_MEM_FRACTION = 0.75
MIN_RUNNER_MEM_MIB = 1024
def parse_mem_fraction(argv):
fraction = DEFAULT_MEM_FRACTION
for arg in argv[1:]:
if arg.startswith("--mem-fraction="):
_, value = arg.split("=", 1)
try:
fraction = float(value)
except ValueError as exc:
raise SystemExit(f"Invalid --mem-fraction value: {value}") from exc
if fraction <= 0 or fraction > 1:
raise SystemExit("--mem-fraction must be > 0 and <= 1")
return fraction
def ssh(host, cmd):
proc = subprocess.run(
[
@@ -55,7 +73,52 @@ def scp(local_path, host, remote_path):
return proc.returncode, proc.stdout + proc.stderr
def build_compose(compose_text):
def upsert_service_key(compose_text, service_names, key, value):
lines = compose_text.splitlines()
i = 0
while i < len(lines):
stripped = lines[i].strip()
if stripped.rstrip(":") not in service_names or not stripped.endswith(":"):
i += 1
continue
service_indent = len(lines[i]) - len(lines[i].lstrip(" "))
field_indent = " " * (service_indent + 2)
block_start = i + 1
block_end = len(lines)
j = block_start
while j < len(lines):
cur = lines[j]
cur_stripped = cur.strip()
if cur_stripped:
cur_indent = len(cur) - len(cur.lstrip(" "))
if cur_indent <= service_indent:
block_end = j
break
j += 1
key_prefix = f"{key}:"
found_idx = None
for k in range(block_start, block_end):
if lines[k].strip().startswith(key_prefix):
found_idx = k
break
new_line = f"{field_indent}{key}: {value}"
if found_idx is not None:
lines[found_idx] = new_line
i = block_end
continue
lines.insert(block_start, new_line)
i = block_end + 1
return "\n".join(lines) + "\n"
def build_compose(compose_text, runner_mem_limit):
updated = compose_text
volume_line = " - ./daemon.json:/etc/docker/daemon.json:ro"
@@ -68,9 +131,31 @@ def build_compose(compose_text):
if add_cfg_env not in updated and force_pull_line in updated:
updated = updated.replace(force_pull_line, force_pull_line + "\n" + add_cfg_env)
updated = upsert_service_key(
updated,
{"act_runner_1", "act_runner_2", "runner1", "runner2"},
"mem_limit",
runner_mem_limit,
)
return updated
def host_mem_limit(host, fraction):
rc, mem_kib = ssh(host, "awk '/MemTotal:/ {print $2}' /proc/meminfo")
if rc != 0 or not mem_kib.strip().isdigit():
raise RuntimeError(f"Could not read MemTotal on {host}")
total_kib = int(mem_kib.strip())
target_mib = int((total_kib / 1024.0) * fraction)
if target_mib < MIN_RUNNER_MEM_MIB:
target_mib = MIN_RUNNER_MEM_MIB
return f"{target_mib}m", target_mib
mem_fraction = parse_mem_fraction(sys.argv)
print(f"Runner mem fraction: {mem_fraction:.2f}")
print("Fetching canonical runner config template")
rc, template = ssh(TEMPLATE_HOST, f"cat {TEMPLATE_PATH}")
if rc != 0 or not template.strip():
@@ -87,12 +172,20 @@ with tempfile.TemporaryDirectory() as tmpdir:
for host, root in HOSTS.items():
print(f"\n===== {host} =====")
try:
runner_mem_limit, runner_mem_mib = host_mem_limit(host, mem_fraction)
except RuntimeError as exc:
print(str(exc))
continue
print(f"runner_mem_limit={runner_mem_limit} ({runner_mem_mib} MiB)")
rc, compose = ssh(host, f"cat {root}/compose.yml")
if rc != 0:
print("Could not read compose.yml")
continue
new_compose = build_compose(compose)
new_compose = build_compose(compose, runner_mem_limit)
rc, _ = ssh(host, f"mkdir -p {root}/backups")
rc, _ = ssh(host, f"cp -f {root}/compose.yml {root}/backups/compose.yml.{now}")
@@ -120,6 +213,8 @@ with tempfile.TemporaryDirectory() as tmpdir:
print(out.strip())
rc, out = ssh(host, f"grep -n 'CONFIG_FILE=/config.yaml' {root}/compose.yml")
print(out.strip())
rc, out = ssh(host, f"grep -n 'mem_limit:' {root}/compose.yml")
print(out.strip())
rc, out = ssh(host, f"docker compose -f {root}/compose.yml up -d act_runner_1")
print(out.strip())