Repository navigation
Test rolling 5.x upgrades and a one-node rollback across a mixed-version cluster - #1008
Merged
Merged
Conversation
Contributor
Release cherry-pick
|
kriszyp
force-pushed
the
test/mixed-version-rolling-upgrade
branch
3 times, most recently
from
October 9, 2026 23:20
8b6f61e to
7f6ae4c
Compare
kriszyp
force-pushed
the
test/mixed-version-rolling-upgrade
branch
from
October 11, 2026 23:54
7f6ae4c to
48cd0ea
Compare
…ion cluster A 3-node cluster starts on a previous published harper-pro release and is rolled onto this build one node at a time on the same data dirs, then one node is rolled back with CONFIRM_DOWNGRADE. Old and new nodes both write at 1/3 and 2/3 upgraded, each stopped node misses writes it must catch up on, and every step asserts exact convergence: fields, the category index, blob bytes over HTTP and in local blob files, on a plain and a typed-struct table. A dedicated CI job installs 5.2.15 and 5.3.1; elsewhere the file skips. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Hbc5pizrkHrpvfWTCnjWK Dispatch-Task: harper-pro-mixed-version-replication Dispatch-Task: pr-maint-a9256969e72fed7e73b4aab12c5b2d82
…view A node joining on a published 5.x release is refused a base copy by this build (no safeCopyAudit; legacy baseline verification needs numeric update timestamps), so its subscription closes 1008 forever. The suite's last step now pins that, and every node writes live after connecting so no peer is left with only a base-copy cursor when a node switches binaries (the race that surfaced the defect). Also from review: bound the identity and add_node requests, tolerate a blob file unlinked mid-scan, fail a stop that needed SIGKILL, skip the remaining steps after one fails, and run one previous release per CI job with re2 rebuilt (the old releases' waf component failed to load without it). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Hbc5pizrkHrpvfWTCnjWK Dispatch-Task: harper-pro-mixed-version-replication Dispatch-Task: pr-maint-a9256969e72fed7e73b4aab12c5b2d82
… a clean exit on stop The join step now reads, from this build's cluster_status, whether the previous release advertises safeCopyAudit: with it the joining node must converge; without it the step pins #1007. A stop now fails on any exit other than code 0 or SIGTERM, not only SIGKILL. The DESIGN.md row names the workflow's previous-release matrix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Hbc5pizrkHrpvfWTCnjWK Dispatch-Task: harper-pro-mixed-version-replication Dispatch-Task: pr-maint-a9256969e72fed7e73b4aab12c5b2d82
… main Rebasing onto main pulled in harper-pro#1025 (Allow base copies to published 5.x peers during rolling upgrades), which fixes harper-pro#1007: this build no longer refuses a base copy to a peer lacking safeCopyAudit as long as a registration_info probe confirms it's a published 5.x release. Both pinned previous releases (5.2.15, 5.3.1) qualify, so the suite's last step — which waited for the now-impossible refusal log line — timed out instead of converging. Drop the dead refusal branch and assert convergence unconditionally, per the comment the prior session already left for this exact moment. Keep the safeCopyAudit capability-decode check as an independent regression guard. Updated the stale header/DESIGN.md prose that still described the fixed defect as live. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Dispatch-Task: pr-maint-a9256969e72fed7e73b4aab12c5b2d82
…ed DESIGN.md row
Pre-push review (Gemini, Cursor Composer, domain adjudication) flagged that the
join step's new unconditional convergence assertion can pass via nodeC (still on
previousVersion after the prior rollback step) instead of nodeA/nodeB (this
build), silently masking a regression in harper-pro#1025's version-probe fix.
Per the plan-mode framing recheck, the chosen fix reuses existing machinery
instead of restructuring the suite's topology: legacyCopyBaseline.test.mjs
already has a deterministic two-node case for this exact handshake
(`a ${engine} v5 peer without safeCopyAudit receives historical records...`),
gated on HARPER_PRO_PREVIOUS_VERSION_PATH to use a real previous-release binary
instead of its default capability-omission simulation. The mixed-version CI job
is the only place a real previous release is installed, so wire that path in
there. Verified locally against the real 5.2.15 binary (both rocksdb and lmdb,
~58s).
Also renamed the join step (it ran after nodeC's rollback, not "while the others
run this build") and restored the `residencyConfirmation.test.mjs` DESIGN.md row
I'd dropped by mistake while resolving the second rebase conflict in that table.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Dispatch-Task: pr-maint-a9256969e72fed7e73b4aab12c5b2d82
Both the live-write and join-step comments restated facts the header and DESIGN.md already cover. Also softened the live-write comment: it narrows the base-copy-vs-live-write race, it doesn't confirm the cursor landed on disk (the suite never observes it directly). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Dispatch-Task: pr-maint-a9256969e72fed7e73b4aab12c5b2d82
kriszyp
force-pushed
the
test/mixed-version-rolling-upgrade
branch
from
October 13, 2026 03:45
48cd0ea to
112c314
Compare
Review found the existing TEST_FILE guard only checked mixedVersionRollingUpgrade.test.mjs; the legacy-copy step's literal path had no equivalent check. Verified empirically: npm run test:integration against a nonexistent path exits 0 with zero tests run, so a future rename of legacyCopyBaseline.test.mjs would silently make that step pass with no coverage — the same false-green class the guard step already exists to catch. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Dispatch-Task: pr-maint-a9256969e72fed7e73b4aab12c5b2d82
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
⊙ Problem
harper-pro has no test where two 5.x versions replicate to each other. The only cross-version tests are 4.x → 5 (
cloneFromLegacy,legacyCopyBaseline,replicationTopology"Replicate data from a legacy node"). A rolling minor upgrade — each node restarted on a new binary over its old data while its peers still run the old one — and a one-node rollback are untested, though past rolling-upgrade bugs (blob bytes lost, typed-structure ids diverging between nodes) lived exactly there.It found a real defect: #1007 (filed P1, milestone v5.3). This build refuses a base copy to any peer that does not advertise
safeCopyAudit— every published 5.x release — when a table has no numeric update-timestamp attribute; the old peer's subscription closes 1008 and retries forever, so it never receives the upgraded node's writes. #848 introduced the gate and is already onv5.3, so the next 5.3.x patch ships it. In a rolling upgrade it bites an old node that has no resume cursor for an upgraded node: one that joins mid-upgrade, or whose only contact was a base copy not yet flushed when the peer switched binaries (how it surfaced: 1 of the first 12 local runs).💡 Solution
A new suite,
mixedVersionRollingUpgrade.test.mjs, runs once per previous install listed inHARPER_PRO_PREVIOUS_VERSION_PATHS. Three nodes A, B, C:add_nodeA; then each writes again, liveCONFIRM_DOWNGRADE=yessafeCopyAudit(#1007, fixed by #1025)Exact convergence means that on every node, within a bounded 120 s poll (no fixed sleeps): the id set and every field equal the test's own expected map, not merely each other; the
categorysecondary index returns exactly the expected ids for every category ever written; every blob reads back byte-equal overGET /MixedVersionItem/<id>.payload; and every 20 KB (file-backed) blob is present in the node's own blob files, because an HTTP read could have been served from a peer. Each write batch inserts records in shapes that vary from batch to batch, updates two records another batch wrote (moving them to a new index category and attaching a new blob), and deletes two, so inserts, updates and deletes all cross the version boundary in both directions. Two tables: an audited table with an indexed attribute and aBlob, and arandomAccessFields: truetable for the typed-struct encoding. Every start asserts the script the process was launched from and the version it reports (5.3.1 and this build report the same version); every stop must exit cleanly on SIGTERM within 30 s (exit code 0), not be killed or crash. Once a step fails, the later ones skip instead of each burning its own timeout.CI: a new job,
run-mixed-version-tests, runs only this file, one matrix entry per previous release (5.2.15,5.3.1). It installs the release with--ignore-scripts, then rebuildsre2in that install — without it the old releases'wafcomponent fails to load, so they would not be running as shipped. A guard step fails the job if either test file, the install, orre2's binary is missing, since a literal nonexistent test path exits 0 with zero tests run (verified empirically) — the same false-green class the comment above it already called out, now also covering thelegacyCopyBaseline.test.mjsstep added below. In the six cluster shards the file skips, so they pay nothing.⚖️ Alternatives
Planning review (before any code):
Framing-Verdict: chosen-approach-sound. Its in-frame findings were adopted: writes while each node is down, seeding on isolated nodes so their typed-structure dictionaries diverge, binary identity from the spawn arguments, the local blob-file check, and a CI guard against an empty install list.replicationTopology's legacy test at a 5.2 install: a fresh node joining, with no in-place upgrade, no mixed-mesh writes from a new node, and no rollback.harperdb@4is): two installs × six shards for one file, inside shards already budgeted at 25 minutes.🔧 Changes
integrationTests/cluster/mixedVersionRollingUpgrade.test.mjs— the suite; its parts are linked from Solution above.integrationTests/cluster/fixture-mixed-version/—config.yaml+schema.graphqlfor the two tables, copied into each node's components before its first start (notsetupHarperWithFixture, which discards the pre-assigned hostname thatreplication.securePortneeds)..github/workflows/integration-tests.yaml— therun-mixed-version-testsjob. It sits betweenrun-integration-testsandrun-copy-gap-runtime-regressionbecause that stretch of the file is identical onmainandv5.3, so the milestone's cherry-pick applies cleanly. Next torun-cluster-tests, its first position, the pick conflicted onv5.3: that branch lacks #983'srun-replication-pool-regressionjob and has a differentrun-cluster-testsname line. Job order has no effect on how the workflow runs. Added on rebase: a second step runslegacyCopyBaseline.test.mjsagainst the job's already-installed real previous release — this job is the only place in the whole workflow that installs one, so it's the only place that test's real-binary mode can run instead of its default capability-omission simulation. Review caught that the existing guard step didn't cover this new step's own test path, so it now checks both files.replication/DESIGN.md— a row for the suite in the integration-test table: when to bump the releases, and (updated on rebase) that the join step converges unconditionally now that Allow base copies to published 5.x peers during rolling upgrades #1025 fixed Rolling upgrade: a node on this build refuses base copies to published 5.x peers (no safeCopyAudit), so they never receive its writes #1007. The rest of that table's diff is prettier re-padding the column plus restoring aresidencyConfirmation.test.mjsrow I'd accidentally dropped mid-rebase, caught by review.✅ Verification
distpatched (not committed) to drop the replicated recordc-down-fileon receive, the 5.2.15 suite fails at "1 of 3 upgraded" after the 120 s bound —127.0.0.142: MixedVersionItem c-down-file is missing, the record old A wrote while C was down never reached C after its upgrade — the 5 later steps skip, exit 1.HARPER_PRO_PREVIOUS_VERSION_PATHS=/nonexistent/...fails the file (ENOENT, exit 1); unset, the file reports one skipped suite (exit 0).Cannot verify replication baseline without numeric update timestampsand closes 1008 on every retry.system.hdb_* already exists; skipping create— replication had created the 5.3.0 system tables on them while they still ran 5.2.15, the pathcore/upgrade/directives/5-3-0.tsdocuments. After there2rebuild no node logsCould not load component.npm run test:unit: 1792 passing (post-rebase).npm run lintandnpm run typecheckpass. Prettier applied to the changed files.main(this round): confirmed the join step's old branch would time out against the real previous-release binaries before fixing it (ran both 5.2.15 and 5.3.1 suites pre-fix: both hung 30s on the now-unreachable refusal log), then confirmed 7/7 pass on both post-fix. RanlegacyCopyBaseline.test.mjslocally against the real 5.2.15 binary (bothrocksdb/lmdbengines, ~58s) to validate the new CI step before pushing.🤖 Generated with Claude Code
https://claude.ai/code/session_017Hbc5pizrkHrpvfWTCnjWK
🤖 Generated by Claude Code (Claude Opus 5.5); posted via @kriszyp.
Release target: v5.3 — changed code is also on v5.3; overlapping #1025 milestoned v5.3
Related PRs: #1025 overlaps (now merged to
main; fixes #1007, see the updated Problem/Solution above), #1026 overlaps (carries #1025's fix for #1007 into the v5.3 release line; no shared changed lines with this PR), #983 overlaps (both edit the jobs in .github/workflows/integration-tests.yaml; no shared behavior), #956 overlaps (both edit replication/DESIGN.md's Tests table; its dropTableOfflinePeer.test.mjs row survives the rebase's re-merge, no shared behavior), #1032 overlaps (both edit DESIGN.md's Tests table; its residencyConfirmation.test.mjs row survives the rebase's re-merge, no shared behavior)Complexity: medium
Origin — the dispatch brief this PR was written from
Add an integration test for mixed-version 5.x clusters in HarperFast/harper-pro, covering a rolling minor upgrade 5.2 -> 5.3 (and rollback). Today the only cross-version replication coverage is 4.x <-> 5 (cloneFromLegacy, legacyCopyBaseline, replicationTopology 'Replicate data from a legacy node', all via HARPER_LEGACY_VERSION_PATH = harperdb@4). Scenario: start a 3-node cluster on @harperfast/harper-pro@5.2.15 (latest 5.2.x), seed data (plain, indexed, audit-enabled table; include a blob attribute), then roll the nodes one at a time to the current build on the SAME data dirs (kill, restart with current binary). At each intermediate state (1 of 3 upgraded, 2 of 3 upgraded) write on BOTH an old-version and a new-version node and assert bidirectional replication converges on every node (record sets equal, indexed search resolves, blobs readable). After all nodes are upgraded, assert convergence and that a cold restart keeps it. Then roll ONE node back to 5.2.15 (CONFIRM_DOWNGRADE=yes) and assert it rejoins and replication still converges both ways. Also run from @harperfast/harper-pro@5.3.1 -> current if cheap (main is 5.4, so 5.3 -> 5.4 is the N-1 path on main).
Acceptance
New suite runs (not skipped) in harper-pro CI from 5.2.15 and passes, covering: partial-upgrade bidirectional replication at 1/3 and 2/3 upgraded, full-upgrade convergence + cold restart, and one-node rollback rejoin. PR body shows it can fail (e.g. assertions catch a deliberately dropped replicated write). Uses bounded polling, not fixed sleeps. No production code changes unless a small real defect fix; otherwise file an issue.
Dispatch: task
harper-pro-mixed-version-replication· queued by kris-session · ran by claude/opus/xhigh · worker kzyp-xps-1Review-Coverage: authored=claude; ran=gemini,codex; adjudicated=domain; declined=cursor-grok,cursor-composer,cursor-kimi,cursor-muse; rounds=15; full=8 @ 5fd8435
Review-Attention: study ~20m (sensitive: integration-tests.yaml; decisions: convergence-only-oracle, disjoint-concurrent-writes, rollback-as-contract, pinned-release-matrix, runs-on-every-pr) @ 5fd8435