Skip to content

Filter offline DataNode URLs and add Java client seed fallback - #18921

Merged
JackieTien97 merged 3 commits into
masterfrom
codex/available-urls-seed-fallback
Oct 20, 2026
Merged

JackieTien97 merged 3 commits into
masterfrom
codex/available-urls-seed-fallback

Conversation

@JackieTien97

@JackieTien97 JackieTien97 commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Description

Fixes #18633.

Supersedes #18920.

Avoid repeatedly routing Java SDK queries to DataNodes that the server already considers offline, while retaining recovery through the client's initial addresses when a rolling restart makes the latest discovery snapshot stale.

Rolling restarts and recovery

A client can discover only nodes 2 and 3 while node 1 is stopped. Node 1 may recover before the next successful refresh, while later attempts to the cached endpoints fail. The Java client first tries the discovered endpoints, then falls back to its initial nodeUrls, so the omitted node remains a recovery candidate if it was configured as a seed.

Business reconnection and background discovery prefer discovered endpoints before fallback seeds. Background discovery defers candidates that returned query errors, as described below. Business reconnection rotates both tiers independently, and background discovery rotates its fallback seed tier. Literal duplicate endpoints are tried once per business reconnection round. A successful business reconnection through a seed requests a coalesced background refresh.

Discovery and lifecycle behavior

  • SHOW AVAILABLE URLS excludes Unknown, Stopped, Removing, and ReadOnly nodes with reason Stopping. Other ReadOnly nodes, including responses from older ConfigNodes without a reason field, remain eligible.
  • Session and all SessionPool constructors copy their initial addresses. Reopening a Session after failover bootstraps discovery from its retained default endpoint while keeping only the initial addresses as permanent seeds. An offline initial seed therefore does not replace the live discovery entry point after reopen. With automatic discovery disabled, the static list keeps the retained default endpoint followed by all initial addresses, deduplicated, so later failover can recover through an earlier node.
  • Refresh runs are serialized separately from client publication and cleanup. Closing a supplier marks it closed and aborts the published socket without waiting for the synchronized RPC client or walking the remaining candidates. The refresh owner releases buffers after its I/O stops. A transport that finishes creation after closure is disposed without publication; aborting before TCP connect cannot reopen the socket.
  • Transport failures can advance to another candidate. A query status error or Thrift application error ends the refresh round and preserves the last usable snapshot, but closes the failed connection. Subsequent rounds put query-failed endpoints after the other candidates, in failure order, until a discovery query succeeds. This lets discovery leave a DataNode that can accept client connections but cannot reach the ConfigNode, and prevents two failing nodes from starving another candidate or recovery seed. When all candidates are reachable but their queries fail, each refresh opens at most one session and issues one query; it does not sweep the cluster in a single round.
  • Empty results and results containing only wildcard addresses retain the last usable snapshot. Query selection reads one snapshot, and seeds are not merged into the normal query rotation.

Upgrade and address compatibility

Upgrade Java clients to a version with seed fallback before upgrading servers. Configure several stable seeds and keep at least one reachable during a rolling upgrade. The server filter affects all clients; older Java SDKs and the C++ client do not acquire fallback through a server upgrade. Their maintenance plan must retain a reachable discovered endpoint, or use stable explicit endpoints with automatic discovery disabled until the client supports fallback.

Deduplication remains based on literal host/IP and port. The builder Javadocs recommend a consistent address representation between seeds and advertised endpoints; mixing aliases such as localhost and 127.0.0.1 can still cause duplicate attempts and extra refreshes/reconnections. DNS resolution is not used to infer permanent node identity. Wildcard bind addresses should be replaced with reachable addresses in nodeUrls.

Recovery requires at least one reachable configured seed when every discovered endpoint fails. Endpoint backoff for nodes reported as Running but unreachable from a particular client is outside this change.

Validation

Validated on JDK 17 in an isolated worktree:

  • 254 targeted unit tests passed: 251 session/client tests and 3 ShowAvailableUrlsTaskTest tests.
  • Regression coverage includes closing during a blocked openSession or discovery query with timeout zero; aborting during or before TCP connect; disposing a transport created after closure; close/reopen after failover for both nodeUrls and single-host Sessions; recovery from persistent node-local status/application errors; bounded cluster-wide query failures and subsequent recovery; discovery through an omitted seed after query failures; static endpoint retention and a second failover after reopen; empty/all-wildcard snapshots; legacy SessionPool constructor mutation; bounded retries; and ReadOnly shutdown reasons.
  • 2 SessionSeedNodeFallbackIT tests passed on a real 1 ConfigNode / 3 DataNode cluster: Session and SessionPool data queries during successive node restarts, and business/independent background recovery through a seed omitted from a deliberately stale discovery snapshot. The latter uses a metadata query with two DataNodes offline.
  • Full-reactor mvn test-compile -DskipTests and mvn test-compile -DskipTests -P with-zh-locale passed.
  • Spotless, whitespace checks, and RAT passed without additional license exclusions in this worktree.

The integration tests poll for the expected snapshot because a direct NodesSupplier.run() call may return while another refresh holds the lock. The immediate business-triggered refresh is still checked independently without polling it manually.

The rolling-restart integration test tolerates IoTDBConnectionException only when closing stale business sessions during cleanup. Query and discovery failures are not ignored.


This PR has:

  • been self-reviewed.
  • documented discovery behavior, address representation, and upgrade order.
  • added comments explaining recovery ordering and connection ownership.
  • added unit tests covering the new paths and concurrency races.
  • added integration tests and been tested in an IoTDB cluster.
Key changed/added classes

INodeSupplier, NodesSupplier, ThriftConnection, SessionConnection, Session, SessionPool, ShowAvailableUrlsTask, their builder documentation, SessionSeedNodeFallbackTest, ThriftConnectionLifecycleTest, ShowAvailableUrlsTaskTest, and SessionSeedNodeFallbackIT.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Session reopening can strand discovery on offline seeds, and refresh synchronization can delay shutdown through connection attempts.

2 open issues
What changed in this PR

Addresses offline-node discovery delays while preserving Java client recovery through configured seeds during rolling restarts.

Changes:

  • Filters offline DataNodes from SHOW AVAILABLE URLS.
  • Adds discovered-first, seed-second recovery and coalesced discovery refreshes.
  • Adds unit and cluster tests for filtering, recovery, and concurrent endpoint selection.
File Description
iotdb-core/​datanode/​src/​test/​java/​org/​apache/​iotdb/​db/​queryengine/​plan/​execution/​config/​metadata/​ShowAvailableUrlsTaskTest.java Tests status filtering and empty results.
iotdb-core/​datanode/​src/​main/​java/​org/​apache/​iotdb/​db/​queryengine/​plan/​execution/​config/​metadata/​ShowAvailableUrlsTask.java Excludes offline DataNodes.
iotdb-client/​session/​src/​test/​java/​org/​apache/​iotdb/​session/​SessionSeedNodeFallbackTest.java Tests seed recovery and discovery snapshots.
iotdb-client/​session/​src/​main/​java/​org/​apache/​iotdb/​session/​SessionConnection.java Adds bounded, deduplicated seed fallback.
iotdb-client/​session/​src/​main/​java/​org/​apache/​iotdb/​session/​Session.java Preserves initial recovery addresses.
iotdb-client/​session/​src/​main/​java/​org/​apache/​iotdb/​session/​pool/​SessionPool.java Copies configured seed URLs.
iotdb-client/​session/​src/​main/​java/​org/​apache/​iotdb/​session/​NodesSupplier.java Adds background fallback and snapshot-safe selection.
iotdb-client/​isession/​src/​main/​java/​org/​apache/​iotdb/​isession/​INodeSupplier.java Adds seed and refresh hooks.
integration-test/​src/​test/​java/​org/​apache/​iotdb/​session/​it/​SessionSeedNodeFallbackIT.java Tests rolling restarts and omitted-seed recovery.

Review effort: Balanced

}

