This is an automated email from the ASF dual-hosted git repository.

JackieTien97 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git


The following commit(s) were added to refs/heads/master by this push:
     new 85277b3b689 Filter offline DataNode URLs and add Java client seed 
fallback (#18921)
85277b3b689 is described below

commit 85277b3b689f4a9cf7a1bd779a57c09c9deeb588
Author: Jackie Tien <[email protected]>
AuthorDate: Tue Oct 20 16:41:17 2026 +0800

    Filter offline DataNode URLs and add Java client seed fallback (#18921)
---
 .../session/it/SessionSeedNodeFallbackIT.java      | 254 +++++++
 .../org/apache/iotdb/isession/INodeSupplier.java   |  11 +
 .../org/apache/iotdb/session/NodesSupplier.java    | 276 ++++++--
 .../java/org/apache/iotdb/session/Session.java     |  31 +-
 .../apache/iotdb/session/SessionConnection.java    |  31 +-
 .../apache/iotdb/session/TableSessionBuilder.java  |   3 +
 .../org/apache/iotdb/session/ThriftConnection.java | 127 +++-
 .../org/apache/iotdb/session/pool/SessionPool.java |  13 +-
 .../session/pool/TableSessionPoolBuilder.java      |   3 +
 .../iotdb/session/SessionSeedNodeFallbackTest.java | 751 +++++++++++++++++++++
 .../session/ThriftConnectionLifecycleTest.java     | 207 ++++++
 .../config/metadata/ShowAvailableUrlsTask.java     |   8 +-
 .../config/metadata/ShowAvailableUrlsTaskTest.java | 101 +++
 13 files changed, 1715 insertions(+), 101 deletions(-)

diff --git 
a/integration-test/src/test/java/org/apache/iotdb/session/it/SessionSeedNodeFallbackIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/session/it/SessionSeedNodeFallbackIT.java
new file mode 100644
index 00000000000..a13fc8183b9
--- /dev/null
+++ 
b/integration-test/src/test/java/org/apache/iotdb/session/it/SessionSeedNodeFallbackIT.java
@@ -0,0 +1,254 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.session.it;
+
+import org.apache.iotdb.common.rpc.thrift.TEndPoint;
+import org.apache.iotdb.commons.cluster.NodeStatus;
+import org.apache.iotdb.consensus.ConsensusFactory;
+import org.apache.iotdb.isession.SessionConfig;
+import org.apache.iotdb.isession.SessionDataSet;
+import org.apache.iotdb.isession.pool.SessionDataSetWrapper;
+import org.apache.iotdb.it.env.EnvFactory;
+import org.apache.iotdb.it.env.cluster.EnvUtils;
+import org.apache.iotdb.it.framework.IoTDBTestRunner;
+import org.apache.iotdb.itbase.category.ClusterIT;
+import org.apache.iotdb.rpc.IoTDBConnectionException;
+import org.apache.iotdb.rpc.UrlUtils;
+import org.apache.iotdb.session.NodesSupplier;
+import org.apache.iotdb.session.Session;
+import org.apache.iotdb.session.pool.SessionPool;
+
+import org.awaitility.Awaitility;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+
+import java.lang.reflect.Field;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(IoTDBTestRunner.class)
+@Category(ClusterIT.class)
+public class SessionSeedNodeFallbackIT {
+  @Before
+  public void setUp() {
+    EnvFactory.getEnv()
+        .getConfig()
+        .getCommonConfig()
+        .setSchemaReplicationFactor(3)
+        .setDataReplicationFactor(3)
+        
.setSchemaRegionConsensusProtocolClass(ConsensusFactory.RATIS_CONSENSUS)
+        .setDataRegionConsensusProtocolClass(ConsensusFactory.RATIS_CONSENSUS);
+    EnvFactory.getEnv().initClusterEnvironment(1, 3);
+  }
+
+  @After
+  public void tearDown() {
+    EnvFactory.getEnv().cleanClusterEnvironment();
+  }
+
+  @Test
+  public void testQueriesDuringRollingRestart() throws Exception {
+    SessionPool pool =
+        new 
SessionPool.Builder().nodeUrls(urls()).maxSize(1).connectionTimeoutInMs(1000).build();
+    Session session = new Session.Builder().nodeUrls(urls()).build();
+    try {
+      // Initial Ratis region creation can exceed one second; do not retry its 
DDL prematurely.
+      session.open(false, 30_000);
+      session.executeNonQueryStatement("CREATE DATABASE root.seed_fallback");
+      session.executeNonQueryStatement(
+          "CREATE TIMESERIES root.seed_fallback.d.s WITH DATATYPE=INT32, 
ENCODING=RLE");
+      session.executeNonQueryStatement("INSERT INTO 
root.seed_fallback.d(time,s) VALUES(1,42)");
+      assertQuery(session, pool);
+      NodesSupplier sessionSupplier = supplier(session);
+      NodesSupplier poolSupplier = supplier(pool);
+      Set<TEndPoint> runningNodes =
+          new HashSet<>(Arrays.asList(endpoint(0), endpoint(1), endpoint(2)));
+      for (int index = 0; index < 3; index++) {
+        EnvFactory.getEnv().shutdownDataNode(index);
+        awaitStatus(index, EnvUtils.getNodeStatusAfterLocalStop());
+        runningNodes.remove(endpoint(index));
+        awaitSnapshot(sessionSupplier, runningNodes);
+        awaitSnapshot(poolSupplier, runningNodes);
+        assertQuery(session, pool);
+
+        EnvFactory.getEnv().startDataNode(index);
+        awaitStatus(index, NodeStatus.Running);
+        runningNodes.add(endpoint(index));
+        awaitSnapshot(sessionSupplier, runningNodes);
+        awaitSnapshot(poolSupplier, runningNodes);
+        assertQuery(session, pool);
+      }
+    } finally {
+      try {
+        session.close();
+      } catch (IoTDBConnectionException e) {
+        // Cached connections may still belong to a stopped process. Only 
cleanup tolerates this;
+        // all query and discovery assertions above must succeed without 
ignoring failures.
+      } finally {
+        pool.close();
+      }
+    }
+  }
+
+  @Test
+  public void testBusinessAndBackgroundRecoveryThroughOmittedSeed() throws 
Exception {
+    EnvFactory.getEnv().shutdownDataNode(0);
+    awaitStatus(0, EnvUtils.getNodeStatusAfterLocalStop());
+    // Control refresh timing so neither supplier learns that node 0 recovered 
before failover.
+    ScheduledThreadPoolExecutor executor =
+        new ScheduledThreadPoolExecutor(1) {
+          @Override
+          public ScheduledFuture<?> scheduleAtFixedRate(
+              Runnable command, long initialDelay, long period, TimeUnit unit) 
{
+            return super.scheduleAtFixedRate(command, 1, 1, TimeUnit.DAYS);
+          }
+        };
+    NodesSupplier business = createSupplier(executor);
+    NodesSupplier background = createSupplier(executor);
+    try (Session session = new 
Session.Builder().nodeUrls(urls()).enableAutoFetch(false).build()) {
+      Set<TEndPoint> discovered = new HashSet<>(Arrays.asList(endpoint(1), 
endpoint(2)));
+      awaitSnapshot(business, discovered);
+      awaitSnapshot(background, discovered);
+      session.open(false, 1000, new ConcurrentHashMap<>(), business);
+
+      EnvFactory.getEnv().startDataNode(0);
+      awaitStatus(0, NodeStatus.Running);
+      EnvFactory.getEnv().getDataNodeWrapper(1).stopForcibly();
+      awaitStatus(1, NodeStatus.Unknown);
+      EnvFactory.getEnv().shutdownDataNode(2);
+      awaitStatus(2, EnvUtils.getNodeStatusAfterLocalStop());
+      assertFalse(business.get().contains(endpoint(0)));
+      assertFalse(background.get().contains(endpoint(0)));
+
+      // Both discovered endpoints are dead. Only the initial seed omitted by 
discovery is live.
+      try (SessionDataSet result = session.executeQueryStatement("SHOW 
AVAILABLE URLS")) {
+        assertEquals(Collections.singleton(endpoint(0)), 
readEndpoints(result));
+      }
+      awaitSnapshot(background, Collections.singleton(endpoint(0)));
+      assertEquals(3, background.getSeedNodes().size());
+      // Wait for the immediate refresh requested by business reconnection, 
not a 60-second poll.
+      executor.submit(() -> {}).get(10, TimeUnit.SECONDS);
+      assertEquals(Collections.singletonList(endpoint(0)), business.get());
+    } finally {
+      business.close();
+      background.close();
+      executor.shutdownNow();
+      assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS));
+    }
+  }
+
+  private void assertQuery(Session session, SessionPool pool) throws Exception 
{
+    String sql = "SELECT s FROM root.seed_fallback.d";
+    try (SessionDataSet result = session.executeQueryStatement(sql)) {
+      assertTrue(result.hasNext());
+      assertEquals(42, result.next().getFields().get(0).getIntV());
+      assertFalse(result.hasNext());
+    }
+    try (SessionDataSetWrapper result = pool.executeQueryStatement(sql)) {
+      assertTrue(result.hasNext());
+      assertEquals(42, result.next().getFields().get(0).getIntV());
+      assertFalse(result.hasNext());
+    }
+  }
+
+  private NodesSupplier supplier(Object owner) throws Exception {
+    Field field = owner.getClass().getDeclaredField("availableNodes");
+    field.setAccessible(true);
+    return (NodesSupplier) field.get(owner);
+  }
+
+  private NodesSupplier createSupplier(ScheduledThreadPoolExecutor executor) {
+    List<TEndPoint> seeds = Arrays.asList(endpoint(0), endpoint(1), 
endpoint(2));
+    return NodesSupplier.createNodeSupplier(
+        seeds,
+        executor,
+        "root",
+        "root",
+        ZoneId.of("UTC"),
+        SessionConfig.DEFAULT_INITIAL_BUFFER_CAPACITY,
+        SessionConfig.DEFAULT_MAX_FRAME_SIZE,
+        1000,
+        false,
+        null,
+        null,
+        null,
+        null,
+        "TLS",
+        false,
+        "V_1_0");
+  }
+
+  private Set<TEndPoint> readEndpoints(SessionDataSet dataSet) throws 
Exception {
+    Set<TEndPoint> result = new HashSet<>();
+    SessionDataSet.DataIterator iterator = dataSet.iterator();
+    while (iterator.next()) {
+      result.add(new TEndPoint(iterator.getString("RpcAddress"), 
iterator.getInt("RpcPort")));
+    }
+    return result;
+  }
+
+  private TEndPoint endpoint(int index) {
+    return new TEndPoint(
+        EnvFactory.getEnv().getDataNodeWrapper(index).getIp(),
+        EnvFactory.getEnv().getDataNodeWrapper(index).getPort());
+  }
+
+  private List<String> urls() {
+    return Arrays.asList(endpoint(0), endpoint(1), endpoint(2)).stream()
+        .map(UrlUtils::convertTEndPointIpv4AndIpv6Url)
+        .collect(Collectors.toCollection(ArrayList::new));
+  }
+
+  private void awaitStatus(int index, NodeStatus status) {
+    EnvFactory.getEnv()
+        .ensureNodeStatus(
+            
Collections.singletonList(EnvFactory.getEnv().getDataNodeWrapper(index)),
+            Collections.singletonList(status));
+  }
+
+  private void awaitSnapshot(NodesSupplier current, Set<TEndPoint> expected) {
+    Awaitility.await()
+        .atMost(30, TimeUnit.SECONDS)
+        .pollInterval(100, TimeUnit.MILLISECONDS)
+        .untilAsserted(
+            () -> {
+              // run() can return immediately while a scheduled refresh is 
still in flight.
+              current.run();
+              assertEquals(expected, new HashSet<>(current.get()));
+            });
+  }
+}
diff --git 
a/iotdb-client/isession/src/main/java/org/apache/iotdb/isession/INodeSupplier.java
 
b/iotdb-client/isession/src/main/java/org/apache/iotdb/isession/INodeSupplier.java
index 60a2a5adb43..9c8d32f9461 100644
--- 
a/iotdb-client/isession/src/main/java/org/apache/iotdb/isession/INodeSupplier.java
+++ 
b/iotdb-client/isession/src/main/java/org/apache/iotdb/isession/INodeSupplier.java
@@ -21,6 +21,7 @@ package org.apache.iotdb.isession;
 
 import org.apache.iotdb.common.rpc.thrift.TEndPoint;
 
+import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
 import java.util.function.Supplier;
@@ -30,4 +31,14 @@ public interface INodeSupplier extends 
Supplier<List<TEndPoint>> {
   void close();
 
   Optional<TEndPoint> getQueryEndPoint();
+
+  /** Initial addresses to try after all discovered endpoints have failed. */
+  default List<TEndPoint> getSeedNodes() {
+    return Collections.emptyList();
+  }
+
+  /** Request a discovery refresh after reconnecting through a seed. */
+  default void requestRefresh() {
+    // Suppliers without automatic discovery have nothing to refresh.
+  }
 }
diff --git 
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/NodesSupplier.java
 
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/NodesSupplier.java
index 81d83450f72..fe91277a27d 100644
--- 
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/NodesSupplier.java
+++ 
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/NodesSupplier.java
@@ -22,20 +22,29 @@ package org.apache.iotdb.session;
 import org.apache.iotdb.common.rpc.thrift.TEndPoint;
 import org.apache.iotdb.isession.INodeSupplier;
 import org.apache.iotdb.isession.SessionDataSet;
+import org.apache.iotdb.rpc.IoTDBConnectionException;
 import org.apache.iotdb.rpc.UrlUtils;
 import org.apache.iotdb.session.i18n.SessionMessages;
 
+import org.apache.thrift.TApplicationException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.time.ZoneId;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Optional;
-import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.Set;
+import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.stream.Collectors;
 
 public class NodesSupplier implements INodeSupplier, Runnable {
 
@@ -54,10 +63,13 @@ public class NodesSupplier implements INodeSupplier, 
Runnable {
 
   private static final int FETCH_SIZE = 10_000;
 
-  // availableNodes won't be updated frequently, so we use 
CopyOnWriteArrayList which is thread-safe
-  // and is optimized for scenarios of reading more and writing less
+  // Publish complete snapshots; discovery must never overwrite the initial 
recovery addresses.
   @SuppressWarnings("java:S3077")
-  private volatile List<TEndPoint> availableNodes = new 
CopyOnWriteArrayList<>();
+  private volatile List<TEndPoint> availableNodes;
+
+  private final List<TEndPoint> seedNodes;
+  private final ScheduledExecutorService executorService;
+  private final AtomicBoolean refreshRequested = new AtomicBoolean();
 
   private final boolean useSSL;
   private final String trustStore;
@@ -82,13 +94,59 @@ public class NodesSupplier implements INodeSupplier, 
Runnable {
 
   private final QueryEndPointPolicy policy = new RoundRobinPolicy();
 
+  // Refresh serialization is independent of the short 
client-publication/cleanup critical section.
+  private final ReentrantLock refreshLock = new ReentrantLock();
+  // Accessed under refreshLock, in failure order, until a discovery query 
succeeds.
+  private final Set<TEndPoint> queryFailedNodes = new LinkedHashSet<>();
+  private final Object clientLock = new Object();
   private ThriftConnection client;
+  private ThriftConnection activeClient;
 
   private volatile boolean closed = false;
 
+  public static NodesSupplier createNodeSupplier(
+      List<TEndPoint> endPointList,
+      ScheduledExecutorService executorService,
+      String userName,
+      String password,
+      ZoneId zoneId,
+      int thriftDefaultBufferSize,
+      int thriftMaxFrameSize,
+      int connectionTimeoutInMs,
+      boolean useSSL,
+      String trustStore,
+      String trustStorePwd,
+      String keyStore,
+      String keyStorePwd,
+      String sslProtocol,
+      boolean enableRPCCompression,
+      String version) {
+
+    return createNodeSupplier(
+        endPointList,
+        endPointList,
+        executorService,
+        userName,
+        password,
+        zoneId,
+        thriftDefaultBufferSize,
+        thriftMaxFrameSize,
+        connectionTimeoutInMs,
+        useSSL,
+        trustStore,
+        trustStorePwd,
+        keyStore,
+        keyStorePwd,
+        sslProtocol,
+        enableRPCCompression,
+        version);
+  }
+
+  /** Bootstrap discovery independently of the permanent recovery seeds (for 
Session reopen). */
   @SuppressWarnings("unsafeThreadSchedule")
   public static NodesSupplier createNodeSupplier(
       List<TEndPoint> endPointList,
+      List<TEndPoint> seedNodes,
       ScheduledExecutorService executorService,
       String userName,
       String password,
@@ -108,6 +166,8 @@ public class NodesSupplier implements INodeSupplier, 
Runnable {
     NodesSupplier nodesSupplier =
         new NodesSupplier(
             endPointList,
+            seedNodes,
+            executorService,
             userName,
             password,
             zoneId,
@@ -133,6 +193,8 @@ public class NodesSupplier implements INodeSupplier, 
Runnable {
 
   private NodesSupplier(
       List<TEndPoint> endPointList,
+      List<TEndPoint> seedNodes,
+      ScheduledExecutorService executorService,
       String userName,
       String password,
       ZoneId zoneId,
@@ -147,7 +209,13 @@ public class NodesSupplier implements INodeSupplier, 
Runnable {
       String sslProtocol,
       boolean enableRPCCompression,
       String version) {
-    this.availableNodes.addAll(new HashSet<>(endPointList));
+    this.seedNodes =
+        Collections.unmodifiableList(
+            
seedNodes.stream().distinct().map(TEndPoint::deepCopy).collect(Collectors.toList()));
+    this.availableNodes =
+        Collections.unmodifiableList(
+            
endPointList.stream().distinct().map(TEndPoint::deepCopy).collect(Collectors.toList()));
+    this.executorService = executorService;
     this.userName = userName;
     this.password = password;
     this.useSSL = useSSL;
@@ -164,43 +232,135 @@ public class NodesSupplier implements INodeSupplier, 
Runnable {
     this.version = version;
   }
 
-  // the method will only be called while reconnecting, so it's ok to copy set 
each time
-  // and the List needn't be thread-safe, because it will only be used in one 
thread.
+  // Readers retain an immutable snapshot while discovery publishes 
replacements.
   @Override
   public List<TEndPoint> get() {
     return availableNodes;
   }
 
   @Override
-  public void run() {
-    if (closed) {
-      if (client != null) {
-        destroyCurrentClient();
+  public List<TEndPoint> getSeedNodes() {
+    return seedNodes;
+  }
+
+  @Override
+  public void requestRefresh() {
+    if (!closed && refreshRequested.compareAndSet(false, true)) {
+      try {
+        executorService.execute(
+            () -> {
+              try {
+                run();
+              } finally {
+                refreshRequested.set(false);
+              }
+            });
+      } catch (RejectedExecutionException e) {
+        // The session or pool may have shut down its executor concurrently.
+        refreshRequested.set(false);
       }
+    }
+  }
+
+  @Override
+  public void run() {
+    if (closed || !refreshLock.tryLock()) {
       return;
     }
-    if (client == null) {
-      for (TEndPoint endPoint : availableNodes) {
-        if (createConnection(endPoint)) {
-          break;
+    try {
+      Set<TEndPoint> attempted = new HashSet<>();
+      ThriftConnection current;
+      synchronized (clientLock) {
+        if (closed) {
+          return;
+        }
+        current = client;
+        activeClient = current;
+      }
+      if (current != null) {
+        attempted.add(current.endPoint);
+        if (refreshFrom(current)) {
+          return;
         }
       }
+
+      Set<TEndPoint> candidates = new LinkedHashSet<>(availableNodes);
+      List<TEndPoint> seeds = new ArrayList<>(seedNodes);
+      if (!seeds.isEmpty()) {
+        Collections.rotate(seeds, 
-ThreadLocalRandom.current().nextInt(seeds.size()));
+      }
+      candidates.addAll(seeds);
+      // Defer query-failed nodes across refresh rounds without losing them as 
recovery candidates.
+      // Remember all failures so two failing nodes cannot starve a third, 
healthy candidate.
+      queryFailedNodes.retainAll(candidates);
+      candidates.removeAll(queryFailedNodes);
+      candidates.addAll(queryFailedNodes);
+      for (TEndPoint endPoint : candidates) {
+        if (closed) {
+          return;
+        }
+        if (attempted.add(endPoint)) {
+          current = createConnection(endPoint);
+          if (current != null && refreshFrom(current)) {
+            return;
+          }
+        }
+      }
+    } finally {
+      ThriftConnection toClose = null;
+      synchronized (clientLock) {
+        if (closed) {
+          toClose = activeClient;
+          client = null;
+        }
+        activeClient = null;
+      }
+      // Only the refresh owner releases buffers that an interrupted operation 
could still use.
+      try {
+        if (toClose != null) {
+          toClose.close();
+        }
+      } finally {
+        refreshLock.unlock();
+      }
     }
+  }
 
-    // make sure the following code block can run thread-safely with close() 
method.
-    synchronized (this) {
-      if (client != null && !updateDataNodeList()) {
-        destroyCurrentClient();
+  /** Return false only when the transport failed and another candidate should 
be tried now. */
+  private boolean refreshFrom(ThriftConnection current) {
+    if (closed) {
+      return true;
+    }
+    try {
+      if (updateDataNodeList(current)) {
+        queryFailedNodes.clear();
+      } else {
+        // End this round on a query error, but try another candidate on the 
next refresh.
+        // Moving repeated failures to the end also permits recovery after a 
cluster-wide outage.
+        queryFailedNodes.remove(current.endPoint);
+        queryFailedNodes.add(current.endPoint);
+        destroyClient(current);
       }
+    } catch (IoTDBConnectionException e) {
+      destroyClient(current);
+      return false;
     }
+    return true;
   }
 
-  private boolean createConnection(TEndPoint endPoint) {
-    client =
+  private ThriftConnection createConnection(TEndPoint endPoint) {
+    ThriftConnection current =
         new ThriftConnection(
             endPoint, thriftDefaultBufferSize, thriftMaxFrameSize, 
connectionTimeoutInMs);
+    synchronized (clientLock) {
+      if (closed) {
+        return null;
+      }
+      client = current;
+      activeClient = current;
+    }
     try {
-      client.init(
+      current.init(
           useSSL,
           trustStore,
           trustStorePwd,
@@ -212,42 +372,75 @@ public class NodesSupplier implements INodeSupplier, 
Runnable {
           enableRPCCompression,
           zoneId,
           version);
-      return true;
+      if (closed) {
+        destroyClient(current);
+        return null;
+      }
+      return current;
     } catch (Exception e) {
-      LOGGER.warn(SessionMessages.FAILED_TO_CREATE_CONNECTION, endPoint);
-      destroyCurrentClient();
-      return false;
+      if (!closed) {
+        LOGGER.warn(SessionMessages.FAILED_TO_CREATE_CONNECTION, endPoint);
+      }
+      destroyClient(current);
+      return null;
     }
   }
 
-  private synchronized void destroyCurrentClient() {
-    if (client != null) {
-      client.close();
-      client = null;
+  private void destroyClient(ThriftConnection current) {
+    synchronized (clientLock) {
+      if (client == current) {
+        client = null;
+      }
     }
+    current.close();
   }
 
   @Override
   public void close() {
-    closed = true;
-    destroyCurrentClient();
+    ThriftConnection current;
+    boolean inUse;
+    synchronized (clientLock) {
+      closed = true;
+      current = client != null ? client : activeClient;
+      inUse = current != null && current == activeClient;
+      client = null;
+    }
+    if (current != null) {
+      if (inUse) {
+        current.abort();
+      } else {
+        current.close();
+      }
+    }
   }
 
   @Override
   public Optional<TEndPoint> getQueryEndPoint() {
-    if (availableNodes == null || availableNodes.isEmpty()) {
+    List<TEndPoint> snapshot = availableNodes;
+    if (snapshot.isEmpty()) {
       return Optional.empty();
     } else {
-      return Optional.of(policy.chooseOne(get()));
+      return Optional.of(policy.chooseOne(snapshot));
     }
   }
 
-  private boolean updateDataNodeList() {
+  private boolean updateDataNodeList(ThriftConnection current) throws 
IoTDBConnectionException {
     try (SessionDataSet sessionDataSet =
-        client.executeQueryStatement(SHOW_AVAILABLE_URLS_COMMAND, 
TIMEOUT_IN_MS, FETCH_SIZE)) {
+        current.executeQueryStatement(SHOW_AVAILABLE_URLS_COMMAND, 
TIMEOUT_IN_MS, FETCH_SIZE)) {
       updateAvailableNodes(sessionDataSet);
-    } catch (Exception e1) {
-      LOGGER.warn(SessionMessages.FAILED_TO_FETCH_DATA_NODE_LIST, 
client.endPoint);
+    } catch (IoTDBConnectionException e) {
+      if (!closed) {
+        LOGGER.warn(SessionMessages.FAILED_TO_FETCH_DATA_NODE_LIST, 
current.endPoint);
+      }
+      if (!(e.getCause() instanceof TApplicationException)) {
+        throw e;
+      }
+      // A Thrift application error also means the peer responded; it is not a 
transport failure.
+      return false;
+    } catch (Exception e) {
+      if (!closed) {
+        LOGGER.warn(SessionMessages.FAILED_TO_FETCH_DATA_NODE_LIST, 
current.endPoint);
+      }
       return false;
     }
     return true;
@@ -266,9 +459,10 @@ public class NodesSupplier implements INodeSupplier, 
Runnable {
         }
       }
     }
-    // replace the older ones.
-    if (!res.isEmpty()) {
-      availableNodes = res;
+    // Wildcard addresses and transient Unknown statuses after leader changes 
convey no usable
+    // discovery information. Retain the last snapshot for routing and 
recovery in both cases.
+    if (!closed && !res.isEmpty()) {
+      availableNodes = Collections.unmodifiableList(res);
     }
   }
 }
diff --git 
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/Session.java 
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/Session.java
index a6deda17491..e5d44af9690 100644
--- a/iotdb-client/session/src/main/java/org/apache/iotdb/session/Session.java
+++ b/iotdb-client/session/src/main/java/org/apache/iotdb/session/Session.java
@@ -88,6 +88,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
@@ -125,6 +126,7 @@ public class Session implements ISession {
           new 
LinkedBlockingQueue<>(SessionConfig.DEFAULT_SESSION_EXECUTOR_TASK_NUM),
           ThreadUtils.createThreadFactory("SessionExecutor", true));
   protected List<String> nodeUrls;
+  private final List<String> initialNodeUrls;
   protected String username;
   protected String password;
   protected boolean useEncryptedPassword;
@@ -367,6 +369,8 @@ public class Session implements ISession {
       boolean enableRedirection,
       Version version) {
     this.defaultEndPoint = new TEndPoint(host, rpcPort);
+    this.initialNodeUrls =
+        
Collections.singletonList(UrlUtils.convertTEndPointIpv4AndIpv6Url(defaultEndPoint));
     this.username = username;
     this.password = password;
     this.fetchSize = fetchSize;
@@ -434,8 +438,9 @@ public class Session implements ISession {
     if (nodeUrls.isEmpty()) {
       throw new IllegalArgumentException(SessionMessages.NODE_URLS_EMPTY);
     }
-    nodeUrls = shuffleNodeUrls(nodeUrls);
+    nodeUrls = shuffleNodeUrls(new ArrayList<>(nodeUrls));
     this.nodeUrls = nodeUrls;
+    this.initialNodeUrls = Collections.unmodifiableList(new 
ArrayList<>(nodeUrls));
     this.username = username;
     this.password = password;
     this.fetchSize = fetchSize;
@@ -451,11 +456,13 @@ public class Session implements ISession {
       if (builder.nodeUrls.isEmpty()) {
         throw new IllegalArgumentException(SessionMessages.NODE_URLS_EMPTY);
       }
-      builder.nodeUrls = shuffleNodeUrls(builder.nodeUrls);
-      this.nodeUrls = builder.nodeUrls;
+      this.nodeUrls = shuffleNodeUrls(new ArrayList<>(builder.nodeUrls));
+      this.initialNodeUrls = Collections.unmodifiableList(new 
ArrayList<>(nodeUrls));
       this.enableQueryRedirection = true;
     } else {
       this.defaultEndPoint = new TEndPoint(builder.host, builder.rpcPort);
+      this.initialNodeUrls =
+          
Collections.singletonList(UrlUtils.convertTEndPointIpv4AndIpv6Url(defaultEndPoint));
       this.enableQueryRedirection = builder.enableRedirection;
     }
     this.enableThriftRpcCompaction = builder.isThriftRpcCompactionEnabled;
@@ -540,6 +547,7 @@ public class Session implements ISession {
       this.availableNodes =
           NodesSupplier.createNodeSupplier(
               getNodeUrls(),
+              SessionUtils.parseSeedNodeUrls(initialNodeUrls),
               executorService,
               username,
               password,
@@ -556,7 +564,10 @@ public class Session implements ISession {
               enableRPCCompaction,
               version.toString());
     } else {
-      this.availableNodes = new DummyNodesSupplier(getNodeUrls());
+      // A static supplier has no separate seed tier. Keep every configured 
address after reopen.
+      Set<TEndPoint> staticNodes = new LinkedHashSet<>(getNodeUrls());
+      staticNodes.addAll(SessionUtils.parseSeedNodeUrls(initialNodeUrls));
+      this.availableNodes = new DummyNodesSupplier(new 
ArrayList<>(staticNodes));
     }
 
     this.connectionTimeoutInMs = connectionTimeoutInMs;
@@ -599,11 +610,9 @@ public class Session implements ISession {
   }
 
   private List<TEndPoint> getNodeUrls() {
-    if (defaultEndPoint != null) {
-      return Collections.singletonList(defaultEndPoint);
-    } else {
-      return SessionUtils.parseSeedNodeUrls(nodeUrls);
-    }
+    return defaultEndPoint == null
+        ? SessionUtils.parseSeedNodeUrls(initialNodeUrls)
+        : Collections.singletonList(defaultEndPoint.deepCopy());
   }
 
   @Override
@@ -4491,6 +4500,10 @@ public class Session implements ISession {
       return this;
     }
 
+    /**
+     * Configure recovery seeds using the same host/IP form as each DataNode's 
dn_rpc_address.
+     * Hostname aliases and IP literals are not DNS-deduplicated during 
recovery.
+     */
     public Builder nodeUrls(List<String> nodeUrls) {
       this.nodeUrls = nodeUrls;
       return this;
diff --git 
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/SessionConnection.java
 
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/SessionConnection.java
index 7b6ec3b03df..ce06b35abb1 100644
--- 
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/SessionConnection.java
+++ 
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/SessionConnection.java
@@ -22,6 +22,7 @@ package org.apache.iotdb.session;
 import org.apache.iotdb.common.rpc.thrift.TAggregationType;
 import org.apache.iotdb.common.rpc.thrift.TEndPoint;
 import org.apache.iotdb.common.rpc.thrift.TSStatus;
+import org.apache.iotdb.isession.INodeSupplier;
 import org.apache.iotdb.isession.SessionConfig;
 import org.apache.iotdb.isession.SessionDataSet;
 import org.apache.iotdb.rpc.DeepCopyRpcTransportFactory;
@@ -84,6 +85,7 @@ import java.security.SecureRandom;
 import java.time.ZoneId;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
@@ -1143,18 +1145,22 @@ public class SessionConnection {
     for (int i = 1; i <= SessionConfig.RETRY_NUM; i++) {
       if (transport != null) {
         transport.close();
-        endPointList = availableNodes.get();
-        int currHostIndex = random.nextInt(endPointList.size());
-        int tryHostNum = 0;
-        for (int j = currHostIndex; j < endPointList.size(); j++) {
-          if (tryHostNum == endPointList.size()) {
-            break;
-          }
-          this.endPoint = endPointList.get(j);
-          if (j == endPointList.size() - 1) {
-            j = -1;
+        List<TEndPoint> discoveredNodes = availableNodes.get();
+        endPointList = new ArrayList<>(discoveredNodes);
+        if (!endPointList.isEmpty()) {
+          Collections.rotate(endPointList, 
-random.nextInt(endPointList.size()));
+        }
+        if (availableNodes instanceof INodeSupplier) {
+          List<TEndPoint> seeds = new ArrayList<>(((INodeSupplier) 
availableNodes).getSeedNodes());
+          if (!seeds.isEmpty()) {
+            Collections.rotate(seeds, -random.nextInt(seeds.size()));
           }
-          tryHostNum++;
+          endPointList.addAll(seeds);
+        }
+        // Try every discovered endpoint before falling back to seeds, once 
per endpoint per round.
+        endPointList = new ArrayList<>(new LinkedHashSet<>(endPointList));
+        for (TEndPoint candidate : endPointList) {
+          this.endPoint = candidate;
           try {
             init(
                 endPoint,
@@ -1165,6 +1171,9 @@ public class SessionConnection {
                 session.keyStorePwd,
                 session.sslProtocol);
             connectedSuccess = true;
+            if (!discoveredNodes.contains(endPoint) && availableNodes 
instanceof INodeSupplier) {
+              ((INodeSupplier) availableNodes).requestRefresh();
+            }
           } catch (IoTDBConnectionException e) {
             logger.warn(SessionMessages.NODE_DOWN_TRY_NEXT, endPoint);
             continue;
diff --git 
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/TableSessionBuilder.java
 
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/TableSessionBuilder.java
index 20147e1cfcf..a39af4c847a 100644
--- 
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/TableSessionBuilder.java
+++ 
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/TableSessionBuilder.java
@@ -49,6 +49,9 @@ public class TableSessionBuilder extends 
AbstractSessionBuilder {
   /**
    * Sets the list of node URLs for the IoTDB cluster.
    *
+   * <p>Use the same host/IP form as each DataNode's dn_rpc_address. Recovery 
deduplicates literal
+   * addresses, not DNS aliases.
+   *
    * @param nodeUrls a list of node URLs.
    * @return the current {@link TableSessionBuilder} instance.
    * @defaultValue Collection.singletonList(" localhost : 6667 ")
diff --git 
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/ThriftConnection.java
 
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/ThriftConnection.java
index 4f8e2a58af7..71cc63526d7 100644
--- 
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/ThriftConnection.java
+++ 
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/ThriftConnection.java
@@ -25,8 +25,8 @@ import org.apache.iotdb.rpc.DeepCopyRpcTransportFactory;
 import org.apache.iotdb.rpc.IoTDBConnectionException;
 import org.apache.iotdb.rpc.RpcUtils;
 import org.apache.iotdb.rpc.StatementExecutionException;
+import org.apache.iotdb.rpc.TElasticFramedTransport;
 import org.apache.iotdb.service.rpc.thrift.IClientRPCService;
-import org.apache.iotdb.service.rpc.thrift.TSCloseSessionReq;
 import org.apache.iotdb.service.rpc.thrift.TSExecuteStatementReq;
 import org.apache.iotdb.service.rpc.thrift.TSExecuteStatementResp;
 import org.apache.iotdb.service.rpc.thrift.TSOpenSessionReq;
@@ -36,12 +36,16 @@ import org.apache.iotdb.session.i18n.SessionMessages;
 import org.apache.thrift.TException;
 import org.apache.thrift.protocol.TBinaryProtocol;
 import org.apache.thrift.protocol.TCompactProtocol;
+import org.apache.thrift.transport.TSocket;
 import org.apache.thrift.transport.TTransport;
 import org.apache.thrift.transport.TTransportException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
+import java.net.Socket;
 import java.time.ZoneId;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 public class ThriftConnection {
 
@@ -55,7 +59,10 @@ public class ThriftConnection {
 
   protected final int connectionTimeoutInMs;
 
-  protected TTransport transport;
+  protected volatile TTransport transport;
+  private final Object transportLock = new Object();
+  private final AtomicBoolean disposed = new AtomicBoolean();
+  private volatile boolean closed;
   protected IClientRPCService.Iface client;
   protected long sessionId;
   protected long statementId;
@@ -87,31 +94,28 @@ public class ThriftConnection {
       ZoneId zoneId,
       String version)
       throws IoTDBConnectionException {
-    DeepCopyRpcTransportFactory transportFactory =
-        DeepCopyRpcTransportFactory.getInstance(thriftDefaultBufferSize, 
thriftMaxFrameSize);
+    checkNotClosed();
     try {
-      if (useSSL) {
-        transport =
-            transportFactory.getTransport(
-                endPoint.getIp(),
-                endPoint.getPort(),
-                connectionTimeoutInMs,
-                trustStore,
-                trustStorePwd,
-                keyStore,
-                keyStorePwd,
-                sslProtocol);
-      } else {
-        transport =
-            transportFactory.getTransport(
-                // as there is a try-catch already, we do not need to use 
TSocket.wrap
-                endPoint.getIp(), endPoint.getPort(), connectionTimeoutInMs);
+      TTransport prepared =
+          createTransport(useSSL, trustStore, trustStorePwd, keyStore, 
keyStorePwd, sslProtocol);
+      boolean published;
+      synchronized (transportLock) {
+        published = !closed;
+        if (published) {
+          transport = prepared;
+        }
       }
-      if (!transport.isOpen()) {
-        transport.open();
+      if (!published) {
+        prepared.close();
+        throw new IoTDBConnectionException(SessionMessages.SESSION_NOT_OPEN);
       }
+      if (!prepared.isOpen()) {
+        prepared.open();
+      }
+      checkNotClosed();
       this.zoneId = zoneId;
     } catch (TTransportException e) {
+      close();
       throw new IoTDBConnectionException(e);
     }
 
@@ -155,7 +159,7 @@ public class ThriftConnection {
       statementId = client.requestStatementId(sessionId);
 
     } catch (Exception e) {
-      transport.close();
+      close();
       throw new IoTDBConnectionException(e);
     }
   }
@@ -193,19 +197,74 @@ public class ThriftConnection {
         execResp.getColumnIndex2TsBlockColumnIndexList());
   }
 
-  public void close() {
-    if (transport != null && transport.isOpen()) {
-      try {
-        if (client != null) {
-          client.closeSession(new TSCloseSessionReq(sessionId));
-        }
-      } catch (TException e) {
-        LOGGER.warn(SessionMessages.CLOSING_SESSION_FAILED, sessionId, 
endPoint);
-      } finally {
-        if (transport.isOpen()) {
-          transport.close();
+  /** Create outside the publication lock: TLS setup itself can perform 
network I/O. */
+  protected TTransport createTransport(
+      boolean useSSL,
+      String trustStore,
+      String trustStorePwd,
+      String keyStore,
+      String keyStorePwd,
+      String sslProtocol)
+      throws TTransportException {
+    DeepCopyRpcTransportFactory factory =
+        DeepCopyRpcTransportFactory.getInstance(thriftDefaultBufferSize, 
thriftMaxFrameSize);
+    return useSSL
+        ? factory.getTransport(
+            endPoint.getIp(),
+            endPoint.getPort(),
+            connectionTimeoutInMs,
+            trustStore,
+            trustStorePwd,
+            keyStore,
+            keyStorePwd,
+            sslProtocol)
+        : factory.getTransport(endPoint.getIp(), endPoint.getPort(), 
connectionTimeoutInMs);
+  }
+
+  private void checkNotClosed() throws IoTDBConnectionException {
+    if (closed) {
+      throw new IoTDBConnectionException(SessionMessages.SESSION_NOT_OPEN);
+    }
+  }
+
+  /**
+   * Abort socket I/O without waiting for the synchronized RPC client or 
releasing buffers still
+   * owned by the refresh thread. Closing the Socket directly also prevents 
TSocket.open() from
+   * creating a replacement socket if closure races with the start of TCP 
connect.
+   */
+  void abort() {
+    TTransport current;
+    synchronized (transportLock) {
+      closed = true;
+      current = transport;
+    }
+    if (current == null) {
+      return;
+    }
+    TTransport underlying =
+        current instanceof TElasticFramedTransport
+            ? ((TElasticFramedTransport) current).getSocket()
+            : current;
+    if (underlying instanceof TSocket) {
+      Socket socket = ((TSocket) underlying).getSocket();
+      if (socket != null) {
+        try {
+          socket.close();
+        } catch (IOException e) {
+          LOGGER.warn(SessionMessages.CLOSING_SESSION_FAILED, sessionId, 
endPoint, e);
         }
       }
+    } else {
+      underlying.close();
+    }
+  }
+
+  /** Release the transport after its refresh owner has finished using the 
buffers. */
+  public void close() {
+    abort();
+    if (disposed.compareAndSet(false, true) && transport != null) {
+      // Closing the transport also releases the server-side session through 
handleClientExit().
+      transport.close();
     }
   }
 }
diff --git 
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/pool/SessionPool.java
 
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/pool/SessionPool.java
index 32feac2cea5..16b79c6415c 100644
--- 
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/pool/SessionPool.java
+++ 
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/pool/SessionPool.java
@@ -52,6 +52,7 @@ import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.time.ZoneId;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -502,7 +503,7 @@ public class SessionPool implements ISessionPool {
     if (nodeUrls.isEmpty()) {
       throw new IllegalArgumentException(SessionMessages.NODE_URLS_EMPTY);
     }
-    this.nodeUrls = nodeUrls;
+    this.nodeUrls = Collections.unmodifiableList(new ArrayList<>(nodeUrls));
     this.user = user;
     this.password = password;
     this.fetchSize = fetchSize;
@@ -519,9 +520,9 @@ public class SessionPool implements ISessionPool {
     this.version = version;
     this.thriftDefaultBufferSize = thriftDefaultBufferSize;
     this.thriftMaxFrameSize = thriftMaxFrameSize;
-    this.formattedNodeUrls = nodeUrls.toString();
+    this.formattedNodeUrls = this.nodeUrls.toString();
     initThreadPool();
-    initAvailableNodes(SessionUtils.parseSeedNodeUrls(nodeUrls));
+    initAvailableNodes(SessionUtils.parseSeedNodeUrls(this.nodeUrls));
   }
 
   public SessionPool(AbstractSessionPoolBuilder builder) {
@@ -564,7 +565,7 @@ public class SessionPool implements ISessionPool {
       if (builder.nodeUrls.isEmpty()) {
         throw new IllegalArgumentException(SessionMessages.NODE_URLS_EMPTY);
       }
-      this.nodeUrls = builder.nodeUrls;
+      this.nodeUrls = Collections.unmodifiableList(new 
ArrayList<>(builder.nodeUrls));
       this.host = null;
       this.port = -1;
       this.formattedNodeUrls = builder.nodeUrls.toString();
@@ -3697,6 +3698,10 @@ public class SessionPool implements ISessionPool {
       return this;
     }
 
+    /**
+     * Configure recovery seeds using the same host/IP form as each DataNode's 
dn_rpc_address.
+     * Hostname aliases and IP literals are not DNS-deduplicated during 
recovery.
+     */
     public Builder nodeUrls(List<String> nodeUrls) {
       this.nodeUrls = nodeUrls;
       return this;
diff --git 
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/pool/TableSessionPoolBuilder.java
 
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/pool/TableSessionPoolBuilder.java
index 790e7816288..f19c5fc8878 100644
--- 
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/pool/TableSessionPoolBuilder.java
+++ 
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/pool/TableSessionPoolBuilder.java
@@ -39,6 +39,9 @@ public class TableSessionPoolBuilder extends 
AbstractSessionPoolBuilder {
   /**
    * Sets the list of node URLs for the IoTDB cluster.
    *
+   * <p>Use the same host/IP form as each DataNode's dn_rpc_address. Recovery 
deduplicates literal
+   * addresses, not DNS aliases.
+   *
    * @param nodeUrls a list of node URLs.
    * @return the current {@link TableSessionPoolBuilder} instance.
    * @defaultValue Collection.singletonList("localhost:6667")
diff --git 
a/iotdb-client/session/src/test/java/org/apache/iotdb/session/SessionSeedNodeFallbackTest.java
 
b/iotdb-client/session/src/test/java/org/apache/iotdb/session/SessionSeedNodeFallbackTest.java
new file mode 100644
index 00000000000..24f7e6ab7cb
--- /dev/null
+++ 
b/iotdb-client/session/src/test/java/org/apache/iotdb/session/SessionSeedNodeFallbackTest.java
@@ -0,0 +1,751 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.session;
+
+import org.apache.iotdb.common.rpc.thrift.TEndPoint;
+import org.apache.iotdb.common.rpc.thrift.TSStatus;
+import org.apache.iotdb.isession.INodeSupplier;
+import org.apache.iotdb.isession.SessionConfig;
+import org.apache.iotdb.isession.SessionDataSet;
+import org.apache.iotdb.isession.pool.SessionDataSetWrapper;
+import org.apache.iotdb.rpc.DeepCopyRpcTransportFactory;
+import org.apache.iotdb.rpc.TSStatusCode;
+import org.apache.iotdb.rpc.UrlUtils;
+import org.apache.iotdb.service.rpc.thrift.IClientRPCService;
+import org.apache.iotdb.service.rpc.thrift.TSExecuteStatementResp;
+import org.apache.iotdb.service.rpc.thrift.TSOpenSessionResp;
+import org.apache.iotdb.service.rpc.thrift.TSProtocolVersion;
+import org.apache.iotdb.session.pool.SessionPool;
+
+import org.apache.thrift.TApplicationException;
+import org.apache.thrift.TException;
+import org.apache.thrift.protocol.TBinaryProtocol;
+import org.apache.thrift.protocol.TProtocol;
+import org.apache.thrift.server.ServerContext;
+import org.apache.thrift.server.TServerEventHandler;
+import org.apache.thrift.server.TThreadPoolServer;
+import org.apache.thrift.transport.TServerSocket;
+import org.apache.thrift.transport.TTransport;
+import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.read.common.block.TsBlockBuilder;
+import org.apache.tsfile.read.common.block.column.TsBlockSerde;
+import org.apache.tsfile.utils.BytesUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.powermock.reflect.Whitebox;
+
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Exercise discovery and reconnection over real Thrift connections with 
controlled availability.
+ */
+public class SessionSeedNodeFallbackTest {
+  private final List<TestNode> nodes = new ArrayList<>();
+  private final List<TEndPoint> attempts = new CopyOnWriteArrayList<>();
+  private final List<TEndPoint> queries = new CopyOnWriteArrayList<>();
+  private final List<Runnable> refreshes = new ArrayList<>();
+  private final List<SessionConnection> connections = new ArrayList<>();
+  private volatile List<TEndPoint> advertised;
+  private ScheduledExecutorService executor;
+  private NodesSupplier supplier;
+
+  @Before
+  public void setUp() throws Exception {
+    for (int i = 0; i < 3; i++) {
+      nodes.add(new TestNode());
+    }
+    advertised = endpoints(0, 1, 2);
+    executor = mock(ScheduledExecutorService.class);
+    doAnswer(
+            invocation -> {
+              refreshes.add(invocation.getArgument(0));
+              return null;
+            })
+        .when(executor)
+        .execute(any(Runnable.class));
+    supplier = createSupplier(endpoints(0, 1, 2));
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    for (SessionConnection connection : connections) {
+      connection.forceCloseTransport();
+    }
+    if (supplier != null) {
+      supplier.close();
+    }
+    for (TestNode node : nodes) {
+      node.close();
+    }
+  }
+
+  @Test
+  public void testBackgroundRefreshRecoversFromStaleSnapshot() {
+    nodes.get(0).online = false;
+    advertised = endpoints(1, 2);
+    supplier.run();
+    assertEquals(advertised, supplier.get());
+
+    // Node 1 has finished upgrading, but both cached candidates are now 
unavailable.
+    nodes.get(0).online = true;
+    nodes.get(1).online = false;
+    nodes.get(2).online = false;
+    advertised = endpoints(0);
+    attempts.clear();
+    supplier.run();
+    assertEquals(endpoints(2, 0), attempts);
+    assertEquals(endpoints(0), supplier.get());
+    assertEquals(endpoints(0, 1, 2), supplier.getSeedNodes());
+  }
+
+  @Test
+  public void testBusinessReconnectFallsBackAndRequestsRefresh() throws 
Exception {
+    advertised = endpoints(1, 2);
+    supplier.run();
+    nodes.get(1).online = false;
+    nodes.get(2).online = false;
+    advertised = endpoints(0);
+    attempts.clear();
+    SessionConnection connection = newConnection();
+    assertTrue(reconnect(connection));
+    assertEquals(nodes.get(0).endpoint, connection.getEndPoint());
+    assertEquals(3, attempts.size());
+    assertEquals(nodes.get(0).endpoint, attempts.get(2));
+    assertEquals(1, refreshes.size());
+    refreshes.remove(0).run();
+    assertEquals(endpoints(0), supplier.get());
+  }
+
+  @Test
+  public void testDiscoveredNodeIsPreferredToHealthySeed() throws Exception {
+    advertised = endpoints(1);
+    supplier.run();
+    attempts.clear();
+    assertTrue(reconnect(newConnection()));
+    assertEquals(endpoints(1), attempts);
+    assertTrue(refreshes.isEmpty());
+    for (int i = 0; i < 6; i++) {
+      assertEquals(nodes.get(1).endpoint, supplier.getQueryEndPoint().get());
+    }
+  }
+
+  @Test
+  public void testEmptyDiscoveryKeepsPreviousSnapshot() {
+    advertised = endpoints(1, 2);
+    supplier.run();
+    attempts.clear();
+    advertised = Collections.emptyList();
+    supplier.run();
+    assertEquals(endpoints(1, 2), supplier.get());
+    assertEquals(nodes.get(1).endpoint, supplier.getQueryEndPoint().get());
+    assertEquals(nodes.get(2).endpoint, supplier.getQueryEndPoint().get());
+    assertEquals(endpoints(0, 1, 2), supplier.getSeedNodes());
+    assertTrue(attempts.isEmpty());
+  }
+
+  @Test
+  public void testWildcardDiscoveryKeepsSeedsAndLastSnapshot() {
+    advertised = Arrays.asList(new TEndPoint("0.0.0.0", 6667), new 
TEndPoint("::", 6667));
+    supplier.run();
+    assertEquals(endpoints(0, 1, 2), supplier.get());
+    List<TEndPoint> selected = new ArrayList<>();
+    for (int i = 0; i < 3; i++) {
+      selected.add(supplier.getQueryEndPoint().get());
+    }
+    assertEquals(new HashSet<>(endpoints(0, 1, 2)), new HashSet<>(selected));
+
+    advertised = endpoints(1, 2);
+    supplier.run();
+    advertised = Collections.singletonList(new TEndPoint("0.0.0.0", 6667));
+    supplier.run();
+    assertEquals(endpoints(1, 2), supplier.get());
+  }
+
+  @Test
+  public void testQueryStatusFailureMovesDiscoveryToAnotherNodeNextRound() {
+    assertQueryFailureMovesDiscovery(false);
+  }
+
+  @Test
+  public void 
testQueryApplicationFailureMovesDiscoveryToAnotherNodeNextRound() {
+    assertQueryFailureMovesDiscovery(true);
+  }
+
+  private void assertQueryFailureMovesDiscovery(boolean applicationError) {
+    supplier.run();
+    nodes.get(0).failQuery = true;
+    nodes.get(0).queryApplicationError = applicationError;
+    advertised = endpoints(1, 2);
+    queries.clear();
+    attempts.clear();
+    supplier.run();
+    assertEquals(endpoints(0), queries);
+    assertTrue(attempts.isEmpty());
+    assertEquals(endpoints(0, 1, 2), supplier.get());
+
+    // Node 0 is still reachable but cannot query the ConfigNode. Do not pin 
discovery to it.
+    supplier.run();
+    assertEquals(endpoints(0, 1), queries);
+    assertEquals(endpoints(1), attempts);
+    assertEquals(endpoints(1, 2), supplier.get());
+
+    advertised = endpoints(2);
+    supplier.run();
+    assertEquals(endpoints(2), supplier.get());
+    assertEquals(endpoints(0, 1, 1), queries);
+    assertEquals(endpoints(1), attempts);
+  }
+
+  @Test
+  public void testClusterQueryStatusFailuresAreBounded() {
+    assertClusterQueryFailuresAreBounded(false);
+  }
+
+  @Test
+  public void testClusterQueryApplicationFailuresAreBounded() {
+    assertClusterQueryFailuresAreBounded(true);
+  }
+
+  private void assertClusterQueryFailuresAreBounded(boolean applicationError) {
+    nodes.forEach(
+        node -> {
+          node.failQuery = true;
+          node.queryApplicationError = applicationError;
+        });
+    for (int i = 0; i < 6; i++) {
+      supplier.run();
+      // One new session and one query per refresh, even when every node 
returns an error.
+      assertEquals(i + 1, attempts.size());
+      assertEquals(i + 1, queries.size());
+      assertEquals(nodes.get(i % 3).endpoint, queries.get(i));
+      assertEquals(endpoints(0, 1, 2), supplier.get());
+    }
+    assertEquals(queries, attempts);
+
+    // The first two candidates still fail. Discovery must eventually reach 
the recovered third.
+    nodes.get(2).failQuery = false;
+    advertised = endpoints(2);
+    for (int i = 0; i < 3; i++) {
+      supplier.run();
+      assertEquals(7 + i, attempts.size());
+      assertEquals(7 + i, queries.size());
+    }
+    assertEquals(endpoints(2), supplier.get());
+    supplier.run();
+    assertEquals(9, attempts.size());
+    assertEquals(10, queries.size());
+    assertEquals(nodes.get(2).endpoint, queries.get(9));
+  }
+
+  @Test
+  public void testDiscoveryQueryFailuresEventuallyReachRecoverySeed() {
+    advertised = endpoints(0, 1);
+    supplier.run();
+    nodes.get(0).failQuery = true;
+    nodes.get(1).failQuery = true;
+    advertised = endpoints(2);
+    queries.clear();
+    attempts.clear();
+    supplier.run();
+    assertEquals(endpoints(0), queries);
+    assertTrue(attempts.isEmpty());
+    assertEquals(endpoints(0, 1), supplier.get());
+
+    supplier.run();
+    assertEquals(endpoints(0, 1), queries);
+    assertEquals(endpoints(1), attempts);
+    assertEquals(endpoints(0, 1), supplier.get());
+
+    supplier.run();
+    assertEquals(endpoints(0, 1, 2), queries);
+    assertEquals(endpoints(1, 2), attempts);
+    assertEquals(endpoints(2), supplier.get());
+    assertEquals(endpoints(0, 1, 2), supplier.getSeedNodes());
+  }
+
+  @Test
+  public void testAllUnavailableRetriesAreBoundedAndDeduplicated() throws 
Exception {
+    supplier.close();
+    supplier = createSupplier(endpoints(0, 0, 1, 2));
+    advertised = endpoints(1);
+    supplier.run();
+    nodes.forEach(node -> node.online = false);
+    attempts.clear();
+    assertFalse(reconnect(newConnection()));
+    assertEquals(SessionConfig.RETRY_NUM * 3, attempts.size());
+    for (int i = 0; i < SessionConfig.RETRY_NUM; i++) {
+      List<TEndPoint> round = attempts.subList(i * 3, (i + 1) * 3);
+      assertEquals(nodes.get(1).endpoint, round.get(0));
+      assertEquals(new HashSet<>(endpoints(0, 1, 2)), new HashSet<>(round));
+    }
+    assertTrue(refreshes.isEmpty());
+    assertEquals(endpoints(0, 1, 2), supplier.getSeedNodes());
+  }
+
+  @Test
+  public void testSeedSnapshotIsIndependentOfCallerAndDiscovery() {
+    supplier.close();
+    List<TEndPoint> seeds = endpoints(0, 1);
+    supplier = createSupplier(seeds);
+    seeds.get(0).setPort(1);
+    seeds.clear();
+    advertised = endpoints(2);
+    supplier.run();
+    assertEquals(endpoints(0, 1), supplier.getSeedNodes());
+    assertEquals(endpoints(2), supplier.get());
+  }
+
+  @Test
+  public void testBackgroundRefreshDoesNotContactSeedsWhileCurrentNodeWorks() {
+    advertised = endpoints(1);
+    supplier.run();
+    attempts.clear();
+    supplier.run();
+    assertTrue(attempts.isEmpty());
+    assertEquals(endpoints(1), supplier.get());
+  }
+
+  @Test
+  public void testNodeUrlsSessionReopenAfterFailover() throws Exception {
+    assertSessionReopenAfterFailover(false);
+  }
+
+  @Test
+  public void testSingleHostSessionReopenAfterFailover() throws Exception {
+    assertSessionReopenAfterFailover(true);
+  }
+
+  @Test
+  public void testStaticNodeUrlsSurviveReopenAndSecondFailover() throws 
Exception {
+    nodes.get(1).online = false;
+    try (Session session =
+        new Session.Builder()
+            .nodeUrls(Arrays.asList(url(0), url(1), url(0)))
+            .enableAutoFetch(false)
+            .build()) {
+      session.open(false, 1000);
+      assertEquals(nodes.get(0).endpoint, session.defaultEndPoint);
+      nodes.get(0).online = false;
+      nodes.get(1).online = true;
+      assertTrue(reconnect(session.defaultSessionConnection));
+      assertEquals(nodes.get(1).endpoint, session.defaultEndPoint);
+      session.close();
+
+      session.open(false, 1000);
+      INodeSupplier reopened = Whitebox.getInternalState(session, 
"availableNodes");
+      assertEquals(endpoints(1, 0), reopened.get());
+      nodes.get(1).online = false;
+      nodes.get(0).online = true;
+      assertTrue(reconnect(session.defaultSessionConnection));
+      assertEquals(nodes.get(0).endpoint, session.defaultEndPoint);
+      advertised = endpoints(0);
+      try (SessionDataSet result = session.executeQueryStatement("SHOW 
AVAILABLE URLS")) {
+        assertTrue(result.hasNext());
+        result.next();
+        assertFalse(result.hasNext());
+      }
+    }
+  }
+
+  private void assertSessionReopenAfterFailover(boolean singleHost) throws 
Exception {
+    List<String> urls = new ArrayList<>(Collections.singletonList(url(0)));
+    Session session =
+        singleHost
+            ? new Session.Builder()
+                .host(nodes.get(0).endpoint.getIp())
+                .port(nodes.get(0).endpoint.getPort())
+                .build()
+            : new Session.Builder().nodeUrls(urls).build();
+    urls.clear();
+    advertised = endpoints(0, 1);
+    try {
+      session.open(false, 1000);
+      NodesSupplier first = Whitebox.getInternalState(session, 
"availableNodes");
+      awaitSnapshot(first, endpoints(0, 1));
+      nodes.get(0).online = false;
+      advertised = endpoints(1);
+      awaitSnapshot(first, endpoints(1));
+      SessionConnection connection = Whitebox.getInternalState(session, 
"defaultSessionConnection");
+      assertTrue(reconnect(connection));
+      assertEquals(nodes.get(1).endpoint, connection.getEndPoint());
+      session.close();
+
+      attempts.clear();
+      session.open(false, 1000);
+      NodesSupplier reopened = Whitebox.getInternalState(session, 
"availableNodes");
+      assertEquals(endpoints(0), reopened.getSeedNodes());
+      assertEquals(endpoints(1), reopened.get());
+      awaitSnapshot(reopened, endpoints(1));
+      for (int i = 0; i < 10; i++) {
+        try (SessionDataSet result = session.executeQueryStatement("SHOW 
AVAILABLE URLS")) {
+          assertTrue(result.hasNext());
+          result.next();
+          assertFalse(result.hasNext());
+        }
+      }
+      assertFalse(attempts.contains(nodes.get(0).endpoint));
+    } finally {
+      session.close();
+    }
+  }
+
+  private void awaitSnapshot(NodesSupplier current, List<TEndPoint> expected) 
throws Exception {
+    long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
+    while (!expected.equals(current.get()) && System.nanoTime() < deadline) {
+      current.run();
+      TimeUnit.MILLISECONDS.sleep(10);
+    }
+    assertEquals(expected, current.get());
+  }
+
+  @Test
+  public void testPoolListConstructorCopiesUrlsForFutureSessions() throws 
Exception {
+    List<String> urls = new ArrayList<>(Collections.singletonList(url(0)));
+    SessionPool pool = new SessionPool(urls, "root", "root", 1);
+    try {
+      urls.clear();
+      try (SessionDataSetWrapper result = pool.executeQueryStatement("SHOW 
AVAILABLE URLS")) {
+        assertTrue(result.hasNext());
+      }
+      NodesSupplier current = Whitebox.getInternalState(pool, 
"availableNodes");
+      assertEquals(endpoints(0), current.getSeedNodes());
+    } finally {
+      pool.close();
+    }
+  }
+
+  @Test
+  public void 
testCloseAbortsBlockedSessionOpenAndDoesNotTryRemainingCandidates() throws 
Exception {
+    supplier.close();
+    ExecutorService tasks = Executors.newCachedThreadPool();
+    Socket peer = null;
+    try (ServerSocket listener = new ServerSocket(0)) {
+      List<TEndPoint> seeds = new ArrayList<>();
+      seeds.add(new TEndPoint("127.0.0.1", listener.getLocalPort()));
+      seeds.addAll(endpoints(1, 2));
+      supplier = createSupplier(seeds, 0);
+      Future<Socket> accepted = tasks.submit(listener::accept);
+      Future<?> refresh = tasks.submit(supplier::run);
+      peer = accepted.get(5, TimeUnit.SECONDS);
+      peer.setSoTimeout(5000);
+      assertTrue(peer.getInputStream().read() >= 0);
+      // Timeout 0 means the peer can stall openSession forever unless close 
aborts the socket.
+      tasks.submit(supplier::close).get(5, TimeUnit.SECONDS);
+      refresh.get(5, TimeUnit.SECONDS);
+      assertTrue(attempts.isEmpty());
+      assertNull(Whitebox.getInternalState(supplier, "client"));
+      assertNull(Whitebox.getInternalState(supplier, "activeClient"));
+      supplier.run();
+      assertTrue(attempts.isEmpty());
+      while (peer.getInputStream().read() != -1) {
+        // Drain the already-written request before observing socket closure.
+      }
+    } finally {
+      if (peer != null) {
+        peer.close();
+      }
+      tasks.shutdownNow();
+      assertTrue(tasks.awaitTermination(5, TimeUnit.SECONDS));
+    }
+  }
+
+  @Test
+  public void testCloseAbortsBlockedDiscoveryQuery() throws Exception {
+    advertised = endpoints(0);
+    supplier.close();
+    supplier = createSupplier(endpoints(0, 1, 2), 0);
+    supplier.run();
+    TestNode node = nodes.get(0);
+    node.queryEntered = new CountDownLatch(1);
+    node.queryRelease = new CountDownLatch(1);
+    ExecutorService tasks = Executors.newCachedThreadPool();
+    attempts.clear();
+    try {
+      Future<?> refresh = tasks.submit(supplier::run);
+      assertTrue(node.queryEntered.await(5, TimeUnit.SECONDS));
+      tasks.submit(supplier::close).get(5, TimeUnit.SECONDS);
+      refresh.get(5, TimeUnit.SECONDS);
+      assertTrue(attempts.isEmpty());
+      assertNull(Whitebox.getInternalState(supplier, "client"));
+      assertNull(Whitebox.getInternalState(supplier, "activeClient"));
+    } finally {
+      node.queryRelease.countDown();
+      tasks.shutdownNow();
+      assertTrue(tasks.awaitTermination(5, TimeUnit.SECONDS));
+    }
+  }
+
+  @Test
+  public void testQuerySelectionDuringEmptyDiscoveryRefresh() throws Exception 
{
+    advertised = endpoints(0);
+    supplier.run();
+    ExecutorService reader = Executors.newSingleThreadExecutor();
+    AtomicBoolean finished = new AtomicBoolean();
+    CountDownLatch started = new CountDownLatch(1);
+    Future<?> selection =
+        reader.submit(
+            () -> {
+              started.countDown();
+              while (!finished.get()) {
+                supplier
+                    .getQueryEndPoint()
+                    .ifPresent(endpoint -> assertEquals(nodes.get(0).endpoint, 
endpoint));
+                assertTrue(supplier.getQueryEndPoint().isPresent());
+              }
+            });
+    try {
+      assertTrue(started.await(5, TimeUnit.SECONDS));
+      for (int i = 0; i < 100; i++) {
+        advertised = i % 2 == 0 ? Collections.emptyList() : endpoints(0);
+        supplier.run();
+      }
+    } finally {
+      finished.set(true);
+      reader.shutdownNow();
+      assertTrue(reader.awaitTermination(5, TimeUnit.SECONDS));
+    }
+    selection.get(5, TimeUnit.SECONDS);
+  }
+
+  @Test
+  public void testRefreshRequestsAreCoalescedAndIgnoredAfterClose() {
+    supplier.requestRefresh();
+    supplier.requestRefresh();
+    assertEquals(1, refreshes.size());
+    refreshes.remove(0).run();
+    supplier.requestRefresh();
+    assertEquals(1, refreshes.size());
+    supplier.close();
+    refreshes.remove(0).run();
+    supplier.requestRefresh();
+    assertTrue(refreshes.isEmpty());
+  }
+
+  private NodesSupplier createSupplier(List<TEndPoint> seeds) {
+    return createSupplier(seeds, 1000);
+  }
+
+  private NodesSupplier createSupplier(List<TEndPoint> seeds, int timeout) {
+    return NodesSupplier.createNodeSupplier(
+        seeds,
+        executor,
+        "root",
+        "root",
+        ZoneId.of("UTC"),
+        SessionConfig.DEFAULT_INITIAL_BUFFER_CAPACITY,
+        SessionConfig.DEFAULT_MAX_FRAME_SIZE,
+        timeout,
+        false,
+        null,
+        null,
+        null,
+        null,
+        "TLS",
+        false,
+        "V_1_0");
+  }
+
+  private SessionConnection newConnection() {
+    SessionConnection connection = new SessionConnection("tree");
+    Session session = new 
Session.Builder().nodeUrls(Collections.singletonList(url(0))).build();
+    session.connectionTimeoutInMs = 1000;
+    Whitebox.setInternalState(connection, "session", session);
+    Whitebox.setInternalState(connection, "zoneId", ZoneId.of("UTC"));
+    Whitebox.setInternalState(connection, "transport", mock(TTransport.class));
+    Whitebox.setInternalState(connection, "availableNodes", supplier);
+    connections.add(connection);
+    return connection;
+  }
+
+  private boolean reconnect(SessionConnection connection) throws Exception {
+    return Whitebox.invokeMethod(connection, "reconnect");
+  }
+
+  private List<TEndPoint> endpoints(int... indices) {
+    List<TEndPoint> result = new ArrayList<>();
+    for (int index : indices) {
+      result.add(nodes.get(index).endpoint.deepCopy());
+    }
+    return result;
+  }
+
+  private String url(int index) {
+    return UrlUtils.convertTEndPointIpv4AndIpv6Url(nodes.get(index).endpoint);
+  }
+
+  private TSStatus success() {
+    return new TSStatus(TSStatusCode.SUCCESS_STATUS.getStatusCode());
+  }
+
+  private class TestNode implements AutoCloseable {
+    private final TEndPoint endpoint;
+    private final TServerSocket socket;
+    private final TThreadPoolServer server;
+    private final ExecutorService workers = Executors.newCachedThreadPool();
+    private final Thread thread;
+    private volatile boolean online = true;
+    private volatile boolean failQuery;
+    private volatile boolean queryApplicationError;
+    private volatile CountDownLatch queryEntered;
+    private volatile CountDownLatch queryRelease;
+    private final ThreadLocal<TTransport> activeTransport = new 
ThreadLocal<>();
+
+    private TestNode() throws Exception {
+      ServerSocket listener = new ServerSocket(0);
+      endpoint = new TEndPoint("127.0.0.1", listener.getLocalPort());
+      socket = new TServerSocket(listener);
+      IClientRPCService.Iface handler = mock(IClientRPCService.Iface.class);
+      when(handler.openSession(any()))
+          .thenAnswer(
+              invocation -> {
+                attempts.add(endpoint);
+                checkOnline();
+                return new TSOpenSessionResp(success(), 
TSProtocolVersion.IOTDB_SERVICE_PROTOCOL_V3)
+                    .setSessionId(1);
+              });
+      when(handler.requestStatementId(anyLong())).thenReturn(1L);
+      when(handler.closeSession(any())).thenReturn(success());
+      when(handler.closeOperation(any())).thenReturn(success());
+      when(handler.executeQueryStatementV2(any()))
+          .thenAnswer(
+              invocation -> {
+                checkOnline();
+                queries.add(endpoint);
+                if (queryEntered != null) {
+                  queryEntered.countDown();
+                  queryRelease.await();
+                }
+                if (failQuery) {
+                  if (queryApplicationError) {
+                    throw new TApplicationException(
+                        TApplicationException.INTERNAL_ERROR, "Discovery query 
failed");
+                  }
+                  return new TSExecuteStatementResp(
+                      new 
TSStatus(TSStatusCode.EXECUTE_STATEMENT_ERROR.getStatusCode())
+                          .setMessage("Discovery query failed"));
+                }
+                TsBlockBuilder block =
+                    new TsBlockBuilder(Arrays.asList(TSDataType.TEXT, 
TSDataType.INT32));
+                for (TEndPoint address : advertised) {
+                  block.getTimeColumnBuilder().writeLong(0);
+                  
block.getColumnBuilder(0).writeBinary(BytesUtils.valueOf(address.getIp()));
+                  block.getColumnBuilder(1).writeInt(address.getPort());
+                  block.declarePosition();
+                }
+                return new TSExecuteStatementResp(success())
+                    .setQueryId(1)
+                    .setColumns(Arrays.asList("RpcAddress", "RpcPort"))
+                    .setDataTypeList(Arrays.asList("TEXT", "INT32"))
+                    .setQueryResult(
+                        advertised.isEmpty()
+                            ? Collections.emptyList()
+                            : Collections.singletonList(
+                                new TsBlockSerde().serialize(block.build())))
+                    .setIgnoreTimeStamp(true)
+                    .setMoreData(false);
+              });
+      server =
+          new TThreadPoolServer(
+              new TThreadPoolServer.Args(socket)
+                  .processor(new IClientRPCService.Processor<>(handler))
+                  .protocolFactory(new TBinaryProtocol.Factory())
+                  .transportFactory(DeepCopyRpcTransportFactory.INSTANCE)
+                  .executorService(workers));
+      server.setServerEventHandler(
+          new TServerEventHandler() {
+            @Override
+            public void preServe() {}
+
+            @Override
+            public ServerContext createContext(TProtocol input, TProtocol 
output) {
+              return mock(ServerContext.class);
+            }
+
+            @Override
+            public void processContext(ServerContext context, TTransport 
input, TTransport output) {
+              activeTransport.set(input);
+            }
+
+            @Override
+            public void deleteContext(ServerContext context, TProtocol input, 
TProtocol output) {
+              activeTransport.remove();
+            }
+          });
+      CountDownLatch ready = new CountDownLatch(1);
+      thread =
+          new Thread(
+              () -> {
+                ready.countDown();
+                server.serve();
+              },
+              "seed-fallback-test-server");
+      thread.setDaemon(true);
+      thread.start();
+      assertTrue(ready.await(5, TimeUnit.SECONDS));
+    }
+
+    private void checkOnline() throws TException {
+      if (!online) {
+        // A stopped node breaks the transport; a reachable peer returning an 
RPC error is
+        // different.
+        TTransport current = activeTransport.get();
+        if (current != null) {
+          current.close();
+        }
+        throw new TException("Node is restarting");
+      }
+    }
+
+    @Override
+    public void close() throws Exception {
+      server.stop();
+      socket.close();
+      workers.shutdownNow();
+      thread.join(5000);
+      assertTrue(workers.awaitTermination(5, TimeUnit.SECONDS));
+    }
+  }
+}
diff --git 
a/iotdb-client/session/src/test/java/org/apache/iotdb/session/ThriftConnectionLifecycleTest.java
 
b/iotdb-client/session/src/test/java/org/apache/iotdb/session/ThriftConnectionLifecycleTest.java
new file mode 100644
index 00000000000..a702f0d3eee
--- /dev/null
+++ 
b/iotdb-client/session/src/test/java/org/apache/iotdb/session/ThriftConnectionLifecycleTest.java
@@ -0,0 +1,207 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.session;
+
+import org.apache.iotdb.common.rpc.thrift.TEndPoint;
+import org.apache.iotdb.isession.SessionConfig;
+import org.apache.iotdb.rpc.IoTDBConnectionException;
+import org.apache.iotdb.rpc.TElasticFramedTransport;
+
+import org.apache.thrift.transport.TSocket;
+import org.apache.thrift.transport.TTransport;
+import org.apache.thrift.transport.TTransportException;
+import org.junit.Test;
+import org.powermock.reflect.Whitebox;
+
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.time.ZoneId;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+public class ThriftConnectionLifecycleTest {
+  @Test
+  public void testAbortWhileTcpConnectIsBlocked() throws Exception {
+    assertTcpConnectAborted(false);
+  }
+
+  @Test
+  public void testAbortBeforeTcpConnectCannotReopenSocket() throws Exception {
+    assertTcpConnectAborted(true);
+  }
+
+  private void assertTcpConnectAborted(boolean beforeConnect) throws Exception 
{
+    CountDownLatch entered = new CountDownLatch(1);
+    CountDownLatch proceed = new CountDownLatch(beforeConnect ? 1 : 0);
+    CountDownLatch closed = new CountDownLatch(1);
+    Socket socket = mock(Socket.class);
+    doAnswer(
+            invocation -> {
+              closed.countDown();
+              return null;
+            })
+        .when(socket)
+        .close();
+    doAnswer(
+            invocation -> {
+              entered.countDown();
+              assertTrue(closed.await(5, TimeUnit.SECONDS));
+              throw new SocketException("Connection cancelled by test");
+            })
+        .when(socket)
+        .connect(any(SocketAddress.class), anyInt());
+    TSocket raw = new TSocket("127.0.0.1", 1, 0);
+    raw.close();
+    Whitebox.setInternalState(raw, "socket_", socket);
+    TTransport prepared =
+        new TElasticFramedTransport(
+            raw,
+            SessionConfig.DEFAULT_INITIAL_BUFFER_CAPACITY,
+            SessionConfig.DEFAULT_MAX_FRAME_SIZE,
+            true) {
+          @Override
+          public void open() throws TTransportException {
+            if (beforeConnect) {
+              entered.countDown();
+              try {
+                assertTrue(proceed.await(5, TimeUnit.SECONDS));
+              } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new TTransportException(e);
+              }
+            }
+            super.open();
+          }
+        };
+    ThriftConnection connection = connectionReturning(prepared, null, null);
+    ExecutorService tasks = Executors.newCachedThreadPool();
+    try {
+      Future<?> initialization =
+          tasks.submit(
+              () -> {
+                initialize(connection);
+                return null;
+              });
+      assertTrue(entered.await(5, TimeUnit.SECONDS));
+      tasks.submit(connection::abort).get(5, TimeUnit.SECONDS);
+      assertTrue(closed.await(5, TimeUnit.SECONDS));
+      if (beforeConnect) {
+        // TSocket.close() would null this field and allow open() to create 
another socket.
+        assertSame(socket, raw.getSocket());
+      }
+      proceed.countDown();
+      assertConnectionFailure(initialization);
+      verify(socket).connect(any(SocketAddress.class), anyInt());
+    } finally {
+      closed.countDown();
+      proceed.countDown();
+      tasks.shutdownNow();
+      assertTrue(tasks.awaitTermination(5, TimeUnit.SECONDS));
+      connection.close();
+    }
+  }
+
+  @Test
+  public void testTransportCreatedAfterClosureIsDisposedWithoutOpening() 
throws Exception {
+    CountDownLatch creating = new CountDownLatch(1);
+    CountDownLatch publish = new CountDownLatch(1);
+    TTransport prepared = mock(TTransport.class);
+    ThriftConnection connection = connectionReturning(prepared, creating, 
publish);
+    ExecutorService tasks = Executors.newCachedThreadPool();
+    try {
+      Future<?> initialization =
+          tasks.submit(
+              () -> {
+                initialize(connection);
+                return null;
+              });
+      assertTrue(creating.await(5, TimeUnit.SECONDS));
+      tasks.submit(connection::close).get(5, TimeUnit.SECONDS);
+      publish.countDown();
+      assertConnectionFailure(initialization);
+      verify(prepared).close();
+      verify(prepared, never()).open();
+    } finally {
+      publish.countDown();
+      tasks.shutdownNow();
+      assertTrue(tasks.awaitTermination(5, TimeUnit.SECONDS));
+      connection.close();
+    }
+  }
+
+  private ThriftConnection connectionReturning(
+      TTransport prepared, CountDownLatch creating, CountDownLatch publish) {
+    return new ThriftConnection(
+        new TEndPoint("127.0.0.1", 1),
+        SessionConfig.DEFAULT_INITIAL_BUFFER_CAPACITY,
+        SessionConfig.DEFAULT_MAX_FRAME_SIZE,
+        0) {
+      @Override
+      protected TTransport createTransport(
+          boolean ssl,
+          String trustStore,
+          String trustStorePwd,
+          String keyStore,
+          String keyStorePwd,
+          String protocol)
+          throws TTransportException {
+        if (creating != null) {
+          creating.countDown();
+          try {
+            assertTrue(publish.await(5, TimeUnit.SECONDS));
+          } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new TTransportException(e);
+          }
+        }
+        return prepared;
+      }
+    };
+  }
+
+  private void initialize(ThriftConnection connection) throws 
IoTDBConnectionException {
+    connection.init(
+        false, null, null, null, null, "TLS", "root", "root", false, 
ZoneId.of("UTC"), "V_1_0");
+  }
+
+  private void assertConnectionFailure(Future<?> initialization) throws 
Exception {
+    try {
+      initialization.get(5, TimeUnit.SECONDS);
+      fail("A cancelled initialization must not succeed");
+    } catch (ExecutionException e) {
+      assertTrue(e.getCause() instanceof IoTDBConnectionException);
+    }
+  }
+}
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowAvailableUrlsTask.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowAvailableUrlsTask.java
index 6d957769b44..4e12fdfffb9 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowAvailableUrlsTask.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowAvailableUrlsTask.java
@@ -19,7 +19,7 @@
 
 package org.apache.iotdb.db.queryengine.plan.execution.config.metadata;
 
-import org.apache.iotdb.commons.cluster.RegionStatus;
+import org.apache.iotdb.commons.cluster.NodeStatus;
 import org.apache.iotdb.commons.schema.column.ColumnHeader;
 import org.apache.iotdb.commons.schema.column.ColumnHeaderConstant;
 import org.apache.iotdb.confignode.rpc.thrift.TDataNodeInfo;
@@ -58,7 +58,11 @@ public class ShowAvailableUrlsTask implements IConfigTask {
     if (showDataNodesResp.getDataNodesInfoList() != null) {
       for (TDataNodeInfo dataNodeInfo : 
showDataNodesResp.getDataNodesInfoList()) {
         String status = dataNodeInfo.getStatus();
-        if (RegionStatus.Removing.getStatus().equals(status)) {
+        if (NodeStatus.Removing.getStatus().equals(status)
+            || NodeStatus.Unknown.getStatus().equals(status)
+            || NodeStatus.Stopped.getStatus().equals(status)
+            || (NodeStatus.ReadOnly.getStatus().equals(status)
+                && 
NodeStatus.STOPPING.equals(dataNodeInfo.getStatusReason()))) {
           continue;
         }
         builder.getTimeColumnBuilder().writeLong(0L);
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowAvailableUrlsTaskTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowAvailableUrlsTaskTest.java
new file mode 100644
index 00000000000..94344f4bed8
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/ShowAvailableUrlsTaskTest.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.db.queryengine.plan.execution.config.metadata;
+
+import org.apache.iotdb.commons.cluster.NodeStatus;
+import org.apache.iotdb.confignode.rpc.thrift.TDataNodeInfo;
+import org.apache.iotdb.confignode.rpc.thrift.TShowDataNodesResp;
+import org.apache.iotdb.db.queryengine.plan.execution.config.ConfigTaskResult;
+
+import com.google.common.util.concurrent.SettableFuture;
+import org.apache.tsfile.read.common.block.TsBlock;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertEquals;
+
+public class ShowAvailableUrlsTaskTest {
+  @Test
+  public void testOnlyRunningAndReadOnlyNodesAreReturned() throws Exception {
+    List<TDataNodeInfo> nodes =
+        Arrays.stream(NodeStatus.values())
+            .map(
+                status ->
+                    new TDataNodeInfo()
+                        .setStatus(status.getStatus())
+                        .setStatusReason("reason")
+                        .setRpcAddresss("127.0.0.1")
+                        .setRpcPort(6667 + status.ordinal()))
+            .collect(Collectors.toList());
+    TsBlock result = execute(nodes);
+    assertEquals(2, result.getPositionCount());
+    assertEquals("127.0.0.1", result.getColumn(0).getBinary(0).toString());
+    assertEquals(6667 + NodeStatus.Running.ordinal(), 
result.getColumn(1).getInt(0));
+    assertEquals(6667 + NodeStatus.ReadOnly.ordinal(), 
result.getColumn(1).getInt(1));
+  }
+
+  @Test
+  public void testNoOnlineNodes() throws Exception {
+    assertEquals(
+        0,
+        execute(
+                Arrays.asList(
+                    new 
TDataNodeInfo().setStatus(NodeStatus.Unknown.getStatus()),
+                    new 
TDataNodeInfo().setStatus(NodeStatus.Stopped.getStatus()),
+                    new 
TDataNodeInfo().setStatus(NodeStatus.Removing.getStatus())))
+            .getPositionCount());
+    assertEquals(0, execute(Collections.emptyList()).getPositionCount());
+    assertEquals(0, execute(null).getPositionCount());
+  }
+
+  @Test
+  public void testReadOnlyStoppingIsExcludedButOtherReasonsRemainReadable() 
throws Exception {
+    List<TDataNodeInfo> nodes =
+        Arrays.asList(
+            readOnlyNode(6667, NodeStatus.STOPPING),
+            readOnlyNode(6668, NodeStatus.DISK_FULL),
+            readOnlyNode(6669, NodeStatus.MANUAL),
+            readOnlyNode(6670, null));
+    TsBlock result = execute(nodes);
+    assertEquals(3, result.getPositionCount());
+    for (int i = 0; i < 3; i++) {
+      assertEquals(6668 + i, result.getColumn(1).getInt(i));
+    }
+  }
+
+  private TDataNodeInfo readOnlyNode(int port, String reason) {
+    return new TDataNodeInfo()
+        .setStatus(NodeStatus.ReadOnly.getStatus())
+        .setStatusReason(reason)
+        .setRpcAddresss("127.0.0.1")
+        .setRpcPort(port);
+  }
+
+  private TsBlock execute(List<TDataNodeInfo> nodes) throws Exception {
+    SettableFuture<ConfigTaskResult> future = SettableFuture.create();
+    ShowAvailableUrlsTask.buildTsBlock(
+        new TShowDataNodesResp().setDataNodesInfoList(nodes), future);
+    return future.get().getResultSet();
+  }
+}

Reply via email to