feat(runners): cap runner memory by host fraction
This commit is contained in:
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user