Skip to content

Commit 5036d22

Browse files
committed
ci(ocapn-guile-interop): cache the Guix runtime store across runs
Iteration III of the guix-CI resilience pattern. PR #82 (iteration I) established the two-substitute-server pattern and the installer-tarball cache; PR #255 (iteration II) reordered the substitute URLs and widened the polling and timeout windows after a Bordeaux outage exposed the "first server unreachable" slow path. The 2026-05-14 outage exposed a third failure mode that neither prior iteration addressed: both substitute servers degraded simultaneously, with the result that the daemon could not resolve the runtime closure (guile + fibers + websocket + gnutls + gcrypt) from either upstream. Reorder and wider timeouts did not help because no amount of waiting brings up a server that is down. The existing cache step amortizes only the installer tarball, not the runtime store the daemon resolves at runtime. Each run pays the full substitute-fetch cost end-to-end, and a both-servers-degraded day means that cost cannot be paid at all. This change adds a second `actions/cache` step that caches the daemon's runtime store (`/gnu/store`) together with the daemon database (`/var/guix/db`). Both paths are root-owned with strict permissions, so the cache targets a runner-owned staging directory containing a zstd tarball of the two store paths; paired `sudo tar` shell steps wrap the actual extract and create, matching the install step's existing `sudo tar --extract` pattern. On a cache hit the daemon's next `guix build` finds the resolved closure already on disk and the daemon DB already records it as valid, so the substitute round-trip is short-circuited entirely. A degraded-substitute-server day no longer blocks the workflow's runtime path. The cache key includes the pinned Guix version (a version bump may change the on-disk DB schema) and a hash of the workflow file (the package set and daemon configuration both live in the workflow, so any change to either forces a fresh snapshot). A `restore-keys` prefix lets a workflow edit that does not actually invalidate the store (a comment tweak, a timeout bump) still seed from the prior snapshot and re-save at job end. The daemon is stopped across the restore extract so the on-disk store and the daemon's in-memory view of it cannot diverge mid-flight; a divergence would surface later as missing-store-item errors from `guix build`. The snapshot step at the end does not need to stop the daemon because tar captures the SQLite DB as a point-in-time copy at the filesystem layer. The change is additive to iteration II's reorder + widen; both mitigations stay in place and remain load-bearing for the one-server-degraded case where the cache key changes (a workflow edit that flushes the snapshot leaves the daemon dependent on substitute fetches for the next run).
1 parent ba26f4c commit 5036d22

1 file changed

Lines changed: 98 additions & 0 deletions

File tree

