diff --git a/scripts/gitea-actions/deploy-runner-config.xsh b/scripts/gitea-actions/deploy-runner-config.xsh index 39bd2e1..95fc691 100755 --- a/scripts/gitea-actions/deploy-runner-config.xsh +++ b/scripts/gitea-actions/deploy-runner-config.xsh @@ -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")