mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
* fix(ci): fail Java lanes before disk exhaustion Co-authored-by: Qwen-Coder <[email protected]> * fix(ci): gate the MySQL pool lane instead of a hosted job The disk floor gate landed in hosted-harness-mysql, which runs on ubuntu-latest, so `runner.environment == 'self-hosted'` is never true there and the step was dead code. The job that actually shares the ECS pool disk - mysql-integration, two Maven suites plus a per-leg MySQL container - stayed ungated. Move the step to mysql-integration, immediately after its checkout so the sdk-java-workflow pin still holds. This also clears the three Hosted real-process gates pins, which reject a conditioned step among the setup steps that precede managed_dependencies in hosted-harness-mysql, and reflow the new test to the repo's prettier style. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmvya43o17i * style(ci): drop the blank line left behind in hosted-harness-mysql Moving the disk floor gate out of hosted-harness-mysql left a stray blank line between its checkout and 'Set up Java'. Restoring it makes the whole workflow diff additive on mysql-integration only, so 'hosted-only jobs are unchanged' holds byte for byte and an A/B census of that job reads clean. Co-authored-by: Qwen-Coder <[email protected]> * ci(sdk-java): record the real workflow size after the disk-floor gates The six #16003 disk-floor gate steps grow sdk-java.yml from 154074 to 155650 bytes. main had already drifted 3742 bytes past the recorded 150332 — inside the 4096 allowance, but it left only 354 bytes of ratchet headroom, so this real growth tripped Lint & Static. Record the file's actual size rather than shrinking the gates: each one is a three-line step calling .github/scripts/check-disk-floor.sh, and the workflow test pins all six gated jobs. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmw02ewzoa2 * fix(ci): derive the disk-floor corpus from runs-on and narrow the broker claim Address the two blocking review findings on the Java disk-floor gate: R1-1: the meta-test skipped any job without the gate and compared a closed six-name list, so a newly pool-routed lane that forgot the gate stayed green, and a neutered gate (continue-on-error) kept every assertion true. Derive the corpus from each job's runs-on (same hazard as the setup-java corpus), require gateIndex > -1 per pool-capable lane, and assert the gate carries no continue-on-error. The derived set is the same six jobs today. Verified by mutation: adding continue-on-error to a gate reddens it, and renaming a gate (ungated pool job) reddens it with 'expected -1 to be greater than -1'. R1-2: the step comment claimed RUNNER_TEMP covers the broker store, but the absolute free-KiB floor cannot express the broker's used-ratio refusal (a 952 GiB volume at 94% used still has ample free bytes). Narrow the comment to the gate's real scope (near-full filesystem / inode exhaustion, #10035) and state the broker disk-threshold investigation stays out of scope, matching the PR description. Bump .size-baseline for the comment change (155650 -> 155888). sdk-java workflow meta-test 117 passed / 5 skipped; eslint exit 0, prettier clean; check-workflow-size.sh passes. * fix(ci): calibrate the Java disk floors and fail closed in the meta-test Review round on this PR found the gate could not fire in the two shapes it exists for, and that the meta-test could go green through the same drift it was added to prevent. R1-2: every lane ran the shared 2 GiB / 100k-inode default. The #16003 incident was a used-ratio refusal — the broker reported 0.94 while the 952 GiB volume still had tens of GiB free — so no absolute floor can express that threshold, and 2 GiB is below what these lanes write: a host with 4 GiB free passed the gate and then died mid-suite. Give the five heavy lanes the repo's build-sized 8 GiB floor, leave flyway-migrations and the shared value alone, and add an opt-in DISK_FLOOR_MAX_USED_PCT arm to check-disk-floor.sh for the two broker lanes (90%). R2-1/R2-2: the corpus came from `runs-on` text and skipped jobs outside it, so a lane routed through an expression or a job output shipped ungated while the suite stayed green, and nothing asserted the hosted-only jobs carry no gate. Fail closed: an explicit hosted-only allowlist, a gate required for every other job, a total-count assertion, and a negative assertion on those two jobs. Pin the per-lane floors so dropping one is a test failure. Verified: sdk-java-workflow suite 117 passed, 5 skipped; it goes red when a gate is deleted, when one is added to a hosted job, and when a floor is removed. check-disk-floor suite 12 passed, including four new ratio cases. --------- Co-authored-by: Qwen-Coder <[email protected]> Co-authored-by: yiliang114 <[email protected]> Co-authored-by: qwen-code patrol <[email protected]>
129 lines
5.6 KiB
Bash
Executable File
129 lines
5.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Pre-job disk floor gate for self-hosted Linux runners (#10035).
|
|
#
|
|
# ENOSPC failed `npm ci` mid-install on a saturated host while the workspace
|
|
# cleanup timer skipped busy runners, so the job died mid-suite and the host
|
|
# looked healthy by the time anyone ran `df`. Failing BEFORE the heavy step
|
|
# turns that into a clear, retryable gate failure instead of a corrupted
|
|
# half-run; a re-run lands on a host with headroom.
|
|
# Checkout populates this script first, so the gate cannot protect checkout.
|
|
#
|
|
# Usage: check-disk-floor.sh [DIR ...]
|
|
# With no directories, checks GITHUB_WORKSPACE and RUNNER_TEMP.
|
|
#
|
|
# The OS temp directory (TMPDIR, defaulting to /tmp) is ALWAYS checked in
|
|
# addition to the caller's directories (deduplicated): on 2026-10-31 the hz
|
|
# fleet's /tmp tmpfs hit 100% inodes while the workspace and RUNNER_TEMP sat
|
|
# on a data disk with millions free, so every gate was green and the jobs
|
|
# died on mkdtemp ENOSPC mid-suite. os.tmpdir() writes (node, surefire,
|
|
# tsx, git subprocess temp files) do not follow RUNNER_TEMP.
|
|
#
|
|
# Floors, checked per directory's filesystem (env-overridable):
|
|
# DISK_FLOOR_MIN_FREE_KB default 2097152 (2 GiB)
|
|
# DISK_FLOOR_MIN_FREE_INODES default 100000
|
|
# DISK_FLOOR_MAX_USED_PCT unset (disabled); 1-100 percent ceiling
|
|
#
|
|
# The percent ceiling exists for the RocketMQ broker lanes: the broker refuses
|
|
# writes on its own used-ratio rule (CL/CQ/INDEX 0.94 in the #16003 incident)
|
|
# while the volume still reports tens of GiB free, so absolute free bytes
|
|
# cannot express that threshold. It is opt-in and per job; the shared default
|
|
# stays absolute so one lane's calibration cannot move another lane's gate.
|
|
#
|
|
# Every check emits a DISKFLOOR sample tagged with runner and run ids so a
|
|
# breached gate can be correlated with the GitHub job that hit it.
|
|
set -u
|
|
|
|
MIN_FREE_KB="${DISK_FLOOR_MIN_FREE_KB:-2097152}"
|
|
MIN_FREE_INODES="${DISK_FLOOR_MIN_FREE_INODES:-100000}"
|
|
MAX_USED_PCT="${DISK_FLOOR_MAX_USED_PCT:-}"
|
|
|
|
validate_floor_override() {
|
|
local name="$1"
|
|
local value="$2"
|
|
|
|
if ! [[ "$value" =~ ^[0-9]{1,18}$ ]]; then
|
|
echo "::error::${name} must be a non-negative integer of at most 18 digits"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
validate_floor_override DISK_FLOOR_MIN_FREE_KB "$MIN_FREE_KB"
|
|
validate_floor_override DISK_FLOOR_MIN_FREE_INODES "$MIN_FREE_INODES"
|
|
|
|
validate_percent_override() {
|
|
local name="$1"
|
|
local value="$2"
|
|
|
|
if [ -z "$value" ]; then
|
|
return 0
|
|
fi
|
|
if ! [[ "$value" =~ ^[0-9]{1,3}$ ]] || [ "$value" -lt 1 ] || [ "$value" -gt 100 ]; then
|
|
echo "::error::${name} must be a percentage between 1 and 100"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
validate_percent_override DISK_FLOOR_MAX_USED_PCT "$MAX_USED_PCT"
|
|
|
|
TAG="runner[${RUNNER_NAME:-unknown}] run[${GITHUB_RUN_ID:-local}/${GITHUB_RUN_ATTEMPT:-1}] job[${GITHUB_JOB:-check-disk-floor}]"
|
|
|
|
if [ "$#" -eq 0 ]; then
|
|
set -- "${GITHUB_WORKSPACE:-.}" "${RUNNER_TEMP:-/tmp}"
|
|
fi
|
|
|
|
# Always include the OS temp directory: it is a distinct filesystem from the
|
|
# caller's directories on this fleet, and its exhaustion is exactly the
|
|
# failure class this gate exists to catch before the heavy step.
|
|
os_tmp="${TMPDIR:-/tmp}"
|
|
for existing in "$@"; do
|
|
if [ "$existing" = "$os_tmp" ]; then
|
|
os_tmp=''
|
|
break
|
|
fi
|
|
done
|
|
if [ -n "$os_tmp" ]; then
|
|
set -- "$@" "$os_tmp"
|
|
fi
|
|
|
|
status=0
|
|
for dir in "$@"; do
|
|
if [ ! -d "$dir" ]; then
|
|
echo "::warning::disk floor check skipped for missing directory: ${dir}"
|
|
continue
|
|
fi
|
|
# df -kP is POSIX-portable: one line per filesystem, sizes in KiB, mount
|
|
# point last. With -i the same layout reports inodes instead of blocks.
|
|
space_line="$(df -kP "$dir" 2>/dev/null | tail -n 1 || true)"
|
|
inode_line="$(df -kPi "$dir" 2>/dev/null | tail -n 1 || true)"
|
|
avail_kb="$(awk '{print $4}' <<< "$space_line")"
|
|
used_pct="$(awk '{print $5}' <<< "$space_line" | tr -d '%')"
|
|
mount="$(awk '{for (i = 6; i <= NF; i++) printf "%s%s", $i, (i < NF ? " " : ""); print ""}' <<< "$space_line")"
|
|
free_inodes="$(awk '{print $4}' <<< "$inode_line")"
|
|
|
|
case "$avail_kb" in '' | *[!0-9]*) avail_kb='' ;; esac
|
|
case "$free_inodes" in '' | *[!0-9]*) free_inodes='' ;; esac
|
|
case "$used_pct" in '' | *[!0-9]*) used_pct='' ;; esac
|
|
|
|
echo "DISKFLOOR $(date -u +%Y-%m-%dT%H:%M:%SZ) ${TAG} dir[${dir}] fs[${mount}] avail_kb[${avail_kb:-unknown}] floor_kb[${MIN_FREE_KB}] used_pct[${used_pct:-unknown}] max_used_pct[${MAX_USED_PCT:-unset}] free_inodes[${free_inodes:-unknown}] floor_inodes[${MIN_FREE_INODES}]"
|
|
|
|
if [ -z "$avail_kb" ] && [ -z "$free_inodes" ]; then
|
|
echo "::warning::could not read disk usage for ${dir}; letting the job proceed"
|
|
continue
|
|
fi
|
|
|
|
if [ -n "$avail_kb" ] && [ "$avail_kb" -lt "$MIN_FREE_KB" ]; then
|
|
echo "::error::Disk floor breached on ${RUNNER_NAME:-unknown}: ${dir} has ${avail_kb} KiB free, below the ${MIN_FREE_KB} KiB floor. Failing before the heavy step instead of dying on ENOSPC mid-run — re-run the job to land on a host with headroom."
|
|
status=1
|
|
fi
|
|
if [ -n "$MAX_USED_PCT" ] && [ -n "$used_pct" ] && [ "$used_pct" -ge "$MAX_USED_PCT" ]; then
|
|
echo "::error::Disk floor breached on ${RUNNER_NAME:-unknown}: ${dir} is ${used_pct}% used, at or above the ${MAX_USED_PCT}% ceiling. The RocketMQ broker refuses writes on its own used-ratio rule while free bytes still look healthy (#16003) — re-run the job to land on a host with headroom."
|
|
status=1
|
|
fi
|
|
if [ -n "$free_inodes" ] && [ "$free_inodes" -lt "$MIN_FREE_INODES" ]; then
|
|
echo "::error::Disk floor breached on ${RUNNER_NAME:-unknown}: ${dir} has ${free_inodes} free inodes, below the ${MIN_FREE_INODES} floor. Failing before the heavy step instead of dying on ENOSPC mid-run — re-run the job to land on a host with headroom."
|
|
status=1
|
|
fi
|
|
done
|
|
|
|
exit "$status"
|