Skip to content

fix(#6267): a peer address that identifies nobody says so, and a membership test stops poisoning its own teardown - #6284

Merged
lvca merged 6 commits into
mainfrom
issue-6267
Aug 17, 2026
Merged

fix(#6267): a peer address that identifies nobody says so, and a membership test stops poisoning its own teardown#6284
lvca merged 6 commits into
mainfrom
issue-6267

Conversation

@lvca

@lvca lvca commented Aug 17, 2026

Copy link
Copy Markdown
Member

Fixes #6267.

Five follow-ups from #6221 / PR #6226. One correction to the issue first, because it changes what item 1 is:

DynamicMembershipTest does not share a cluster between its methods. beginTest/endTest are @BeforeEach/@AfterEach, servers is an instance field on a default-lifecycle JUnit class, and the Raft storage lives under the database directory that beginTest deletes - so a removal never reaches the next method (verified: the ports rebind per method, and the class is green in isolation). Two of the three reported failures (:68, :116) are both the assertThat(leaderIndex).isGreaterThanOrEqualTo(0) line, i.e. the leaderless-window failure that #6226 already fixed by making findLeaderIndex() wait - d75174c92 predates that merge. The third one is real, and it is a different bug: a method mutates the peer set that its own teardown then holds to a replica's contract.

1. A withheld peer-to-peer endpoint now says so

getUnambiguousPeerHttpAddress / getUnambiguousPeerHttpsAddress refuse an address two peers both resolve to by answering null (#6202), and every caller then decided for itself whether to log anything. Neither existing warning covers the refusal: deriveHttpAddressWithWarning and its HTTPS twin fire whenever an address is derived at all, which is also what a perfectly healthy homogeneous Kubernetes StatefulSet does, so they cannot distinguish "deriving, and fine" from "deriving, and two peers just collapsed onto one address"; warnAmbiguousRouting says exactly the right thing but about the client routing tables of #6183.

There is now a one-time WARNING per protocol, modelled on warnAmbiguousRouting, naming the peers that could not be told apart, the address they share, and the host:{raft:..,http:..} field to declare. HTTP and HTTPS get separate latches, for the reason #6221 already established: they are read from independent fields with independent derive fallbacks, so a cluster that declares distinct http ports and shares an https one must still hear about the second.

2. It is visible in cluster status, and the status endpoint was not the one the issue pointed at

The issue names RaftClusterStatusExporter:85. That method - exportClusterStatus() - has no callers anywhere in the repo; the live status endpoint is GetClusterHandler, which assembles its own document. Decorating dead code would have changed nothing an operator sees, so:

  • GET /api/v1/cluster now reports each peer's httpAddress and, only when that address is not the peer's alone, httpAddressAmbiguous: true. The OpenAPI spec documents both (and PluginApiSpecTest pins the exact per-peer field set, so this could not be added silently). Studio renders the flag as a warning line on the node's card.
  • exportClusterStatus() is removed rather than kept in step by hand. A second, unreachable builder of the same document is how two views of one cluster drift apart - and it had: the reachable one now reports the ambiguity and this one never would have.

3. The presence matrix dialled the address nothing had checked

GET /api/v1/cluster?presence=true asks every peer which databases it holds and attributes the answer to that peer, which makes it exactly the unattended dial PeerDialAddress exists to guard - the issue read GetClusterHandler:253 as display, but it is a dial. Unguarded, on a cluster whose peers collapse onto one derived address, every peer was queried on the leader's own endpoint and reported the leader's database list as its own, so the matrix showed every database present on every node: the same false all-clear the verify endpoint gave before #6221.

It resolves through PeerDialAddress now, so a peer it cannot identify goes into unreachable - with the refusal logged - instead of being answered for by whoever picked up. This is the better alternative to the flag the issue suggested for this call site: a flag next to a wrong answer is still a wrong answer.

4. RESYNC_RETRY_TIMEOUT_MS: 120 s -> 30 s, from the measurement

#6226 added the instrument rather than guessing, and it has now reported. Across nine full ha-integration-tests runs since that merge (31968696717, 31969178061, 31969810563, 31972218219, 31975575222, 31977924355, 31980155942, 31980224898 and the merge run itself), 235 tests each: not one wait exceeded the 10 s report threshold. The corroboration is the per-class wall clock - all ten classes that call withResyncRetry/awaitValue/awaitCountOn ran in every one of those runs, and the slowest took 53 s for the whole class, cluster startup and teardown included, so no single wait inside it can have approached even half the old budget.

30 s is what the rest of BaseRaftHATest already treats as "long enough for the cluster to do anything it is going to do" - waitForReplicationIsCompleted, waitAllReplicasAreConnected and LEADER_ELECTION_TIMEOUT_MS all use it. The budget with no measurement behind it was also the only one four times larger than its siblings; it is now one of them, still three times the largest wait the instrument can prove any of those runs needed, and a genuine hang costs 90 s less before it is reported. SLOW_WAIT_REPORT_MS drops to 5 s with it: at 10 s a wait could consume a third of the new budget and still say nothing, which is the blindness that let the 120 s stand unmeasured for as long as it did.

5. The membership test, and the redundant awaits

BaseRaftHATest.checkDatabasesAreIdentical() waits for, and compares, exactly the servers getServerToCheck() names - the hook the base class already had for this - rather than every configured one. The two sets differ only for a test that takes a server out of the group: a peer that is no longer a member never applies another entry, so waiting for it can only burn 30 s and log a timeout, and comparing it can only report the divergence the eviction asked for. DynamicMembershipTest records what it evicted and excludes it. The class drops from 152 s to 77 s with one more test in it.

Seven await().until(() -> findLeaderIndex() >= 0) wrappers made redundant by #6226 are gone (RaftTimeSeriesReplication3NodesIT x4 including its private awaitLeaderElected(), SuperNodeAppendHAConsistencyIT, RaftTimeSeriesOversizedSealedIT, SuperNodeConcurrentAppendHABenchmark), and three verbatim copies of "only the servers still running" collapse into BaseRaftHATest.startedServers().

6. Docs

docs/release-26.9.1.md covers the operator-visible half. The arcadedb-docs side is ArcadeData/arcadedb-docs#443: the retries default (arcadedb.txRetries, with the run-more-than-once caveat for side effects outside the database), a VERIFICATION_INCOMPLETE row in a new "Verify Outcomes" table with the note for alerting that keys on INCONSISTENCY_DETECTED, and a "Peer-to-Peer Endpoints" section for the withheld-address behaviour above.

Verification

New:

  • Issue6267AmbiguousPeerAddressWarningTest - 3 unit tests against a real RaftHAServer built from a server list: the warning fires once across repeated asks, names both peers and the shared address; a correct cluster is silent; the HTTPS warning is not muted by the HTTP one.
  • Issue6267AmbiguousAddressVisibilityIT - 3-node cluster: every peer flagged when the leader can identify none of them, no flag on the cluster's real addresses, and the presence matrix reporting the two peers unreachable with nothing attributed to them. Confirmed failing against the pre-fix dial (expected: 2 unreachable, got 0).
  • DynamicMembershipTest#removedPeerIsNotHeldToTheClusterConsistencyCheck - evicts a peer, writes through the remaining members, and asserts the evicted one does not see the write. Confirmed failing without the getServerToCheck fix with exactly the reported shape: endTest > checkDatabasesAreIdentical: DatabaseAreNotIdentical Types: DB1 5 <> DB2 6, plus the 30 s "Timeout waiting for server 0 to replicate" (37.7 s vs 11.8 s with the fix).

CapturingTestLogger gained countFormattedContaining (it captured only the raw template, which cannot assert which peers a warning named); the existing template-based assertions are untouched.

Run locally: full ha-raft unit lane (918 tests), the 18 IT classes these changes reach (35 tests) - the three whose getServerToCheck was collapsed, the four touched by the await cleanup, the verify guards, and all ten that use the shortened budget - server's PluginApiSpecTest, and a full reactor build.

…ership test stops poisoning its own teardown

Five follow-ups from #6221 / #6226.

1. A withheld peer-to-peer endpoint is now reported. getUnambiguousPeerHttpAddress and its HTTPS twin
   refuse an address two peers resolve to by returning null (#6202), and every caller decided for itself
   whether to say anything. Neither existing warning covers it: the derive warnings fire whenever an
   address is derived at all, which a healthy homogeneous StatefulSet also does. There is now a one-time
   WARNING per protocol - modelled on warnAmbiguousRouting (#6183), but for the peer-to-peer endpoints -
   naming the peers that could not be told apart, the address they share and the field to declare. HTTP
   and HTTPS have separate latches.

2. GET /api/v1/cluster reports each peer's httpAddress and, only when it is not that peer's alone,
   httpAddressAmbiguous: true. Studio renders it as a warning line on the node's card, and the OpenAPI
   spec documents both. A correctly declared cluster carries neither.

3. The presence matrix (?presence=true) dialled the best-effort address and attributed the answer to the
   peer - the same unattended dial #6221 fixed for the verify endpoint, with the same failure: every peer
   was queried on the leader's own endpoint and reported the leader's databases as its own, so the matrix
   showed every database present everywhere. It resolves through PeerDialAddress now and reports a peer it
   cannot identify as unreachable, with the reason logged.

   RaftClusterStatusExporter.exportClusterStatus(), a second cluster-status JSON builder that nothing
   called, is removed rather than taught about this: the live endpoint is GetClusterHandler, and an
   unreachable second view of one cluster is how two views drift apart.

4. RESYNC_RETRY_TIMEOUT_MS drops from 120s to 30s, set from the instrument #6226 added rather than from a
   suspicion: across nine full ha-integration-tests runs (235 tests each) not one wait exceeded the 10s
   report threshold, and the slowest of the ten classes that use those helpers took 53s wall-clock for the
   whole class, cluster startup and teardown included. 30s is what the rest of the class already treats as
   long enough - waitForReplicationIsCompleted, waitAllReplicasAreConnected and the leader-election wait
   all use it. The report threshold drops to 5s to keep the same resolution for the next cut.

5. DynamicMembershipTest no longer leaves its own teardown holding a peer it evicted to a replica's
   contract. Each method already gets its own cluster (@BeforeEach/@AfterEach, Raft storage under the
   deleted database directory), so the issue's shared-cluster premise does not apply and two of the three
   reported failures were the leaderless-window assert that #6226 already fixed by making findLeaderIndex
   wait. The third is real and now has a regression test: the base class waits for, and compares, exactly
   the servers getServerToCheck() names, turning a 30s-per-evicted-server timeout and a
   DatabaseAreNotIdentical charged to endTest into neither (152s -> 77s for the class, with one more test).

   Seven await().until(() -> findLeaderIndex() >= 0) wrappers that #6226 made redundant are gone, and
   three copies of "only the servers still running" collapse into BaseRaftHATest.startedServers().

Tests: new Issue6267AmbiguousPeerAddressWarningTest (3, warning fires once per protocol, names the peers,
stays silent on a correct cluster, HTTPS not muted by HTTP) and Issue6267AmbiguousAddressVisibilityIT (3,
the status flag on an ambiguous and on a correct cluster, and the presence matrix reporting a peer it
cannot identify as unreachable - confirmed failing against the pre-fix dial). New
DynamicMembershipTest#removedPeerIsNotHeldToTheClusterConsistencyCheck confirmed failing without the
getServerToCheck fix with exactly the reported "DatabaseAreNotIdentical Types: DB1 5 <> DB2 6" at endTest.

Run locally: full ha-raft unit lane (918), the 18 IT classes reached by these changes (35 tests) including
all ten that use the shortened budget, server PluginApiSpecTest, and a full reactor build.
@mergify

mergify Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

@codacy-production

codacy-production Bot commented Aug 17, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics -15 complexity

Metric Results
Complexity -15

View in Codacy

🟢 Coverage 96.77% diff coverage · -6.50% coverage variation

Metric Results
Coverage variation -6.50% coverage variation
Diff coverage 96.77% diff coverage

View coverage diff in Codacy

Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (6b20fb1) 159115 125535 78.90%
Head commit (1e186b1) 191455 (+32340) 138602 (+13067) 72.39% (-6.50%)

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#6284) 62 60 96.77%

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@claude

claude Bot commented Aug 17, 2026

Copy link
Copy Markdown

Review

I read through the full diff (RaftHAServer, GetClusterHandler, RaftClusterStatusExporter, BaseRaftHATest/DynamicMembershipTest, CapturingTestLogger, the two new Issue6267* test classes, the OpenAPI spec/test, and the Studio JS) plus the current state of the checked-out files. Note: I was not able to run mvn compile/verify in this sandbox (Maven invocations were blocked), so the notes below are from static review only - please make sure CI is green before merge.

Overall this is a careful, well-documented follow-up. The reasoning in the PR description matches what's actually in the diff (e.g. confirmed exportClusterStatus() has no remaining callers, confirmed the getServerToCheck() / checkDatabasesAreIdentical() wiring is consistent between BaseRaftHATest and BaseGraphServerTest), and the new tests genuinely exercise the fixed behavior rather than just re-asserting the happy path.

Code quality

  • unambiguousPeerAddresswarnAmbiguousPeerAddress is a clean extension of the existing claimsByAddress/identifiesOnePeer machinery, and reuses the same "declared beats derived" tie-break already used for routing tables. Good consistency.
  • PeerDialAddress.resolve(...) reuse in the presence-matrix fan-out (GetClusterHandler.buildPresenceMatrix) is the right move - it closes the same unattended-dial hole that The cluster-verify endpoint dials peers on an unguarded derived address: it can fan out to itself unboundedly, and reports the self-comparison as ALL_CONSISTENT #6221 already closed for the verify endpoint, through the same guarded helper, instead of hand-rolling a third variant.
  • BaseRaftHATest.serversMatching/startedServers is a nice de-duplication of the three copy-pasted "only running servers" blocks in RaftLeaderDown2NodesIT/RaftLeaderFailoverIT/RaftQuorumLostIT.
  • CapturingTestLogger.countFormattedContaining correctly distinguishes "a warning fired" from "the warning named the right peers/address," which is what lets the new unit tests assert on content, not just occurrence.

Potential issues (minor, non-blocking)

  1. Redundant per-peer resolution in GetClusterHandler (ha-raft/.../GetClusterHandler.java:133-137): getPeerHttpAddress(peer.getId()) and getUnambiguousPeerHttpAddress(peer.getId()) are called back-to-back for every peer, and each independently re-resolves and re-scans all peers to build the claims map (unambiguousPeerAddress is O(peers) per call). That's O(N²) work per /api/v1/cluster request. For real HA cluster sizes this is negligible, so not worth blocking on, but if this pattern gets reused somewhere with a larger peer count it'd be worth returning {address, ambiguous} from a single resolver call instead of two.
  2. One-time-per-protocol WARNING latch vs. a cluster whose topology changes (httpAmbiguityWarned/httpsAmbiguityWarned): once tripped, the log line never fires again even if the specific peers causing the ambiguity change later (e.g. after a membership change). This mirrors the existing httpFallbackWarned/routingFallbackWarned convention in the same file, so it's consistent rather than a new problem, but the API-visible httpAddressAmbiguous flag on /api/v1/cluster stays live/correct regardless - only the log line goes stale. Might be worth a short note in warnAmbiguousPeerAddress's javadoc for the next person who wonders why the log is silent on a cluster they know is misconfigured.
  3. RESYNC_RETRY_TIMEOUT_MS: 120s → 30s is backed by a real measurement (nine CI runs, no wait over the old 10s report threshold) and matches the CLAUDE.md guidance about not lowering budgets on suspicion. The new 5s SLOW_WAIT_REPORT_MS retains resolution. This looks sound as described.

Test coverage

Strong. Issue6267AmbiguousPeerAddressWarningTest covers the once-per-protocol dedup, the correct-cluster silence case, and (importantly) that the HTTPS latch isn't muted by the HTTP one - that last case is exactly the kind of thing that's easy to get wrong with two independent latches. Issue6267AmbiguousAddressVisibilityIT verifies both the status-flag behavior and that the presence matrix reports peers it can't identify as unreachable rather than attributing the local node's databases to them (confirmed failing against the pre-fix dial per the PR description). DynamicMembershipTest#removedPeerIsNotHeldToTheClusterConsistencyCheck is a proper regression test for the actual teardown bug, with a clear "must not receive entries after removal" assertion.

Security

No concerns. The new httpAddress/httpAddressAmbiguous fields expose only configuration the operator already declared (or a derived value already computed for internal routing), not new user input. The Studio JS change escapes the address via escapeHtml before rendering. No new dependencies introduced.

Performance

Changes are confined to status/reporting paths (cluster status endpoint, opt-in presence fan-out, one-time warnings) and test infrastructure - nothing on the replication/write hot path, consistent with the CLAUDE.md performance mandate.

Nice cleanup on the dead exportClusterStatus() builder too - removing an unreachable second view of the same data instead of teaching it about the new field is the right call given it had already silently drifted once.

…roup, not once per peer

Code review follow-up on the two actionable points.

1. GetClusterHandler asked getPeerHttpAddress and getUnambiguousPeerHttpAddress per peer, and
   answering the second means resolving every peer to see who else claims the address - so a
   /api/v1/cluster request resolved the group twice per peer, O(peers²) for a question that is
   O(peers). Cluster sizes make that harmless today, which is why it is worth removing before the
   shape is copied somewhere it is not. RaftHAServer.getPeerHttpEndpoints() answers it for the whole
   group in one pass, returning each peer's address with its verdict; the handler does a lookup.

   It shares claimsByAddress/identifiesOnePeer with the per-peer accessors, so it cannot answer
   differently, and a new test asserts exactly that for every peer of an ambiguous and of a correctly
   declared cluster. An ambiguity seen here trips the same one-time warning.

2. warnAmbiguousPeerAddress's javadoc now says what the latch does NOT do: it is per JVM and never
   rearms, so a membership change that makes a different pair of peers collide is not logged again.
   The line names the peers that tripped it first; httpAddressAmbiguous on GET /api/v1/cluster is
   recomputed per request and is the current answer.

The third point (the latch convention itself) was raised as consistent-not-a-problem and is
unchanged, as is the timeout measurement.

Run: ha-raft unit lane (919), Issue6267AmbiguousAddressVisibilityIT.
@claude

claude Bot commented Aug 17, 2026

Copy link
Copy Markdown

Review

Thorough PR with an unusually well-documented rationale (including CI-run evidence for the timeout reduction). Went through the ha-raft and server changes in detail; nothing blocking, a few small things worth a look.

Code quality

  • RaftClusterStatusExporter.java: after removing exportClusterStatus(), the com.arcadedb.serializer.json.JSONArray and JSONObject imports are now unused (the class no longer references either). Small cleanup, no functional impact since there's no unused-import enforcement in the build, but worth trimming while the file is already touched.
  • RaftHAServer.getPeerHttpEndpoints() reuses claimsByAddress/identifiesOnePeer cleanly, and the new theGroupWideEndpointsAgreeWithThePerPeerAccessors test is a nice guard against the group-wide resolver ever answering differently from the per-peer one.

Potential bugs

  • getPeerHttpEndpoints() (RaftHAServer.java) tracks a single ambiguous variable across the loop and only calls warnAmbiguousPeerAddress with whichever colliding address was seen last in iteration order:
    String ambiguous = null;
    for (int i = 0; i < resolved; i++) {
      final boolean shared = !identifiesOnePeer(claims.get(addresses[i]), fromConfig[i]);
      if (shared)
        ambiguous = addresses[i];
      ...
    }
    if (ambiguous != null)
      warnAmbiguousPeerAddress(false, ambiguous, addresses, owners, resolved);
    If a cluster has two independent colliding pairs in one resolution pass (e.g. peers A/B share one derived address and, separately, C/D share another), only the second pair ever gets named in the one-time log line - the first pair's collision is silently absorbed. The httpAddressAmbiguous flag in the JSON response is still correct for every peer (that part doesn't go through this variable), so this only affects the operator-facing log message's completeness, not the API. Given the latch is documented as "not exhaustive, first-fires-wins", this is a pretty narrow edge case, but since it's new code it seemed worth flagging rather than assuming it was considered.

Performance

  • GetClusterHandler.buildPresenceMatrix() now resolves each non-local peer's dial address via PeerDialAddress.resolve(), which calls getUnambiguousPeerHttpAddress() and re-resolves the entire peer group per peer - i.e. back to the O(peers²) pattern that this same PR's second commit explicitly removed from the main peers array by introducing getPeerHttpEndpoints(). The httpEndpoints map is already computed once earlier in execute() and carries the same ambiguity verdict per peer; the presence-matrix loop could look up httpEndpoints.get(peerId).ambiguous() instead of re-deriving it through PeerDialAddress.resolve() (the self-dial check would still need to run separately, since httpEndpoints doesn't do that). Not a real-world concern at typical cluster sizes (as the PR itself notes elsewhere), just an inconsistency worth a follow-up if the O(peers²) shape is a concern in general.

Security

  • Nothing concerning. The new Studio warning line runs the address through escapeHtml() before interpolating into the card markup, so no XSS surface there.

Test coverage

  • Genuinely strong: Issue6267AmbiguousPeerAddressWarningTest covers the fire-once-per-protocol latch, the HTTPS-not-muted-by-HTTP case, and group-wide/per-peer agreement; Issue6267AmbiguousAddressVisibilityIT exercises the real HTTP endpoint end to end (status flag + presence-matrix unreachable reporting) and is confirmed failing pre-fix. DynamicMembershipTest#removedPeerIsNotHeldToTheClusterConsistencyCheck reproduces the exact reported failure shape. PluginApiSpecTest's exact-field-set pin means the two new peer fields can't silently drift from the spec. Nice touch adding countFormattedContaining to CapturingTestLogger rather than only asserting on the raw template - that's what actually lets the tests check which peers got named.

Overall this looks solid and ready modulo the minor items above.

…a stable order

Round 2 of the code review. Two points taken, one declined.

1. The one-time warning reported a single colliding address - whichever the pass saw last - so a cluster
   with two independent colliding pairs got one of them named and the other silently absorbed. Since the
   latch never rearms, that pair would then be withheld with the log permanently quiet, and an operator
   who declared the two ports the line named would still have a broken cluster. It now names every
   collision the pass found, grouped by address, and takes the latch only once there is something to say.

   The groups and the peers within them are sorted, so the same misconfiguration produces the same line
   whichever node logs it - Ratis hands the group over in its own order, which is what made the first
   version of the new test order-dependent.

2. Dropped the JSONArray/JSONObject/ArcadeDBServer imports that exportClusterStatus() left behind in
   RaftClusterStatusExporter.

Declined: routing buildPresenceMatrix's dial through the precomputed getPeerHttpEndpoints map instead of
PeerDialAddress.resolve(). It would save one O(peers) resolution per peer on a path that then makes a
network round trip per peer with a 5 s timeout - and it would do it by hand-assembling the guard
(self-dial check, the HTTPS endpoint, the refusal text) that #6202/#6221 consolidated into one helper
precisely because call sites had drifted apart. That trade is the wrong way round; the endpoint map is
for reporting, PeerDialAddress is for dialling.

Tests: oneWarningNamesEveryCollisionItFound covers two independent colliding pairs in one line.
Run: ha-raft unit lane (920), Issue6267AmbiguousAddressVisibilityIT, Issue6221VerifyFanOutGuardIT.
@claude

claude Bot commented Aug 17, 2026

Copy link
Copy Markdown

Review

I read through the full diff (docs, GetClusterHandler, RaftClusterStatusExporter, RaftHAServer, the HA test base/fixtures, the two new Issue6267* tests, CapturingTestLogger, the OpenAPI spec/tests, and the Studio JS), and traced the shared claimsByAddress/identifiesOnePeer logic across getPeerHttpEndpoints(), unambiguousPeerAddress(), and selectUnambiguousRouting() to check they can't disagree. Overall this is a well-scoped, well-tested set of follow-ups - the reasoning for each of the 5 items is spelled out in code comments and is easy to verify against the code itself. A few notes:

Correctness - looks solid

  • getPeerHttpEndpoints() (RaftHAServer.java:1951) correctly reuses claimsByAddress/identifiesOnePeer so it can't produce a different ambiguity verdict than the per-peer accessors - verified against the new theGroupWideEndpointsAgreeWithThePerPeerAccessors test, and by inspection.
  • The declared-beats-derived tie-break and the "two declared addresses colliding is unresolvable for both" case are consistent with the pre-existing selectUnambiguousRouting semantics.
  • warnAmbiguousPeerAddress's one-time latch, grouped/sorted collision output, and the "declared peer is never named as withheld" behavior all match their doc comments.
  • checkDatabasesAreIdentical()/getServerToCheck() change in BaseRaftHATest composes correctly with BaseGraphServerTest.checkDatabasesAreIdentical() (which also reads getServerToCheck()), so the evicted-peer exclusion applies consistently to both the wait and the comparison - confirmed by reading BaseGraphServerTest.java:522-527,547-568.
  • DynamicMembershipTest.evict() only records eviction on a successful removal, so the refused (non-forced) removal in removePeerRefusedWhenItWouldBreakQuorum correctly does not exclude that peer from teardown checks.
  • Removing RaftClusterStatusExporter.exportClusterStatus() is safe - confirmed no remaining callers/references anywhere in the repo.

Minor nit

  • SuperNodeConcurrentAppendHABenchmark.java still imports java.util.concurrent.TimeUnit (line 37), but the only use of it was in the Awaitility.await()...TimeUnit.SECONDS/.MILLISECONDS block that this PR removes. It's now an unused import - harmless (won't fail the build, no checkstyle rule enforces this here), but worth a quick cleanup pass.

Design points already discussed in the PR description

  • The declined optimization of routing buildPresenceMatrix's dial through the precomputed getPeerHttpEndpoints() map is reasonable - that path already pays a network round trip per peer, so the extra O(peers) resolution is noise, and keeping PeerDialAddress as the single dial guard avoids re-deriving the self-dial/HTTPS-fallback checks by hand.
  • RESYNC_RETRY_TIMEOUT_MS 120s->30s is backed by nine measured CI runs rather than a guess, and stays 3x the largest observed wait - reasonable margin.

Test coverage

Good coverage for the behavior change: Issue6267AmbiguousPeerAddressWarningTest unit-tests the warning latch (including the two-independent-collisions and HTTP/HTTPS-independence cases) against a real RaftHAServer, and Issue6267AmbiguousAddressVisibilityIT exercises the full HTTP path for both the status flag and the presence-matrix guard on a real 3-node cluster, with a control test asserting a correctly-addressed cluster stays silent. PluginApiSpecTest pins the exact per-peer field set so the new fields can't silently disappear or multiply.

Nothing blocking found. Nice use of CapturingTestLogger.countFormattedContaining to assert on substituted message content (which peers/address) rather than just the raw template.

… SuperNodeConcurrentAppendHABenchmark

Round 3 of the code review, its only point: the class's sole use of TimeUnit was the
Awaitility.await(...).atMost(30, TimeUnit.SECONDS) wrapper this PR removed.
@claude

claude Bot commented Aug 17, 2026

Copy link
Copy Markdown

Review

I read through the full diff (main-code changes in RaftHAServer.java, GetClusterHandler.java, RaftClusterStatusExporter.java, the BaseRaftHATest/DynamicMembershipTest test-hygiene changes, and the two new test classes) and traced the code paths by hand (compiling wasn't available in this environment, so this is a careful static read rather than a build-verified one).

Overall this is a well-scoped, well-documented change. A few observations, nothing blocking:

Correctness

  • RaftHAServer.getPeerHttpEndpoints() correctly reuses the shared claimsByAddress/identifiesOnePeer pair, so it can't disagree with the per-peer accessors, and theGroupWideEndpointsAgreeWithThePerPeerAccessors test actually pins that invariant. Good.
  • warnAmbiguousPeerAddress's %s placeholder order lines up correctly with the passed arguments (protocol, collisions, protocol, protocol, field, HA_SERVER_LIST.getKey()), easy to get wrong with 6 placeholders but it checks out.
  • GetClusterHandler's peer-loop writes exactly the 14 fields PluginApiSpecTest now pins (httpAddress/httpAddressAmbiguous added to the existing 12), and both are written conditionally as documented.
  • RaftClusterStatusExporter.exportClusterStatus() removal is confirmed dead, no remaining callers anywhere in the repo (grep -rn "exportClusterStatus" turns up only the removed definition).
  • The getServerToCheck()/serversMatching()/startedServers() refactor in BaseRaftHATest is behavior-preserving for the three pre-existing overrides (RaftLeaderDown2NodesIT, RaftLeaderFailoverIT, RaftQuorumLostIT), they now just delegate to the new helper instead of hand-rolling the same two-pass count-then-fill loop.
  • Removing the redundant Awaitility.await(...).until(() -> findLeaderIndex() >= 0) wrappers is safe: findLeaderIndex() already polls internally up to LEADER_ELECTION_TIMEOUT_MS (added in fix(#6221): a cluster verify dials a peer it can identify, or reports it unverified #6226), so the wrapper was just calling it twice.
  • CapturingTestLogger.substitute() drops only trailing nulls before calling String.format, and catches RuntimeException around the format call, falling back to the raw template on mismatch, reasonable given the fixed 17-arg overload always pads with nulls.

Minor / non-blocking nits

  • httpAmbiguityWarned/httpsAmbiguityWarned (and the pre-existing httpFallbackWarned/httpsFallbackWarned they're modeled on) are instance fields, not static, but several javadoc comments describe the latch as "per JVM." In production there's one RaftHAServer per process so it's effectively true, but the wording is a little imprecise, worth a follow-up doc tweak if anyone touches this area again, not worth a change on its own.
  • DynamicMembershipTest.evict() centralizes eviction bookkeeping nicely, but it's worth double-checking under CI load that removePeer() truly blocks until the Raft reconfiguration is committed before removedPeerIsNotHeldToTheClusterConsistencyCheck writes through the leader, if it's asynchronous under any code path, the new test could be flaky. The other membership tests' immediate getLivePeers() assertions right after removePeer() suggest it is synchronous, so this is likely a non-issue.

Test coverage

Good, new behavior is covered from three angles: a unit test against a detached RaftHAServer for the warning itself (including the two-independent-collisions and HTTP/HTTPS-don't-mute-each-other cases), an integration test for the GET /api/v1/cluster visibility and presence-matrix behavior, and a regression test for the DynamicMembershipTest teardown-poisoning bug with a reproduction note in the PR description. The PluginApiSpecTest pin on the exact field set is a nice touch that prevents silent schema drift.

Security / performance

No security concerns; the new code doesn't touch authentication/authorization paths and only exposes address/ambiguity metadata already implicitly derivable from log warnings. getPeerHttpEndpoints() explicitly fixes an O(peers squared) shape in GetClusterHandler (each peer no longer re-resolves the whole group), which is a nice performance win beyond the correctness fix.

I did not find any functional bugs in this diff.

…iction test is not racy

Round 4 of the code review; it found no functional issue, both notes are wording.

- The javadoc called the warning latch "per JVM". It is a field on this RaftHAServer, like every other
  warn-once latch in the class - true per process where there is one server, and not true in a test that
  builds several, which is exactly what the new unit tests rely on. Says that now.

- The review flagged that removedPeerIsNotHeldToTheClusterConsistencyCheck would be flaky if removePeer()
  did not block until the reconfiguration commits. It does: RaftClusterManager.removePeer drives Ratis's
  blocking admin().setConfiguration(...) and returns only on a successful reply. Written down on evict(),
  where the next reader will ask the same question - the assertions that follow it read committed
  membership, and a write issued after it cannot still reach the evicted peer.

Run: DynamicMembershipTest (7), Issue6267AmbiguousPeerAddressWarningTest (5).
@lvca
lvca merged commit 77f8b45 into main Aug 17, 2026
12 of 15 checks passed
@lvca
lvca deleted the issue-6267 branch August 17, 2026 03:43
@claude

claude Bot commented Aug 17, 2026

Copy link
Copy Markdown

Review

I went through the diff (docs, RaftHAServer/GetClusterHandler/RaftClusterStatusExporter in ha-raft, the test-harness changes in BaseRaftHATest/DynamicMembershipTest, the two new IT/unit test classes, PluginApiSpec, and studio-cluster.js). Overall this is very solid work: precise root-causing of the original issue, a coherent design, and the test coverage is unusually thorough (positive + negative cases, a control test for "no false positives," and a group-wide/per-peer consistency test). No blocking issues found. A few notes:

Correctness (verified, no bugs found)

  • RaftHAServer.getPeerHttpEndpoints() / warnAmbiguousPeerAddress() / unambiguousPeerAddress(): the shared claimsByAddress/identifiesOnePeer pair is used consistently between the per-peer accessors and the new group-wide method, so they can't drift apart - and theGroupWideEndpointsAgreeWithThePerPeerAccessors pins that directly. Good defensive test.
  • Log format string in warnAmbiguousPeerAddress - I counted the %s placeholders against the vararg list (protocol, collisions, protocol, protocol, field, HA_SERVER_LIST.getKey()) and they match (6/6). No MissingFormatArgumentException risk.
  • GetClusterHandler.buildPresenceMatrix now goes through PeerDialAddress.resolve(...) for the non-local peer case; the self-peer branch stays unguarded by design and is checked before the dial, so no accidental self-dial or double counting.
  • BaseRaftHATest.checkDatabasesAreIdentical() / getServerToCheck() refactor: super.checkDatabasesAreIdentical()'s comparison loop starts at index 1, so getServerToCheck() returning a single element (as happens in removePeerRefusedWhenItWouldBreakQuorum once both non-leaders are evicted) is a safe no-op rather than an off-by-one.
  • DynamicMembershipTest.evict() records evictedServers only after removePeer returns, so a refused (quorum-guard) removal correctly leaves the peer in the teardown check - exactly the case the class's own comment calls out, and it's right.
  • Confirmed RaftClusterStatusExporter.exportClusterStatus() genuinely has no remaining callers repo-wide before removal (grepped exportClusterStatus - zero hits after the change).
  • studio-cluster.js: the new httpAddress/httpAddressAmbiguous rendering goes through escapeHtml(...), consistent with the rest of the file.

Minor / non-blocking

  • docs/release-26.9.1.md: the new ## A peer address... section is inserted directly above ## The same iteration-knob guard... without a blank line separating the two ## headings. Might render fine depending on the markdown processor, but worth a quick visual check since every other section in that file appears to have a blank line before the next ##.
  • getPeerHttpEndpoints() only ever calls warnAmbiguousPeerAddress(false, ...) (HTTP only) - so an HTTPS-only collision on a cluster where nothing has yet called getUnambiguousPeerHttpsAddress (e.g. no resync/verify has fired) won't be flagged by GET /api/v1/cluster or Studio; only the HTTP field is exposed there. That matches the stated scope of this PR (OpenAPI spec/Studio changes only add the HTTP field), so it's presumably intentional, but a one-line callout in the docs section that HTTPS ambiguity isn't yet surfaced in cluster status could help set the right operator expectation.
  • DynamicMembershipTest's new imports mix java.util.* and com.arcadedb.* in a slightly unconventional order (com.arcadedb.database.Database grouped after the java.util block rather than with com.arcadedb.exception.ConfigurationException's neighbors) - but this matches the file's pre-existing (already non-standard) import grouping, so it's not something this PR introduced.

Test coverage
Strong. Issue6267AmbiguousPeerAddressWarningTest covers: single collision reporting, latch-doesn't-rearm, multiple independent collisions named in one line, a "stays silent" control, HTTP/HTTPS latch independence, and group-wide/per-peer agreement. Issue6267AmbiguousAddressVisibilityIT covers the same shape at the HTTP-endpoint level (status flag + presence-matrix unreachable reporting), including a control test for the non-ambiguous case so the assertions can't pass vacuously. DynamicMembershipTest#removedPeerIsNotHeldToTheClusterConsistencyCheck is a good regression test with a clear "confirmed failing before the fix" note in the PR description.

I did not run the build in this environment (sandboxed, no mvn execution available to me here), so please make sure CI is green, but nothing in the diff itself looks like it would fail to compile or break an existing test.

Nice work on the investigation write-up in the PR description too - the correction about DynamicMembershipTest's per-method cluster lifecycle before diagnosing the real bug is a good example of not taking the issue's premise at face value.

@codecov

codecov Bot commented Aug 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.93548% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.21%. Comparing base (6b20fb1) to head (1e186b1).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
...com/arcadedb/server/ha/raft/GetClusterHandler.java 72.72% 1 Missing and 2 partials ⚠️
...java/com/arcadedb/server/ha/raft/RaftHAServer.java 95.91% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6284      +/-   ##
==========================================
+ Coverage   70.14%   70.21%   +0.06%     
==========================================
  Files        1840     1840              
  Lines      159115   159102      -13     
  Branches    33556    33556              
==========================================
+ Hits       111619   111711      +92     
+ Misses      33619    33512     -107     
- Partials    13877    13879       +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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.

Follow-ups from #6221: a self-poisoning membership test, an unmeasured timeout, an invisible address ambiguity, and two cleanups

1 participant