Skip to content

perf: cache ChromaDB PersistentClient instead of re-instantiating per call#135

Merged
bensig merged 2 commits intoMemPalace:mainfrom
igorls:fix/chromadb-singleton
Apr 8, 2026
Merged

perf: cache ChromaDB PersistentClient instead of re-instantiating per call#135
bensig merged 2 commits intoMemPalace:mainfrom
igorls:fix/chromadb-singleton

Conversation

@igorls
Copy link
Copy Markdown
Member

@igorls igorls commented Apr 7, 2026

Problem

_get_collection() creates a new chromadb.PersistentClient(path=...) on every MCP tool call. This re-opens the SQLite metadata DB and reloads the HNSW index each time, adding unnecessary latency.

Finding:
HIGH — PersistentClient re-instantiation

Fix

Cache the client and collection at module level:

_client_cache = None
_collection_cache = None

def _get_collection(create=False):
    global _client_cache, _collection_cache
    try:
        if _client_cache is None:
            _client_cache = chromadb.PersistentClient(path=_config.palace_path)
        if create:
            _collection_cache = _client_cache.get_or_create_collection(...)
        elif _collection_cache is None:
            _collection_cache = _client_cache.get_collection(...)
        return _collection_cache
    except Exception:
        return None

The MCP server runs as a subprocess (one per session), so the cache lifetime matches the session. No concurrency concerns — MCP is single-threaded stdin/stdout.