@Override
public synchronized void run() {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, with one nuance about how much of this is new. Measured with close() called 300 ms into a refresh (A/B, same harness as above):

candidates base close() head close()
3 hosts that accept TCP but never answer, connectionTimeoutInMs = 1500 1251 ms 4274 ms
3 black-holed hosts (SYN dropped), connectionTimeoutInMs = 2000 0 ms 5726 ms
  • Base already waits for an in-flight attempt whose TCP connection has been accepted. ThriftConnection.close() sends closeSession through the synchronized client, so it waits for the pending openSession. While the TCP connect itself is still pending, TSocket.isOpen() is false, close() is a no-op, and base returns immediately. The refresh then carries on in the background: about 6 s in both versions.
  • Head waits for every remaining candidate (discovered + seeds) plus the final SHOW AVAILABLE URLS. run() holds the monitor for the whole loop, and the loop never rechecks closed.

With the default connectionTimeoutInMs = 0, a peer that accepts but never answers blocks close() forever on both base and head; that part is pre-existing. Head extends the wait to every candidate. With timeout 0, each black-holed host costs the OS SYN timeout: about 21 s on Windows, about 127 s on Linux. Session.close(), re-open() and SessionPool.close() all go through supplier.close(). Session.close() and SessionPool.close() are synchronized, so their callers are blocked as well.

Suggestion:

  • Don't hold the monitor that close() needs across network I/O. Serialize refreshes with a separate lock or tryLock.
  • Check closed before each candidate and again after a successful connect.
  • Let close() close the in-flight transport directly so a blocked attempt is aborted.

Please keep the race this synchronized fixes as a side effect: on base, run() can still create and leak a connection after close().

} else {
return SessionUtils.parseSeedNodeUrls(nodeUrls);
}
return SessionUtils.parseSeedNodeUrls(initialNodeUrls);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed with an A/B run: the same test class against base bacbd5a6dc and head c600593, reusing the real-Thrift TestNode harness from SessionSeedNodeFallbackTest.

Scenario: nodeUrls = [A]; discovery returns [A, B]; A stops and the server now advertises only B; the default connection's reconnect() moves to B; then close(), open() and 10 executeQueryStatement calls.

base head
new supplier's list after reopen [B] [A] (A is unreachable, so it never refreshes)
connection attempts to A during the 10 queries 0 10

On head, getQueryEndPoint() returns A for every query, so each query first tries to open a connection to A before falling back to the default connection. Against a host that is really down, each of those attempts is a full connect timeout. That is the #18633 symptom, now for every query instead of 1/N. Host/port sessions that failed over or were redirected behave the same way. SessionPool is not affected because pooled sessions share the pool's supplier.

Suggested fix: bootstrap discovery from defaultEndPoint (when set) followed by the initial URLs, and keep only the initial URLs as permanent seeds. testSessionRetainsInitialUrlsAfterDefaultEndpointChanges currently asserts the regressing behaviour, so it needs to change. A close/reopen-after-failover test would lock the fix in.

@JackieTien97 JackieTien97 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary

The direction looks right. Filtering offline DataNodes on the server, plus a seed fallback on the client, fixes the #18633 scenario. SessionSeedNodeFallbackTest passes locally (JDK 21) and in CI (10/10).

I reran the suspicious paths with one test class against base bacbd5a6dc and head c600593, reusing the real-Thrift TestNode harness from SessionSeedNodeFallbackTest:

Scenario base head
close() during a refresh; 3 hosts that accept TCP but never answer; 1.5 s timeout blocked 1251 ms blocked 4274 ms
close() during a refresh; 3 black-holed hosts; 2 s timeout blocked 0 ms blocked 5726 ms
nodeUrls = [A]; A stops → fail over to B → close()/open() → 10 queries list [B], 0 attempts to A list stuck at [A], 10 of 10 queries try A first
every SHOW AVAILABLE URLS row is 0.0.0.0 list = seeds; queries round-robin list []; no query endpoint
SHOW AVAILABLE URLS fails on every node; one refresh 1 failed query, 0 new sessions, ~1.0 s 3 failed queries + 2 new sessions, ~3.0 s
seeds as localhost, discovery returns 127.0.0.1; all nodes down; one reconnect() 6 attempts 12 attempts

Should fix before merge. Both Copilot threads hold up; my replies there have the details and suggested fixes.

  1. After a failover, reopening a Session seeds discovery with the offline initial node (Session.java L608).
  2. synchronized run() makes close() wait for every candidate (NodesSupplier.java L213).

