Skip to content

test: clone a per-binary schema template per fixture and shard the largest packages - #736

Open
mariusvniekerk wants to merge 2 commits into
t3code/fix-main-ci-failures-1from
t3code/test-fixture-templates
Open

test: clone a per-binary schema template per fixture and shard the largest packages#736
mariusvniekerk wants to merge 2 commits into
t3code/fix-main-ci-failures-1from
t3code/test-fixture-templates

Conversation

@mariusvniekerk

@mariusvniekerk mariusvniekerk commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

What changed

  • Test fixtures no longer replay InitSchema() per test. Each test binary builds the schema once and clones it: a file copy on SQLite, CREATE DATABASE … TEMPLATE on PostgreSQL (internal/testutil/sqlite_template.go, internal/testutil/pg_template.go). The PostgreSQL warm pool and its /proc-based sweep are gone; template ownership is a session advisory lock the server releases when the binary exits, so it works on macOS too. A role without CREATEDB falls back to the per-schema fixture (MSGVAULT_TEST_PG_TEMPLATE=0 forces it).
  • Each clone gets its own archive UID; InitSchema() mints one per database and tests that hand one archive's cursor to another rely on fixtures being distinct archives.
  • cmd/msgvault/cmd, internal/store and internal/api run as 4-way shards in their own CI jobs (test-sharded, test-postgres-sharded, each PostgreSQL shard job with its own service container) via scripts/test-package-shards.sh, the Linux twin of the Windows lane's script. The test and test-postgres lanes run everything else (make test-unsharded, make test-pg-shipped-unsharded); make test and make test-pg-shipped are unchanged.

Why

The three largest packages hold 1,300–2,000 tests each, every test built a fresh database from ~330 DDL statements, and a test binary runs its tests one at a time — so internal/store alone took 39 minutes on the managed runner and set the wall clock of the whole lane.

Measured locally: a SQLite fixture goes from ~140 ms to ~12 ms and a PostgreSQL fixture from ~1.5 s to ~60 ms; internal/store on SQLite drops from 241 s to 120 s whole, and the three packages together from 470 s whole to 173 s in shards. On PostgreSQL, internal/api's 970 tests run in 69 s across four shards.

Usage

make test                 # unchanged: every package
make test-shards          # the three large packages, 4 shards each (TEST_SHARDS=n)
make test-unsharded       # everything else (what CI's test lane runs)

Stacked on #735. The new jobs and lane flags were exercised pre-merge with gh workflow run ci.yml --ref <branch> (PR runs use ci.yml@main): https://github.com/kenn-io/msgvault/actions/runs/33529929253 — 21/21 jobs green in 26 minutes on GitHub-hosted runners.

generated by a clanker

🤖 Generated with Claude Code

mariusvniekerk and others added 2 commits September 1, 2026 11:55
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>
@mariusvniekerk
mariusvniekerk requested a review from wesm as a code owner September 1, 2026 15:57
@roborev-ci

roborev-ci Bot commented Sep 1, 2026

Copy link
Copy Markdown

roborev: Combined Review (e484c2f)

No issues found.


Reviewers: 2 done | Synthesis: codex | Total: 10m16s

@mariusvniekerk mariusvniekerk self-assigned this Sep 2, 2026
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.

1 participant