diff --git a/backend/app/services/memory/metrics/collector.py b/backend/app/services/memory/metrics/collector.py index f428a0d..7a89c89 100644 --- a/backend/app/services/memory/metrics/collector.py +++ b/backend/app/services/memory/metrics/collector.py @@ -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 diff --git a/backend/app/services/memory/reflection/service.py b/backend/app/services/memory/reflection/service.py index 2e01620..93ccc70 100644 --- a/backend/app/services/memory/reflection/service.py +++ b/backend/app/services/memory/reflection/service.py @@ -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 diff --git a/backend/app/services/memory/scoping/scope.py b/backend/app/services/memory/scoping/scope.py index d6325f6..a6c8fe8 100644 --- a/backend/app/services/memory/scoping/scope.py +++ b/backend/app/services/memory/scoping/scope.py @@ -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 diff --git a/backend/tests/unit/services/memory/metrics/test_collector.py b/backend/tests/unit/services/memory/metrics/test_collector.py index e89270f..0a69ae8 100644 --- a/backend/tests/unit/services/memory/metrics/test_collector.py +++ b/backend/tests/unit/services/memory/metrics/test_collector.py @@ -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() diff --git a/backend/tests/unit/services/memory/reflection/test_service.py b/backend/tests/unit/services/memory/reflection/test_service.py index 4ee124d..2c7a237 100644 --- a/backend/tests/unit/services/memory/reflection/test_service.py +++ b/backend/tests/unit/services/memory/reflection/test_service.py @@ -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