Worth changing or documenting. Details are inline.

  1. A query failure on a reachable node is treated like an unreachable node.
  2. Publishing an empty discovery result changes behaviour in two cases: a wildcard dn_rpc_address, and the window right after a ConfigNode leader change.
  3. A node in graceful shutdown (ReadOnly + Stopping) is still advertised after its client RPC port has closed.
  4. Seeds and discovered endpoints are deduplicated by literal TEndPoint equality.

Compatibility. The server-side filter applies to every client, but only the updated Java SDK gets the seed fallback. Older Java SDKs and the C++ client have no fallback: C++ NodesSupplier::refreshEndpointList() replaces its list, and SessionConnection::reconnect() only uses the discovered list. Until now these clients kept Stopped nodes as recovery candidates. During a rolling restart they now only have the nodes that were online at the last refresh. The #18633 thread already noted that this server-side change "involves server-side API semantics and compatibility with other clients". Please state the upgrade order (clients first) in the release note, or port the seed fallback to the C++ client.

Missing tests:

  • close() during a blocked refresh
  • close()/open() after a failover
  • all-wildcard rows
  • ReadOnly + Stopping in ShowAvailableUrlsTaskTest

CI:

  • unit-test (17, ubuntu/windows, others) fails in the ConfigNode tests RestartDuringRemovePeerTest and RemovePeerRestartConsensusTest. These fail on master too (run 39260519861), so they are unrelated.
  • dual-table-manual-basic fails in IoTDBPipePermissionIT.testSourcePermission. That test runs over JDBC + pipe and doesn't use Session discovery, so it looks unrelated; worth a re-run.

FYI, unrelated and pre-existing: in the C++ client, SessionConnection.h calls std::find(availableNodes->getEndPointList().begin(), availableNodes->getEndPointList().end(), ...). getEndPointList() returns by value, so begin() and end() come from two different temporaries. That is undefined behaviour on the retry path.

candidates.addAll(seedNodes);
for (TEndPoint endPoint : candidates) {
if (attempted.add(endPoint)) {
if (createConnection(endPoint) && updateDataNodeList()) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A query failure is treated like an unreachable node. createConnection(endPoint) && updateDataNodeList() moves on to the next candidate in both cases: when the node can't be reached, and when it was reached but SHOW AVAILABLE URLS failed.

So a cluster-wide query failure opens a new session on every discovered node and every seed. One example is a cluster with no ConfigNode leader, where showDataNodes fails on every DataNode. This happens on every refresh of every Session, while the monitor from the L213 thread is held.

Measured for one refresh with 3 nodes whose query fails after 1 s:

  • base: 1 failed query, 0 new sessions, ~1.0 s
  • head: 3 failed queries + 2 new sessions, ~3.0 s

Suggestion: move to the next candidate only when the connection fails. If a reachable node fails the query, end this round and keep the current snapshot.

availableNodes = res;
}
// An empty successful result is authoritative too; seeds remain available for recovery.
availableNodes = Collections.unmodifiableList(res);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An empty result used to mean "no information"; now it means "no nodes". In practice an empty filtered result appears while the responding node itself is alive:

  • With dn_rpc_address=0.0.0.0, the wildcard filter above drops every row. The official docker/src/main/Dockerfile-1c1d sets exactly this.
  • Right after a ConfigNode leader change, node statistics default to Unknown (NodeStatistics.generateDefaultNodeStatistics()). Until the first heartbeats arrive, the server filters out every node.

Measured with all rows 0.0.0.0: base keeps the seeds, so getQueryEndPoint() keeps round-robining. Head publishes [], and getQueryEndPoint() returns empty.

Consequences:

  • Queries are no longer spread across nodes.
  • Some RPCs go through callWithRetryAndReconnect: createTimeseries, templates, setTimeZone, raw/last/aggregation queries and others. For these, the !availableNodes.get().contains(this.endPoint) check (SessionConnection.java L1034) is always true, so every retryable status forces a reconnect plus a refresh request.
  • The empty snapshot stays until the next periodic refresh, up to 60 s.

Suggestion: if every row was a wildcard, treat the result as no information and keep the previous snapshot. If the server genuinely returned no rows, keep the previous snapshot, or at least publish the responder (client.endPoint). testEmptyDiscoveryFallsBackWithoutLosingSeeds and testQuerySelectionDuringEmptyDiscoveryRefresh would need to follow.

Comment on lines +61 to +63
if (NodeStatus.Removing.getStatus().equals(status)
|| NodeStatus.Unknown.getStatus().equals(status)
|| NodeStatus.Stopped.getStatus().equals(status)) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A node in graceful shutdown is still advertised. DataNodeShutdownHook.run() does three things in this order:

  1. It stops ExternalRPCService (L117).
  2. It sets ReadOnly with reason NodeStatus.STOPPING (L120-122).
  3. It reports Stopped to the ConfigNode leader at the very end, after DataNode.stop() (L203).

Between steps 2 and 3 the node runs the WAL flush, syncCloseAllProcessor() and Ratis snapshots, and waits for pipes. Heartbeats keep reporting ReadOnly/Stopping, so this filter still returns the node although its client port is already closed. SessionSeedNodeFallbackIT waits for Stopped before calling run(), so it does not cover this window.

TDataNodeInfo already carries statusReason; NodeManager.getRegisteredDataNodeInfoList() fills it from the heartbeat sample. This filter could also skip ReadOnly when NodeStatus.STOPPING.equals(dataNodeInfo.getStatusReason()). An older ConfigNode that doesn't send the reason keeps today's behaviour. A ShowAvailableUrlsTaskTest case for ReadOnly + Stopping would cover it.

endPointList.addAll(((INodeSupplier) availableNodes).getSeedNodes());
}
// Try every discovered endpoint before falling back to seeds, once per endpoint per round.
endPointList = new ArrayList<>(new LinkedHashSet<>(endPointList));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deduplication is by literal TEndPoint. If nodeUrls use host names and dn_rpc_address uses IPs (or the reverse), every node appears twice: here, and in the candidates of NodesSupplier.run().

