Skip to content

AsyncClient takes a shared System reference and has no way to release it (no close(), no async context manager) #7694

Description

@vasilisnasopoulos

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions