Skip to content

perf(docker): add sccache fallback to the builder stage - #81

Open
kkroo wants to merge 2 commits into
mainfrom
chore/sccache-builder-stage
Open

perf(docker): add sccache fallback to the builder stage#81
kkroo wants to merge 2 commits into
mainfrom
chore/sccache-builder-stage

Conversation

@kkroo

@kkroo kkroo commented Sep 2, 2026

Copy link
Copy Markdown

What

Wires sccache into the three cargo steps in the builder stage
(Dockerfile:7): the dependency build, the real-source build, and the
profiling-variant build (which contains both cargo rustc and cargo build).
All three were previously unwired — this Dockerfile and FFmpeg/Dockerfile
were the only hot-path Rust images in the pim-multicast-gateway build graph with
no sccache at all.

Why — and what this is not

This is tail-risk insurance, not a steady-state speedup. The expected win on a
warm daemon is zero.

All three cargo steps already have BuildKit cache mounts (--mount=type=cache
for /usr/local/cargo/registry and /build/target), and those keep the common
case fast. The gap is that those mounts are daemon-local, and the parent
repo (Blockcast/pim-multicast-gateway) builds this image against two
persistent BuildKit daemons holding separate RWO caches. From its
.github/workflows/docker-build.yml:

Pin each service to a stable remote BuildKit daemon so its cargo/npm/pnpm
cache mounts stay warm (the two daemons have separate RWO caches; a random
pick cold-missed ~50% of builds).

When that pin lands on the peer daemon, or the mount is evicted, the whole
workspace recompiles with no shared fallback. sccache is keyed on the
compilation itself and stores objects in the cluster's ceph RGW, so it covers
exactly those builds — roughly 5–15 slot-minutes recovered when it fires, and
nothing otherwise.

How

Mirrors the reference implementation at
packages/dual-stack-relay/Dockerfile:17-33 in the parent repo. No new
mechanism is introduced.

  • Existing BuildKit cache mounts are kept. sccache layers under them as a
    fallback; it does not replace them.
  • Pinned toolchain is untouched: rust:1.96-bookworm is unchanged.
  • Credentials come only from the existing mechanism — BuildKit
    --mount=type=secret for ceph_s3_key / ceph_s3_secret. Nothing is
    hardcoded, and secrets never enter a layer or a cache key.

Why the scripts are vendored here. The parent builds this image with
context: moq-rs (its build-images matrix entry for moq-pub-mmtp), so the
parent's .github/scripts/ sits outside this build context and cannot be
COPY'd. Vendoring under .github/scripts/ is the same pattern hang-mmt-fec
and libmmt already use for their setup-rust-sccache copies. Each vendored
file carries a provenance header naming the source of truth plus a one-line
drift check; apart from that header both are byte-identical to the parent's,
verified by running the documented command:

diff <(sed '2,21d' .github/scripts/sccache-env.sh) \
     <pim-multicast-gateway>/.github/scripts/sccache-env.sh   # 0 bytes differ

Why the per-stage verdict file matters here specifically. Unlike FFmpeg's
builder — where each crate has its own target/ mount — all three cargo steps
here share the builder stage and its single /build/target mount.
RUSTC_WRAPPER participates in cargo's fingerprint, so a mid-stage probe flap
that enabled sccache for the dependency build and then disabled it for the
source build would not merely lose the cache, it would discard the dependency
layer and recompile it — strictly worse than never enabling sccache.
sccache-env.sh's verdict file makes the first decision in the stage bind the
rest of it, which is exactly the case it was written for.

No workflow change is required.
.github/scripts/docker-buildx-with-session-retry.sh:83-99 in the parent repo
already appends --build-arg SCCACHE_RGW_ENDPOINT and both --secret flags to
every buildx invocation in the build-images job.

Verification

Positive control (the instrument works before trusting a negative):
grep -c -i sccache on the parent's packages/dual-stack-relay/Dockerfile
15, while the same grep on this Dockerfile returned 0. The absence was
real, not a tooling artifact.

Coverage audit — every RUN block containing a cargo invocation now sources
sccache-env.sh and carries both secret mounts (3/3 covered; the profiling
block's cargo rustc and cargo build are both inside the covered RUN).

Lintdocker buildx build --check: clean apart from one pre-existing
FromAsCasing warning at FROM debian:bookworm-slim as moq-pub, confirmed
present on origin/main and deliberately left alone rather than fixed as a
drive-by.

No # syntax= directive here — this Dockerfile uses the built-in frontend.
Verified RUN --mount=type=secret works there with a real build (this file
already uses --mount=type=cache under the same frontend, so it is parity, not
a new requirement).

Fail-open, verified by real builds on rust:1.96-bookworm with the exact
preamble and RUN shape from this diff. All exit 0 and compile:

case log line result
no endpoint, no secrets — this repo's own pr.yml / heap-profile-image.yml sccache disabled (no SCCACHE_RGW_ENDPOINT build-arg) compiles, exit 0
endpoint set, no credentials sccache disabled (RGW credentials not mounted) compiles, exit 0
endpoint + credentials, RGW unreachable sccache disabled (RGW endpoint probe failed: ...) compiles, exit 0
sccache binary absent falls through, build continues exit 0

That first row is the important one for this repo: pr.yml's
docker build --file Dockerfile ... . and heap-profile-image.yml pass none of
these build-args or secrets, so their behaviour is byte-for-byte unchanged.

Expected one-time cost

The RUN command strings changed, so their layer cache keys change. The first
build after this merges rebuilds those layers once. Secret mounts themselves are
excluded from cache keys, so there is no recurring cost.

Required follow-up

The parent repo's moq-rs submodule pointer must be bumped to include this
commit before it takes effect. Deliberately not done here — that is a
separate PR in Blockcast/pim-multicast-gateway once this merges.

Companion

Blockcast/FFmpeg cloudflare#149 — same change for FFmpeg/Dockerfile, the other unwired
hot-path Rust image.

🤖 Generated with Claude Code

The three cargo steps in the builder stage already use BuildKit
`--mount=type=cache` for the cargo registry and /build/target, so on a
warm daemon this changes nothing. Those mounts are daemon-local,
though, and the parent repo (Blockcast/pim-multicast-gateway) builds
this image against two persistent BuildKit daemons holding separate RWO
caches — a random pick cold-missed ~50% of builds, which is why its
docker-build.yml pins a daemon per service. When that pin lands on the
peer daemon, or the mount is evicted, the whole workspace recompiles
with no shared fallback.

sccache is keyed on the compilation itself and stores objects in the
cluster's ceph RGW, so it covers exactly those builds. This is
tail-risk insurance, not a steady-state speedup: 0 on a warm daemon.

Mirrors packages/dual-stack-relay/Dockerfile:17-33 in the parent repo.
Existing cache mounts are kept — sccache layers under them, it does not
replace them. The pinned rust:1.96 base is unchanged.

The two helper scripts are vendored under .github/scripts/ because the
parent builds this image with `context: moq-rs`, which puts its own
copies outside this build context. Same pattern hang-mmt-fec and libmmt
already use for their setup-rust-sccache copies. Each vendored file
carries a provenance header and a one-line drift check; apart from that
header both are byte-identical to the parent's.

All three cargo steps share the builder stage and its single
/build/target mount, so sccache-env.sh's per-stage verdict file is
load-bearing here: it stops a mid-stage probe flap from changing
RUSTC_WRAPPER between steps and discarding the dependency layer.

No workflow change is required — the parent's
docker-buildx-with-session-retry.sh:83-99 already appends
SCCACHE_RGW_ENDPOINT and the two ceph_s3_* BuildKit secrets to every
buildx invocation. This repo's own `docker build` in pr.yml and
heap-profile-image.yml passes none of them, so it fails open and behaves
exactly as before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: a10285a

Critical Issues (0)

Important Issues (0)

Suggestions (0)

Strengths

  • The cache setup fails open for local and uncredentialed builds while keeping BuildKit secrets out of image layers.
  • The per-stage verdict avoids changing RUSTC_WRAPPER across cargo steps sharing /build/target.
  • The vendored scripts match the parent repository's source-of-truth implementation after the documented provenance header.

Recommended Action

  1. No blocking changes requested.

…tes to errors

setup-rust-toolchain pins nothing, so the pr workflow now runs clippy 1.98
with -D warnings and fails on two lints main never saw:

- needless_late_init in SubgroupsWriter::append: bind (group_id,
  subgroup_id) from the branch expression instead of declaring them first.
- result_unit_err on Queue::push_and_wait_until_popped: return a typed
  QueueClosed error (thiserror, already a dependency) instead of Err(()).
  The one caller only .ok()s the result, so behavior is unchanged.

Verified locally with rustc 1.98.0: cargo clippy --no-deps under
RUSTFLAGS=-D warnings is clean and cargo fmt --check passes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: df6de2a

Scope note: the Dockerfile and the two vendored sccache-*.sh scripts are byte-identical to the tree already reviewed clean at a10285ad; the only new material at this head is the clippy commit touching moq-transport. Both prior-head sections were zero, so there are no prior findings to disposition.

Critical Issues (0)

Important Issues (0)

Suggestions (2)

  • [types/errors] moq-transport/src/watch/queue.rs:86QueueClosed is a unit struct, so the "closed before push" arm at line 93 drops the caller's item with no way to recover it. Sibling Queue::push (line 20) deliberately hands it back as Err(item). Since you are designing this error type now, it may be worth QueueClosed<T>(pub T) on that arm — though it costs the Copy/Eq derives and the current sole caller does not need it, so declining is entirely reasonable.
  • [build/pre-existing] Dockerfile:7 — this commit exists because CI compiles on floating stable (1.98) while the shipped image pins rust:1.96-bookworm, so the release artifact and the lint gate use different compilers and this class of break recurs on every stable bump. Out of scope here, but a rust-toolchain.toml (or a toolchain: input on setup-rust-toolchain) would make it a deliberate bump rather than a surprise.

Strengths

  • Verified independently rather than taken on faith: push_and_wait_until_popped has exactly one caller (session/publisher.rs:900) and it .ok()s the result, so the signature change is genuinely behavior-preserving as the commit message claims.
  • The result_unit_err fix is complete, not partial. The two remaining Result<(), ()> signatures (session/publisher.rs:888, session/subscriber.rs:178) are pub(super) and private respectively, so the lint does not reach them — no follow-up breakage queued behind this.
  • subgroup.rs:117 binds both ids from one branch expression with all three fields u64, so the 0 literal infers correctly and the two arms are value-for-value identical to the old assignments.
  • thiserror was already a direct dependency of moq-transport, so cargo machete stays clean and no new supply-chain surface is added to clear a lint.
  • Commit message states the exact toolchain used for local verification and names both lints, which is what made the above checks cheap to reproduce.

Recommended Action

  1. No blocking changes requested.
  2. build, reuse, and heap-profile-image were still queued at review time — this approval attests to the diff as read, so merge on green.
  3. Consider the two suggestions opportunistically; neither gates this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant