Files
Johnathan Corgan 076c6cf60d rpm-install: run-scoped image tags, bounded dnf, kept dnf metadata
rpm-install: give each scenario image the run's own tag.

Each scenario image bakes the package under test into it, but its tag was
fixed (fips-rpm-test:<label>, :no-resolver, :upgrade) while the container
names already carried the run's suffix. Two local CI runs on one Docker
daemon, such as master and next on the internal builder, therefore rebuilt
the same tags in turn, and one run booted containers holding the other
run's package: dnf then failed to find the RPM it was told to install, or
worse, a run could pass on a package that was not its own.

The three tags now end in ${FIPS_CI_NAME_SUFFIX:-}, and every scenario
removes its image when it ends, on the failure paths too. Under local CI
the image carries the CI label and the run's label, as the containers do,
so the end-of-run reap removes any image a killed run left behind and a
sweep by the CI label finds it too. check-image-scoping.sh now also fails
on a fips-rpm-test tag that does not end in the run suffix, and refuses to
pass if it finds no such tag at all. Its list holds tag prefixes, so
another install harness's tags can be added to it.

rpm: bound each dnf attempt that sets up a package build or test image.

Installing the packaging tools in a fresh AlmaLinux container downloads the
whole repository metadata, about 80 MB, from whichever mirror the mirror
list hands out. On GitHub that silent step has taken from seconds to nine
and a half minutes in jobs that still passed, and once held the RPM
install job until its 25-minute limit, followed by the runtime image's dnf,
which stalled the same way. Nothing bounded either step: they were retried
on failure only, and printed nothing that said where the time went.

packaging/rpm/dnf-bounded.sh runs one dnf command with a wall-clock bound
on each attempt and with dnf told to abandon a mirror slower than 50 kB/s
for 20 s, and reports each attempt's time, its transfer rates and the
mirrors it fetched from. It handles dnf 4 and dnf5. It is used for the
packaging tools in build-rpm-container.sh (three attempts of 720 s each),
for the runtime image's packages in rpm-install (420 s per attempt, retried
by the image build's own retry), and for the cpio install in
make_next_package. The bounds come from the slow jobs that passed: 720 s is
above the 9.5 minutes the packaging step took there and leaves room in the
job for a second attempt after a stall, and 420 s is above the image
step's 5 minutes. The image's report is printed when its scenario boots,
since the build's own output is shown only when the build fails.

rpm-install: keep the runtime image's dnf metadata for the test-time calls.

The runtime image ran dnf clean all after installing its packages, so the
test-time install, erase and upgrade each downloaded the whole repository
metadata from a live mirror again. A mirror part-way through a sync then
failed the install check with nothing wrong in the package, as on GitHub on
2026-10-16 (404s for metadata files that repomd.xml already named), and
on the internal builder the refetch pushed install times to 485 s against
the 600 s bound.

The image now keeps its metadata (dnf makecache, bounded like the install
before it), and the three test-time calls run with
--setopt=*.metadata_expire=-1, so they resolve against that metadata however
old it is and go to a mirror only to download a package the image does not
hold. The option names every repository because each repository file in the
base images sets its own expiry (a day in AlmaLinux, six hours for Fedora
updates), and a repository's own value wins over a global
--setopt=metadata_expire. With the global form, an image layer the internal
builder's BuildKit cache kept for longer than that would fetch the metadata
again. The install still resolves the package's requirements against the
repositories' metadata, so it still proves they resolve; the repositories
are not disabled.
2026-10-22 07:20:26 +00:00

112 lines
4.2 KiB
Bash
Executable File