92 tests pass (includes test infrastructure from PR #131 + a _reset_mcp_cache fixture for test isolation).

Copilot AI review requested due to automatic review settings April 7, 2026 20:20
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR reduces MCP tool-call latency by caching the ChromaDB PersistentClient/collection in the MCP server process, and updates the project’s packaging/test setup alongside a version bump.

Changes:

  • Cache chromadb.PersistentClient and the target collection in mempalace/mcp_server.py to avoid re-opening SQLite + reloading indexes on every request.
  • Add/expand pytest coverage for MCP server handlers, the programmatic search API, the dialect, and the temporal knowledge graph (with shared isolation fixtures).
  • Update packaging metadata/build configuration (move to hatchling, add uv-style dependency groups), remove requirements.txt, and bump __version__ to 3.0.0.

Reviewed changes

Copilot reviewed 8 out of 10 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
mempalace/mcp_server.py Adds module-level caching for ChromaDB client/collection used by MCP tools.
mempalace/__init__.py Updates package version constant to 3.0.0.
pyproject.toml Switches build backend to hatchling and updates dev dependency definitions.
requirements.txt Removed in favor of pyproject.toml / uv workflow.
tests/conftest.py Adds isolation fixtures (HOME redirection) and MCP cache reset for test independence.
tests/test_mcp_server.py Adds unit/integration tests for MCP protocol dispatch + tool handlers.
tests/test_searcher.py Adds tests for search_memories programmatic API behavior.
tests/test_knowledge_graph.py Adds tests for KnowledgeGraph CRUD + temporal queries + stats/timeline.
tests/test_dialect.py Adds tests for dialect compression/encoding/decoding utilities.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread mempalace/__init__.py
Comment on lines 1 to 5
"""MemPalace — Give your AI a memory. No API key required."""

__version__ = "2.0.0"
__version__ = "3.0.0"

from .cli import main
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

__version__ is bumped to 3.0.0 here, but the MCP protocol serverInfo.version reported by mempalace.mcp_server.handle_request() is still hardcoded to 2.0.0. Consider deriving the MCP server version from mempalace.__version__ (or importlib.metadata.version("mempalace")) so clients see a consistent version.

Copilot uses AI. Check for mistakes.
Comment thread tests/conftest.py Outdated
Comment on lines +40 to +46
yield
try:
from mempalace import mcp_server
mcp_server._client_cache = None
mcp_server._collection_cache = None
except (ImportError, AttributeError):
pass
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_reset_mcp_cache only clears the MCP server caches in teardown. If a previous test populated the cache, the next test will still start with whatever state is left until teardown runs again. Resetting the cache in the fixture setup (before yield) as well would make isolation deterministic and reduce the chance of temp-dir cleanup issues due to lingering open ChromaDB handles.

Suggested change
yield
try:
from mempalace import mcp_server
mcp_server._client_cache = None
mcp_server._collection_cache = None
except (ImportError, AttributeError):
pass
def _clear_cache():
try:
from mempalace import mcp_server
mcp_server._client_cache = None
mcp_server._collection_cache = None
except (ImportError, AttributeError):
pass
_clear_cache()
yield
_clear_cache()

Copilot uses AI. Check for mistakes.
Comment thread tests/test_searcher.py Outdated
Comment on lines +36 to +37
def test_no_palace_returns_error(self):
result = search_memories("anything", "/nonexistent/path")
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test uses a hard-coded absolute path (/nonexistent/path) to simulate a missing palace. That can behave differently across OSes (and could even be creatable in privileged CI), making the test flaky and potentially leaving junk on the filesystem. Prefer using a per-test temp path (e.g. tmp_path / "missing") that is guaranteed not to exist.

Suggested change
def test_no_palace_returns_error(self):
result = search_memories("anything", "/nonexistent/path")
def test_no_palace_returns_error(self, tmp_path):
missing_palace_path = tmp_path / "missing"
result = search_memories("anything", str(missing_palace_path))

Copilot uses AI. Check for mistakes.
Comment thread tests/test_mcp_server.py Outdated
Comment on lines +147 to +153
def test_no_palace_returns_error(self, monkeypatch, config, kg):
_patch_mcp_server(monkeypatch, config, "/nonexistent/path", kg)
from mempalace.mcp_server import tool_status

result = tool_status()
assert "error" in result

Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test uses a hard-coded absolute path (/nonexistent/path) to simulate a missing palace. That can behave differently across OSes (and could even be creatable in privileged CI), making the test flaky and potentially leaving junk on the filesystem. Prefer using a per-test temp path (e.g. tmp_path / "missing") that is guaranteed not to exist.

Copilot uses AI. Check for mistakes.
@bensig
Copy link
Copy Markdown
Collaborator

bensig commented Apr 7, 2026

Hey Igor — CI is failing, looks like it needs a rebase on main (we've merged a bunch of changes recently). Can you rebase and push?

@igorls igorls force-pushed the fix/chromadb-singleton branch 4 times, most recently from 07f1f86 to e4d938c Compare April 7, 2026 21:59
@bensig
Copy link
Copy Markdown
Collaborator

bensig commented Apr 7, 2026

Merged your other 5 PRs — this one now has conflicts from those merges. Can you rebase on main one more time?

igorls added 2 commits April 8, 2026 04:39
… call

The MCP server previously created a new PersistentClient on every tool
call via _get_collection(). This incurs HNSW index loading overhead
on each request.

Cache the client and collection at module level. The cache resets
naturally on process restart (MCP runs as a subprocess).

Also adds a _reset_mcp_cache fixture to conftest.py for test isolation.

Includes test infrastructure from PR MemPalace#131.
92 tests pass.
@igorls igorls force-pushed the fix/chromadb-singleton branch from e4d938c to 47696be Compare April 8, 2026 07:41
@bensig bensig merged commit 3489a09 into MemPalace:main Apr 8, 2026
4 checks passed
gnusam pushed a commit to gnusam/mempalace-pgsql that referenced this pull request Apr 8, 2026
Audited PR MemPalace#135 (ChromaDB singleton cache): none of its 3 commits port
to this fork. The client-caching perf gain is already achieved via the
module-level get_db() singleton in db.py, and the companion test fixes
target upstream-only test files (test_mcp_server.py, test_searcher.py).
@milla-jovovich milla-jovovich mentioned this pull request Apr 9, 2026
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants