* 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>
* build(deps): bump constant_time_eq from 0.5.0 to 0.6.0
Bumps constant_time_eq from 0.5.0 to 0.6.0.
---
updated-dependencies:
- dependency-name: constant_time_eq
dependency-version: 0.6.0
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot] <support@github.com>
* fix(deps): bump constant_time_eq version pin to 0.6.0
Dependabot updated Cargo.lock to 0.6.0 but left Cargo.toml at 0.5.0,
so --locked builds failed and cargo regenerated the lockfile back to 0.5.0.
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: qdrant-cloud-bot <111755117+qdrant-cloud-bot@users.noreply.github.com>
* Remove MessagePack (rmp-serde)
The WAL switched from msgpack to CBOR in v0.3.5 (2021-07-11), so v0.3.4
is the last version that wrote msgpack entries. Drop the read fallback
kept for those entries, plus the remaining test and bench usages.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Drop unused fs4 dependency from collection
Not referenced anywhere in the crate. Still used by wal and common, so
the workspace entry stays.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Add optional dial9 Tokio telemetry behind a `dial9` feature
Integrate dial9 so storage runtimes can emit production-friendly Tokio
traces. Recording is off unless the crate is built with `--features dial9`
and DIAL9_ENABLED=true is set at runtime; with the feature off, runtime
construction is byte-for-byte unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y7X6MkjY3P7wpP2MfHdTDY
* Enable dial9 CPU and schedule profiling
Turn on cpu-profiling and sched events behind the same `dial9` feature,
add the DIAL9_CPU_* / DIAL9_SCHEDULE_* env knobs, and document the frame
pointer rustflags the stack unwinder needs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y7X6MkjY3P7wpP2MfHdTDY
* Harden dial9 env parsing and the writer-failure path
- Reset Cargo.lock to the branch point and re-resolve, so the diff is
additive instead of re-resolving unrelated packages. This drops the
heck 0.5.0 -> 0.4.1 downgrade, which sat in the default build graph and
would have changed proto codegen identifier casing. The remaining
non-additive entry, toml_parser 1.0.9 -> 1.1.3, is forced by
proc-macro-crate via dial9-trace-format-derive.
- Parse DIAL9_* booleans the way dial9 does, accepting 1/y/yes/on and
0/n/no/off and warning on anything else. `str::parse::<bool>` took only
exact lowercase true/false, so DIAL9_CPU_PROFILE_ENABLED=0 silently left
99 Hz sampling on and DIAL9_ENABLED=1 silently left recording off.
- Require the numeric knobs to be positive. A zero disk budget made dial9
evict everything and stop recording within seconds while the log still
reported telemetry enabled.
- Treat a set-but-empty DIAL9_TRACE_DIR as unset. It skipped the /tmp
fallback and wrote up to the full budget into the working directory,
which is /qdrant next to storage/ in the official image.
- Return a disabled guard as soon as the trace writer fails, before
with_cpu_profiling and with_sched_events run. Those start their profilers
eagerly, opening a perf event per thread and installing a process-global
signal handler that build() would then discard.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y7X6MkjY3P7wpP2MfHdTDY
* Correct the dial9 docs and give them their own section
- `--cfg tokio_unstable` is required for any task data at all, not merely
for fuller coverage: dial9's poll, spawn and terminate hooks are all
`#[cfg(tokio_unstable)]`, and nothing in the repo sets the flag. Without
it there is no task timeline and DIAL9_TASK_TRACKING_ENABLED does nothing.
- Document `-C debuginfo=2`. `[profile.perf]` inherits `release` and sets no
`debug` key, so the documented build symbolized off the ELF symtab with
inlined callees collapsed and no file or line, unlike `[profile.bench]`
which sets `debug = true` for this reason.
- Move the dial9 material out from between the feature list and the prose
that belongs to it. Those paragraphs describe `tracing` instrumentation
and read as dial9's when the example is wedged in front of them, which
points readers at `#[tracing::instrument]` for a tool that records Tokio
runtime events and no tracing spans.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y7X6MkjY3P7wpP2MfHdTDY
* Use cfg_select!
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: timvisee <tim@visee.me>
* feat: create appendable segments when the write target is optimizing or the shard is empty
* review: SegmentManifestState::is_writable, caller-supplied temp dir, uuid from token
- `SegmentManifestState::is_writable` with a full match replaces the ad-hoc
`matches!` in the manifest enumerator.
- `ListedSegment` is destructured in `open` so every field is accounted for.
- `create_appendable_from` is test-only; `create_appendable` is the API.
- `create_appendable` builds the scratch segment in a caller-supplied local
`temp_path` (conventionally `<shard>/temp_segments`) instead of the system
temp dir, and takes the uuid from the build token instead of parsing the path.
Upload speed of `copy_dir_via` is tracked in #10433.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: generall <andrey@vasnetsov.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* Split async IO into extension traits; only async-capable backends implement them
Move `read_bytes_async` / `open_async` off the universal `UniversalRead` /
`UniversalReadFs` traits into dedicated extension traits, `UniversalReadAsync`
and `UniversalReadFsAsync` (traits/async_io.rs). Only backends with a genuine
async story implement them — the blob family, the disk caches layered over it,
and a trivial ready-impl for mmap (tests and the mmap lookup path) — each in a
dedicated async_io.rs next to its sync impl.
`CachedFs` now requires its inner filesystem to be `UniversalReadFsAsync`; the
requirement reaches segment code through one supertrait bound on
`UniversalReadExt`. io_uring implements no async surface anymore: the
tokio_uring bridge thread, its tests, the musl-gated tokio-uring dependency,
and the `IoUringFile` read-only-segment wiring (`UniversalReadExt` impl and
the *RoIoUring condition-checker variants) are deleted — io_uring is not a
read-only-segment backend.
The payoff for live reload: `CachedFs::resolve_prefetched` awaits every parked
prefetch, and the edge refresh flow now runs preload -> resolve -> reload, so
the per-segment write locks never wait on IO.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Decouple UniversalReadExt from the async filesystem requirement
UniversalReadExt is condition-checker dispatch; it never consumed the async
surface itself. Drop its `Fs: UniversalReadFsAsync` supertrait bound and relax
CachedFs's struct-level bound back to `UniversalReadFs` — the async requirement
now lives on the one impl that consumes it, `CachedReadFs for CachedFs`
(schedule_open parks the inner filesystem's `open_async` futures).
The bound then surfaces only on the lifecycle/preload impl blocks that go
through CachedReadFs (segment open, live-preload/reload, config reload, edge
load/refresh); the search path carries no async bounds at all.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* existing segments: wait for IO outside of search pool
* new segments: wait for IO outside of search pool
* extract reload into separate function
* Update lib/edge/Cargo.toml
---------
Co-authored-by: Tim Visée <tim+github@visee.me>
* `UniversalReadFs::open_async`
* `schedule_open` polls once
Scheduled opens must start eagerly: sync backends complete their
`open_async` on the first poll, preserving the prefetch contract
(handles outlive later file deletions/replacements). Moved down from
the integration branch so this PR stays green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Drops 6 crates from the release build and 7 from the workspace test
build, with no source changes.
- geo: no triangulation, only Contains/Intersects/Haversine (spade, earcut)
- jsonwebtoken: HS256 from_secret only, no PEM keys (pem, simple_asn1)
- tar: nothing sets unpack_xattrs, which defaults to false (xattr)
- duplicate: every duplicate_item names its module (proc-macro2-diagnostics)
- pprof: no C++ frames to demangle (cpp_demangle)
Also promotes duplicate to a workspace dependency.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Replace cgroups-rs with direct cgroup memory file reads
We used cgroups-rs in exactly one place, to read the memory limit and
usage of our own cgroup, so read those files directly instead. Drops 34
crates from the lockfile, including the zbus stack that carries
RUSTSEC-2026-0221.
Also fixes two latent cgroup v1 bugs (the LONG_MAX unlimited sentinel
reported ~9 EB of total memory, an unreadable limit file reported 0
bytes) and the hierarchy mix-up on hybrid hosts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Decline cgroup memory reporting when the usage read fails
Reporting a usage of 0 made available_memory_bytes claim the whole cgroup
limit as free. Fall back to sysinfo when the usage file cannot be read at
init, and keep the last known value on a failed refresh.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Keep the last known memory limit when its read fails
A transient read failure cleared the cached limit and silently fell back
to host memory while the process was still capped, the same direction of
over-reporting as the usage read. Both now keep their last known value,
and a limit lifted at runtime still clears.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Treat a malformed memory limit as an error, not as unlimited
Parse failures returned Ok(None), so garbage in the limit file cleared a
valid cached limit on refresh and read as unlimited at init. Reserve
Ok(None) for "max" and the v1 sentinel, and report anything else as
InvalidData so the last known limit survives.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>