Skip to content

CI: run each fixture database job in three parallel parts - #746

Merged
rmcdaniel merged 23 commits into
mainfrom
rust-325-ci-db-parts
Oct 24, 2026
Merged

rmcdaniel merged 23 commits into
mainfrom
rust-325-ci-db-parts

Conversation

@rmcdaniel

Copy link
Copy Markdown
Member

Review class: R0 (CI efficiency; every check keeps its coverage). Stacked on #742, which already contains #741; this PR's own change is the last commit.

Why

Measured on #729's final run, every fixture database job ran one long serial chain:

Job Bootstrap Corpus comparison Cohorts Total
mysql-execution (mysql-8.0) 15.1 min 32.9 min 18 min 69 min
postgres-storage (postgres-17) 11.4 min 34.4 min 17.5 min 64 min
execution-and-codec 5.3 min (codec) 27.6 min 15 min 49 min

#741 takes about 14 minutes out of the corpus comparison. This PR removes the serial chain itself.

What

Each database job becomes three matrix parts on separate runners:

  • native-tests: the native database suites (runtime::mysql:: / qualification_ plus --test execution), or the SQLite codec and --all-targets suite, and the TLS trust checks.
  • corpus: the shared corpus record and compare, published-SDK patch, legacy-marker and range-repair profiles, the SDK direction lanes, restart recovery, and the published-PHP takeover refusal.
  • cohorts: role tokens, both upgrade sources, corrected markers, external consumers, query authority, worker fencing and pressure.

Every part bootstraps the same database and builds the same native binary; no part depends on another. The corpus and cohorts parts build without running the suites. The corrected-marker cohort used native-b (or SQLite's recovered node) left over from the corpus step. The cohorts part now starts that node itself (step prepare_cohorts), on the same bootstrapped database. Each part retains its own summary artifact. The job timeout drops from 105 to 60 minutes.

Expected: each part takes about 20–35 minutes, so the critical path is about 40 minutes, down from about 85 (about 60 with #741 alone). Runner use rises by the two extra bootstraps per database. There is no queueing today: no runs were queued while 20 were in progress, against the plan's 60 concurrent jobs.

Evidence

  • New tests/Unit/ServerParityPartsTest.mjs, run in PHPUnit Feature beside the aggregate test. For all three jobs it pins:
    • the three parts;
    • every step either preparing the part (checkout, fetches, bootstrap, collect, summary) or belonging to exactly one part;
    • each cohort in cohorts, the corpus comparison in corpus, and TLS/codec in native-tests;
    • the Retire … cleanups as always() && cohorts;
    • the corrected cohort waiting on prepare_cohorts;
    • a summary per part;
    • the native suites gated to native-tests in both database bootstraps.
  • 46/46 pass with ServerParityAggregateTest and MergeGroupTriggersTest. Against the unsplit workflow, 0/4 of the new tests pass.
  • The Docker isolation policy passes; actionlint 1.7.7 passes.
  • This PR's own run is the timing measurement. I'll post the per-part durations here when it finishes.

PHPUnit Feature, Shared Server fixtures, SonarQube and Public Boundary now
also run on merge_group, so a merge queue can replace the require-up-to-date
rerun loop. Base-revision checks use the queue group's base_sha, the boundary
scan covers the group's exact base..head range, and the push trigger skips
gh-readonly-queue/** branches that the merge_group run already scans.
Shared Server fixtures has no path filter for merge_group, so every queued
group gets the full profile.
tests/Unit/MergeGroupTriggersTest.mjs checks that PHPUnit Feature,
Shared Server fixtures, SonarQube and Public Boundary run on merge_group,
that base-revision checks and the boundary range use the queue group's
base and head, that queue branches are not scanned twice, and that
superseded-run cancellation never applies to a queue group. It runs in
Repository contracts. It fails 6 of 7 checks on the train head without
merge_group support.
The OpenAPI evolution and regression corpus contracts pin the exact base
revision expression; include the merge_group base so a queued group diffs
against the revision it was built on.
The Sonar job receives SONAR_TOKEN and its fork guard is always true for a
merge group, so it no longer runs on merge_group; the contract test now
requires that. Public Boundary scans every push again, including queue
branches, and the test requires the unfiltered push trigger alongside the
exact merge-group range.
…y group

#723 removed PHPUnit Feature's concurrency group, because its required
gate runs with always() under the central action policy. The test now
requires that any cancel-in-progress in a workflow the queue waits for
cancels only pull request runs, and that Shared Server fixtures keeps
its pull-request-only cancellation.
In each database job of Shared Server fixtures, the nine published-SDK
direction profiles ran one after another (two pairs already overlapped),
about 22 of the job's 64-72 minutes (train #729, PostgreSQL 17 job
120866907013: 12:19:50 to 12:41:36). Each profile already has its own
containers, PHP and embedded databases, evidence and state directory,
and its own workflow-ID prefix on the shared native server.

sdk-direction-lanes.sh now runs them in three lanes balanced by those
durations, in order within a lane. Every profile runs even after another
fails; the step fails if any did, after printing each profile's log in
its own group. The job's existing cleanup steps still stop and remove
every profile's containers. Expected saving: about 14 minutes per
database job.
heartbeat_renews_only_progress_deadline_and_cannot_resurrect_elapsed_authority
compared two lease_expires_at strings. Native times are whole
milliseconds and serialize without a fraction on a whole second, so a
claim lease of ...:26Z sorted after a renewal of ...:26.045Z and the
assertion failed about one run in a thousand (train #729, PostgreSQL 17
job 120852521096). The test now parses both and reports the values.
A required check and the merge queue both need a result that always
reports. The pull_request path filter meant a docs-only pull request got
no Shared Server fixtures result at all, so the workflow could not be
required.

- Pull requests now run the workflow without a path filter. A short
  `changes` job applies the push trigger's path list to the pull
  request's merge commit (renames count on both sides), so one list
  governs both triggers. Push, merge_group and manual runs run every
  fixture job, as does any pull request that cannot be classified.
- `published-sdk-adapters` runs only when the classifier says so; the
  other fixture jobs need it, so they skip with it.
- A terminal `Shared Server fixtures result` job runs with always(). It
  passes only when every fixture job passed, or when no input changed
  and every fixture job was skipped. A failed, cancelled or unexpectedly
  skipped job, a failed classifier, or an unreadable needs context fails
  it.
- ServerParityAggregateTest covers the workflow structure, the result
  decisions and the classifier against scratch pull request merge
  commits. Repository contracts runs it.

No permissions, secrets, actions or artifact paths change. Making the
result a required check, and enabling the merge queue, stay separate
repository-setting steps.
The Docker workflow isolation policy accepts only literal .sh and .py
repository helpers in workflow steps, so the new jobs' direct node
calls failed Public Boundary. changes.sh and aggregate.sh now run the
Node scripts from the repository root, as the workflow does, and the
classifier tests go through changes.sh.
Every database job waited for published-sdk-adapters (about 15 minutes),
but 10 of those minutes built the optimized native binary only the
codec-deadline-health cells use (train #729: "Build one bounded optimized
native artifact for deadline health", 10.0 minutes). The build, its
upload and its container cleanup now run in a codec-native-build job
beside the adapters; codec-deadline-health needs both, and the database
jobs still need only the adapters, so they start about 10 minutes
sooner. The build script and its bounded resources are unchanged.
…est the lane wrapper

Each lane stops its profile's PHP server as soon as the profile ends, so at
most three run at once, and a failed stop only warns. The profile's status
is kept either way. A shell-level test runs the wrapper with stub profiles
and docker: all nine profiles run, every server is stopped after its profile,
a failed profile does not stop its lane, and the wrapper fails only after
printing every status and log. Also resolves the wrapper's Sonar findings.
… fixture when patterns or names cannot be classified

The file list is read as bytes with --name-only -z and split only on NUL, so
a quoted name can no longer slip past a required prefix; wildcards match
newlines; a name that is not UTF-8 or an unsupported pattern runs every
fixture job instead of failing the classifier.
…and wait for it

codec-native-build is a second root fixture job. It runs only through the
change classifier, and the result needs it like every other fixture job.
Each MySQL, PostgreSQL and SQLite job becomes three matrix parts on separate
runners: native-tests (the native database suites, or the codec suite, and
the TLS checks), corpus (the shared corpus, its profiles and the restart and
takeover cases), and cohorts (role tokens, upgrade sources, corrected
markers, external consumers, query authority, worker fencing and pressure).
Every part bootstraps the same database and native build, and none depends
on another; the cohorts start their own native node for the corrected
markers. Every check still runs exactly once, which a structure test pins.
@rmcdaniel

Copy link
Copy Markdown
Member Author

Independent CI review of exact head 4face31d34eb7438c25ebfd488f1ad9b4f432862.

Source review found no blocking regression in the split itself. All original workflow steps are retained; the four additions are the three independent cohort-node preparations and SQLite's build-only preparation. The native suites/TLS/codec remain in native-tests, comparison/restart/direction lanes remain in corpus, and the original cohorts remain in cohorts. The corrected-marker prerequisite now refers to its own part's successful preparation. Cleanup still runs after failure, artifact names include the matrix part, and the terminal aggregate requires the complete matrix jobs plus the existing native-build/deadline jobs. No scenario deadline, resource ceiling or required result was waived.

Independent qualification: 46/46 ServerParityPartsTest.mjs, ServerParityAggregateTest.mjs and MergeGroupTriggersTest.mjs tests pass on Node 22.23.3, Git 2.39.5, UID/GID1000, read-only source and network disabled. An earlier slim-image attempt lacked Git and is a harness limitation, not a product failure. No native/database fixture or paid runner experiment was started for this review. The real database matrix remains a normal merge gate.

One performance-evidence correction is needed: the earlier “no queue” observation is no longer representative. At 14:33:09 UTC, run40101730551 had 18 queued jobs and six running jobs. Queued split parts were created at 14:22:42 UTC, so some had already waited more than ten minutes. The run remains nonterminal; no measured end-to-end speedup is established yet. GitHub's queued job records also populate started_at, so use actual step/runner execution evidence when separating queue time from execution time.

Please retain the expected40-minute result as an estimate until this exact run finishes, and report queue delay, bootstrap/execution, total runner minutes and end-to-end required-gate duration separately. The split preserves coverage, but additional concurrent bootstraps consume shared runner capacity; current queue delay must be included in judging its throughput benefit. This review changed no repository settings, runs or merge authority.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security/release-path disposition for the proposed stacked landing at exact head 4face31d34eb7438c25ebfd488f1ad9b4f432862:

This permits one combined source landing without discarding any security, required-check, authorship, or queue-activation boundary.

… the native server

The namespace and selected-run signal profiles ran beside the plain signal
profile, and on PostgreSQL 17 one's worker showed in another's routing
diagnostics (server-parity-sdk-signals). The old serial workflow never
overlapped them. A test now reads each profile's queues from its fixtures and
refuses any two profiles that share one in different lanes.
@rmcdaniel
rmcdaniel changed the base branch from rust-325-merge-group-v2 to main October 24, 2026 14:46
@rmcdaniel rmcdaniel closed this Oct 24, 2026
@rmcdaniel rmcdaniel reopened this Oct 24, 2026
@rmcdaniel

Copy link
Copy Markdown
Member Author

Follow-up independent source review at 2271b0b66b63b620e18157200526919a6ffa9641.

The delta from the earlier reviewed 4face31d is exactly the lane wrapper and its focused test. All three signal profiles now run serially within one lane because they share server-parity-sdk-signals; the other six profiles remain covered. Independent read-only, network-disabled, UID1000 execution on Node22.23.3 passes 52/52 across the lane, split, aggregate and merge-group contracts.

The new queue-isolation counterfactual is effective: mounting only the previous 4face31d wrapper into the unchanged current test tree produces 5/6 passing, with the new test failing specifically because namespace-signal and selected-run-signal share a queue in different lanes. This used mocked profile/container commands, not native or database execution. The reviewed correction addresses the reported overlap mechanism without dropping a profile or weakening failure propagation.

The earlier run40101730551 is now canceled, not performance acceptance. Its original queued-job evidence remains a historical observation. Owner cancellation of duplicate feature/lower-stack runs and the direct-main stack qualification are acknowledged. Current main-base exact-head hosted checks and terminal fixture evidence still govern landing; no measured end-to-end speedup or queue activation is claimed here. No source, settings, runs or provider resources were changed by this review.

…ing it world-writable

SonarCloud refused the new chmod 0777 (githubactions:S2612). Only the published
PHP containers' user 1000 needs to write there; acl is on the hosted runner.
@sonarqubecloud

Copy link
Copy Markdown

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security follow-up on exact head 38697fe8e8baa1a02c3f47f40bfe1986a0b15cc2: no new source blocker in the one-file delta from 2271b0b66b63b620e18157200526919a6ffa9641.

The only change replaces world-writable chmod 0777 on the two disposable cohort directories with access/default ACLs granting rwx specifically to UID 1000. This preserves the published PHP container's required write access without granting every local identity write access. git diff --check is clean. Sonar is now green at this head.

This is delta/source acceptance only. Merge still waits for every exact-head main-base check and every fixture cell to finish terminal and green under the previously recorded landing conditions; several native, corpus, cohort, PHPUnit, and Rust CodeQL jobs are currently running. I changed no source and started/cancelled no CI.

@rmcdaniel
rmcdaniel merged commit 60936f1 into main Oct 24, 2026
42 checks passed
@rmcdaniel

Copy link
Copy Markdown
Member Author

Merged as 60936f17333d60527be33d0fde55aa847dea46e2, a merge commit with parents 61219609ba9eb7e429cc0d2951a779ae42edf79e (main) and 38697fe8e8baa1a02c3f47f40bfe1986a0b15cc2 (the exact reviewed head). Main's tree 6f25b9030d7c131de141c6db33b6442684590dd6 equals the head's tree.

The head contains #741 (e5d2186e64d6a563e35b4c15772daf36a917e481) and #742 (261a557561eafb44e87862196241ca4cae17ef89); GitHub marks both merged.

Gates at the head (41 success, 1 path-skipped):

  • Shared Server fixtures result: success.
  • SonarCloud: green after the ACL change.
  • CodeQL, PHPUnit Feature, Public Boundary: green.

No merge-queue or settings change.

Measured timing (run 40104569657, end to end)

The run took 36m11s from first job created to last job completed. The previous serial layout took 63–72 min per database job (#729's run). Queue wait was 1–3 s for every job.

Part MySQL 8.0 existing MySQL MariaDB PostgreSQL 17 existing PostgreSQL SQLite
native-tests 18m55s 15m07s 12m59s 8m58s 10m04s 6m06s
corpus 31m12s 24m20s 22m24s 29m29s 30m10s 21m32s
cohorts 21m46s 21m58s 20m14s 20m28s 20m55s 19m34s

The roots took 4m37s (published-sdk-adapters) and 7m20s (codec-native-build). The six deadline-health cells took 1–1.5 min each.

The critical path is now the adapters plus the longest corpus part (MySQL 8.0, 31 min). Next cut, ready as a follow-up: run the six deadline-health cells in turn on one runner, which frees five runners per run at no wall-time cost.

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