fix: prevent thread and memory leaks from PostHog telemetry (#4535)

Co-authored-by: utkarsh240799 <utkarsh240799@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
This commit is contained in:
Utkarsh
2026-04-03 20:01:04 +05:30
committed by GitHub
co-authored by utkarsh240799 Claude Opus 4.6 kartik-mem0
parent 33d2bc495d
commit fcbb70ab3b
3 changed files with 382 additions and 13 deletions
+29
View File
@@ -304,6 +304,23 @@ class Memory(MemoryBase):
)
capture_event("mem0.init", self, {"sync_type": "sync"})
def close(self):
"""Release resources held by this Memory instance (SQLite connections, etc.).
The global telemetry singleton is intentionally *not* shut down here
because it is shared across all Memory instances in the process. It is
cleaned up automatically at process exit via an ``atexit`` handler.
"""
if hasattr(self, "db") and self.db is not None:
self.db.close()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
return False
@classmethod
def from_config(cls, config_dict: Dict[str, Any]):
try:
@@ -1425,6 +1442,18 @@ class AsyncMemory(MemoryBase):
self._telemetry_vector_store = VectorStoreFactory.create(self.config.vector_store.provider, telemetry_config)
capture_event("mem0.init", self, {"sync_type": "async"})
def close(self):
"""Release resources held by this AsyncMemory instance."""
if hasattr(self, "db") and self.db is not None:
self.db.close()
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
self.close()
return False
@classmethod
def from_config(cls, config_dict: Dict[str, Any]):
try:
+49 -5
View File
@@ -1,7 +1,9 @@
import atexit
import logging
import os
import platform
import sys
import threading
from posthog import Posthog
@@ -55,20 +57,62 @@ class AnonymousTelemetry:
def close(self):
if self.posthog is not None:
self.posthog.shutdown()
self.posthog = None
# Thread-safe lazy singleton for OSS telemetry.
# A single AnonymousTelemetry instance (and its underlying PostHog client /
# background thread) is reused for the lifetime of the process instead of
# creating a new one on every capture_event() call. The singleton is shut down
# once at process exit via an atexit handler.
_oss_telemetry_instance = None
_oss_telemetry_lock = threading.Lock()
_oss_telemetry_shutting_down = False
def _get_oss_telemetry():
"""Return the process-wide AnonymousTelemetry singleton, creating it on first call.
Returns None after _shutdown_oss_telemetry() has run (interpreter exit).
"""
global _oss_telemetry_instance
if _oss_telemetry_shutting_down:
return None
if _oss_telemetry_instance is not None:
return _oss_telemetry_instance
with _oss_telemetry_lock:
if _oss_telemetry_shutting_down:
return None
# Double-checked locking
if _oss_telemetry_instance is not None:
return _oss_telemetry_instance
_oss_telemetry_instance = AnonymousTelemetry()
atexit.register(_shutdown_oss_telemetry)
return _oss_telemetry_instance
def _shutdown_oss_telemetry():
global _oss_telemetry_instance, _oss_telemetry_shutting_down
with _oss_telemetry_lock:
_oss_telemetry_shutting_down = True
if _oss_telemetry_instance is not None:
_oss_telemetry_instance.close()
_oss_telemetry_instance = None
# Module-level client telemetry singleton (used by capture_client_event).
client_telemetry = AnonymousTelemetry()
atexit.register(client_telemetry.close)
def capture_event(event_name, memory_instance, additional_data=None):
if not MEM0_TELEMETRY:
return
oss_telemetry = AnonymousTelemetry(
vector_store=memory_instance._telemetry_vector_store
if hasattr(memory_instance, "_telemetry_vector_store")
else None,
)
oss_telemetry = _get_oss_telemetry()
if oss_telemetry is None:
return
event_data = {
"collection": memory_instance.collection_name,