feat(runners): add host-share CPU limits
All checks were successful
CICD Start / Sanity and Base Decision (pull_request) Successful in 21s
CICD Start / Sanity and Base Decision (push) Successful in 59s

This commit is contained in:
copilotcoder
2026-07-05 13:41:45 -04:00
parent 2eb94d18f3
commit 92549b8edd

View File

@@ -21,6 +21,9 @@ HOSTS = {
DEFAULT_MEM_FRACTION = 0.75
DEFAULT_JOB_MEM_FRACTION = 0.9
DEFAULT_HOST_MEM_SHARE = 0.25
DEFAULT_HOST_CPU_SHARE = 0.25
DEFAULT_CPU_FRACTION = 1.0
DEFAULT_JOB_CPU_FRACTION = 0.9
MIN_RUNNER_MEM_MIB = 1024
MIN_JOB_MEM_MIB = 128
@@ -66,6 +69,48 @@ def parse_host_mem_share(argv):
raise SystemExit("--host-mem-share must be > 0 and <= 1")
return share
def parse_host_cpu_share(argv):
share = DEFAULT_HOST_CPU_SHARE
for arg in argv[1:]:
if arg.startswith("--host-cpu-share="):
_, value = arg.split("=", 1)
try:
share = float(value)
except ValueError as exc:
raise SystemExit(f"Invalid --host-cpu-share value: {value}") from exc
if share <= 0 or share > 1:
raise SystemExit("--host-cpu-share must be > 0 and <= 1")
return share
def parse_cpu_fraction(argv):
fraction = DEFAULT_CPU_FRACTION
for arg in argv[1:]:
if arg.startswith("--cpu-fraction="):
_, value = arg.split("=", 1)
try:
fraction = float(value)
except ValueError as exc:
raise SystemExit(f"Invalid --cpu-fraction value: {value}") from exc
if fraction <= 0 or fraction > 1:
raise SystemExit("--cpu-fraction must be > 0 and <= 1")
return fraction
def parse_job_cpu_fraction(argv):
fraction = DEFAULT_JOB_CPU_FRACTION
for arg in argv[1:]:
if arg.startswith("--job-cpu-fraction="):
_, value = arg.split("=", 1)
try:
fraction = float(value)
except ValueError as exc:
raise SystemExit(f"Invalid --job-cpu-fraction value: {value}") from exc
if fraction <= 0 or fraction > 1:
raise SystemExit("--job-cpu-fraction must be > 0 and <= 1")
return fraction
def ssh(host, cmd):
proc = subprocess.run(
[
@@ -176,7 +221,7 @@ def remove_service_key(compose_text, service_names, key):
return "\n".join(lines) + "\n"
def upsert_service_deploy_memory(compose_text, service_names, value):
def upsert_service_deploy_limits(compose_text, service_names, memory_value, cpu_value):
lines = compose_text.splitlines()
i = 0
@@ -193,24 +238,41 @@ def upsert_service_deploy_memory(compose_text, service_names, value):
memory_indent = " " * (service_indent + 8)
memory_idx = None
cpus_idx = None
for idx in range(block_start, block_end):
if lines[idx].strip().startswith("memory:"):
memory_idx = idx
break
if lines[idx].strip().startswith("cpus:"):
cpus_idx = idx
if memory_idx is not None:
lines[memory_idx] = f"{memory_indent}memory: {value}"
if cpus_idx is not None:
lines[cpus_idx] = f"{memory_indent}cpus: '{cpu_value}'"
if memory_idx is not None and cpus_idx is not None:
i = block_end
continue
insert_lines = [
f"{deploy_indent}deploy:",
f"{resources_indent}resources:",
f"{limits_indent}limits:",
f"{memory_indent}memory: {value}",
]
lines = lines[:block_start] + insert_lines + lines[block_start:]
i = block_end + len(insert_lines)
if memory_idx is None and cpus_idx is None:
insert_lines = [
f"{deploy_indent}deploy:",
f"{resources_indent}resources:",
f"{limits_indent}limits:",
f"{memory_indent}memory: {memory_value}",
f"{memory_indent}cpus: '{cpu_value}'",
]
lines = lines[:block_start] + insert_lines + lines[block_start:]
i = block_end + len(insert_lines)
continue
missing_lines = []
if memory_idx is None:
missing_lines.append(f"{memory_indent}memory: {memory_value}")
if cpus_idx is None:
missing_lines.append(f"{memory_indent}cpus: '{cpu_value}'")
lines = lines[:block_start] + missing_lines + lines[block_start:]
i = block_end + len(missing_lines)
return "\n".join(lines) + "\n"
@@ -308,7 +370,7 @@ def find_service_memory_value(compose_text, service_names):
return None
def build_compose(compose_text, runner_mem_limit, job_mem_limit):
def build_compose(compose_text, runner_mem_limit, runner_cpu_limit, job_mem_limit, job_cpu_limit):
updated = compose_text
volume_line = " - ./daemon.json:/etc/docker/daemon.json:ro"
@@ -323,7 +385,7 @@ def build_compose(compose_text, runner_mem_limit, job_mem_limit):
updated = upsert_compose_env_var(
updated,
"GITEA_RUNNER_JOB_CONTAINER_OPTIONS",
f"--memory={job_mem_limit} --memory-swap={job_mem_limit}",
f"--memory={job_mem_limit} --memory-swap={job_mem_limit} --cpus={job_cpu_limit}",
force_pull_line,
)
elif add_cfg_env not in updated:
@@ -331,7 +393,7 @@ def build_compose(compose_text, runner_mem_limit, job_mem_limit):
runner_services = {"act_runner_1", "act_runner_2", "runner1", "runner2"}
updated = remove_service_key(updated, runner_services, "mem_limit")
updated = upsert_service_deploy_memory(updated, runner_services, runner_mem_limit)
updated = upsert_service_deploy_limits(updated, runner_services, runner_mem_limit, runner_cpu_limit)
return updated
@@ -343,6 +405,20 @@ def host_total_mem_mib(host):
return int(int(mem_kib.strip()) / 1024.0)
def host_total_cpus(host):
rc, cpu_count = ssh(host, "nproc")
if rc != 0:
raise RuntimeError(f"Could not read CPU count on {host}")
value = cpu_count.strip()
try:
cpus = float(value)
except ValueError as exc:
raise RuntimeError(f"Invalid CPU count on {host}: {value}") from exc
if cpus <= 0:
raise RuntimeError(f"Non-positive CPU count on {host}: {value}")
return cpus
def host_mem_limit(host, host_mem_share, fraction):
total_mib = host_total_mem_mib(host)
host_share_mib = int(total_mib * host_mem_share)
@@ -356,13 +432,32 @@ def host_mem_limit(host, host_mem_share, fraction):
)
def host_cpu_limit(host, host_cpu_share, fraction):
total_cpus = host_total_cpus(host)
host_share_cpus = total_cpus * host_cpu_share
target_cpus = host_share_cpus * fraction
if target_cpus <= 0.01:
target_cpus = 0.01
return (
f"{target_cpus:.2f}",
target_cpus,
f"host-total({total_cpus:.0f}cpu) * host-share({host_cpu_share:.2f}) -> {host_share_cpus:.2f}cpu * cpu-fraction({fraction:.2f})",
)
mem_fraction = parse_mem_fraction(sys.argv)
job_mem_fraction = parse_job_mem_fraction(sys.argv)
host_mem_share = parse_host_mem_share(sys.argv)
host_cpu_share = parse_host_cpu_share(sys.argv)
cpu_fraction = parse_cpu_fraction(sys.argv)
job_cpu_fraction = parse_job_cpu_fraction(sys.argv)
print(f"Host mem share: {host_mem_share:.2f}")
print(f"Runner mem fraction: {mem_fraction:.2f}")
print(f"Job mem fraction: {job_mem_fraction:.2f}")
print(f"Host cpu share: {host_cpu_share:.2f}")
print(f"Runner cpu fraction: {cpu_fraction:.2f}")
print(f"Job cpu fraction: {job_cpu_fraction:.2f}")
print("Fetching canonical runner config template")
rc, template = ssh(TEMPLATE_HOST, f"cat {TEMPLATE_PATH}")
if rc != 0 or not template.strip():
@@ -390,12 +485,19 @@ with tempfile.TemporaryDirectory() as tmpdir:
host_mem_share,
mem_fraction,
)
runner_cpu_limit, runner_cpu_value, cpu_source = host_cpu_limit(
host,
host_cpu_share,
cpu_fraction,
)
except RuntimeError as exc:
print(str(exc))
continue
print(f"runner_mem_baseline={baseline_source}")
print(f"runner_mem_limit={runner_mem_limit} ({runner_mem_mib} MiB)")
print(f"runner_cpu_baseline={cpu_source}")
print(f"runner_cpu_limit={runner_cpu_limit} cpu")
job_mem_mib = int(runner_mem_mib * job_mem_fraction)
if job_mem_mib < MIN_JOB_MEM_MIB:
@@ -405,7 +507,21 @@ with tempfile.TemporaryDirectory() as tmpdir:
job_mem_limit = f"{job_mem_mib}m"
print(f"job_mem_limit={job_mem_limit} ({job_mem_mib} MiB)")
new_compose = build_compose(compose, runner_mem_limit, job_mem_limit)
job_cpu_value = runner_cpu_value * job_cpu_fraction
if job_cpu_value <= 0.01:
job_cpu_value = 0.01
if job_cpu_value >= runner_cpu_value and runner_cpu_value > 0.02:
job_cpu_value = runner_cpu_value - 0.01
job_cpu_limit = f"{job_cpu_value:.2f}"
print(f"job_cpu_limit={job_cpu_limit} cpu")
new_compose = build_compose(
compose,
runner_mem_limit,
runner_cpu_limit,
job_mem_limit,
job_cpu_limit,
)
rc, _ = ssh(host, f"mkdir -p {root}/backups")
rc, _ = ssh(host, f"cp -f {root}/compose.yml {root}/backups/compose.yml.{now}")
@@ -435,7 +551,7 @@ with tempfile.TemporaryDirectory() as tmpdir:
print(out.strip())
rc, out = ssh(host, f"grep -n 'GITEA_RUNNER_JOB_CONTAINER_OPTIONS=' {root}/compose.yml")
print(out.strip())
rc, out = ssh(host, f"grep -n 'memory:' {root}/compose.yml")
rc, out = ssh(host, f"grep -nE 'memory:|cpus:' {root}/compose.yml")
print(out.strip())
rc, out = ssh(host, f"docker compose -f {root}/compose.yml up -d act_runner_1")