#!/bin/bash
# Run a dnf command inside an RPM-family container with a wall-clock bound on
# each attempt, and report how long each attempt took and which mirrors it
# fetched from.
#
# Usage: dnf-bounded.sh --bound SECONDS [--attempts N] [--record FILE] -- DNF-ARGS...
#
# --bound seconds one attempt may run before it is killed (required)
# --attempts attempts before giving up, default 1; 5 s, then 10 s, ... between
# --record also append the per-attempt report to FILE
#
# Exit status: 0 once an attempt succeeds, otherwise the last attempt's status
# (124 when the bound ended it).
#
# Why: a fresh container downloads the whole repository metadata (about 80 MB
# for AlmaLinux 9) from whichever mirror the mirror list hands it. On GitHub
# that silent download has taken from seconds to nearly ten minutes, and once
# ran until the job's own limit with nothing to say which mirror or step held
# it. dnf's own limits do not help there: its default minrate trips only below
# 1 kB/s. So each attempt carries a wall-clock bound, a mirror slower than
# 50 kB/s for 20 s is abandoned for the next one, and the report names the
# mirrors and the transfer rates, so a slow job's log says where the time went.
#
# Runs inside the container, so it needs only bash, coreutils and dnf. It
# handles both dnf 4 (EL9, which takes -v) and dnf5 (Fedora, which does not and
# logs to a different file).
set -uo pipefail
bound=""
attempts=1
record=""
while [ $# -gt 0 ]; do
case "$1" in
--bound) bound="${2:?missing value for --bound}"; shift 2 ;;
--attempts) attempts="${2:?missing value for --attempts}"; shift 2 ;;
--record) record="${2:?missing value for --record}"; shift 2 ;;
--) shift; break ;;
*) echo "dnf-bounded: unknown option $1" >&2; exit 2 ;;
esac
done
if ! [[ "$bound" =~ ^[1-9][0-9]*$ && "$attempts" =~ ^[1-9][0-9]*$ ]] || [ $# -eq 0 ]; then
echo "dnf-bounded: usage: dnf-bounded.sh --bound SECONDS [--attempts N] [--record FILE] -- DNF-ARGS..." >&2
exit 2
fi
# dnf5 rejects -v; its progress lines and librepo's URLs come without it. The
# version is read whole before it is matched: `dnf --version | grep -q` lets
# grep exit at the first line, dnf then dies of SIGPIPE, and under pipefail the
# test fails on dnf5 some of the time.
verbose=(-v)
librepo_log=/var/log/dnf.librepo.log
version=$(dnf --version 2>/dev/null)
if [[ "$version" == dnf5* ]]; then
verbose=()
librepo_log=/var/log/dnf5.log
fi
out=/var/tmp/dnf-bounded.out
# Print to stderr, and to the record file when one was asked for.
report() {
echo "$*" >&2
if [ -n "$record" ]; then
echo "$*" >>"$record"
fi
return 0
}
attempt=1
while true; do
seen=0
[ -f "$librepo_log" ] && seen=$(wc -l <"$librepo_log")
start=$SECONDS
rc=0
timeout -k 10 "$bound" dnf "${verbose[@]}" \
--setopt=timeout=20 --setopt=minrate=50k "$@" >"$out" 2>&1 || rc=$?
took=$((SECONDS - start))
urls=""
[ -f "$librepo_log" ] && urls=$(tail -n +"$((seen + 1))" "$librepo_log" \
| grep -oE 'Downloading: [a-z]+://[^ ]+' | sed 's/^Downloading: //')
mirrors=$(grep -oE '^[a-z]+://[^/]+' <<<"$urls" | sort | uniq -c \
| awk '{ printf "%s%s (%s)", sep, $2, $1; sep = ", " }')
outcome="exit $rc"
[ "$rc" -eq 124 ] || [ "$rc" -eq 137 ] && outcome="killed at the ${bound}s bound"
report "dnf-bounded: $(date -u +%FT%TZ) attempt $attempt of $attempts, dnf $1: $outcome after ${took}s; mirrors: ${mirrors:-none}"
# The transfer lines, rate and size, from both dnf versions: one per
# repository's metadata and the packages' total, not one per package.
while IFS= read -r line; do
report "dnf-bounded: $line"
done < <(grep -E '[0-9.]+ +[kKMG]i?B/s +\|' "$out" \
| awk '!/^[[:space:]]*[([] *[0-9]+\/[0-9]+[])]/ || / Total /' \
| sed 's/[[:space:]]\+$//')
if [ "$rc" -eq 0 ]; then
rm -f "$out"
exit 0
fi
if [ -n "$urls" ]; then
report "dnf-bounded: last fetch started: $(tail -n 1 <<<"$urls")"
fi
echo "dnf-bounded: --- last lines of dnf's output ---" >&2
tail -n 20 "$out" >&2
if [ "$attempt" -ge "$attempts" ]; then
rm -f "$out"
exit "$rc"
fi
sleep $((attempt * 5))
attempt=$((attempt + 1))
done