fix(memory): add thread-safe singleton initialization

- Add threading.Lock with double-check locking to ScopeManager
- Add asyncio.Lock with double-check locking to MemoryReflection
- Make reset_memory_metrics async with proper locking
- Update test fixtures to handle async reset functions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-05 17:39:39 +01:00
parent 33ec889fc4
commit f057c2f0b6
5 changed files with 36 additions and 17 deletions

View File

@@ -499,10 +499,11 @@ async def get_memory_metrics() -> MemoryMetrics:
return _metrics
def reset_memory_metrics() -> None:
async def reset_memory_metrics() -> None:
"""Reset the singleton instance (for testing)."""
global _metrics
_metrics = None
async with _lock:
_metrics = None
# Convenience functions

View File

@@ -7,6 +7,7 @@ Implements pattern detection, success/failure analysis, anomaly detection,
and insight generation.
"""
import asyncio
import logging
import statistics
from collections import Counter, defaultdict
@@ -1425,8 +1426,9 @@ class MemoryReflection:
)
# Singleton instance
# Singleton instance with async-safe initialization
_memory_reflection: MemoryReflection | None = None
_reflection_lock = asyncio.Lock()
async def get_memory_reflection(
@@ -1434,7 +1436,7 @@ async def get_memory_reflection(
config: ReflectionConfig | None = None,
) -> MemoryReflection:
"""
Get or create the memory reflection service.
Get or create the memory reflection service (async-safe).
Args:
session: Database session
@@ -1445,11 +1447,15 @@ async def get_memory_reflection(
"""
global _memory_reflection
if _memory_reflection is None:
_memory_reflection = MemoryReflection(session=session, config=config)
async with _reflection_lock:
# Double-check locking pattern
if _memory_reflection is None:
_memory_reflection = MemoryReflection(session=session, config=config)
return _memory_reflection
def reset_memory_reflection() -> None:
"""Reset the memory reflection singleton."""
async def reset_memory_reflection() -> None:
"""Reset the memory reflection singleton (async-safe)."""
global _memory_reflection
_memory_reflection = None
async with _reflection_lock:
_memory_reflection = None

View File

@@ -7,6 +7,7 @@ Global -> Project -> Agent Type -> Agent Instance -> Session
"""
import logging
import threading
from dataclasses import dataclass, field
from typing import Any, ClassVar
from uuid import UUID
@@ -448,13 +449,24 @@ class ScopeManager:
return False
# Singleton manager instance
# Singleton manager instance with thread-safe initialization
_manager: ScopeManager | None = None
_manager_lock = threading.Lock()
def get_scope_manager() -> ScopeManager:
"""Get the singleton scope manager instance."""
"""Get the singleton scope manager instance (thread-safe)."""
global _manager
if _manager is None:
_manager = ScopeManager()
with _manager_lock:
# Double-check locking pattern
if _manager is None:
_manager = ScopeManager()
return _manager
def reset_scope_manager() -> None:
"""Reset the scope manager singleton (for testing)."""
global _manager
with _manager_lock:
_manager = None

View File

@@ -21,9 +21,9 @@ def metrics() -> MemoryMetrics:
@pytest.fixture(autouse=True)
def reset_singleton() -> None:
async def reset_singleton() -> None:
"""Reset singleton before each test."""
reset_memory_metrics()
await reset_memory_metrics()
class TestMemoryMetrics:
@@ -333,7 +333,7 @@ class TestSingleton:
metrics1 = await get_memory_metrics()
await metrics1.inc_operations("get", "working", None, True)
reset_memory_metrics()
await reset_memory_metrics()
metrics2 = await get_memory_metrics()
summary = await metrics2.get_summary()

View File

@@ -59,9 +59,9 @@ def create_mock_episode(
@pytest.fixture(autouse=True)
def reset_singleton() -> None:
async def reset_singleton() -> None:
"""Reset singleton before each test."""
reset_memory_reflection()
await reset_memory_reflection()
@pytest.fixture
@@ -757,7 +757,7 @@ class TestSingleton:
) -> None:
"""Should create new instance after reset."""
r1 = await get_memory_reflection(mock_session)
reset_memory_reflection()
await reset_memory_reflection()
r2 = await get_memory_reflection(mock_session)
assert r1 is not r2