Skip to content

Commit c60fc49

Browse files
authored
perf: incremental metadata regen (stop syncing the full pool) (#10)
The publish re-downloaded the entire pool/channel from R2 on every run so apt-ftparchive / createrepo_c could rescan all historical packages. The pool is ~233 x ~80 MB (~18 GB) and grows every snapshot, so sync time ballooned past 15 min and every run was one transient R2 failure away from dying. Make the regen incremental instead: - apt: pull only the channel's Packages index (~KBs), append the new .deb's stanza with apt-ftparchive (single file), and rebuild Release - which only needs the index files, not the pool. - yum: pull only the channel's repodata, then merge it with a throwaway single-package repo via `mergerepo_c --all --omit-baseurl` (createrepo_c --update alone drops packages that aren't on disk, so it can't be used incrementally). Both approaches were validated in containers against the live repo's exact arch/index layout (Architecture:all packages land in both binary-amd64 and binary-arm64; mergerepo_c preserves every historical version and keeps location hrefs relative). Uploads keep the retry from the previous commit. Sync drops from ~15 min / ~18 GB to ~seconds / ~KBs. Signed-off-by: Peter Amiri <peter@alurium.com>
1 parent fb3f49e commit c60fc49

2 files changed

Lines changed: 63 additions & 70 deletions

File tree

‎.github/workflows/wheels-released.yml‎

Lines changed: 24 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -130,54 +130,40 @@ jobs:
130130
echo "$FINGERPRINT:6:" | gpg --batch --yes --import-ownertrust
131131
echo "GPG_KEY_ID=$KEY_ID" >> "$GITHUB_ENV"
132132
133-
- name: Sync existing pool from R2
133+
- name: Pull existing Packages index from R2
134134
env:
135135
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
136136
CHANNEL: ${{ steps.inputs.outputs.channel }}
137137
run: |
138138
set -euo pipefail
139-
# Retry transient R2 failures (timeouts / 5xx / connection resets).
140-
# The pool is unbounded and re-downloaded in full on every publish, so
141-
# the sync is a long sequence of single-shot `wrangler r2 object get`
142-
# calls; one transient failure used to kill the whole run. 3 attempts
143-
# with backoff absorbs that, and failures are logged instead of being
144-
# discarded to /dev/null so a persistent error stays diagnosable.
145-
r2get() {
139+
# Incremental regen: download the channel's current Packages index
140+
# (~KBs) instead of the whole pool (~18 GB). regenerate-apt-metadata.sh
141+
# appends the new package's stanza and rebuilds Release, so the
142+
# historical .deb files never need to be pulled again.
143+
pull_index() {
146144
local key="$1" attempt out
147145
for attempt in 1 2 3; do
148146
if out="$(wrangler r2 object get "${R2_BUCKET}/${key}" --file="$key" --remote 2>&1)"; then
147+
echo " pulled ${key} ($(wc -c < "$key") bytes)"
149148
return 0
150149
fi
151-
echo "::warning::r2 object get failed for ${key} (attempt ${attempt}/3): ${out}" >&2
150+
# Missing index → first publish on this channel: start empty.
151+
if echo "$out" | grep -qiE "does not exist|NoSuchKey|NoSuchObject"; then
152+
: > "$key"
153+
echo " ${key} absent — starting from an empty index"
154+
return 0
155+
fi
156+
echo "::warning::index pull failed for ${key} (attempt ${attempt}/3): ${out}" >&2
152157
sleep $((attempt * 5))
153158
done
154-
echo "::error::r2 object get failed permanently for ${key}" >&2
159+
echo "::error::index pull failed permanently for ${key}" >&2
155160
return 1
156161
}
157-
# List all R2 objects under pool/<channel>/ and download each.
158-
# apt-ftparchive needs the actual .deb files locally so it can compute
159-
# size + sha256 + read package metadata for the Packages index.
160-
mkdir -p pool/${CHANNEL}
161-
PREFIX="pool/${CHANNEL}/"
162-
CURSOR=""
163-
while :; do
164-
URL="https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/r2/buckets/${R2_BUCKET}/objects?prefix=${PREFIX}&per_page=1000"
165-
[ -n "$CURSOR" ] && URL="${URL}&cursor=${CURSOR}"
166-
RESP=$(curl -sS "$URL" -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}")
167-
echo "$RESP" | jq -r '.result[].key' | while IFS= read -r key; do
168-
[ -z "$key" ] && continue
169-
mkdir -p "$(dirname "$key")"
170-
echo " pulling $key"
171-
r2get "$key"
172-
done
173-
CURSOR=$(echo "$RESP" | jq -r '.result_info.cursor // empty')
174-
[ -z "$CURSOR" ] && break
162+
for arch in amd64 arm64; do
163+
key="dists/${CHANNEL}/main/binary-${arch}/Packages"
164+
mkdir -p "$(dirname "$key")"
165+
pull_index "$key"
175166
done
176-
echo "Local pool/${CHANNEL}/ contents after sync:"
177-
# Diagnostic only. `head` closes the pipe after 20 lines; find then
178-
# gets SIGPIPE and `pipefail` would fail the step after a successful
179-
# R2 pull (run 369: last pull was …/wheels-be_4.0.6-snapshot.2273_all.deb).
180-
find pool/${CHANNEL}/ -type f | head -20 || true
181167
182168
- name: Download new .deb from upstream Release
183169
env:
@@ -220,10 +206,11 @@ jobs:
220206
env:
221207
GPG_PASSPHRASE: ${{ secrets.WHEELS_REPO_GPG_PASSPHRASE }}
222208
GPG_KEY_ID: ${{ env.GPG_KEY_ID }}
223-
# Scope regen to the dispatched channel only. The sync step above pulls
224-
# just pool/<channel>/ from R2, so regenerating the OTHER channel would
225-
# scan an empty local pool and clobber its R2 index on upload
226-
# (#3218 / #2838). The other channel's dists are left untouched.
209+
# Scope regen to the dispatched channel only. The workflow pulls only
210+
# that channel's Packages index and slots only its new .deb, so
211+
# regenerating the OTHER channel would append against a missing index
212+
# and clobber its R2 dists on upload (#3218 / #2838). The other
213+
# channel's dists are left untouched.
227214
CHANNELS: ${{ steps.inputs.outputs.channel }}
228215
run: |
229216
set -euo pipefail

‎scripts/regenerate-apt-metadata.sh‎

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,30 @@
11
#!/bin/bash
2-
# Regenerates apt metadata for both `stable` and `bleeding-edge` distributions
3-
# under dists/, then signs Release with GPG (detached → Release.gpg, inline →
2+
# Incrementally regenerates apt metadata for one dispatched distribution under
3+
# dists/, then signs Release with GPG (detached → Release.gpg, inline →
44
# InRelease). Both signed forms are required: older apt clients read Release +
55
# Release.gpg, newer clients prefer InRelease.
66
#
7+
# This is the incremental replacement for the old full-pool scan. The old flow
8+
# ran `apt-ftparchive packages pool/<dist>` over EVERY historical .deb, which
9+
# forced the workflow to re-download the whole pool (~18 GB, hundreds of files,
10+
# ~15 min and growing) on every publish. Instead:
11+
#
12+
# * the workflow slots the single new .deb into pool/<dist>/… and downloads
13+
# the channel's current Packages index into dists/<dist>/main/binary-*/,
14+
# * this script appends the new package's stanza (apt-ftparchive over just
15+
# that one .deb) to each architecture index, gzips, and regenerates Release
16+
# via `apt-ftparchive release` — which only needs the index files, not the
17+
# pool.
18+
#
19+
# A package of Architecture `all` is appended to BOTH binary-amd64 and
20+
# binary-arm64 (apt-ftparchive includes `all` in every arch index, verified
21+
# against the live repo); an arch-specific package lands only in its own index.
22+
#
723
# Inputs (env vars):
824
# GPG_PASSPHRASE — passphrase for the imported signing key
925
# GPG_KEY_ID — long-form key ID (set by the workflow after `gpg --import`)
10-
#
11-
# Idempotent: safe to run by hand against an existing tree to repair a torn
12-
# release. Re-reads everything in pool/ and rewrites dists/ from scratch.
26+
# CHANNELS — space-separated distributions (workflow passes the single
27+
# dispatched channel; default "stable bleeding-edge")
1328

1429
set -euo pipefail
1530

@@ -20,56 +35,47 @@ fi
2035

2136
ARCHITECTURES="amd64 arm64"
2237
COMPONENTS="main"
23-
# Per-channel regen (CHANNELS env), default = both for manual full rebuilds.
24-
#
25-
# CRITICAL (#3218, recurrence of #2838): the publish workflow only syncs
26-
# pool/<dispatched-channel>/ from R2, so any channel NOT being published has an
27-
# empty local pool here. Regenerating it would emit an empty Packages, and the
28-
# upload step's `find dists` would then clobber that channel's good R2 index.
29-
# A bleeding-edge snapshot publish was wiping the stable index minutes after
30-
# every stable release. Scoping to the dispatched channel keeps the other
31-
# channel's R2 dists untouched. The workflow passes CHANNELS=<channel>; a bare
32-
# manual run still rebuilds both (only safe when both pools are present locally).
3338
DISTRIBUTIONS="${CHANNELS:-stable bleeding-edge}"
3439

35-
# apt-ftparchive uses a config file to know where the pool lives. The same
36-
# config drives both distributions — only the dist-name and the scan path
37-
# change between invocations.
3840
APT_CONF_TEMPLATE="templates/aptftparchive.conf"
39-
4041
if [ ! -f "$APT_CONF_TEMPLATE" ]; then
4142
echo "::error::Missing $APT_CONF_TEMPLATE — template is expected to ship in the bucket repo."
4243
exit 1
4344
fi
4445

4546
for DIST in $DISTRIBUTIONS; do
46-
echo "── Regenerating dists/${DIST}/ ──"
47+
echo "── Incrementally updating dists/${DIST}/ ──"
4748
DIST_DIR="dists/${DIST}"
4849
mkdir -p "$DIST_DIR"
49-
# First publish for a brand-new channel: the pool dir may not exist yet.
50-
# apt-ftparchive aborts on a missing scan path, so create an empty pool
51-
# for now — it'll be backfilled by the first publish dispatch on that channel.
5250
mkdir -p "pool/${DIST}"
5351

52+
# Only the just-slotted .deb is present locally (the pool is not synced).
53+
NEW_DEB=$(find "pool/${DIST}" -type f -name '*.deb' | head -1 || true)
54+
if [ -z "$NEW_DEB" ]; then
55+
echo "::warning::No .deb under pool/${DIST} — nothing to append; skipping."
56+
continue
57+
fi
58+
echo " new package: ${NEW_DEB}"
59+
5460
for COMPONENT in $COMPONENTS; do
5561
for ARCH in $ARCHITECTURES; do
5662
BIN_DIR="${DIST_DIR}/${COMPONENT}/binary-${ARCH}"
5763
mkdir -p "$BIN_DIR"
5864

59-
# apt-ftparchive packages <override> <pool-path> emits Packages on stdout.
60-
# We don't use an override file (no priority overrides for now).
61-
apt-ftparchive \
62-
--arch "$ARCH" \
63-
packages "pool/${DIST}" \
64-
> "${BIN_DIR}/Packages"
65+
# Existing index was downloaded by the workflow; first publish starts empty.
66+
[ -f "${BIN_DIR}/Packages" ] || : > "${BIN_DIR}/Packages"
67+
68+
# Emit the new package's stanza and append. `--arch` matches the old full
69+
# scan: an `all` package is emitted for both arches, `amd64` only for
70+
# binary-amd64 (arm64 yields an empty append, which is a no-op).
71+
apt-ftparchive --arch "$ARCH" packages "$NEW_DEB" >> "${BIN_DIR}/Packages"
6572

6673
gzip -9 --keep --force "${BIN_DIR}/Packages"
6774
done
6875
done
6976

70-
# apt-ftparchive release emits the Release file metadata. The config template
71-
# provides Origin/Label/Codename/Description etc.; we override -o APT::FTPArchive::Release::Codename
72-
# per distribution so a single conf can drive both.
77+
# apt-ftparchive release emits the Release metadata from the index files in
78+
# dists/<dist>/ — it does not read pool/, so it works without the pool present.
7379
apt-ftparchive \
7480
-c "$APT_CONF_TEMPLATE" \
7581
-o "APT::FTPArchive::Release::Codename=${DIST}" \
@@ -96,7 +102,7 @@ for DIST in $DISTRIBUTIONS; do
96102
--output "${DIST_DIR}/InRelease" \
97103
"${DIST_DIR}/Release"
98104

99-
echo " ✓ Release + Release.gpg + InRelease written for ${DIST}"
105+
echo " ✓ appended ${NEW_DEB}, Release + Release.gpg + InRelease written for ${DIST}"
100106
done
101107

102108
echo "Done."

0 commit comments

Comments
 (0)