Measured with seeds localhost:p1/p2 and discovery returning 127.0.0.1:p1/p2, all nodes down: one reconnect() makes 12 attempts on head versus 6 on base. Each attempt can be a full connect timeout.

There are two more side effects:

  • Connecting through the host-name form of an already discovered node also calls requestRefresh().
  • After that, this.endPoint is never in the discovered list, so callWithRetryAndReconnect reconnects on every retryable status.

Suggestion: document that seeds should use the same address form as dn_rpc_address, or deduplicate on resolved addresses.

Collections.rotate(endPointList, -random.nextInt(endPointList.size()));
}
if (availableNodes instanceof INodeSupplier) {
endPointList.addAll(((INodeSupplier) availableNodes).getSeedNodes());

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: seeds are appended in a fixed order, and for SessionPool the shared supplier's seeds are never shuffled. When all discovered endpoints fail, every pooled session falls back to the same first live seed, and nothing rebalances them afterwards. Rotating the seed part randomly in each round, as already done for the discovered part, would spread that load.

throw new IllegalArgumentException(SessionMessages.NODE_URLS_EMPTY);
}
this.nodeUrls = builder.nodeUrls;
this.nodeUrls = Collections.unmodifiableList(new ArrayList<>(builder.nodeUrls));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: only the builder constructor copies nodeUrls. The List<String> constructors (L506) still keep the caller's list, and later constructNewSession() calls read it. Copying there too would match "independent copies" in the description.

@JackieTien97 JackieTien97 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round 2 on c240197

I reran the round-1 A/B harness on this commit. Every round-1 item is fixed:

  • close() during a refresh returns in 0 ms in all three cases. That includes the timeout-0 peer that accepts but never answers, which hangs on base.
  • Reopen after a failover makes 0 attempts to the stopped node.
  • A cluster-wide query failure costs 1 query and no new sessions per refresh.
  • Wildcard and empty results keep the previous snapshot.
  • ReadOnly + Stopping is filtered.
  • Seeds are rotated, and the pool copies the URL list.

Locally, SessionSeedNodeFallbackTest passes 18/18 and ThriftConnectionLifecycleTest passes 3/3. Dropping the explicit closeSession is fine, because the server's deleteContext() → handleClientExit() closes the session.

One new issue and three nits, inline:

  1. A node that keeps failing the discovery query now pins discovery (NodesSupplier.java L420-426).
  2. run() became best-effort (tryLock), but SessionSeedNodeFallbackIT still calls it as a synchronous refresh.
  3. nit, pre-existing: with enableAutoFetch(false), a reopen shrinks the static list to one endpoint.
  4. nit: placement of the RELEASE_NOTES.md section.

