Skip to content

Commit 47a4965

Browse files
committed
Added cancelation tests
1 parent fd444b0 commit 47a4965

1 file changed

Lines changed: 181 additions & 0 deletions

File tree

client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,19 @@
2121
import com.clickhouse.client.api.insert.InsertResponse;
2222
import com.clickhouse.client.api.insert.InsertSettings;
2323
import com.clickhouse.client.api.internal.DataTypeConverter;
24+
import com.clickhouse.client.api.internal.HttpAPIClientHelper;
2425
import com.clickhouse.client.api.internal.ServerSettings;
2526
import com.clickhouse.client.api.internal.ValidationUtils;
2627
import com.clickhouse.client.api.query.GenericRecord;
2728
import com.clickhouse.client.api.query.QueryResponse;
2829
import com.clickhouse.client.api.query.QuerySettings;
30+
import com.clickhouse.client.api.transport.Endpoint;
31+
import com.clickhouse.client.api.transport.HttpEndpoint;
32+
import com.clickhouse.client.api.transport.internal.TransportRequest;
33+
import com.clickhouse.client.api.transport.internal.TransportResponse;
2934
import com.clickhouse.client.config.ClickHouseClientOption;
3035
import com.clickhouse.data.ClickHouseFormat;
36+
import net.jpountz.lz4.LZ4Factory;
3137
import com.github.tomakehurst.wiremock.WireMockServer;
3238
import com.github.tomakehurst.wiremock.client.WireMock;
3339
import com.github.tomakehurst.wiremock.common.ConsoleNotifier;
@@ -64,6 +70,7 @@
6470
import javax.net.ssl.SSLHandshakeException;
6571
import java.io.ByteArrayInputStream;
6672
import java.io.ByteArrayOutputStream;
73+
import java.io.InputStream;
6774
import java.io.StringWriter;
6875
import java.math.BigInteger;
6976
import java.net.InetAddress;
@@ -102,11 +109,16 @@
102109
import java.util.concurrent.Executors;
103110
import java.util.concurrent.ThreadLocalRandom;
104111
import java.util.concurrent.TimeUnit;
112+
import java.util.concurrent.atomic.AtomicBoolean;
105113
import java.util.concurrent.atomic.AtomicInteger;
114+
import java.util.concurrent.atomic.AtomicLong;
115+
import java.util.concurrent.atomic.AtomicReference;
116+
import java.util.function.Supplier;
106117
import java.util.regex.Pattern;
107118
import java.util.zip.GZIPOutputStream;
108119

109120
import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED;
121+
import static org.testng.Assert.assertTrue;
110122
import static org.testng.Assert.fail;
111123

112124
@Test(groups = {"integration"})
@@ -2532,4 +2544,173 @@ private static String toPem(X509Certificate certificate) throws Exception {
25322544
}
25332545
return stringWriter.toString();
25342546
}
2547+
2548+
/**
2549+
* Exercises {@code HttpAPIClientHelper.TransportRequestImpl#cancel()}, which is not reachable through the
2550+
* high-level {@link Client} API. A {@link HttpAPIClientHelper} is created directly and used to start a request
2551+
* for an effectively endless stream (reading from {@code system.numbers}) to emulate a long-running query.
2552+
* The test verifies the whole life cycle against the server:
2553+
* <ol>
2554+
* <li>the query is observed running on the server ({@code system.processes});</li>
2555+
* <li>the in-flight request is cancelled from another thread and the reader stops with an error;</li>
2556+
* <li>the query is no longer running on the server;</li>
2557+
* <li>{@code system.query_log} contains a record proving the query was interrupted (not finished).</li>
2558+
* </ol>
2559+
*/
2560+
@Test(groups = {"integration"})
2561+
@SuppressWarnings("java:S2925")
2562+
public void testTransportRequestCancel() throws Exception {
2563+
if (isCloud()) {
2564+
return; // relies on direct transport access and local system tables (processes / query_log)
2565+
}
2566+
2567+
ClickHouseNode server = getServer(ClickHouseProtocol.HTTP);
2568+
String queryId = "transport-cancel-" + UUID.randomUUID();
2569+
2570+
Map<String, Object> configuration = new HashMap<>();
2571+
configuration.put(ClientConfigProperties.USER.getKey(), "default");
2572+
configuration.put(ClientConfigProperties.PASSWORD.getKey(), ClickHouseServerForTest.getPassword());
2573+
configuration.put(ClientConfigProperties.DATABASE.getKey(), ClickHouseServerForTest.getDatabase());
2574+
configuration.put(ClientConfigProperties.COMPRESS_SERVER_RESPONSE.getKey(), Boolean.FALSE);
2575+
configuration.put(ClientConfigProperties.COMPRESS_CLIENT_REQUEST.getKey(), Boolean.FALSE);
2576+
2577+
HttpAPIClientHelper helper = new HttpAPIClientHelper(new HashMap<>(configuration), null, false,
2578+
LZ4Factory.fastestInstance());
2579+
2580+
try (Client verifyClient = new Client.Builder()
2581+
.addEndpoint(Protocol.HTTP, server.getHost(), server.getPort(), false)
2582+
.setUsername("default")
2583+
.setPassword(ClickHouseServerForTest.getPassword())
2584+
.setDefaultDatabase(ClickHouseServerForTest.getDatabase())
2585+
.compressClientRequest(false)
2586+
.compressServerResponse(false)
2587+
.build()) {
2588+
2589+
Endpoint endpoint = new HttpEndpoint(server.getHost(), server.getPort(), false, "/");
2590+
2591+
Map<String, Object> requestConfig = new HashMap<>(configuration);
2592+
requestConfig.put(ClientConfigProperties.INPUT_OUTPUT_FORMAT.getKey(), ClickHouseFormat.TSV);
2593+
requestConfig.put(ClientConfigProperties.QUERY_ID.getKey(), queryId);
2594+
2595+
// Endless result set so the query stays active on the server until the request is cancelled.
2596+
TransportRequest request = helper.createRequest(endpoint, requestConfig,
2597+
"SELECT number FROM system.numbers");
2598+
2599+
AtomicBoolean readStarted = new AtomicBoolean(false);
2600+
AtomicLong bytesRead = new AtomicLong(0);
2601+
AtomicReference<Throwable> readError = new AtomicReference<>();
2602+
2603+
Thread reader = new Thread(() -> {
2604+
long start = System.currentTimeMillis();
2605+
try (TransportResponse response = helper.executeRequest(request);
2606+
InputStream in = response.createDataInputStream()) {
2607+
byte[] buffer = new byte[8192];
2608+
int read;
2609+
while ((read = in.read(buffer)) != -1) {
2610+
readStarted.set(true);
2611+
bytesRead.addAndGet(read);
2612+
// Safety valve so the test can never hang on the endless stream if cancel() fails.
2613+
if (System.currentTimeMillis() - start > 30_000) {
2614+
break;
2615+
}
2616+
}
2617+
} catch (Throwable t) {
2618+
readStarted.set(true);
2619+
readError.set(t);
2620+
}
2621+
}, "transport-request-reader");
2622+
reader.start();
2623+
2624+
// 1. The query must actually be running on the server.
2625+
Assert.assertTrue(waitForCondition(() -> isQueryRunning(verifyClient, queryId), 20_000),
2626+
"query was not observed running on the server (query_id=" + queryId + ")");
2627+
Assert.assertNull(readError.get(), "reading should still be in progress while the query runs");
2628+
2629+
// 2. Cancel the in-flight request from a separate thread.
2630+
AtomicBoolean cancelled = new AtomicBoolean(false);
2631+
AtomicReference<Throwable> cancelError = new AtomicReference<>();
2632+
Thread canceller = new Thread(() -> {
2633+
try {
2634+
cancelled.set(request.cancel());
2635+
} catch (Throwable t) {
2636+
cancelError.set(t);
2637+
}
2638+
}, "transport-request-canceller");
2639+
canceller.start();
2640+
canceller.join(5_000);
2641+
2642+
Assert.assertNull(cancelError.get(), "cancel() must not throw");
2643+
Assert.assertTrue(cancelled.get(), "cancel() should report the request as cancelled");
2644+
2645+
// The reader must stop with an error once the underlying connection is aborted.
2646+
reader.join(15_000);
2647+
Assert.assertFalse(reader.isAlive(), "reading must stop after the request was cancelled");
2648+
Assert.assertNotNull(readError.get(),
2649+
"reading a cancelled request must fail, but read " + bytesRead.get() + " bytes");
2650+
2651+
// 3. The server must stop running the query shortly after the client disconnects.
2652+
Assert.assertTrue(waitForCondition(() -> !isQueryRunning(verifyClient, queryId), 20_000),
2653+
"query is still running on the server after cancellation (query_id=" + queryId + ")");
2654+
2655+
// 4. The query_log must show the query was interrupted instead of finishing successfully.
2656+
assertQueryWasInterrupted(verifyClient, queryId);
2657+
assertTrue(request.cancel());
2658+
} finally {
2659+
helper.close();
2660+
}
2661+
}
2662+
2663+
private static boolean isQueryRunning(Client client, String queryId) {
2664+
List<GenericRecord> rows = client.queryAll(
2665+
"SELECT count() AS c FROM system.processes WHERE query_id = '" + queryId + "'");
2666+
return !rows.isEmpty() && rows.get(0).getLong("c") > 0;
2667+
}
2668+
2669+
private static boolean waitForCondition(Supplier<Boolean> condition, long timeoutMillis)
2670+
throws InterruptedException {
2671+
long deadline = System.currentTimeMillis() + timeoutMillis;
2672+
while (System.currentTimeMillis() < deadline) {
2673+
try {
2674+
if (Boolean.TRUE.equals(condition.get())) {
2675+
return true;
2676+
}
2677+
} catch (Exception ignore) {
2678+
// transient query failures (e.g. server busy) are retried until the timeout elapses
2679+
}
2680+
Thread.sleep(100);
2681+
}
2682+
return false;
2683+
}
2684+
2685+
private static void assertQueryWasInterrupted(Client client, String queryId) throws InterruptedException {
2686+
long deadline = System.currentTimeMillis() + 30_000;
2687+
String seenTypes = "";
2688+
while (System.currentTimeMillis() < deadline) {
2689+
client.queryAll("SYSTEM FLUSH LOGS");
2690+
List<GenericRecord> rows = client.queryAll(
2691+
"SELECT toString(type) AS type, exception_code FROM system.query_log " +
2692+
"WHERE query_id = '" + queryId + "' AND event_date >= today() - 1");
2693+
2694+
StringBuilder types = new StringBuilder();
2695+
for (GenericRecord row : rows) {
2696+
String type = row.getString("type");
2697+
int exceptionCode = row.getInteger("exception_code");
2698+
types.append(type).append('(').append(exceptionCode).append(") ");
2699+
2700+
Assert.assertNotEquals(type, "QueryFinish",
2701+
"query unexpectedly finished successfully instead of being interrupted (query_id="
2702+
+ queryId + ")");
2703+
if ("ExceptionWhileProcessing".equals(type)) {
2704+
Assert.assertNotEquals(exceptionCode, 0,
2705+
"an interrupted query must be logged with a non-zero exception code (query_id="
2706+
+ queryId + ")");
2707+
return; // found the proof the query was interrupted on the server
2708+
}
2709+
}
2710+
seenTypes = types.toString();
2711+
Thread.sleep(250);
2712+
}
2713+
Assert.fail("no query_log record proving the query was interrupted (query_id=" + queryId
2714+
+ ", seen types: [" + seenTypes + "])");
2715+
}
25352716
}

0 commit comments

Comments
 (0)