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 return _metrics
def reset_memory_metrics() -> None: async def reset_memory_metrics() -> None:
"""Reset the singleton instance (for testing).""" """Reset the singleton instance (for testing)."""
global _metrics global _metrics
_metrics = None async with _lock:
_metrics = None
# Convenience functions # Convenience functions

View File

@@ -7,6 +7,7 @@ Implements pattern detection, success/failure analysis, anomaly detection,
and insight generation. and insight generation.
""" """
import asyncio
import logging import logging
import statistics import statistics
from collections import Counter, defaultdict from collections import Counter, defaultdict
@@ -1425,8 +1426,9 @@ class MemoryReflection:
) )
# Singleton instance # Singleton instance with async-safe initialization
_memory_reflection: MemoryReflection | None = None _memory_reflection: MemoryReflection | None = None
_reflection_lock = asyncio.Lock()
async def get_memory_reflection( async def get_memory_reflection(
@@ -1434,7 +1436,7 @@ async def get_memory_reflection(
config: ReflectionConfig | None = None, config: ReflectionConfig | None = None,
) -> MemoryReflection: ) -> MemoryReflection:
""" """
Get or create the memory reflection service. Get or create the memory reflection service (async-safe).
Args: Args:
session: Database session session: Database session
@@ -1445,11 +1447,15 @@ async def get_memory_reflection(
""" """
global _memory_reflection global _memory_reflection
if _memory_reflection is None: 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 return _memory_reflection
def reset_memory_reflection() -> None: async def reset_memory_reflection() -> None:
"""Reset the memory reflection singleton.""" """Reset the memory reflection singleton (async-safe)."""
global _memory_reflection 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 logging
import threading
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any, ClassVar from typing import Any, ClassVar
from uuid import UUID from uuid import UUID
@@ -448,13 +449,24 @@ class ScopeManager:
return False return False
# Singleton manager instance # Singleton manager instance with thread-safe initialization
_manager: ScopeManager | None = None _manager: ScopeManager | None = None
_manager_lock = threading.Lock()
def get_scope_manager() -> ScopeManager: def get_scope_manager() -> ScopeManager:
"""Get the singleton scope manager instance.""" """Get the singleton scope manager instance (thread-safe)."""
global _manager global _manager
if _manager is None: if _manager is None:
_manager = ScopeManager() with _manager_lock:
# Double-check locking pattern
if _manager is None:
_manager = ScopeManager()
return _manager 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) @pytest.fixture(autouse=True)
def reset_singleton() -> None: async def reset_singleton() -> None:
"""Reset singleton before each test.""" """Reset singleton before each test."""
reset_memory_metrics() await reset_memory_metrics()
class TestMemoryMetrics: class TestMemoryMetrics:
@@ -333,7 +333,7 @@ class TestSingleton:
metrics1 = await get_memory_metrics() metrics1 = await get_memory_metrics()
await metrics1.inc_operations("get", "working", None, True) await metrics1.inc_operations("get", "working", None, True)
reset_memory_metrics() await reset_memory_metrics()
metrics2 = await get_memory_metrics() metrics2 = await get_memory_metrics()
summary = await metrics2.get_summary() summary = await metrics2.get_summary()

View File

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