|
30 | 30 | import org.apache.iotdb.it.framework.IoTDBTestRunner; |
31 | 31 | import org.apache.iotdb.itbase.category.ClusterIT; |
32 | 32 | import org.apache.iotdb.rpc.IoTDBConnectionException; |
| 33 | +import org.apache.iotdb.rpc.StatementExecutionException; |
33 | 34 | import org.apache.iotdb.rpc.UrlUtils; |
34 | 35 | import org.apache.iotdb.session.NodesSupplier; |
35 | 36 | import org.apache.iotdb.session.Session; |
|
63 | 64 | @RunWith(IoTDBTestRunner.class) |
64 | 65 | @Category(ClusterIT.class) |
65 | 66 | public class SessionSeedNodeFallbackIT { |
| 67 | + private static final int RECOVERY_CONNECTION_TIMEOUT_MS = 30_000; |
| 68 | + |
66 | 69 | @Before |
67 | 70 | public void setUp() { |
68 | 71 | EnvFactory.getEnv() |
@@ -137,32 +140,41 @@ public ScheduledFuture<?> scheduleAtFixedRate( |
137 | 140 | return super.scheduleAtFixedRate(command, 1, 1, TimeUnit.DAYS); |
138 | 141 | } |
139 | 142 | }; |
140 | | - NodesSupplier business = createSupplier(executor); |
141 | | - NodesSupplier background = createSupplier(executor); |
142 | | - try (Session session = new Session.Builder().nodeUrls(urls()).enableAutoFetch(false).build()) { |
143 | | - Set<TEndPoint> discovered = new HashSet<>(Arrays.asList(endpoint(1), endpoint(2))); |
144 | | - awaitSnapshot(business, discovered); |
145 | | - awaitSnapshot(background, discovered); |
146 | | - session.open(false, 1000, new ConcurrentHashMap<>(), business); |
| 143 | + // Node 2 keeps a Ratis majority alive for login queries, but is unknown to these clients until |
| 144 | + // discovery recovers. Initialize a controlled stale snapshot without running discovery first. |
| 145 | + List<TEndPoint> discovered = Collections.singletonList(endpoint(1)); |
| 146 | + List<TEndPoint> seeds = Arrays.asList(endpoint(0), endpoint(1)); |
| 147 | + NodesSupplier business = createSupplier(executor, discovered, seeds); |
| 148 | + NodesSupplier background = createSupplier(executor, discovered, seeds); |
| 149 | + List<String> seedUrls = |
| 150 | + seeds.stream().map(UrlUtils::convertTEndPointIpv4AndIpv6Url).collect(Collectors.toList()); |
| 151 | + try (Session session = |
| 152 | + new Session.Builder().nodeUrls(seedUrls).enableAutoFetch(false).build()) { |
| 153 | + assertEquals(discovered, business.get()); |
| 154 | + assertEquals(discovered, background.get()); |
| 155 | + session.open(false, RECOVERY_CONNECTION_TIMEOUT_MS, new ConcurrentHashMap<>(), business); |
| 156 | + assertEquals(endpoint(1), connectedEndpoint(session)); |
147 | 157 |
|
148 | 158 | EnvFactory.getEnv().startDataNode(0); |
149 | 159 | awaitStatus(0, NodeStatus.Running); |
| 160 | + awaitLogin(0); |
150 | 161 | EnvFactory.getEnv().getDataNodeWrapper(1).stopForcibly(); |
151 | 162 | awaitStatus(1, NodeStatus.Unknown); |
152 | | - EnvFactory.getEnv().shutdownDataNode(2); |
153 | | - awaitStatus(2, EnvUtils.getNodeStatusAfterLocalStop()); |
154 | | - assertFalse(business.get().contains(endpoint(0))); |
155 | | - assertFalse(background.get().contains(endpoint(0))); |
| 163 | + assertEquals(discovered, business.get()); |
| 164 | + assertEquals(discovered, background.get()); |
156 | 165 |
|
157 | | - // Both discovered endpoints are dead. Only the initial seed omitted by discovery is live. |
| 166 | + // Every discovered endpoint is dead. Node 0 is the only reachable initial seed. |
| 167 | + Set<TEndPoint> recoveredNodes = new HashSet<>(Arrays.asList(endpoint(0), endpoint(2))); |
158 | 168 | try (SessionDataSet result = session.executeQueryStatement("SHOW AVAILABLE URLS")) { |
159 | | - assertEquals(Collections.singleton(endpoint(0)), readEndpoints(result)); |
| 169 | + assertEquals(recoveredNodes, readEndpoints(result)); |
160 | 170 | } |
161 | | - awaitSnapshot(background, Collections.singleton(endpoint(0))); |
162 | | - assertEquals(3, background.getSeedNodes().size()); |
| 171 | + assertEquals(endpoint(0), connectedEndpoint(session)); |
| 172 | + awaitSnapshot(background, recoveredNodes); |
| 173 | + assertEquals(seeds, background.getSeedNodes()); |
163 | 174 | // Wait for the immediate refresh requested by business reconnection, not a 60-second poll. |
164 | | - executor.submit(() -> {}).get(10, TimeUnit.SECONDS); |
165 | | - assertEquals(Collections.singletonList(endpoint(0)), business.get()); |
| 175 | + executor.submit(() -> {}).get(60, TimeUnit.SECONDS); |
| 176 | + assertEquals(recoveredNodes, new HashSet<>(business.get())); |
| 177 | + assertEquals(seeds, business.getSeedNodes()); |
166 | 178 | } finally { |
167 | 179 | business.close(); |
168 | 180 | background.close(); |
@@ -191,17 +203,24 @@ private NodesSupplier supplier(Object owner) throws Exception { |
191 | 203 | return (NodesSupplier) field.get(owner); |
192 | 204 | } |
193 | 205 |
|
194 | | - private NodesSupplier createSupplier(ScheduledThreadPoolExecutor executor) { |
195 | | - List<TEndPoint> seeds = Arrays.asList(endpoint(0), endpoint(1), endpoint(2)); |
| 206 | + private TEndPoint connectedEndpoint(Session session) throws Exception { |
| 207 | + Field field = Session.class.getDeclaredField("defaultEndPoint"); |
| 208 | + field.setAccessible(true); |
| 209 | + return (TEndPoint) field.get(session); |
| 210 | + } |
| 211 | + |
| 212 | + private NodesSupplier createSupplier( |
| 213 | + ScheduledThreadPoolExecutor executor, List<TEndPoint> discovered, List<TEndPoint> seeds) { |
196 | 214 | return NodesSupplier.createNodeSupplier( |
| 215 | + discovered, |
197 | 216 | seeds, |
198 | 217 | executor, |
199 | 218 | SessionConfig.DEFAULT_USER, |
200 | 219 | SessionConfig.DEFAULT_PASSWORD, |
201 | 220 | ZoneId.of("UTC"), |
202 | 221 | SessionConfig.DEFAULT_INITIAL_BUFFER_CAPACITY, |
203 | 222 | SessionConfig.DEFAULT_MAX_FRAME_SIZE, |
204 | | - 1000, |
| 223 | + RECOVERY_CONNECTION_TIMEOUT_MS, |
205 | 224 | false, |
206 | 225 | null, |
207 | 226 | null, |
@@ -240,6 +259,30 @@ private void awaitStatus(int index, NodeStatus status) { |
240 | 259 | Collections.singletonList(status)); |
241 | 260 | } |
242 | 261 |
|
| 262 | + private void awaitLogin(int index) { |
| 263 | + TEndPoint recovered = endpoint(index); |
| 264 | + Awaitility.await() |
| 265 | + .atMost(60, TimeUnit.SECONDS) |
| 266 | + .pollInterval(100, TimeUnit.MILLISECONDS) |
| 267 | + .untilAsserted( |
| 268 | + () -> { |
| 269 | + // Probe the recovered seed independently so the tested snapshots remain stale. |
| 270 | + try (Session probe = |
| 271 | + new Session.Builder() |
| 272 | + .host(recovered.getIp()) |
| 273 | + .port(recovered.getPort()) |
| 274 | + .enableAutoFetch(false) |
| 275 | + .build()) { |
| 276 | + probe.open(false, RECOVERY_CONNECTION_TIMEOUT_MS); |
| 277 | + try (SessionDataSet result = probe.executeQueryStatement("SHOW AVAILABLE URLS")) { |
| 278 | + assertTrue(readEndpoints(result).contains(recovered)); |
| 279 | + } |
| 280 | + } catch (IoTDBConnectionException | StatementExecutionException e) { |
| 281 | + throw new AssertionError("Seed is not ready for session queries: " + recovered, e); |
| 282 | + } |
| 283 | + }); |
| 284 | + } |
| 285 | + |
243 | 286 | private void awaitSnapshot(NodesSupplier current, Set<TEndPoint> expected) { |
244 | 287 | Awaitility.await() |
245 | 288 | .atMost(30, TimeUnit.SECONDS) |
|
0 commit comments