Summary
AsyncClient inherits SharedSystemClient.__init__, which calls _increment_refcount, but it defines no counterpart: no close(), and no __aenter__/__aexit__. The sync Client has all three.
So an AsyncClient takes a reference to the shared System and there is no supported way to give it back.
|
close() |
__aenter__/__aexit__ |
refcount |
Client (sync) |
yes |
yes |
increments and decrements |
AsyncClient |
no |
no |
increments, never decrements |
Why it matters — from your own docstring
chromadb/api/client.py on Client.close():
Close the client and release all resources. This method decrements the reference count for the underlying System. When the last client using a shared System calls close(), the System is stopped and all resources (database connections, etc.) are released.
This is particularly important for PersistentClient to avoid SQLite file locking issues.
The sync path states exactly why the release is needed. The async path takes the same reference and offers no way to perform it.
Reproduction
No server required — only the SharedSystemClient lifecycle is exercised.
import chromadb, tempfile
from chromadb.config import Settings
from chromadb.api.async_client import AsyncClient
from chromadb.api.shared_system_client import SharedSystemClient as S
def total(): return sum(S._identifier_to_refcount.values())
st = Settings(is_persistent=True, persist_directory=tempfile.mkdtemp())
# control: the sync client, same mechanism
before = total()
c = chromadb.PersistentClient(path=st.persist_directory)
after = total()
c.close()
print("sync :", before, "->", after, "->", total())
# the async client
before = total()
a = AsyncClient(settings=st)
print("async:", before, "->", total())
print("AsyncClient defines:", [m for m in ("close", "__aenter__", "__aexit__")
if m in AsyncClient.__dict__])
Output on chromadb 1.5.9:
sync : 0 -> 2 -> 0
async: 0 -> 1
AsyncClient defines: []
The sync control returning to baseline is what shows the refcount mechanism itself is fine — only the async class is missing the way back.
There is no error and no warning. The client works; the System simply never stops.
Scope, stated honestly
An application that creates a single long-lived AsyncClient is unaffected in practice — one retained reference costs nothing. This is not a leak that brings a server down, and I do not want it read that way.
The defect is that there is no correct way to release the reference even when you want to. Where clients are created repeatedly — per request, per task, per test — the System and everything it holds stay alive for the life of the process, and for a persistent backend that is the SQLite file-locking case the sync docstring warns about, reached through the async path.
Suggested fix
Mirroring the sync Client.close(), which guards on _closed, releases the internal admin client's reference, then its own. _release_system is already a classmethod on the shared base:
async def close(self) -> None:
if getattr(self, "_closed", False):
return
self._closed = True
if hasattr(self, "_admin_client"):
SharedSystemClient._release_system(self._admin_client._identifier)
SharedSystemClient._release_system(self._identifier)
async def __aenter__(self) -> "AsyncClient":
return self
async def __aexit__(self, *exc: Any) -> None:
await self.close()
Run against 1.5.9, this returns the refcount to baseline, a second close() is a no-op, and async with holds:
after close() -> 0 (baseline)
second close() -> 0 (idempotent)
async with -> in 1, out 0
AsyncAdminClient needs the same three methods: it defines no close either, and constructing one on its own takes a reference (measured separately). AsyncClient.create() builds one internally.
Version
chromadb 1.5.9 (PyPI), Python 3.12. Source read at chroma-core/chroma@34f8e76.
Summary
AsyncClientinheritsSharedSystemClient.__init__, which calls_increment_refcount, but it defines no counterpart: noclose(), and no__aenter__/__aexit__. The syncClienthas all three.So an
AsyncClienttakes a reference to the sharedSystemand there is no supported way to give it back.close()__aenter__/__aexit__Client(sync)AsyncClientWhy it matters — from your own docstring
chromadb/api/client.pyonClient.close():The sync path states exactly why the release is needed. The async path takes the same reference and offers no way to perform it.
Reproduction
No server required — only the
SharedSystemClientlifecycle is exercised.Output on chromadb 1.5.9:
The sync control returning to baseline is what shows the refcount mechanism itself is fine — only the async class is missing the way back.
There is no error and no warning. The client works; the System simply never stops.
Scope, stated honestly
An application that creates a single long-lived
AsyncClientis unaffected in practice — one retained reference costs nothing. This is not a leak that brings a server down, and I do not want it read that way.The defect is that there is no correct way to release the reference even when you want to. Where clients are created repeatedly — per request, per task, per test — the System and everything it holds stay alive for the life of the process, and for a persistent backend that is the SQLite file-locking case the sync docstring warns about, reached through the async path.
Suggested fix
Mirroring the sync
Client.close(), which guards on_closed, releases the internal admin client's reference, then its own._release_systemis already a classmethod on the shared base:Run against 1.5.9, this returns the refcount to baseline, a second
close()is a no-op, andasync withholds:AsyncAdminClientneeds the same three methods: it defines nocloseeither, and constructing one on its own takes a reference (measured separately).AsyncClient.create()builds one internally.Version
chromadb 1.5.9 (PyPI), Python 3.12. Source read at
chroma-core/chroma@34f8e76.