.github/workflows/ocapn-guile-interop.yml

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,41 @@ jobs:
9898
# Cache key includes the version and sha256 so a version
9999
# bump or upstream re-publish forces a fresh download.
100100
key: guix-binary-x86_64-linux-${{ env.GUIX_VERSION }}-${{ env.GUIX_TARBALL_SHA256 }}
101+
- name: Restore Guix store cache
102+
# Iteration III of the guix-CI resilience pattern (#82 was I,
103+
# #255 was II). The tarball cache above amortizes the
104+
# installer download; this second cache amortizes the
105+
# *runtime store* the daemon resolves on every run. Without
106+
# it, both substitute servers being degraded means each
107+
# `guix build` re-fetches the full guile + fibers + websocket
108+
# + gnutls + gcrypt closure end-to-end. With it, the daemon
109+
# finds the resolved store paths already present and short-
110+
# circuits the substitute round-trip, so a degraded-server
111+
# day no longer blocks the workflow's runtime path.
112+
#
113+
# `/gnu/store` and `/var/guix/db` are both root-owned with
114+
# strict permissions, so `actions/cache` (which runs as
115+
# `runner`) cannot read or write them directly. We cache a
116+
# runner-owned staging directory containing a zstd tarball
117+
# of the two store paths; the *Restore* and *Snapshot* shell
118+
# steps below wrap the actual `sudo tar` extract and create.
119+
# The same pattern matches the install step's existing
120+
# `sudo tar --extract` of the installer tarball.
121+
id: guix-store-cache
122+
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
123+
with:
124+
path: ~/guix-store-cache
125+
# The key includes the pinned Guix version (a version bump
126+
# may change the on-disk DB schema) and a hash of the
127+
# workflow file (the package set and daemon configuration
128+
# both live here, so any change to either forces a fresh
129+
# snapshot). `restore-keys` lets a workflow edit that does
130+
# not actually invalidate the store (a comment tweak, a
131+
# timeout bump) still seed from the prior snapshot and
132+
# re-save at job end.
133+
key: guix-store-${{ env.GUIX_VERSION }}-${{ hashFiles('.github/workflows/ocapn-guile-interop.yml') }}
134+
restore-keys: |
135+
guix-store-${{ env.GUIX_VERSION }}-
101136
- name: Download Guix stable tarball
102137
# `ftp.gnu.org/gnu/guix/` is the GNU project's canonical
103138
# mirror for release binaries. It is operationally independent
@@ -191,6 +226,38 @@ jobs:
191226
sudo systemctl enable --now gnu-store.mount guix-daemon.service
192227
193228
echo "$GUIX_PATH/bin" >> "$GITHUB_PATH"
229+
- name: Restore Guix store from cache snapshot
230+
# Paired with the `Restore Guix store cache` step above. When
231+
# a prior run's snapshot is present, extract it on top of the
232+
# just-installed store so the daemon's next `guix build`
233+
# finds the resolved closure already on disk and skips the
234+
# substitute-server round-trip entirely. `--no-overwrite-dir`
235+
# preserves the installer-laid `/gnu` and `/var` directory
236+
# entries themselves. Files inside those directories overlap
237+
# in two ways: store paths that the installer ships are
238+
# byte-identical to the same store paths in the snapshot
239+
# (Guix store contents are content-addressed by hash), so
240+
# the default overwrite is safe; the daemon database under
241+
# `/var/guix/db` in the snapshot is a strict superset of the
242+
# installer's blank DB (it records every store path the
243+
# daemon resolved on the prior run), so the overwrite is
244+
# exactly what makes the cache effective.
245+
#
246+
# The daemon is stopped across the extract so the on-disk
247+
# store and the daemon's in-memory view of it cannot diverge
248+
# mid-flight. A divergence would surface later as missing-
249+
# store-item errors from `guix build`.
250+
if: steps.guix-store-cache.outputs.cache-matched-key != ''
251+
run: |
252+
set -euo pipefail
253+
if [ ! -f ~/guix-store-cache/store.tar.zst ]; then
254+
echo "cache key matched but no snapshot present at ~/guix-store-cache/store.tar.zst"
255+
exit 0
256+
fi
257+
sudo systemctl stop guix-daemon.service
258+
sudo tar --extract --file ~/guix-store-cache/store.tar.zst \
259+
--directory / --no-overwrite-dir
260+
sudo systemctl start guix-daemon.service
194261
- name: Authorize build farm
195262
# Authorizes both ci.guix.gnu.org and bordeaux.guix.gnu.org;
196263
# both .pub files ship inside the Guix tarball under
@@ -268,6 +335,37 @@ jobs:
268335
} > "$scratch"
269336
cat "$scratch" >> "$GITHUB_ENV"
270337
338+
- name: Snapshot Guix store for cache
339+
# Paired with the `Restore Guix store cache` step at the top
340+
# of the job. Snapshots `/gnu/store` and `/var/guix/db`
341+
# together so a future run restores a coherent view: the
342+
# store paths exist on disk *and* the daemon's DB records
343+
# them as valid. Caching `/gnu/store` alone would leave the
344+
# daemon re-resolving substitutes anyway because it would
345+
# not trust the unrecorded paths.
346+
#
347+
# Skipped on an exact-key cache hit; the snapshot we would
348+
# write is byte-equivalent to the one we restored. The post-
349+
# job save in `actions/cache` then picks up the directory
350+
# contents (the tarball we just wrote) and uploads it under
351+
# the current key.
352+
if: steps.guix-store-cache.outputs.cache-hit != 'true'
353+
run: |
354+
set -euo pipefail
355+
mkdir -p ~/guix-store-cache
356+
# zstd compression keeps the snapshot well inside the 10 GB
357+
# per-repo cache budget; the test's runtime closure is on
358+
# the order of a few hundred megabytes uncompressed. The
359+
# daemon does not need to be stopped for a snapshot: tar
360+
# reads at the filesystem layer and the DB is a SQLite
361+
# file that tar captures as a point-in-time copy.
362+
sudo tar --create --file ~/guix-store-cache/store.tar.zst --zstd \
363+
/gnu/store /var/guix/db
364+
# `actions/cache` runs as the `runner` user and cannot
365+
# upload a root-owned file. Hand the tarball to the runner
366+
# so the post-job save sees a readable path.
367+
sudo chown -R "$(id -u):$(id -g)" ~/guix-store-cache
368+
271369
- name: Start Guile goblin-chat host
272370
working-directory: endo
273371
env:

0 commit comments

Comments
 (0)