Skip to content

ci: cut Linux test lanes from ~50 to ~11 minutes - #735

Merged
wesm merged 8 commits into
mainfrom
t3code/fix-main-ci-failures-1
Sep 2, 2026
Merged

ci: cut Linux test lanes from ~50 to ~11 minutes#735
wesm merged 8 commits into
mainfrom
t3code/fix-main-ci-failures-1

Conversation

@mariusvniekerk

@mariusvniekerk mariusvniekerk commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

The Linux test lanes now finish in about 11 minutes. On main they take 22 to 55 minutes for SQLite and 40 to 46 minutes for PostgreSQL, even on hosted runners, because the three largest packages (cmd/msgvault/cmd, internal/store, internal/api) each built a fresh database from about 330 DDL statements for every one of their 1,300 to 2,000 tests, and a test binary runs its tests one at a time. internal/store alone set the wall clock of the whole lane.

Three changes stack:

  • Each test binary builds the schema once and clones it per test. SQLite copies a file. PostgreSQL clones a template database with CREATE DATABASE ... TEMPLATE, which replaces the warm pool and its /proc-based cleanup. Every fixture is still a private, unused database produced by the same InitSchema() path, and each clone gets its own archive UID. A role without CREATEDB falls back to the per-schema fixture; MSGVAULT_TEST_PG_TEMPLATE=0 forces that path.
  • The Linux test lanes run on hosted Ubuntu. The managed Linux runners are about ten times slower for this suite: main's test job has hit Go's one-hour test timeout in three of its last six runs there, and this branch's unsharded packages alone took 33 minutes on one. The test and test-sharded jobs now use hosted Ubuntu with Go caching, as ci: keep PostgreSQL lanes on hosted runners #737 already did for the PostgreSQL lanes. The other Linux jobs stay on the managed runners.
  • The three largest packages run as four shards each, in their own CI jobs. scripts/test-package-shards.sh compiles the package once, lists its tests, and runs them as four processes of the same binary, the way the Windows lane already does. Each PostgreSQL shard job gets its own service container. The test and test-postgres lanes cover every other package.

A PostgreSQL template is owned through a session advisory lock that the server releases when the binary exits, so the next binary reclaims what a crashed one left behind. Advisory locks are local to one database, so template names carry a hash of the configured database and a sweep only judges templates in its own scope.

The PostgreSQL targets also cap concurrent test binaries at four (PG_TEST_PARALLEL). Every PostgreSQL-backed binary opens its own connections, and a wide runner starting them all at once exceeded a stock server's connection and lock limits. The CI service containers get a larger lock table.

Pull-request checks run ci.yml from main, so the new shard jobs only appear in a dispatch run of this branch: https://github.com/kenn-io/msgvault/actions/runs/33679438735.

Job times on hosted runners, first main run after #737 versus that dispatch run:

Job main this branch
test-postgres 46m 8m, plus 7m for the slowest shard job
test (SQLite) 55m 11m, plus 3m for the slowest shard job

Main's SQLite figure is from the managed runner, which this PR stops using for that job.

make test             # unchanged coverage: every package, the three large ones as shards
make test-pg-shipped  # the same against PostgreSQL
make test-shards      # only the three large packages (TEST_SHARDS=n)
make test-unsharded   # every other package, what CI's test lane runs

generated by a clanker

@mariusvniekerk
mariusvniekerk requested a review from wesm as a code owner September 1, 2026 14:46
@roborev-ci

roborev-ci Bot commented Sep 1, 2026

Copy link
Copy Markdown

roborev: Combined Review (69aef08)

No issues found.


Reviewers: 2 done | Synthesis: codex | Total: 1m7s

@wesm

wesm commented Sep 2, 2026

Copy link
Copy Markdown
Member

looking

@wesm
wesm force-pushed the t3code/fix-main-ci-failures-1 branch from 69aef08 to 3e097ae Compare September 2, 2026 10:32
@roborev-ci

roborev-ci Bot commented Sep 2, 2026

Copy link
Copy Markdown

roborev: Combined Review (3e097ae)

Summary: Changes are limited to CI orchestration and test-only database/sharding helpers. No concrete security vulnerability or material weakening of a security boundary was found.

No issues found.


Reviewers: 2 done | Synthesis: codex | Total: 13m29s

@roborev-ci

roborev-ci Bot commented Sep 2, 2026

Copy link
Copy Markdown

roborev: Combined Review (0956d26)

Verdict: Two medium-severity issues require attention, both related to macOS compatibility and PostgreSQL fallback test assumptions.

