qdrant-client's embedded backends (:memory: and path= local mode) run
every operation synchronously on the calling thread despite the
AsyncQdrantClient surface — AsyncQdrantLocal contains no thread offload,
and this is unchanged through the latest v1.18.x (the async surface is
autogenerated from the synchronous QdrantLocal).
On a CPU-constrained host a background scan of thousands of tagged files
issues ~3 Qdrant queries per file, all on the event loop thread, pinning
one core at 100% and stalling /health/live, /health/ready, and the
outbound Nextcloud-reachability probe for minutes — the failure mode in
issue #926 (health-check timeouts, "nextcloud_reachable: error").
Wrap the embedded client in a transparent proxy that offloads every
coroutine-returning call to a worker thread via anyio.to_thread.run_sync,
keeping the event loop responsive. A dedicated CapacityLimiter(1)
serialises those offloads to preserve QdrantLocal's single-access
invariant (today guaranteed implicitly by the single-threaded loop).
Network mode (QDRANT_URL) is left untouched — it already does
non-blocking I/O. Centralised at get_qdrant_client() so all call sites
benefit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>