ADR-002: threading.RLock для thread safety памяти¶
Статус: ✅ Принято
Дата: 2025-05
Авторы: cognitive-core contributors
Контекст¶
ConsolidationEngine работает как daemon thread и периодически читает/пишет в модули памяти. Одновременно основной поток выполняет CognitiveCore.run(), который также обращается к памяти. Без синхронизации возникают race conditions и потеря данных.
Затронутые модули (P0-E1):
- WorkingMemory
- SemanticMemory
- EpisodicMemory
- SourceMemory
- ProceduralMemory
- EventBus
Рассмотренные варианты¶
Вариант 1: threading.Lock (простой мьютекс)¶
Плюсы: Минимальный overhead
Минусы: Не реентерабельный — deadlock при рекурсивных вызовах внутри одного потока (например, store() вызывает _evict(), который тоже берёт lock)
Вариант 2: threading.RLock (реентерабельный мьютекс)¶
Плюсы: Безопасен при рекурсивных вызовах, тот же поток может взять lock повторно
Минусы: Чуть больше overhead чем Lock (счётчик рекурсии)
Вариант 3: asyncio.Lock (асинхронный)¶
Плюсы: Подходит для async-архитектуры
Минусы: Требует полного перехода на asyncio — несовместимо с текущей синхронной архитектурой
Вариант 4: queue.Queue (lock-free через очередь)¶
Плюсы: Нет явных lock'ов
Минусы: Требует actor-model рефакторинга всей системы памяти — слишком большой scope
Решение¶
Выбран threading.RLock для всех 6 модулей.
Паттерн реализации:
import threading
class SemanticMemory:
def __init__(self):
self._lock = threading.RLock()
self._nodes: Dict[str, SemanticNode] = {}
def learn_fact(self, concept: str, description: str) -> SemanticNode:
with self._lock:
# безопасная запись
...
def get_all_nodes(self) -> List[SemanticNode]:
with self._lock:
return list(self._nodes.values()) # возвращаем копию!
Ключевое правило: методы, возвращающие коллекции, всегда возвращают копию (list(...), dict(...)), а не ссылку на внутреннее состояние.
Последствия¶
Положительные: - Устранены race conditions в ConsolidationEngine daemon thread - Реентерабельность позволяет вызывать методы памяти из других методов того же класса - Единообразный паттерн во всех 6 модулях
Отрицательные: - Небольшой overhead на каждую операцию (счётчик рекурсии RLock) - Возможен deadlock при взаимной блокировке двух модулей (A ждёт B, B ждёт A) — но в текущей архитектуре такого нет
Нейтральные:
- MemoryDatabase (SQLite) имеет собственный RLock — двойная защита для операций через storage
Связанные решения¶
- ADR-001: SQLite backend (также использует RLock)
- P0-E2: Race condition в
ResourceMonitor._apply_state()— аналогичное исправление