Medium

  • macOS Bash compatibilityscripts/test-package-shards.sh:33; .github/workflows/ci.yml:150
    The macOS lane runs a script using mapfile, which is unavailable in stock macOS Bash 3.2. The workflow does not install or invoke a newer Bash.
    Fix: Replace mapfile with a Bash 3-compatible read loop or explicitly install and invoke modern Bash.

  • PostgreSQL template fallback testsinternal/testutil/pg_template_test.go:91,113,185
    These tests assume template cloning always succeeds, but the implementation falls back to per-schema fixtures when CREATEDB or template setup is unavailable. In fallback mode, the database-removal and no-DDL assertions test the wrong behavior, while the sweep test requires template creation.
    Fix: Skip template-specific tests when templateFor(dbURL).ensure() fails and add separate coverage for fallback behavior.


Reviewers: 2 done | Synthesis: codex, 8s | Total: 12m23s

mariusvniekerk and others added 5 commits September 2, 2026 07:37
Every push to main since #717 failed in test-postgres and test-pgvector.
Small packages died in under a second with "sorry, too many clients
already", the pgvector schema migration hit "out of shared memory", and the
heavy packages then ran into the 60-minute timeout.

Each PostgreSQL-backed test binary opens its own connections: two for the
admin handle, four for a warm-pool schema refill, plus the store under test.
`go test ./...` starts up to one binary per CPU, and nothing budgets
connections across binaries. On the managed runner the twenty-four packages
that use PostgreSQL started together and exceeded the service container's
100-connection limit in the first seconds. GitHub-hosted runners have four
CPUs, so the same lanes passed there with -p effectively at 4.

The Makefile's PostgreSQL targets and the inline pgvector lane now pass
-p 4 (PG_TEST_PARALLEL), so the connection footprint no longer depends on
the host's CPU count. The pgvector service also gets the
max_locks_per_transaction=256 setting the test-postgres service already
had; its migration ran out of exactly that.

Slower per-package times on the managed runner are a separate matter: the
cap reduces contention on the one container but does not change the
runner's I/O.

Generated with Claude Code (claude-fable-5)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every test fixture used to replay InitSchema() into an empty database: a
few hundred DDL statements, ~140ms on SQLite and ~1.5s on PostgreSQL per
fixture on a laptop, and several times that on a contended CI runner. With
about a thousand fixtures across the suite, that replay was most of the
wall clock of the largest packages — internal/store spent 39 minutes on the
managed runner for tests that do milliseconds of real work each.

The initialized schema is a pure function of the test binary, so each
binary now builds it once and hands every fixture a copy. On SQLite the
template is a file written into the test's temp dir. On PostgreSQL it is a
template database and each fixture is a CREATE DATABASE ... TEMPLATE clone,
a file-level copy the server makes in tens of milliseconds. Locally
internal/store drops from 241s to 120s on SQLite, and a PostgreSQL fixture
from ~750ms to ~200ms.

A template database has to be reclaimed when the binary that built it is
gone. Ownership is a session advisory lock held on a connection pinned for
the binary's life; the server releases it on any exit, so the next binary
reclaims whatever it can lock. That replaces the warm pool's pid-namespace
sweep, which could not run on hosts without /proc and so left macOS on the
slow path. A role without CREATEDB falls back to the per-schema fixture.

One thing a clone must not copy: InitSchema() mints a durable archive UID,
and tests that hand one archive's cursor to another depend on fixtures
being distinct archives. The fixture assigns each clone its own UID.

Generated with Claude Code (claude-fable-5)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cmd/msgvault/cmd, internal/store and internal/api hold 1,300–2,000 tests
each, and a Go test binary runs them one at a time. Whichever of the three
is slowest sets the wall clock of the whole Linux lane no matter how many
cores the runner has — 39 minutes for internal/store on the managed runner
before the template fixtures, and still the critical path after them.

The Windows lane already solves this with scripts/test-package-shards.ps1:
compile the package once, list its tests, deal them into shards, and run
each shard as its own process of the same binary. This adds the Linux twin
and uses it for those three packages in their own matrix jobs, on both the
SQLite lane and the PostgreSQL lane, where each sharded job also gets its
own service container so four shards' connections never compete with the
rest of the suite. The existing lanes run every other package through the
new *-unsharded targets; `make test` and `make test-pg-shipped` still run
everything, so a local run is unchanged.

Locally the three packages take 225s, 120s and 125s whole; in four shards
each they take 173s in total.

Generated with Claude Code (claude-fable-5)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The new PostgreSQL shard matrix must follow the hosted-runner policy from
#737. Running those service-container jobs on the managed fleet would retain
the same slow Docker storage path that caused the original timeouts.

Keep hosted Go caching enabled so each isolated shard job avoids a cold module
and build cache.

Generated with Codex
Co-authored-by: Codex <noreply@openai.com>
Pull request checks use the workflow from main until this change merges. That
workflow calls the full Make targets, so internal/store still ran as one test
binary and hit its 60-minute PostgreSQL timeout even though the new branch
workflow passed the same package in shards.

Compose the full targets from the unsharded remainder and the existing shard
runner. Keep those parts sequential for local PostgreSQL servers so no more
than the configured shard count competes for one database.

Generated with Codex
Co-authored-by: Codex <noreply@openai.com>
@wesm
wesm force-pushed the t3code/fix-main-ci-failures-1 branch from 0956d26 to 8fc9c31 Compare September 2, 2026 12:38
@roborev-ci

roborev-ci Bot commented Sep 2, 2026

Copy link
Copy Markdown

roborev: Combined Review (8fc9c31)

Verdict: Three medium-severity issues remain in test sharding and PostgreSQL fixture cleanup.

Medium

  • scripts/test-package-shards.sh:33-36 — The test-list command runs inside process substitution, discarding its nonzero exit status. Package initialization failures can yield an empty list while the shard exits successfully. Run the command into a temporary file and check its status before loading names.

  • scripts/test-package-shards.sh:33mapfile is unavailable in macOS’s system Bash 3.2, yet this script runs in macOS CI. Use a Bash 3-compatible read loop or require Bash 4+ explicitly.

  • internal/testutil/pg_template.go:348-367 — Ownership locks are scoped to the configured PostgreSQL database, while cleanup sweeps all databases in the cluster. Runs using different databases can interfere and force-drop live templates or clones. Include database identity in names and lock keys, or coordinate through a cluster-wide database.


Reviewers: 2 done | Synthesis: codex, 14s | Total: 9m41s

wesm and others added 2 commits September 2, 2026 14:06
The shard script listed a package's tests through a process substitution,
which throws away the exit status of the test binary. A package whose init
or TestMain fails produced an empty list, and the script reported "No tests
found" and exited 0. CI would have passed a package that never ran.

The same line used mapfile, which the macOS system Bash 3.2 does not have.
The macOS lane runs `make test`, which now reaches this script, and it only
runs on pushes to main, so the first push after merge would have failed
there without a pull-request run to warn about it.

Listing now writes to a file, checks the binary's exit status, and reads
the names back with a plain read loop.

Generated with Claude Code
Co-authored-by: Claude <noreply@anthropic.com>
PostgreSQL advisory locks are local to the database a session is connected
to. The template sweep took its locks in the configured database but listed
candidate templates from the whole server. Two runs on one server that were
configured against different databases could not see each other's locks, so
one run's sweep could force-drop the other's live template and clones.

Template and clone names now carry a hash of the configured database, and
a sweep only judges names in its own scope, where the lock it tries is the
same lock a live owner holds.

The template tests now skip when fixtures are on the per-schema path, such
as a role without CREATEDB, instead of failing on template-only contracts.

Generated with Claude Code
Co-authored-by: Claude <noreply@anthropic.com>
@wesm wesm changed the title ci: cap concurrent test binaries in the PostgreSQL lanes ci: cut Linux test lanes from ~50 to ~11 minutes Sep 2, 2026
@roborev-ci

roborev-ci Bot commented Sep 2, 2026

Copy link
Copy Markdown

roborev: Combined Review (86c2928)

No issues found.


Reviewers: 2 done | Synthesis: codex | Total: 15m42s

The managed Linux runners are about ten times slower than hosted Ubuntu for
the Go test suite. On main the test job has hit go test's one-hour timeout
in cmd/msgvault/cmd and internal/store in three of the last six runs, and on
this branch the unsharded packages alone took 33 minutes there, followed by
20 minutes for the cmd/msgvault/cmd shards, during which a test that allows
five seconds for a command to finish timed out. On hosted Ubuntu the same
shards finish in under three minutes.

The test and test-sharded jobs now run on hosted Ubuntu with Go caching, as
#737 already did for the PostgreSQL lanes. The other Linux jobs stay on the
managed runners.

Generated with Claude Code
Co-authored-by: Claude <noreply@anthropic.com>
@roborev-ci

roborev-ci Bot commented Sep 2, 2026

Copy link
Copy Markdown

roborev: Combined Review (1e755de)

No issues found.

Summary: The change adds template-based test fixtures, PostgreSQL connection limits, and CI test sharding while preserving fallback isolation paths.


Reviewers: 2 done | Synthesis: codex | Total: 12m37s

@wesm
wesm merged commit 684fd70 into main Sep 2, 2026
41 of 42 checks passed
@wesm
wesm deleted the t3code/fix-main-ci-failures-1 branch September 2, 2026 22:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants