Describe the bug
Client.cancelTransportRequest(queryId) (client-v2) has no effect when it lands in the window between two attempts of a retried operation: the operation issues the next attempt anyway and can complete successfully, so the caller's cancellation is silently dropped.
Cancellation state is kept per transport request (ConcurrentHashMap<String, TransportRequest> ongoingRequests, keyed by query id) instead of per operation, while a retried operation creates a new TransportRequest per attempt:
- Query path: between attempt i and attempt i+1 the registry still holds attempt i's already-completed request, so
cancel() is applied to a dead request. Attempt i+1 then calls registerTransportReq(queryId, newRequest), which overwrites the entry with a fresh, un-cancelled request, so the requestIsNotCancelled(queryId) retry guard returns true and the loop keeps retrying.
- Insert paths (POJO and stream): the entry is unregistered at the end of every attempt, so in the same window
ongoingRequests.get(queryId) returns null and cancelTransportRequest is a complete no-op (requestIsNotCancelled also returns true for a missing entry).
For the stream-insert path this window is reachable deterministically through public API: the client calls DataStreamWriter#onRetry() exactly between two attempts, so a cancelTransportRequest issued from a callback, another thread, or an operation-timeout watchdog that happens to fire there is lost.
Steps to reproduce
- Point a client at a mocked server whose first response is a retryable error (HTTP 503 +
X-ClickHouse-Exception-Code: 202) and whose second response is 200.
- Insert through
Client#insert(String, DataStreamWriter, ClickHouseFormat, InsertSettings) with an explicit query id and a writer whose onRetry() calls client.cancelTransportRequest(queryId).
- The insert completes successfully and the mocked server records two requests.
Expected behaviour
Cancellation belongs to the operation, not to a single transport request: once an operation is cancelled it stays cancelled for every following attempt, so the retry loop stops instead of issuing another request and the operation fails.
Code example
String queryId = "cancel-between-attempts";
DataStreamWriter writer = new DataStreamWriter() {
@Override public void onOutput(OutputStream out) throws IOException { out.write("1\n".getBytes()); }
@Override public void onRetry() { client.cancelTransportRequest(queryId); }
};
// completes successfully today; a second attempt is sent after the cancel
client.insert("t", writer, ClickHouseFormat.TSV, new InsertSettings().setQueryId(queryId)).get();
Configuration
- Client version: 0.11.0-rc1-SNAPSHOT (
main), pre-existing on earlier versions
- Module:
client-v2 (Client.java, all three retry loops: query, POJO insert, stream insert)
Describe the bug
Client.cancelTransportRequest(queryId)(client-v2) has no effect when it lands in the window between two attempts of a retried operation: the operation issues the next attempt anyway and can complete successfully, so the caller's cancellation is silently dropped.Cancellation state is kept per transport request (
ConcurrentHashMap<String, TransportRequest> ongoingRequests, keyed by query id) instead of per operation, while a retried operation creates a newTransportRequestper attempt:cancel()is applied to a dead request. Attempt i+1 then callsregisterTransportReq(queryId, newRequest), which overwrites the entry with a fresh, un-cancelled request, so therequestIsNotCancelled(queryId)retry guard returnstrueand the loop keeps retrying.ongoingRequests.get(queryId)returnsnullandcancelTransportRequestis a complete no-op (requestIsNotCancelledalso returnstruefor a missing entry).For the stream-insert path this window is reachable deterministically through public API: the client calls
DataStreamWriter#onRetry()exactly between two attempts, so acancelTransportRequestissued from a callback, another thread, or an operation-timeout watchdog that happens to fire there is lost.Steps to reproduce
X-ClickHouse-Exception-Code: 202) and whose second response is200.Client#insert(String, DataStreamWriter, ClickHouseFormat, InsertSettings)with an explicit query id and a writer whoseonRetry()callsclient.cancelTransportRequest(queryId).Expected behaviour
Cancellation belongs to the operation, not to a single transport request: once an operation is cancelled it stays cancelled for every following attempt, so the retry loop stops instead of issuing another request and the operation fails.
Code example
Configuration
main), pre-existing on earlier versionsclient-v2(Client.java, all three retry loops: query, POJO insert, stream insert)