Repository navigation
Conversation
…eFromNode (#470) shouldReplicateFromNode's primary predicate ended in getHDBNodeTable().primaryStore.get(getThisNodeName())?.replicates — a point-lookup of this node's own hdb_nodes self-record. On a repeatedly-upgraded node that point-lookup can return an empty/undecodable value (the harper-pro#352 shared-structure misread), making ?.replicates undefined and the whole predicate falsy. Because this predicate is the isDesired gate for BOTH the wedge backstop (findWedgedNodeUrls/reconcileWorkers) and the onDatabase re-subscribe path, a still-desired peer was silently excluded from all recovery (live 4-node preprod wedge: connected:false for hours, retries:0). New selfNodeReplicates(store, name) mirrors the #461/#460 decode-resilient pattern: prefer the point-lookup replicates, fall back to the RANGE/scan-decoded self record, and only default to true when the self key is range-visible but nothing decodes (add_node always writes self replicates:true per setNode.ts). A genuine, decodable replicates:false and a genuinely-absent self-record are both preserved. harper#1463 (make the point-lookup decode like the scan path) is the durable root fix; this is the harper-pro-side guard. Unblocks harper-pro#467's reconnect-recovery backstop, which is gated behind this predicate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… in selfNodeReplicates Codex cross-model review: the range-visibility default could revive a removed node. A genuine self-node deletion leaves a CLEAN null tombstone that stays range-visible, so the prior `storeRecordRangeVisible ? true` default would re-enable replication for an intentionally-removed node. Use the existing probeNodeRow tombstone-vs-decode-failure distinction (clean null = deleted, do not revive; throw/present-invalid = #352 decode failure, recover via scan then default true). Adds a tombstone-guard unit test (red without this change). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…aware resolution order Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
|
Reviewed; no blockers found. |
The system database is RocksDB, whose get() returns a MaybePromise: the value synchronously when the row is in the block cache/memtable, but a Promise on a cache-miss disk read. The replication point-lookups in knownNodes.ts assumed a synchronous (LMDB-style) get and read .replicates/.name off the result directly. Once `system` grows past what stays in block cache the hdb_nodes rows are evicted, get() returns a Promise, and (promise)?.replicates is undefined - silently disabling the self-gate (shouldReplicateFromNode), the auth lookup (resolveNodeForAuth) and the change-stream fullRecord reads. That breaks replication recovery: a still-desired peer is excluded from findWedgedNodeUrls/reconcileWorkers and onDatabase re-subscribe with no re-drive (the observed preprod wedge: connected:false for hours, retries:0). Use getSync() - the synchronous point read already used throughout Table.ts/search.ts - at the affected sites. This replaces the earlier scan-fallback workaround, which only appeared to help because getRange() is synchronous and routed around the async get(). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… probe (#470) Codex review of the first revision caught two more unawaited point reads of the same MaybePromise hazard: - probeNodeRow: a RocksDB cache-miss tombstone is Promise<null>, so `record == null` was false and a removed node would be misclassified as a decode-failure and revived. getSync forces the synchronous read so a tombstone is a clean null. - storeRecordRangeVisible fallback: `get(name) != null` would be truthy for Promise<...> on a cache miss, making an absent key look range-visible. Use getSync there too. Test stubs updated to model the getSync contract. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cross-model review (Codex) — dispositionsCodex raised three findings against the first revision; two were real and are now fixed, one is declined with reasoning.
Full replication unit suite: 185 passing, 0 failing after both fixes. |
…x P1) shouldReplicateFromNode is the load-bearing predicate for BOTH shouldSubscribe (onDatabase) and the wedge-reconcile isDesired check. Its final self-record term still used an un-awaited primaryStore.get(getThisNodeName())?.replicates, so a RocksDB block-cache miss (cold restart / grown hdb_nodes) made the whole predicate falsy and silently unsubscribed a still-desired peer — upstream of the readNodeRowSync guard added earlier in this branch. Switch it to getSync. PR #474 refactors this same expression to selfNodeReplicates, which must likewise read sync. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…s PR Supersedes #474. Adopts its comprehensive knownNodes.ts get()->getSync fix verbatim, including readNodeForAuth / storeRecordRangeVisible / probeNodeRow — sites this branch's original scan had under-classified as safe '#352 defensive readers' (a Promise from get() does not throw and is truthy, so it slips through their try/catch + existence checks and strands the peer / makes an absent key look present / revives a tombstone). Brings #474's selfNodeReplicates helper + its unit test and the updated readNodeForAuth and scanNodesForSubscription tests. shouldReplicateFromNode now reads via selfNodeReplicates (getSync), replacing this branch's earlier inline getSync there. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Superseded by #476, which folds this PR's knownNodes.ts Nice call converging on the MaybePromise root cause — that reframing (the — KrAIs (Claude Opus 4.8) |
…ePromise) On RocksDB store.get() returns a Promise on a block-cache miss; the self-node replication gate, ensureThisNode/ensureNode, the retrieval-connection lookup, cluster_status, and the node-update watcher consumed that result synchronously, so once hdb_nodes grew past the block cache (or right after a cold-cache restart) a Promise self-row silently disabled replication, dropped peers from cluster_status, or skipped opening a connection. Switch those reads to getSync (extracted readNodeRowSync helper for the self-gate). Leaves shouldReplicateFromNode to PR #474. Adds a deterministic unit test (readNodeRowSync) and a cluster integration test (small block cache + cold restart + remove/add node). Bumps the core submodule to the paired system-table getSync fix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…x P1) shouldReplicateFromNode is the load-bearing predicate for BOTH shouldSubscribe (onDatabase) and the wedge-reconcile isDesired check. Its final self-record term still used an un-awaited primaryStore.get(getThisNodeName())?.replicates, so a RocksDB block-cache miss (cold restart / grown hdb_nodes) made the whole predicate falsy and silently unsubscribed a still-desired peer — upstream of the readNodeRowSync guard added earlier in this branch. Switch it to getSync. PR #474 refactors this same expression to selfNodeReplicates, which must likewise read sync. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…s PR Supersedes #474. Adopts its comprehensive knownNodes.ts get()->getSync fix verbatim, including readNodeForAuth / storeRecordRangeVisible / probeNodeRow — sites this branch's original scan had under-classified as safe '#352 defensive readers' (a Promise from get() does not throw and is truthy, so it slips through their try/catch + existence checks and strands the peer / makes an absent key look present / revives a tombstone). Brings #474's selfNodeReplicates helper + its unit test and the updated readNodeForAuth and scanNodesForSubscription tests. shouldReplicateFromNode now reads via selfNodeReplicates (getSync), replacing this branch's earlier inline getSync there. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ePromise) (#476) * fix(replication): use getSync for hdb_nodes point reads (RocksDB MaybePromise) On RocksDB store.get() returns a Promise on a block-cache miss; the self-node replication gate, ensureThisNode/ensureNode, the retrieval-connection lookup, cluster_status, and the node-update watcher consumed that result synchronously, so once hdb_nodes grew past the block cache (or right after a cold-cache restart) a Promise self-row silently disabled replication, dropped peers from cluster_status, or skipped opening a connection. Switch those reads to getSync (extracted readNodeRowSync helper for the self-gate). Leaves shouldReplicateFromNode to PR #474. Adds a deterministic unit test (readNodeRowSync) and a cluster integration test (small block cache + cold restart + remove/add node). Bumps the core submodule to the paired system-table getSync fix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(replication): read shouldReplicateFromNode self-record sync (Codex P1) shouldReplicateFromNode is the load-bearing predicate for BOTH shouldSubscribe (onDatabase) and the wedge-reconcile isDesired check. Its final self-record term still used an un-awaited primaryStore.get(getThisNodeName())?.replicates, so a RocksDB block-cache miss (cold restart / grown hdb_nodes) made the whole predicate falsy and silently unsubscribed a still-desired peer — upstream of the readNodeRowSync guard added earlier in this branch. Switch it to getSync. PR #474 refactors this same expression to selfNodeReplicates, which must likewise read sync. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fold #474 (knownNodes.ts getSync sweep + selfNodeReplicates) into this PR Supersedes #474. Adopts its comprehensive knownNodes.ts get()->getSync fix verbatim, including readNodeForAuth / storeRecordRangeVisible / probeNodeRow — sites this branch's original scan had under-classified as safe '#352 defensive readers' (a Promise from get() does not throw and is truthy, so it slips through their try/catch + existence checks and strands the peer / makes an absent key look present / revives a tombstone). Brings #474's selfNodeReplicates helper + its unit test and the updated readNodeForAuth and scanNodesForSubscription tests. shouldReplicateFromNode now reads via selfNodeReplicates (getSync), replacing this branch's earlier inline getSync there. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(cluster): fix blockCacheEviction startup hang (viable cache, 2 nodes, sentinel convergence) The 1MB blockCacheSize + 512KB WriteBufferManager (allowStall) was sub-viable for opening Harper's RocksDB databases — every node hung at startup (HarperStartupError, no output for 60s), failing the suite. Use a small-but-viable 32MB block cache and drop the WBM override; the COLD RESTART is the deterministic out-of-cache trigger (the field repro). Also trimmed to 2 nodes and switched convergence checks to sentinel records (no fragile full-count/pagination) for speed and reliability. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(cluster): harden remove_node/add_node step against post-reload ECONNREFUSED remove_node reloads the node, so its operations API briefly refuses connections; the immediate add_node hit ECONNREFUSED (TypeError: fetch failed). Wait for the node to be reachable (pollHealth) and retry the re-add. (Cold-restart test 1 already passes.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(cluster): drop orthogonal remove_node/add_node case from blockCacheEviction Test 2 failed on convergence after removing a node's leader and re-adding: the node ends up with a null self-record and (correctly) disables replication, not re-converging in-window. The log shows selfNodeRow=null (a concrete value, NOT a Promise) — so this is a remove_node re-subscription behavior orthogonal to the get() MaybePromise fix this suite guards. The cold-restart test already exercises ensureThisNode/shouldReplicateFromNode/ cluster_status on a cold cache; add_node is covered by the before hook and replicationReconnect/replicationTopology tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Update integrationTests/cluster/blockCacheEviction.test.mjs Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> * feat(types): type hdb_nodes store so sync get() misuse is a compile error Adds a NodeStore type whose get() returns MaybePromise<NodeRecord> (the honest RocksDB contract — a Promise on a block-cache miss) and types getHDBNodeTable()'s return, so any SYNCHRONOUS consumption of get() (e.g. store.get(id)?.replicates) is now a TypeScript error — use getSync() or await/when. The value type reuses the canonical Node type plus an index signature for incidental fields (e.g. authorization). tsc is clean against this branch with the typing applied — proving the get->getSync fixes already on this branch leave ZERO remaining sync misuse on hdb_nodes — and a deliberate store.get(x)?.shard is correctly rejected. This is the compiler-driven backstop for the store.get() MaybePromise class (would have caught the readNodeForAuth/probeNodeRow misses the manual audit under-classified). Pairs with the rocksdb-js DBI<T,V> value-generic. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Update integrationTests/cluster/blockCacheEviction.test.mjs Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Fixes #470
Root cause (corrected — supersedes this PR's first revision)
The
systemdatabase is RocksDB, whoseget()returns aMaybePromise: synchronous when therow is in the block cache / memtable, a
Promiseon a cache-miss disk read. The replicationpoint-lookups in
knownNodes.tsassumed a synchronous (LMDB-style)get()and read.replicates/.nameoff the result directly. Oncesystemgrows past what stays in block cache, the hdb_nodes rowsare evicted,
get()returns a Promise, and(promise)?.replicatesisundefined— silently disablingshouldReplicateFromNode's self-gate (theisDesiredpredicate forfindWedgedNodeUrls/reconcileWorkersand
onDatabasere-subscribe),resolveNodeForAuth, and the change-streamfullRecordreads. Thatbreaks replication recovery: a still-desired peer is excluded from all re-drive/re-subscribe with no
signal (the observed preprod wedge —
connected:false,retries:0). Confirmed live via CDP:ps.get(name)→ Promise,await ps.get(name)/ps.getSync(name)→ the full pristine record.Change
Use
getSync()— the synchronous point read already used throughoutcore/resources/Table.tsandsearch.ts— at the four affected sites:selfNodeReplicates→store.getSync(name)?.replicates(a one-line testable seam; the scan-fallbackhelper from the first revision is removed).
resolveNodeForAuth→ synchronousgetSyncpoint read (the range-visible{ name }reconstructionremains only as defense-in-depth and should now be rare).
fullRecordreads (get()→getSync()).A deleted/absent row yields
null/undefined(?.→ falsy, correct); a genuinereplicates: falseis preserved.
Tests
unitTests/replication/selfNodeReplicates.test.mjsrewritten to model the real RocksDBMaybePromisecontract: a store stub whose
get()returns a Promise on a block-cache miss butgetSync()returns thevalue. The bug-closing case asserts that the un-awaited
get(...)?.replicatesisundefined(the oldhazard) while
selfNodeReplicatesreturns the correct value viagetSync; plus cache-hit, replicatesobject, genuine
falsepreserved, absent → undefined, and clean-null tombstone → undefined (no revive).readNodeForAuth.test.mjsstub updated to exposegetSync. Full replication unit suite: 185 passing,0 failing. Built via
tsc(only pre-existing_socket/etc. errors inreplicationConnection.ts,none in changed files).
Notes
systemhas these unawaited sync point reads; itsurfaces once
systemexceeds block-cache size. Worth a guard test asserting the self-gate works on aRocksDB-backed
system.#352narrative in fix(replication): decode-resilient outbound subscriptions + idempotent node-update watcher on deploy reload (#460) #461's surroundingcomments is superseded — a comment-only cleanup can follow.
🤖 Generated with Claude Code