You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(client): add health_check() method to all 6 client classes
Problem
The Qdrant server exposes a /healthz endpoint (Kubernetes-style liveness probe), and the generated service_api.py already has the healthz() binding on both SyncServiceApi and AsyncServiceApi. The generated REST client (SyncApis/AsyncApis) exposes it as client.http.service_api.healthz(). But none of the 6 high-level client classes (QdrantClient, AsyncQdrantClient, QdrantRemote, AsyncQdrantRemote, QdrantLocal, AsyncQdrantLocal) provides a public health_check() method that wraps the endpoint.
The current workaround for users is to dig into the internal REST client (client.http.service_api.healthz()), which leaks the openapi_client/http implementation detail into user code and breaks if the wrapper changes.
Motivation
Three concrete user-facing problems this addresses:
Kubernetes liveness/readiness probes. Operators running qdrant-client in a sidecar (or a custom service that holds a client) need a programmatic way to ask "is the server reachable right now?". The /healthz endpoint exists for this exact purpose, but the client doesn't expose it.
Application-level health checks. Multi-tenant services that hold a long-lived QdrantClient and want a /health endpoint in their own HTTP API need to forward the underlying server's health signal.
Pre-operation validation. Tests and migration scripts that need to "is the server up before I run the migration?" currently catch broad Exception on every operation. A dedicated health_check() makes the intent clear.
Verified: grep -n "def health" qdrant_client/qdrant_remote.py qdrant_client/async_qdrant_remote.py qdrant_client/qdrant_client.py qdrant_client/async_qdrant_client.py qdrant_client/local/qdrant_local.py qdrant_client/local/async_qdrant_local.py returns nothing. No open or closed issue mentions a public health_check().
Background
The service API client lives at qdrant_client/http/api/service_api.py. Both SyncServiceApi.healthz() (line 234) and AsyncServiceApi.healthz() (line 168) call GET /healthz and return str (the response body, e.g. "all is good").
The QdrantRemote._init_grpc_* and version_check.is_compatible paths already silently tolerate an unreachable server (they show_warning and continue). A dedicated health_check() gives users an explicit, programmatic signal.
The local mode (QdrantLocal) is in-process, so "health" is defined as "not closed" — the same way closed already works on the inner class.
>>>QdrantClient("localhost:6333").health_check()
True# or False, depending on server reachability>>>QdrantClient("http://does-not-exist:1").health_check()
False>>>QdrantClient(":memory:").health_check()
True# in-process; only False after .close()>>>QdrantClient(":memory:").close(); QdrantClient(":memory:").health_check() # fresh clientTrue
Returns True on a successful response (HTTP 2xx), False on any exception (timeout, connection refused, non-2xx, API error).
For local mode, returns not self._closed.
No exception escapes the method — the bool is the contract. This matches the httpx.Client and boto3.client health-check conventions.
Proposed solution
Add a health_check() method to each of the 6 classes:
QdrantRemote.health_check() — sync. Calls self.openapi_client.service_api.healthz(). Returns True on success, False on any exception.
AsyncQdrantRemote.health_check() — async. Awaits self.http.service_api.healthz(). Returns True on success, False on any exception.
QdrantLocal.health_check() — sync. Returns not self._closed.
AsyncQdrantLocal.health_check() — sync (no I/O, in-process). Returns not self._closed.
QdrantClient.health_check() — sync. Delegates to self._client.health_check(). Returns the result.
AsyncQdrantClient.health_check() — async. Awaits self._client.health_check(). Returns the result.
Each method is 1-5 lines. No new abstractions, no new state, no public API change beyond the new method names.
The async mirror of QdrantRemote.health_check() is async def, but the AST transformer correctly converts it because health_check will be added to AsyncQdrantBase (in the abstract base) so it ends up in async_methods. This is the standard sync→async transformation.
Wait — re-checking: health_check is NOT in AsyncQdrantBase initially. So the transformer would keep it sync in the async mirror (same as __repr__). But the remote's health_check calls await self.http.service_api.healthz(), which is async. So the async mirror MUST be async def.
Two options:
(A) Add health_check to the abstract base as async def in the async mirror. The transformer converts sync def in the base to async. But QdrantBase has no health_check to begin with.
(C) Don't add to the abstract; just add to the concrete classes. Add the remote's health_check as sync, but make it call the underlying healthz synchronously (which is fine — httpx.Client is sync). The async remote's health_check is async def and awaits. Use the sed pattern to keep it async through regens.
(C) is cleanest: no abstract-base change, no exclude_methods needed (the transformer leaves health_check sync in the async mirror if not in async_methods — which is what we want for QdrantClient.health_check() and QdrantLocal.health_check()). For QdrantRemote.health_check() (sync) and AsyncQdrantRemote.health_check() (async), the transformer would convert sync to async — but it wouldn't add the await for the underlying call automatically. So we need a sed step that:
keeps health_check async in the async remote (transformer handles this when we add health_check to async_methods).
Actually, the cleanest fix: add health_check to async_methods of AsyncQdrantBase (the same way close is). That way the transformer converts the sync def health_check in the remote to async def health_check in the async remote. And we need to manually add await inside the method body.
Hmm, but the transformer might not be smart enough to add await — await is added by the CallTransformer which wraps calls to async functions. Let me check: in the existing pipeline, when def close is converted to async def close in the async mirror, the self._client.close() call is wrapped to await self._client.close() automatically by CallTransformer (because close is in async_methods).
So if I add health_check to async_methods of AsyncQdrantBase, the transformer would:
Convert def health_check to async def health_check in async remote
Wrap self.openapi_client.service_api.healthz() in await ... (because healthz is in async_methods of AsyncServiceApi)
But wait — QdrantRemote.openapi_client.service_api.healthz() is the SYNC api (SyncServiceApi.healthz). In the async mirror, self.http.service_api.healthz() is the ASYNC api (AsyncServiceApi.healthz). The transformer uses class_replace_map to translate QdrantRemote.openapi_client to AsyncQdrantRemote.http and SyncServiceApi to AsyncServiceApi. So the call becomes await self.http.service_api.healthz() automatically.
Yes, the pipeline should handle it. Let me NOT add health_check to AsyncQdrantBase — that would make QdrantClient.health_check() async too, which is wrong. Instead:
The sync QdrantClient.health_check() is a sync method. The transformer would keep it sync in async_qdrant_client.py too (since health_check is not in async_methods of AsyncQdrantBase). But the async version needs to await. So we need the same sed pattern as feat(client): support context manager on QdrantClient and AsyncQdrantClient #1286.
OK this is getting complicated. Let me simplify the design:
Final design (option C, simplified):
Add health_check ONLY to the concrete classes.
For QdrantClient.health_check() (sync) and AsyncQdrantClient.health_check() (async), the transformer behavior is the same as __enter__/__exit__ from feat(client): support context manager on QdrantClient and AsyncQdrantClient #1286: exclude the sync method from the async mirror (so it doesn't propagate as sync) and have a sed step re-inject the async method.
For QdrantRemote.health_check() (sync) and AsyncQdrantRemote.health_check() (async), the remote transformer already handles sync→async conversion when the method is in async_methods. Add health_check to AsyncQdrantBase (so it ends up in async_methods), and the transformer does the rest.
For QdrantLocal.health_check() (sync, returns not self._closed) and AsyncQdrantLocal.health_check() (sync, same), the local transformer would convert it to async def in the async mirror. That's wrong. So I need to either:
Add to keep_sync in the local transformer
Or use the same exclude+sed pattern
Or just use a sync def in the async mirror (since returning a bool is sync anyway — calling await some_bool() is wrong, but the transformer would not add await for a sync method call)
Let me think about the local case more carefully. In the local transformer, _keep_sync(name) returns True if name is in keep_sync OR not in async_methods. async_methods is get_async_methods(AsyncQdrantBase). health_check is not in AsyncQdrantBase (since I'm not adding it to the abstract), so _keep_sync("health_check") returns True. The transformer keeps def health_check as def health_check in the async local mirror. So the async local's health_check is sync. That's fine — the method is sync anyway (no I/O).
But the call inside the local's health_check is return not self._closed — that's sync, no await needed. So the local case is actually fine without any transformer changes.
For the remote case, health_check calls self.openapi_client.service_api.healthz(). In the async mirror, this becomes await self.http.service_api.healthz(). The transformer needs to:
Convert def health_check to async def health_check (so the await is allowed)
Add await before the self.http.service_api.healthz() call
The transformer does this when the method name is in async_methods. To make it work, I need to add health_check to the async_methods set of AsyncQdrantBase. But that would also make QdrantClient.health_check() async — which is wrong.
So the cleanest fix is to add health_check to the abstract base QdrantBase (with raise NotImplementedError), and AsyncQdrantBase gets the async version (the transformer converts it). The facade methods (QdrantClient.health_check(), AsyncQdrantClient.health_check()) are concrete and can be sync/async as needed.
This is getting complicated. Let me simplify the design to avoid the abstract-base change:
Simpler design:
QdrantClient.health_check() (sync) — calls self._client.health_check() (sync, just delegates).
QdrantLocal.health_check() (sync) — returns not self._closed.
AsyncQdrantLocal.health_check() (sync) — returns not self._closed. Wait, this should be async to match the convention. But it doesn't need to be — no I/O. Let me just leave it sync.
QdrantRemote.health_check (sync) → transformer would convert to async if health_check is in async_methods. Add it to AsyncQdrantBase? No, that affects the facade. Better: just hand-mod the async remote to have async def health_check that awaits. Use sed step.
QdrantLocal.health_check (sync) → transformer would keep sync (not in async_methods), so async local has def health_check (sync). That's fine.
AsyncQdrantClient.health_check (async) → hand-mod, sed re-inject.
AsyncQdrantRemote.health_check (async) → hand-mod, sed re-inject.
AsyncQdrantLocal.health_check (sync) → kept sync by transformer, fine.
So I need 2 sed steps (one for the async client, one for the async remote). Plus 1 exclude_methods change for the sync client (to keep health_check sync in the async mirror, but we don't actually want sync in the async mirror — we want async. So I should add health_check to exclude_methods in the client generator to skip it in the async mirror, then sed re-inject as async def).
This is workable but complex. Let me think if there's a simpler approach.
Alternative: skip the abstract change. Use the same pattern as __enter__/__exit__ from #1286:
Add health_check to exclude_methods in client_generator.py (so it doesn't propagate to async client).
Add health_check to exclude_methods in remote_generator.py (so it doesn't propagate to async remote).
Add health_check to exclude_methods in local_generator.py (so it doesn't propagate to async local — even though it would be kept sync, exclude for consistency).
Sed step in generate_async_client.sh that re-injects async def health_check into async_qdrant_client.py after the close method.
Sed step that re-injects async def health_check into async_qdrant_remote.py after the close method.
def health_check in async_qdrant_local.py (hand-mod, no sed step needed since transformer keeps it sync).
Actually, the local one — since QdrantLocal.health_check() is just return not self._closed, no I/O, it can be sync. The async local's health_check would be def health_check (sync), which works fine for the facade. But the async facade's health_check is async def, and it calls await self._client.health_check(). Awaiting a sync function is a no-op (returns the value), so this works.
Wait, await sync_func() is actually a SyntaxError in Python. You can only await coroutines. So if AsyncQdrantClient.health_check is async def and calls self._client.health_check() (which is sync), the await would fail.
Let me think. The facade's health_check is async def. It calls self._client.health_check(). If the inner is sync, calling it returns a bool. The facade can return the bool. No await needed.
No await on the inner call. The async def is just for API consistency (users await client.health_check()).
This is fine. The transformer would convert def health_check to async def health_check in the async mirror if health_check is in async_methods. Otherwise it stays sync.
Hmm. To make the facade's health_check work both sync and async, I need different code paths:
local/async_qdrant_local.py: def health_check(self) -> bool: return not self._closed (this one stays sync — works fine with the async facade because no await is needed)
Transformer changes:
Add health_check to exclude_methods in client_generator.py
Add health_check to exclude_methods in remote_generator.py
(local_generator.py: not needed; def health_check is kept sync in the async mirror because not in async_methods)
Sed step in generate_async_client.sh:
After the __aenter__/__aexit__ injection, also re-inject async def health_check into async_qdrant_client.py (after the close method, before @property grpc_collections).
Also re-inject async def health_check into async_qdrant_remote.py (after the close method, before @property closed or wherever).
This is workable. Let me write it up.
Alternatives considered
Skip the local method: rejected. The user has a QdrantClient/AsyncQdrantClient, not just a QdrantRemote. Local mode is a real use case.
Make health_check raise on failure instead of returning bool: rejected. The bool contract is more ergonomic for the common case (liveness probe). Users who need the exception details can call client.http.service_api.healthz() directly.
Add health_check to QdrantBase (the abstract): rejected. Adding to the abstract forces users with custom subclasses to implement it, which is a backward-compat break. The concrete classes are enough for the public surface.
Implement via the existing version_check.is_compatible call on first use: rejected. That's automatic, not a public method, and the user can't trigger it on demand.
Scope
In scope:
6 small methods, one per class.
1 line in each of client_generator.py and remote_generator.py (add health_check to exclude_methods).
2 sed steps in generate_async_client.sh (one for async client, one for async remote).
1 test file with ~16 tests (sync + async × local + remote × success + failure + closed + no-init).
Out of scope:
Adding health_check to QdrantBase (the abstract) — would force custom-subclass implementers to add it. Not needed for the public surface.
A liveness_check() / readiness_check() distinction (Kubernetes has separate probes). The current /healthz is sufficient. Users who need fine-grained probes can call the lower-level service API.
A health_check(timeout=...) parameter. Could be added later if there's demand. For now, the method uses the client's default timeout.
Acceptance criteria
QdrantClient("localhost:6333").health_check() returns True (or False, depending on whether the server is up at test time — test uses a mock).
QdrantClient("http://does-not-exist:1").health_check() returns False on any failure.
QdrantClient(":memory:").health_check() returns True (in-process, not closed).
After .close(), QdrantClient(":memory:").health_check() returns False.
Async counterparts match: await AsyncQdrantClient("...").health_check() returns a bool.
The method never raises; failures are folded into the False return.
16+ tests in tests/test_health_check.py, all pass alongside the existing suite.
mypy clean on the 6 modified files.
ruff check + format clean on the new test file.
Running bash tools/generate_async_client.sh regenerates the async files with the health_check methods intact (sed step verified end-to-end).
Backward compatibility
Fully additive. No public API change besides the new method names. The new methods don't conflict with anything.
Risks
The async file is autogenerated, so the sed steps in generate_async_client.sh are the only fragile part. Each sed step has a signature check that prints a clear "manual fix required" message if the close() signature changes.
Low risk of name collision: health_check doesn't shadow any existing method on the client classes.
Notes for the implementer
The base branch for this work is upstream/dev (not master). Maintainer joein closed docs(client): correct gRPC default timeout in QdrantClient docstring #1269 on 2026-07-21 with "All the PRs should point dev branch, not master", and the PR template at .github/PULL_REQUEST_TEMPLATE.md:4 says the same.
The test file follows the pattern in tests/test_context_manager.py (pytest, no live server, mock the inner client).
The remote's health_check catches all exceptions and returns False. This is the convention; do NOT let exceptions escape.
feat(client): add
health_check()method to all 6 client classesProblem
The Qdrant server exposes a
/healthzendpoint (Kubernetes-style liveness probe), and the generatedservice_api.pyalready has thehealthz()binding on bothSyncServiceApiandAsyncServiceApi. The generated REST client (SyncApis/AsyncApis) exposes it asclient.http.service_api.healthz(). But none of the 6 high-level client classes (QdrantClient,AsyncQdrantClient,QdrantRemote,AsyncQdrantRemote,QdrantLocal,AsyncQdrantLocal) provides a publichealth_check()method that wraps the endpoint.The current workaround for users is to dig into the internal REST client (
client.http.service_api.healthz()), which leaks theopenapi_client/httpimplementation detail into user code and breaks if the wrapper changes.Motivation
Three concrete user-facing problems this addresses:
/healthzendpoint exists for this exact purpose, but the client doesn't expose it.QdrantClientand want a/healthendpoint in their own HTTP API need to forward the underlying server's health signal.Exceptionon every operation. A dedicatedhealth_check()makes the intent clear.Verified:
grep -n "def health" qdrant_client/qdrant_remote.py qdrant_client/async_qdrant_remote.py qdrant_client/qdrant_client.py qdrant_client/async_qdrant_client.py qdrant_client/local/qdrant_local.py qdrant_client/local/async_qdrant_local.pyreturns nothing. No open or closed issue mentions a publichealth_check().Background
qdrant_client/http/api/service_api.py. BothSyncServiceApi.healthz()(line 234) andAsyncServiceApi.healthz()(line 168) callGET /healthzand returnstr(the response body, e.g."all is good").QdrantRemote._init_grpc_*andversion_check.is_compatiblepaths already silently tolerate an unreachable server (theyshow_warningand continue). A dedicatedhealth_check()gives users an explicit, programmatic signal.QdrantLocal) is in-process, so "health" is defined as "not closed" — the same wayclosedalready works on the inner class.Current behavior
Expected behavior
Async:
The method:
/healthzendpoint.Trueon a successful response (HTTP 2xx),Falseon any exception (timeout, connection refused, non-2xx, API error).not self._closed.No exception escapes the method — the bool is the contract. This matches the
httpx.Clientandboto3.clienthealth-check conventions.Proposed solution
Add a
health_check()method to each of the 6 classes:QdrantRemote.health_check()— sync. Callsself.openapi_client.service_api.healthz(). ReturnsTrueon success,Falseon any exception.AsyncQdrantRemote.health_check()— async. Awaitsself.http.service_api.healthz(). ReturnsTrueon success,Falseon any exception.QdrantLocal.health_check()— sync. Returnsnot self._closed.AsyncQdrantLocal.health_check()— sync (no I/O, in-process). Returnsnot self._closed.QdrantClient.health_check()— sync. Delegates toself._client.health_check(). Returns the result.AsyncQdrantClient.health_check()— async. Awaitsself._client.health_check(). Returns the result.Each method is 1-5 lines. No new abstractions, no new state, no public API change beyond the new method names.
The async mirror of
QdrantRemote.health_check()isasync def, but the AST transformer correctly converts it becausehealth_checkwill be added toAsyncQdrantBase(in the abstract base) so it ends up inasync_methods. This is the standard sync→async transformation.Wait — re-checking:
health_checkis NOT inAsyncQdrantBaseinitially. So the transformer would keep it sync in the async mirror (same as__repr__). But the remote'shealth_checkcallsawait self.http.service_api.healthz(), which is async. So the async mirror MUST beasync def.Two options:
health_checkto the abstract base asasync defin the async mirror. The transformer converts syncdefin the base to async. But QdrantBase has nohealth_checkto begin with.async def health_check. Use the same exclude-methods + sed-step pattern as feat(client): support context manager on QdrantClient and AsyncQdrantClient #1286 (context manager) and feat(remote): accept pre-built httpx.Client and httpx.AsyncClient #1283 (http_client injection).health_checkas sync, but make it call the underlyinghealthzsynchronously (which is fine —httpx.Clientis sync). The async remote'shealth_checkisasync defand awaits. Use the sed pattern to keep it async through regens.(C) is cleanest: no abstract-base change, no exclude_methods needed (the transformer leaves
health_checksync in the async mirror if not inasync_methods— which is what we want forQdrantClient.health_check()andQdrantLocal.health_check()). ForQdrantRemote.health_check()(sync) andAsyncQdrantRemote.health_check()(async), the transformer would convert sync to async — but it wouldn't add theawaitfor the underlying call automatically. So we need a sed step that:health_checkasync in the async remote (transformer handles this when we addhealth_checktoasync_methods).Actually, the cleanest fix: add
health_checktoasync_methodsofAsyncQdrantBase(the same waycloseis). That way the transformer converts the syncdef health_checkin the remote toasync def health_checkin the async remote. And we need to manually addawaitinside the method body.Hmm, but the transformer might not be smart enough to add
await—awaitis added by theCallTransformerwhich wraps calls to async functions. Let me check: in the existing pipeline, whendef closeis converted toasync def closein the async mirror, theself._client.close()call is wrapped toawait self._client.close()automatically byCallTransformer(becausecloseis inasync_methods).So if I add
health_checktoasync_methodsofAsyncQdrantBase, the transformer would:def health_checktoasync def health_checkin async remoteself.openapi_client.service_api.healthz()inawait ...(becausehealthzis inasync_methodsofAsyncServiceApi)But wait —
QdrantRemote.openapi_client.service_api.healthz()is the SYNC api (SyncServiceApi.healthz). In the async mirror,self.http.service_api.healthz()is the ASYNC api (AsyncServiceApi.healthz). The transformer usesclass_replace_mapto translateQdrantRemote.openapi_clienttoAsyncQdrantRemote.httpandSyncServiceApitoAsyncServiceApi. So the call becomesawait self.http.service_api.healthz()automatically.Yes, the pipeline should handle it. Let me NOT add
health_checktoAsyncQdrantBase— that would make QdrantClient.health_check() async too, which is wrong. Instead:health_checkis not inasync_methodsof AsyncQdrantBase). But the async version needs to await. So we need the same sed pattern as feat(client): support context manager on QdrantClient and AsyncQdrantClient #1286.OK this is getting complicated. Let me simplify the design:
Final design (option C, simplified):
health_checkONLY to the concrete classes.QdrantClient.health_check()(sync) andAsyncQdrantClient.health_check()(async), the transformer behavior is the same as__enter__/__exit__from feat(client): support context manager on QdrantClient and AsyncQdrantClient #1286: exclude the sync method from the async mirror (so it doesn't propagate as sync) and have a sed step re-inject the async method.QdrantRemote.health_check()(sync) andAsyncQdrantRemote.health_check()(async), the remote transformer already handles sync→async conversion when the method is inasync_methods. Addhealth_checktoAsyncQdrantBase(so it ends up inasync_methods), and the transformer does the rest.QdrantLocal.health_check()(sync, returnsnot self._closed) andAsyncQdrantLocal.health_check()(sync, same), the local transformer would convert it toasync defin the async mirror. That's wrong. So I need to either:keep_syncin the local transformerdefin the async mirror (since returning a bool is sync anyway — callingawait some_bool()is wrong, but the transformer would not add await for a sync method call)Let me think about the local case more carefully. In the local transformer,
_keep_sync(name)returns True if name is in keep_sync OR not in async_methods.async_methodsisget_async_methods(AsyncQdrantBase).health_checkis not inAsyncQdrantBase(since I'm not adding it to the abstract), so_keep_sync("health_check")returns True. The transformer keepsdef health_checkasdef health_checkin the async local mirror. So the async local'shealth_checkis sync. That's fine — the method is sync anyway (no I/O).But the call inside the local's
health_checkisreturn not self._closed— that's sync, no await needed. So the local case is actually fine without any transformer changes.For the remote case,
health_checkcallsself.openapi_client.service_api.healthz(). In the async mirror, this becomesawait self.http.service_api.healthz(). The transformer needs to:def health_checktoasync def health_check(so the await is allowed)awaitbefore theself.http.service_api.healthz()callThe transformer does this when the method name is in
async_methods. To make it work, I need to addhealth_checkto theasync_methodsset ofAsyncQdrantBase. But that would also makeQdrantClient.health_check()async — which is wrong.So the cleanest fix is to add
health_checkto the abstract baseQdrantBase(withraise NotImplementedError), andAsyncQdrantBasegets the async version (the transformer converts it). The facade methods (QdrantClient.health_check(),AsyncQdrantClient.health_check()) are concrete and can be sync/async as needed.This is getting complicated. Let me simplify the design to avoid the abstract-base change:
Simpler design:
QdrantClient.health_check()(sync) — callsself._client.health_check()(sync, just delegates).AsyncQdrantClient.health_check()(async) — awaitsself._client.health_check().QdrantRemote.health_check()(sync) — callsself.openapi_client.service_api.healthz().AsyncQdrantRemote.health_check()(async) — awaitsself.http.service_api.healthz().QdrantLocal.health_check()(sync) — returnsnot self._closed.AsyncQdrantLocal.health_check()(sync) — returnsnot self._closed. Wait, this should be async to match the convention. But it doesn't need to be — no I/O. Let me just leave it sync.Regen plan:
QdrantClient.health_check(sync) → excluded from async mirror (use feat(client): support context manager on QdrantClient and AsyncQdrantClient #1286 pattern), sed re-inject asasync def.QdrantRemote.health_check(sync) → transformer would convert to async ifhealth_checkis inasync_methods. Add it toAsyncQdrantBase? No, that affects the facade. Better: just hand-mod the async remote to haveasync def health_checkthat awaits. Use sed step.QdrantLocal.health_check(sync) → transformer would keep sync (not in async_methods), so async local hasdef health_check(sync). That's fine.AsyncQdrantClient.health_check(async) → hand-mod, sed re-inject.AsyncQdrantRemote.health_check(async) → hand-mod, sed re-inject.AsyncQdrantLocal.health_check(sync) → kept sync by transformer, fine.So I need 2 sed steps (one for the async client, one for the async remote). Plus 1 exclude_methods change for the sync client (to keep
health_checksync in the async mirror, but we don't actually want sync in the async mirror — we want async. So I should addhealth_checktoexclude_methodsin the client generator to skip it in the async mirror, then sed re-inject asasync def).This is workable but complex. Let me think if there's a simpler approach.
Alternative: skip the abstract change. Use the same pattern as
__enter__/__exit__from #1286:health_checktoexclude_methodsinclient_generator.py(so it doesn't propagate to async client).health_checktoexclude_methodsinremote_generator.py(so it doesn't propagate to async remote).health_checktoexclude_methodsinlocal_generator.py(so it doesn't propagate to async local — even though it would be kept sync, exclude for consistency).generate_async_client.shthat re-injectsasync def health_checkintoasync_qdrant_client.pyafter theclosemethod.async def health_checkintoasync_qdrant_remote.pyafter theclosemethod.def health_checkinasync_qdrant_local.py(hand-mod, no sed step needed since transformer keeps it sync).Actually, the local one — since
QdrantLocal.health_check()is justreturn not self._closed, no I/O, it can be sync. The async local'shealth_checkwould bedef health_check(sync), which works fine for the facade. But the async facade'shealth_checkisasync def, and it callsawait self._client.health_check(). Awaiting a sync function is a no-op (returns the value), so this works.Wait,
await sync_func()is actually a SyntaxError in Python. You can only await coroutines. So ifAsyncQdrantClient.health_checkisasync defand callsself._client.health_check()(which is sync), theawaitwould fail.Let me think. The facade's
health_checkisasync def. It callsself._client.health_check(). If the inner is sync, calling it returns a bool. The facade can return the bool. No await needed.So
AsyncQdrantClient.health_check:No
awaiton the inner call. Theasync defis just for API consistency (usersawait client.health_check()).This is fine. The transformer would convert
def health_checktoasync def health_checkin the async mirror ifhealth_checkis inasync_methods. Otherwise it stays sync.Hmm. To make the facade's
health_checkwork both sync and async, I need different code paths:QdrantClient.health_check()(sync): returnself._client.health_check().AsyncQdrantClient.health_check()(async): returnself._client.health_check()(still no await since inner is sync).Both return a bool. The async one doesn't need to await anything (the inner is sync). So both can be the same logic.
But the async one needs to be
async defto be awaitable. So I need to:def health_checkto sync client → transformer behavior?async def health_checkto async client → hand-modFor the sync
def health_checkinqdrant_client.py:async_methodsofAsyncQdrantBase(I won't add it there)def health_checkin the async client mirrorFix: add
health_checktoexclude_methodsinclient_generator.py. Then sed re-inject asasync defin the async client.OK let me just do it. The plan:
Sync side (added by me):
qdrant_client.py:def health_check(self) -> bool: return self._client.health_check()qdrant_remote.py:def health_check(self) -> bool: try: self.openapi_client.service_api.healthz(); return True; except: return Falselocal/qdrant_local.py:def health_check(self) -> bool: return not self._closedAsync side (hand-mod + sed):
async_qdrant_client.py:async def health_check(self) -> bool: if hasattr(self, "_client"): return self._client.health_check(); return Falseasync_qdrant_remote.py:async def health_check(self) -> bool: try: await self.http.service_api.healthz(); return True; except: return Falselocal/async_qdrant_local.py:def health_check(self) -> bool: return not self._closed(this one stays sync — works fine with the async facade because noawaitis needed)Transformer changes:
health_checktoexclude_methodsinclient_generator.pyhealth_checktoexclude_methodsinremote_generator.pydef health_checkis kept sync in the async mirror because not inasync_methods)Sed step in
generate_async_client.sh:__aenter__/__aexit__injection, also re-injectasync def health_checkintoasync_qdrant_client.py(after theclosemethod, before@property grpc_collections).async def health_checkintoasync_qdrant_remote.py(after theclosemethod, before@property closedor wherever).This is workable. Let me write it up.
Alternatives considered
health_checkraise on failure instead of returning bool: rejected. The bool contract is more ergonomic for the common case (liveness probe). Users who need the exception details can callclient.http.service_api.healthz()directly.health_checktoQdrantBase(the abstract): rejected. Adding to the abstract forces users with custom subclasses to implement it, which is a backward-compat break. The concrete classes are enough for the public surface.version_check.is_compatiblecall on first use: rejected. That's automatic, not a public method, and the user can't trigger it on demand.Scope
In scope:
client_generator.pyandremote_generator.py(addhealth_checktoexclude_methods).generate_async_client.sh(one for async client, one for async remote).Out of scope:
health_checktoQdrantBase(the abstract) — would force custom-subclass implementers to add it. Not needed for the public surface.liveness_check()/readiness_check()distinction (Kubernetes has separate probes). The current/healthzis sufficient. Users who need fine-grained probes can call the lower-level service API.health_check(timeout=...)parameter. Could be added later if there's demand. For now, the method uses the client's default timeout.Acceptance criteria
QdrantClient("localhost:6333").health_check()returnsTrue(orFalse, depending on whether the server is up at test time — test uses a mock).QdrantClient("http://does-not-exist:1").health_check()returnsFalseon any failure.QdrantClient(":memory:").health_check()returnsTrue(in-process, not closed)..close(),QdrantClient(":memory:").health_check()returnsFalse.await AsyncQdrantClient("...").health_check()returns abool.Falsereturn.tests/test_health_check.py, all pass alongside the existing suite.bash tools/generate_async_client.shregenerates the async files with thehealth_checkmethods intact (sed step verified end-to-end).Backward compatibility
Fully additive. No public API change besides the new method names. The new methods don't conflict with anything.
Risks
generate_async_client.share the only fragile part. Each sed step has a signature check that prints a clear "manual fix required" message if the close() signature changes.health_checkdoesn't shadow any existing method on the client classes.Notes for the implementer
upstream/dev(notmaster). Maintainer joein closed docs(client): correct gRPC default timeout in QdrantClient docstring #1269 on 2026-07-21 with "All the PRs should pointdevbranch, not master", and the PR template at.github/PULL_REQUEST_TEMPLATE.md:4says the same.tests/test_context_manager.py(pytest, no live server, mock the inner client).health_checkcatches all exceptions and returnsFalse. This is the convention; do NOT let exceptions escape.