ci: serialise Android builds and cap Gradle/npm to fit the shared host #25
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "ci/cap-android-build-resources"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Stops a routine Android build from taking the git server down, and serialises builds so they
supersede instead of stacking.
What happened
The Android build runs on the same 16 GB host as Forgejo, Postgres and tailscaled, configured for
full runner capacity (
1b8896a4e). One build already runs it hot. On 2026-09-01 aworkflow_dispatchon a branch and a push toncw/mainbuilt the same commit concurrently:timeout-minutes: 60I caused the second concurrent build by dispatching one while a push build was queued. But one
build at full capacity on that host is the underlying condition; the collision only made it
visible.
Changes
concurrencygroup, constant rather than keyed ongithub.ref. Per-ref grouping wouldnot have prevented this - the two builds were on different refs. There is one runner, so only
one of these builds may hold it. Tag builds are exempt from cancellation because they publish
versioned releases and must not be superseded by a later branch push.
--no-parallelso the RN module graph does not fanout. 4g is deliberately not lowered further - RN Android builds OOM below roughly that.
--maxsockets 4,--no-audit --no-fund, install heap 4096m -> 3072m.Verification
Dispatched against this branch head, which sits on top of
ncw/mainand therefore also carriesthe
NtfyPushServicespecialUsechange from #18 - so this run doubles as the compile check thatnever completed for that fix.
Worth reviewing specifically:
cancel-in-progressuses an expression(
${{ !startsWith(github.ref, 'refs/tags/') }}). If this Forgejo version does not evaluateexpressions there it could silently resolve falsy and never cancel, which would look identical to
working. The dispatch run is the test - if the workflow fails to parse or never supersedes, this
should become a plain
trueand accept that tag builds can be cancelled.Not addressed here
timeout-minutesdid not reap the orphaned jobs. A wedged build can hold the queueindefinitely. Needs its own look.
entirely rather than rationing around it. That is the durable fix; this PR is the containment.
Correction to this PR description: the dominant cause is storage latency, not memory or CPU
I framed this PR as "two builds do not fit in 16 GB" and sized the caps accordingly. Having
actually measured the host during a build, that attribution was wrong, and I would rather correct
it here than let it become the received explanation.
What the host is: an LXC container, 24 cores, 16 GB RAM with no swap, and the root
filesystem on
/dev/rbd2- a Ceph RBD network block device, currently 80% full. Because thejob runs
runs-on: native, the build shares that device with Forgejo, Postgres and the git reposthemselves.
What it looks like mid-build:
io some avg10=12.46againstcpu some avg10=5.51- I/O stalls areroughly 2.3x CPU stalls.
per read. (
/sys/blockwrite_ms includes queueing, so treat it as an upper bound rather thanpure per-op latency.) Local NVMe would be ~0.1 ms.
Why this profile is pathological here.
npm installfor React Native unpacks tens ofthousands of tiny files, each one a metadata operation crossing the network to the Ceph cluster;
Gradle, Kotlin and the NDK then layer large caches on top. Every write costs milliseconds instead
of microseconds, so workers sit in D state.
And load average counts blocked-on-I/O processes, not just runnable ones. That is the entire
explanation for a 15-minute load average of 228 on a box whose CPUs were mostly idle: a queue
waiting on storage, not a CPU stampede. It is also why the git service itself stopped answering -
same block device.
Aggravating, but secondary: 24 cores invites tools to scale parallelism to core count while 16 GB
cannot feed it, and with no swap a memory spike is fatal rather than slow.
What this PR does and does not fix
The changes here are still worth having, and they are cheap - the verification build ran in
8 min 18 s with workers at 4 and a 4 GB heap, on a host that stayed in single-digit load
throughout. Serialising builds halves device contention, and
--maxsockets 4plus fewer Gradleworkers genuinely reduce concurrent I/O.
But none of it reduces the latency per write. This is containment, not a fix.
What would actually fix it, in order of leverage
GRADLE_USER_HOMEonto local SSD/NVMe scratch, off Ceph.This converts the bulk of build I/O from network round-trips to local writes. Far and away the
biggest win.
node_modulesevery build.--prefer-offlineavoids the network fetch butstill writes every file; a cache keyed on
package-lock.jsonremoves most of the small-filestorm.
first raised it: the contention is not just CPU and RAM, it is the same block device as the git
repositories and the database.
Mitigations, not fixes.
The in-file comments added by this PR still describe the sizing in terms of fitting alongside
Forgejo and Postgres on a 16 GB host. That is true but incomplete; happy to follow up with a small
commit rewording them to point at storage latency instead.