Comment on lines +420 to +426
if (!(e.getCause() instanceof TApplicationException)) {
throw e;
}
// A Thrift application error also means the peer responded; it is not a transport failure.
} catch (Exception e) {
// A query error (for example no ConfigNode leader) is not evidence of a dead endpoint.
// Keep this connection and snapshot, and let the next scheduled refresh retry the query.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A node that keeps failing the discovery query now pins discovery. Both branches here keep the connection and end the round. The next scheduled refresh starts from the same client, so discovery never moves away from this node while it stays reachable.

A typical case is a DataNode that is cut off from the ConfigNode while clients can still reach it. The ConfigNode marks it Unknown, which is exactly the node the new server-side filter is meant to drop.

A/B with the same harness:

  • Discovery first connects to A while A is healthy; snapshot [B, C, A].
  • Then only A's query keeps failing, while B and C answer [B, C].
  • Three refreshes follow.
snapshot after 3 refreshes queries A / B / C
base bacbd5a6dc [B, C] 1 / 2 / 0
c600593 [B, C] 1 / 3 / 0
c240197 [B, C, A] (frozen) 3 / 0 / 0

While discovery is pinned, the snapshot is frozen: A stays in the query rotation, and restarts of other nodes are not picked up. testQueryStatusFailureDoesNotRotateConnections and testQueryApplicationFailureDoesNotRotateConnections assert this behaviour.

Ending the round on a query error is right; it removed the sweep. The problem is keeping the connection for the next round. Suggestion: after a query error, close the connection and start the next round from a different candidate, for example by putting the failed endpoint last. A cluster-wide failure then costs at most one new connection and one query per refresh, which is still bounded.


@Override
public void run() {
if (closed || !refreshLock.tryLock()) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

run() is now best-effort. With tryLock(), a direct call returns immediately, without refreshing, while another refresh holds the lock. Scheduled and requested refreshes share the session/pool executor, so they are already serialized and unaffected. Callers outside that executor are affected.

SessionSeedNodeFallbackIT calls sessionSupplier.run() / poolSupplier.run() directly and asserts right afterwards (L100-101 and L108-109). With synchronized in c600593, such a call waited for the in-flight refresh and then refreshed again. Now, if a scheduled refresh happens to be running, it can return before the snapshot changes. The unit tests already poll (awaitSnapshot); polling in the IT as well would keep it deterministic.

Comment on lines +609 to +611
return defaultEndPoint == null
? SessionUtils.parseSeedNodeUrls(initialNodeUrls)
: Collections.singletonList(defaultEndPoint.deepCopy());

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, pre-existing: with enableAutoFetch(false), open() builds new DummyNodesSupplier(getNodeUrls()), and DummyNodesSupplier has no seeds. After a failover and close()/open(), this method returns only [defaultEndPoint], so the static list shrinks to a single endpoint.

Checked with nodeUrls = [A, B]: after a failover to B and a reopen, the static list is [B]. When B then stops, reconnect() fails even though A is back. Base behaves the same way. c600593 happened to keep the full list, because getNodeUrls() always returned initialNodeUrls there.

Passing [defaultEndPoint] + initialNodeUrls (deduplicated) to DummyNodesSupplier would keep failover working after a reopen.

Comment thread RELEASE_NOTES.md Outdated

-->

# Unreleased

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: on master, RELEASE_NOTES.md only changes in the release-info PRs (#18164, #17468, #17275, ...), so an # Unreleased section added by a feature PR may be moved or rewritten by the release manager. The upgrade-order note (clients before servers) might fit better in the PR description or the user docs, where it gets picked up at release time.

Close query-failed connections and defer their endpoints across refresh
rounds while preserving the last usable snapshot and bounded query retries.
Retain all initial addresses when reopening a Session without discovery.
Poll for integration-test snapshots and keep upgrade guidance in the PR.
@JackieTien97
JackieTien97 merged commit 85277b3 into master Oct 20, 2026
35 of 40 checks passed
@JackieTien97
JackieTien97 deleted the codex/available-urls-seed-fallback branch October 20, 2026 08:41
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.

[Bug] Java SDK 自动发现持续选择已断网的 DataNode,导致查询周期性等待连接超时

2 participants