Skip to content

Commit 783a508

Browse files
committed
test(facade): server-down -> start -> write -> server-up -> reader connects
End-to-end resilience test for the QuestDB facade: build with the server down (ingest initial_connect_retry=async + query_pool_min=0), buffer a write, then bring the server up and assert the write side reconnects and the previously-deferred reader connects on the first query. Uses two TestWebSocketServers bound-but-not-accepting to model a reachable -but-down server (handshakeCount stays 0 until start()). The mock cannot serve real SELECT rows, so the read step asserts the query client connects once the server is up, not the row contents. Stable across repeated runs.
1 parent 00d3dcd commit 783a508

1 file changed

Lines changed: 134 additions & 0 deletions

File tree

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/*+*****************************************************************************
2+
* ___ _ ____ ____
3+
* / _ \ _ _ ___ ___| |_| _ \| __ )
4+
* | | | | | | |/ _ \/ __| __| | | | _ \
5+
* | |_| | |_| | __/\__ \ |_| |_| | |_) |
6+
* \__\_\\__,_|\___||___/\__|____/|____/
7+
*
8+
* Copyright (c) 2014-2019 Appsicle
9+
* Copyright (c) 2019-2026 QuestDB
10+
*
11+
* Licensed under the Apache License, Version 2.0 (the "License");
12+
* you may not use this file except in compliance with the License.
13+
* You may obtain a copy of the License at
14+
*
15+
* http://www.apache.org/licenses/LICENSE-2.0
16+
*
17+
* Unless required by applicable law or agreed to in writing, software
18+
* distributed under the License is distributed on an "AS IS" BASIS,
19+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20+
* See the License for the specific language governing permissions and
21+
* limitations under the License.
22+
*
23+
******************************************************************************/
24+
25+
package io.questdb.client.test;
26+
27+
import io.questdb.client.QuestDB;
28+
import io.questdb.client.Sender;
29+
import io.questdb.client.cutlass.qwp.client.QwpColumnBatch;
30+
import io.questdb.client.cutlass.qwp.client.QwpColumnBatchHandler;
31+
import io.questdb.client.test.cutlass.qwp.websocket.TestWebSocketServer;
32+
import org.junit.Assert;
33+
import org.junit.Test;
34+
35+
import java.util.concurrent.TimeUnit;
36+
import java.util.function.BooleanSupplier;
37+
38+
/**
39+
* End-to-end resilience: the facade starts with the server down, the producer
40+
* keeps writing (buffered), and once the server comes up the write side
41+
* reconnects and the read side -- previously deferred so it could not fail-fast
42+
* the build -- can connect.
43+
* <p>
44+
* The mock cannot answer a real SELECT (result frames are exercised against a
45+
* real server in the parent repo), so the read step asserts the query client
46+
* <em>connects</em> once the server is up, not the row contents.
47+
*/
48+
public class QuestDBServerRecoveryTest {
49+
50+
private static final QwpColumnBatchHandler NOOP = new QwpColumnBatchHandler() {
51+
@Override
52+
public void onBatch(QwpColumnBatch batch) {
53+
}
54+
55+
@Override
56+
public void onEnd(long totalRows) {
57+
}
58+
59+
@Override
60+
public void onError(byte errorCode, String message) {
61+
}
62+
};
63+
64+
@Test(timeout = 60_000)
65+
public void testFacadeStartsWhileServerDownThenWritesAndReaderConnectsOnRecovery() throws Exception {
66+
// Two mock servers, bound (so the ports are known) but NOT accepting
67+
// yet: the address is reachable but no WebSocket upgrade completes, so
68+
// the server is effectively "down". One serves ingest ACK, the other
69+
// query SERVER_INFO.
70+
TestWebSocketServer ingest = new TestWebSocketServer(new TestWebSocketServer.WebSocketServerHandler() {
71+
});
72+
TestWebSocketServer query = new TestWebSocketServer(new TestWebSocketServer.WebSocketServerHandler() {
73+
});
74+
query.setSendServerInfo(true); // the egress client's connect() waits for SERVER_INFO
75+
try {
76+
// ingest: async initial connect so the producer thread never blocks
77+
// and writes buffer until the wire is up.
78+
String ingestCfg = "ws::addr=localhost:" + ingest.getPort()
79+
+ ";sender_pool_min=1;sender_pool_max=1;initial_connect_retry=async"
80+
+ ";auth_timeout_ms=200;reconnect_initial_backoff_millis=20"
81+
+ ";reconnect_max_backoff_millis=100;reconnect_max_duration_millis=600000"
82+
+ ";close_flush_timeout_millis=1000;";
83+
// query: lazy (min=0) so the otherwise fail-fast reader never sinks
84+
// the build while the server is down.
85+
String queryCfg = "ws::addr=localhost:" + query.getPort()
86+
+ ";query_pool_min=0;query_pool_max=1;auth_timeout_ms=2000;";
87+
88+
// (1) server down + (2) client starts:
89+
QuestDB db = QuestDB.builder().ingestConfig(ingestCfg).queryConfig(queryCfg).build();
90+
try {
91+
Assert.assertEquals("no ingest handshake while the server is down", 0, ingest.handshakeCount());
92+
Assert.assertEquals("no query handshake while the server is down", 0, query.handshakeCount());
93+
94+
// (3) client writes -> buffers in the cursor SF engine; the call
95+
// must not throw even though the server is down.
96+
Sender sender = db.borrowSender();
97+
sender.table("t").longColumn("v", 1L).atNow();
98+
99+
// (4) server starts:
100+
ingest.start();
101+
query.start();
102+
Assert.assertTrue(ingest.awaitStart(5, TimeUnit.SECONDS));
103+
Assert.assertTrue(query.awaitStart(5, TimeUnit.SECONDS));
104+
105+
// The write side reconnects on its own once the server is up.
106+
awaitTrue("ingest must connect after the server comes up",
107+
() -> ingest.handshakeCount() >= 1);
108+
109+
// (5) client can now read: the deferred reader connects on the
110+
// first query (the mock does not serve rows, so we assert the
111+
// connection, not the result).
112+
db.executeSql("select 1", NOOP);
113+
awaitTrue("query client must connect after the server comes up",
114+
() -> query.handshakeCount() >= 1);
115+
} finally {
116+
db.close();
117+
}
118+
} finally {
119+
ingest.close();
120+
query.close();
121+
}
122+
}
123+
124+
private static void awaitTrue(String message, BooleanSupplier condition) throws InterruptedException {
125+
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(15);
126+
while (System.nanoTime() < deadline) {
127+
if (condition.getAsBoolean()) {
128+
return;
129+
}
130+
Thread.sleep(20);
131+
}
132+
Assert.assertTrue(message, condition.getAsBoolean());
133+
}
134+
}

0 commit comments

Comments
 (0)