What's wrong
integrations/lightrag-memgraph/src/lightrag_memgraph/_connection.py caches a single shared AsyncMemgraph driver per event loop:
_clients: dict[int, AsyncMemgraph] = {}
keyed by id(asyncio.get_running_loop()). Two issues with this:
-
No reference counting. Every KV/vector/doc-status storage instance's finalize() independently calls the shared close_driver(), which unconditionally pops-and-closes the one cached client for the loop. A single LightRAG instance owns ~10-11 separate storage objects sharing this one driver; today this is only safe because finalize_storages() calls them all sequentially in one LightRAG instance and close_driver()'s pop(..., None) is idempotent. It breaks if two MemgraphLightRAGWrapper/LightRAG instances are ever alive concurrently on the same event loop (e.g. a multi-tenant server keeping several workspace-scoped RAG instances resident in one process) — not exercised anywhere in this codebase's current tests/examples today, so this is a latent architectural gap rather than an active bug.
-
id()-reuse risk (lower confidence, flagged for a closer look). id() is a memory address; if a closed event loop is garbage-collected before its cache entry is ever explicitly removed via close_driver() (e.g. a retry loop, or a long-lived worker wrapping each request in its own asyncio.run()), a later, unrelated loop could reuse the same address and get handed a driver bound to the dead loop. Two independent review passes disagreed on whether the cached client's internal state keeps the loop referenced long enough to prevent this in practice — worth resolving with a definitive test rather than reasoning alone.
Acceptance criteria
Blocked by
None - can start immediately
What's wrong
integrations/lightrag-memgraph/src/lightrag_memgraph/_connection.pycaches a single sharedAsyncMemgraphdriver per event loop:keyed by
id(asyncio.get_running_loop()). Two issues with this:No reference counting. Every KV/vector/doc-status storage instance's
finalize()independently calls the sharedclose_driver(), which unconditionally pops-and-closes the one cached client for the loop. A singleLightRAGinstance owns ~10-11 separate storage objects sharing this one driver; today this is only safe becausefinalize_storages()calls them all sequentially in oneLightRAGinstance andclose_driver()'spop(..., None)is idempotent. It breaks if twoMemgraphLightRAGWrapper/LightRAGinstances are ever alive concurrently on the same event loop (e.g. a multi-tenant server keeping several workspace-scoped RAG instances resident in one process) — not exercised anywhere in this codebase's current tests/examples today, so this is a latent architectural gap rather than an active bug.id()-reuse risk (lower confidence, flagged for a closer look).id()is a memory address; if a closed event loop is garbage-collected before its cache entry is ever explicitly removed viaclose_driver()(e.g. a retry loop, or a long-lived worker wrapping each request in its ownasyncio.run()), a later, unrelated loop could reuse the same address and get handed a driver bound to the dead loop. Two independent review passes disagreed on whether the cached client's internal state keeps the loop referenced long enough to prevent this in practice — worth resolving with a definitive test rather than reasoning alone.Acceptance criteria
finalize()— reference-count storages per loop, or close once after all of aLightRAGinstance's storages are doneid(event loop)cache key can collide across a discarded and a new loop in this codebase's actual usage patternsBlocked by
None - can start immediately