mirror of
https://github.com/qdrant/qdrant.git
synced 2026-09-21 13:37:46 -05:00
* Send the queue proxy batch as a pre-encoded gRPC body The parent commits moved the WAL read, the operation clone and the request build off the async runtime. Two passes over the batch were left on it. Measured cost of each synchronous pass over a 26.2 MiB send batch (800 ops x 30 points x 256 dims), release build: pass 1 clone WAL operations 72.4 ms moved by the parent commits pass 2 build gRPC request 22.2 ms moved by the parent commits pass 3 clone request to send 65.7 ms on the async runtime pass 4 protobuf encode (tonic) 41.4 ms on the async runtime Pass 3 is there because `with_points_client` takes `impl Fn` and the channel pool calls that closure once per attempt, so each attempt needs its own owned message. Pass 4 runs inside `poll_next`: for a unary call tonic encodes the whole message in a single synchronous `encode_item`, so a worker is blocked for the full 41 ms, once per attempt. Encoding the batch up front removes both. The generated client cannot take a pre-encoded body, `update_batch` is typed `impl IntoRequest<UpdateBatchInternal>`, but all it does is pick a codec and a path and call `Grpc::unary`, and we already build the client ourselves from a pooled channel. `update_batch_pre_encoded` does the same three things with a codec that writes the encoded bytes through and decodes the response with prost. What stays on the runtime is the copy of the encoded body into tonic's send buffer: 14.9 ms for 26.2 MiB under jemalloc, nearly all of it faulting in freshly mapped pages rather than the copy itself (0.9 ms when the allocator hands back warm pages). Retries share the same refcounted bytes instead of cloning and re-encoding, so they drop with it. on the runtime before 107.2 ms per attempt on the runtime after 14.9 ms per attempt Bypassing the generated client means the RPC path and message types no longer follow the proto automatically, so a test checks them against the compiled descriptor set. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WZjWYpKeYdGpKiLeEZU9oc * Hand the pre-encoded update batch a configured Grpc, take the service name from the generated code `update_batch_pre_encoded` took a bare channel plus a `max_decoding_message_size` argument, and its only caller passed `usize::MAX`. Every other internal client applies that limit inside its `with_*_client` helper, so do the same: `with_grpc` hands out the `tonic::client::Grpc` the generated clients wrap, already configured, and the argument goes away. The service half of the RPC identity now comes from the generated `points_internal_server::SERVICE_NAME` instead of a second literal. Only the method name and the path literal remain hand-written, still pinned to the descriptor set by the test. `PreEncodedMessage::encode` uses `encode_to_vec`: one pass instead of a separate `encoded_len` call, and no `expect`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * Adapt pre-encoding to the build/forward split from #10599 `forward_update_batch` takes `Arc<UpdateBatchInternal>` and encodes it once on the blocking pool, so the channel pool's attempts share the bytes. The queue proxy keeps the built request in that `Arc` across `BATCH_RETRIES` and for the per-operation isolation path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: generall <andrey@qdrant.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>