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:
@@ -499,9 +499,10 @@ 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
|
||||||
|
async with _lock:
|
||||||
_metrics = None
|
_metrics = None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1444,12 +1446,16 @@ async def get_memory_reflection(
|
|||||||
MemoryReflection instance
|
MemoryReflection instance
|
||||||
"""
|
"""
|
||||||
global _memory_reflection
|
global _memory_reflection
|
||||||
|
if _memory_reflection is None:
|
||||||
|
async with _reflection_lock:
|
||||||
|
# Double-check locking pattern
|
||||||
if _memory_reflection is None:
|
if _memory_reflection is None:
|
||||||
_memory_reflection = MemoryReflection(session=session, config=config)
|
_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
|
||||||
|
async with _reflection_lock:
|
||||||
_memory_reflection = None
|
_memory_reflection = None
|
||||||
|
|||||||
@@ -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:
|
||||||
|
with _manager_lock:
|
||||||
|
# Double-check locking pattern
|
||||||
if _manager is None:
|
if _manager is None:
|
||||||
_manager = ScopeManager()
|
_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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user