Files
0ff1c4e2b7 Send the queue proxy batch as a pre-encoded gRPC body (#10617)
* 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>
2026-09-14 15:38:30 +02:00
..