Skip to content

client-v2: cancelTransportRequest is lost when it lands between two retry attempts #2989

Description

@polyglotAI-bot

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

  1. 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.
  2. Insert through Client#insert(String, DataStreamWriter, ClickHouseFormat, InsertSettings) with an explicit query id and a writer whose onRetry() calls client.cancelTransportRequest(queryId).
  3. 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions