Repository navigation
Add cluster integration test: sourcedFrom replicationSource: true (§5.5 Cat 15) - #415
Conversation
…(§5.5 Cat 15)
Adds `integrationTests/cluster/cacheReplicationSource.test.mjs` — the remaining
sub-case of the v5 Integration Test Plan §5.5 Caching / Category 15 that requires
a live 2-node cluster and cannot be covered by the single-node caching.test.ts or
the unit suite (harper#1189).
Test covers:
(a) A cache miss on the non-source node contacts the origin exactly once (stampede
prevention applies cluster-wide; includes TODO marker to tighten to per-node
assertion once replicationSource routing is implemented in core).
(b) The fetched cache entry is byte-identical on both nodes after replication.
(c) A second request on the non-source node is served from the replicated cache
with zero additional origin fetches.
(d) A separate id fetched from nodeA also replicates to nodeB and is served from
cache on nodeB without a re-fetch.
A lightweight mock-origin HTTP server runs in the test process and tracks per-node
fetch counts. The Harper fixture defines an OriginCache table with
`sourcedFrom(OriginSource, { replicationSource: true })` and fetches from the mock
origin URL injected via HARPER_TEST_ORIGIN_URL. Nodes are started with startHarper
(not setupHarperWithFixture) so the pre-allocated loopback address matches the
replication securePort in the config.
All 4 tests pass locally (9 s).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a new cross-node integration test and associated fixture files to verify the behavior of the sourcedFrom cache with replicationSource: true in a two-node cluster. The review feedback highlights several critical improvements: resolving loopback addresses sequentially instead of concurrently to prevent a race condition, wrapping rawOperation calls in try/catch blocks to ensure retry loops do not crash on network errors, and using fileURLToPath to safely resolve paths in ES modules without relying on CommonJS globals.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| import { startHarper, teardownHarper, getNextAvailableLoopbackAddress } from '@harperfast/integration-testing'; | ||
| import { resolve, basename, join } from 'node:path'; | ||
| import { sendOperation, fetchWithRetry } from './clusterShared.mjs'; |
There was a problem hiding this comment.
Add fileURLToPath import from node:url to safely resolve the installation script path in ES modules without relying on CommonJS globals.
| import { startHarper, teardownHarper, getNextAvailableLoopbackAddress } from '@harperfast/integration-testing'; | |
| import { resolve, basename, join } from 'node:path'; | |
| import { sendOperation, fetchWithRetry } from './clusterShared.mjs'; | |
| import { startHarper, teardownHarper, getNextAvailableLoopbackAddress } from '@harperfast/integration-testing'; | |
| import { resolve, basename, join } from 'node:path'; | |
| import { fileURLToPath } from 'node:url'; | |
| import { sendOperation, fetchWithRetry } from './clusterShared.mjs'; |
| process.env.HARPER_INTEGRATION_TEST_INSTALL_SCRIPT = resolve( | ||
| import.meta.dirname ?? module.path, | ||
| '..', | ||
| '..', | ||
| 'dist', | ||
| 'bin', | ||
| 'harper.js' | ||
| ); |
There was a problem hiding this comment.
In ES modules (.mjs), the CommonJS module global is not defined. If this test is run on a Node.js version that does not support import.meta.dirname (Node.js < 20.11.0), the fallback to module.path will throw a ReferenceError. Using fileURLToPath(import.meta.url) is the standard, cross-version compatible way to resolve paths in ES modules.
| process.env.HARPER_INTEGRATION_TEST_INSTALL_SCRIPT = resolve( | |
| import.meta.dirname ?? module.path, | |
| '..', | |
| '..', | |
| 'dist', | |
| 'bin', | |
| 'harper.js' | |
| ); | |
| process.env.HARPER_INTEGRATION_TEST_INSTALL_SCRIPT = resolve( | |
| fileURLToPath(import.meta.url), | |
| '..', | |
| '..', | |
| '..', | |
| 'dist', | |
| 'bin', | |
| 'harper.js' | |
| ); |
| for (let i = 0; i < 20; i++) { | ||
| const result = await rawOperation(nodeA, { | ||
| operation: 'create_authentication_tokens', | ||
| authorization: nodeA.admin, | ||
| }); | ||
| if (result.status === 200 && result.body.operation_token) { | ||
| tokenResp = result.body; | ||
| break; | ||
| } | ||
| await delay(300); | ||
| } |
There was a problem hiding this comment.
If rawOperation throws a network error (such as ECONNREFUSED because the server is still starting up), the error is uncaught and the retry loop will terminate immediately on the first attempt. Wrapping the call in a try/catch block ensures the retry logic functions as intended.
for (let i = 0; i < 20; i++) {
try {
const result = await rawOperation(nodeA, {
operation: 'create_authentication_tokens',
authorization: nodeA.admin,
});
if (result.status === 200 && result.body.operation_token) {
tokenResp = result.body;
break;
}
} catch (err) {
if (i === 19) throw err;
}
await delay(300);
}| for (let i = 0; i < 30; i++) { | ||
| const result = await rawOperation(nodeB, { | ||
| operation: 'add_node', | ||
| rejectUnauthorized: false, | ||
| hostname: nodeA.hostname, | ||
| authorization: 'Bearer ' + tokenResp.operation_token, | ||
| }); | ||
| // Success or "already exists" are both fine; ECONNREFUSED means replication | ||
| // server not ready yet — wait and retry. | ||
| if (result.status === 200) break; | ||
| const errMsg = JSON.stringify(result.body); | ||
| if (errMsg.includes('ECONNREFUSED') || errMsg.includes('ECONNRESET') || errMsg.includes('connect ')) { | ||
| await delay(500); | ||
| continue; | ||
| } | ||
| // Other 5xx: unexpected, propagate | ||
| throw new Error(`add_node failed (${result.status}): ${errMsg}`); | ||
| } |
There was a problem hiding this comment.
Similar to the first loop, if rawOperation throws a network error, the loop will crash immediately instead of retrying. Wrapping this in a try/catch block ensures robust retry behavior.
for (let i = 0; i < 30; i++) {
try {
const result = await rawOperation(nodeB, {
operation: 'add_node',
rejectUnauthorized: false,
hostname: nodeA.hostname,
authorization: 'Bearer ' + tokenResp.operation_token,
});
// Success or "already exists" are both fine; ECONNREFUSED means replication
// server not ready yet — wait and retry.
if (result.status === 200) break;
const errMsg = JSON.stringify(result.body);
if (errMsg.includes('ECONNREFUSED') || errMsg.includes('ECONNRESET') || errMsg.includes('connect ')) {
await delay(500);
continue;
}
// Other 5xx: unexpected, propagate
throw new Error(`add_node failed (${result.status}): ${errMsg}`);
} catch (err) {
if (i === 29) throw err;
await delay(500);
}
}|
The prior blocker (orphaned node handle on partial startup failure) has been correctly resolved by commit One minor robustness nit remains but is not a blocker: 1.
|
…d test for unimplemented routing Suite and test-(a) names now accurately state what is verified today (cache-miss fetch + cross-node replication) vs what is not (per-node replicationSource routing). Add an explicit skip test named after harper-pro#416 so the gap is visible in test output rather than buried in a TODO comment. Hostname guard note added to the TODO block for the tighter per-node assertion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ial startup failure (review) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Addressed the orphaned-node concern from the review thread: node handles are now captured inside — Claude Sonnet 4.6 |
Summary
integrationTests/cluster/cacheReplicationSource.test.mjs— the last uncovered sub-case of the v5 Integration Test Plan §5.5 Caching / Category 15 (tracked in harper#1189 which noted it requires a 2-node cluster and could not be added to the single-nodecaching.test.ts).fixture-cache-repl-source/with anOriginCachetable declaredsourcedFrom(OriginSource, { replicationSource: true }).What the test asserts
A lightweight mock HTTP "origin" server runs in the test process and counts per-node fetch calls. Two Harper nodes start with the fixture pre-loaded:
replicationSourcerouting is implemented in core.Mapping to v5 Integration Test Plan
§5.5 Caching / Category 15 —
replicationSource: truesub-case. The single-nodecore/integrationTests/server/caching.test.tsleft a TODO ("requires a 2-node cluster setup") that this PR closes.Passing output
Notes
startHarper(vssetupHarperWithFixture) approach was necessary to ensure the pre-allocated loopback address matches thereplication.securePortin the config.setupHarperWithFixtureoverwritesctx.harperbefore callingstartHarper, causing the hostname to be re-allocated and resulting in a securePort mismatch (silent ECONNREFUSED).replicationSource: trueoption is stored insourceOptionsbut the routing logic (origin fetch only on the designated source node) is not yet implemented in core. The TODO in test (a) tracks the stricter per-node assertion.Generated by Claude Sonnet 4.6 (claude-sonnet-4-6) as a subagent.