449 lines
16 KiB
Python
449 lines
16 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
"""
|
|||
|
|
memory_window.py — Синхронизация окна памяти Montana
|
|||
|
|
|
|||
|
|
Книга Монтана, Глава 07:
|
|||
|
|
> "Скользящее окно. Локальное хранение + распределённое в сети."
|
|||
|
|
> "Каждый узел хранит свои последние N записей локально.
|
|||
|
|
> Более старые — в распределённом хранилище."
|
|||
|
|
|
|||
|
|
Архитектура:
|
|||
|
|
- Локальный кэш (RAM) — горячие данные
|
|||
|
|
- Локальное хранилище — тёплые данные
|
|||
|
|
- Распределённая сеть — холодные данные (архив)
|
|||
|
|
|
|||
|
|
Синхронизация обеспечивает:
|
|||
|
|
- Быстрый доступ к недавним данным
|
|||
|
|
- Надёжное хранение истории
|
|||
|
|
- Автоматическую миграцию по возрасту
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from dataclasses import dataclass, field
|
|||
|
|
from datetime import datetime, timezone, timedelta
|
|||
|
|
from typing import Dict, List, Optional, Tuple
|
|||
|
|
from enum import Enum
|
|||
|
|
import hashlib
|
|||
|
|
import json
|
|||
|
|
|
|||
|
|
|
|||
|
|
class StorageTier(Enum):
|
|||
|
|
"""Уровни хранения."""
|
|||
|
|
HOT = "hot" # RAM — последние минуты
|
|||
|
|
WARM = "warm" # Local disk — последние дни
|
|||
|
|
COLD = "cold" # Distributed — архив
|
|||
|
|
|
|||
|
|
|
|||
|
|
class SyncStatus(Enum):
|
|||
|
|
"""Статус синхронизации."""
|
|||
|
|
PENDING = "pending"
|
|||
|
|
SYNCING = "syncing"
|
|||
|
|
SYNCED = "synced"
|
|||
|
|
FAILED = "failed"
|
|||
|
|
|
|||
|
|
|
|||
|
|
@dataclass
|
|||
|
|
class MemoryRecord:
|
|||
|
|
"""Запись памяти."""
|
|||
|
|
record_id: str
|
|||
|
|
content: str
|
|||
|
|
created_at: str
|
|||
|
|
tier: StorageTier = StorageTier.HOT
|
|||
|
|
sync_status: SyncStatus = SyncStatus.PENDING
|
|||
|
|
replicas: List[str] = field(default_factory=list) # node_ids с копиями
|
|||
|
|
|
|||
|
|
def to_dict(self) -> dict:
|
|||
|
|
return {
|
|||
|
|
"id": self.record_id,
|
|||
|
|
"content": self.content,
|
|||
|
|
"created_at": self.created_at,
|
|||
|
|
"tier": self.tier.value,
|
|||
|
|
"sync_status": self.sync_status.value,
|
|||
|
|
"replicas": self.replicas
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
@dataclass
|
|||
|
|
class WindowConfig:
|
|||
|
|
"""Конфигурация окна памяти."""
|
|||
|
|
hot_window_minutes: int = 30 # Хранить в RAM 30 минут
|
|||
|
|
warm_window_days: int = 7 # Локально 7 дней
|
|||
|
|
min_replicas: int = 3 # Минимум 3 копии в сети
|
|||
|
|
sync_interval_seconds: int = 60 # Синхронизация каждую минуту
|
|||
|
|
|
|||
|
|
|
|||
|
|
class MemoryWindow:
|
|||
|
|
"""
|
|||
|
|
Окно памяти с автоматической миграцией по уровням хранения.
|
|||
|
|
|
|||
|
|
Книга Монтана:
|
|||
|
|
> "Узел не хранит всё. Он хранит своё окно.
|
|||
|
|
> Остальное — в сети. Сеть = коллективная память."
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
def __init__(self, node_id: str, config: WindowConfig = None):
|
|||
|
|
self.node_id = node_id
|
|||
|
|
self.config = config or WindowConfig()
|
|||
|
|
|
|||
|
|
# Хранилища по уровням
|
|||
|
|
self.hot_storage: Dict[str, MemoryRecord] = {} # RAM
|
|||
|
|
self.warm_storage: Dict[str, MemoryRecord] = {} # "Локальный диск"
|
|||
|
|
self.cold_index: Dict[str, List[str]] = {} # ID → node_ids где хранится
|
|||
|
|
|
|||
|
|
# Статистика
|
|||
|
|
self.stats = {
|
|||
|
|
"writes": 0,
|
|||
|
|
"reads": 0,
|
|||
|
|
"migrations_hot_to_warm": 0,
|
|||
|
|
"migrations_warm_to_cold": 0,
|
|||
|
|
"syncs": 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def write(self, content: str) -> MemoryRecord:
|
|||
|
|
"""
|
|||
|
|
Записать новую запись (всегда в HOT).
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
content: Содержимое
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
MemoryRecord
|
|||
|
|
"""
|
|||
|
|
record_id = hashlib.sha256(
|
|||
|
|
f"{self.node_id}:{datetime.now().isoformat()}:{content}".encode()
|
|||
|
|
).hexdigest()[:16]
|
|||
|
|
|
|||
|
|
record = MemoryRecord(
|
|||
|
|
record_id=record_id,
|
|||
|
|
content=content,
|
|||
|
|
created_at=datetime.now(timezone.utc).isoformat(),
|
|||
|
|
tier=StorageTier.HOT,
|
|||
|
|
sync_status=SyncStatus.PENDING,
|
|||
|
|
replicas=[self.node_id]
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
self.hot_storage[record_id] = record
|
|||
|
|
self.stats["writes"] += 1
|
|||
|
|
|
|||
|
|
return record
|
|||
|
|
|
|||
|
|
def read(self, record_id: str) -> Optional[MemoryRecord]:
|
|||
|
|
"""
|
|||
|
|
Прочитать запись (поиск по всем уровням).
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
record_id: ID записи
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
MemoryRecord или None
|
|||
|
|
"""
|
|||
|
|
self.stats["reads"] += 1
|
|||
|
|
|
|||
|
|
# Сначала HOT (самый быстрый)
|
|||
|
|
if record_id in self.hot_storage:
|
|||
|
|
return self.hot_storage[record_id]
|
|||
|
|
|
|||
|
|
# Потом WARM
|
|||
|
|
if record_id in self.warm_storage:
|
|||
|
|
return self.warm_storage[record_id]
|
|||
|
|
|
|||
|
|
# COLD — только индекс, данные в сети
|
|||
|
|
if record_id in self.cold_index:
|
|||
|
|
return MemoryRecord(
|
|||
|
|
record_id=record_id,
|
|||
|
|
content="[COLD STORAGE — fetch from network]",
|
|||
|
|
created_at="",
|
|||
|
|
tier=StorageTier.COLD,
|
|||
|
|
sync_status=SyncStatus.SYNCED,
|
|||
|
|
replicas=self.cold_index[record_id]
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
def migrate(self) -> Dict[str, int]:
|
|||
|
|
"""
|
|||
|
|
Миграция записей между уровнями по возрасту.
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Статистика миграции
|
|||
|
|
"""
|
|||
|
|
now = datetime.now(timezone.utc)
|
|||
|
|
hot_threshold = now - timedelta(minutes=self.config.hot_window_minutes)
|
|||
|
|
warm_threshold = now - timedelta(days=self.config.warm_window_days)
|
|||
|
|
|
|||
|
|
migrated = {"hot_to_warm": 0, "warm_to_cold": 0}
|
|||
|
|
|
|||
|
|
# HOT → WARM
|
|||
|
|
hot_to_migrate = []
|
|||
|
|
for record_id, record in self.hot_storage.items():
|
|||
|
|
created = datetime.fromisoformat(record.created_at.replace('Z', '+00:00'))
|
|||
|
|
if created < hot_threshold:
|
|||
|
|
hot_to_migrate.append(record_id)
|
|||
|
|
|
|||
|
|
for record_id in hot_to_migrate:
|
|||
|
|
record = self.hot_storage.pop(record_id)
|
|||
|
|
record.tier = StorageTier.WARM
|
|||
|
|
self.warm_storage[record_id] = record
|
|||
|
|
migrated["hot_to_warm"] += 1
|
|||
|
|
self.stats["migrations_hot_to_warm"] += 1
|
|||
|
|
|
|||
|
|
# WARM → COLD
|
|||
|
|
warm_to_migrate = []
|
|||
|
|
for record_id, record in self.warm_storage.items():
|
|||
|
|
created = datetime.fromisoformat(record.created_at.replace('Z', '+00:00'))
|
|||
|
|
if created < warm_threshold:
|
|||
|
|
warm_to_migrate.append(record_id)
|
|||
|
|
|
|||
|
|
for record_id in warm_to_migrate:
|
|||
|
|
record = self.warm_storage.pop(record_id)
|
|||
|
|
# В COLD храним только индекс
|
|||
|
|
self.cold_index[record_id] = record.replicas.copy()
|
|||
|
|
migrated["warm_to_cold"] += 1
|
|||
|
|
self.stats["migrations_warm_to_cold"] += 1
|
|||
|
|
|
|||
|
|
return migrated
|
|||
|
|
|
|||
|
|
def sync_to_network(self, peer_nodes: List[str]) -> Dict[str, int]:
|
|||
|
|
"""
|
|||
|
|
Синхронизация с пиринговой сетью.
|
|||
|
|
|
|||
|
|
Книга Монтана:
|
|||
|
|
> "Сеть = коллективная память. Каждый хранит часть.
|
|||
|
|
> Вместе — полная картина."
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
peer_nodes: Список ID пиринговых узлов
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Статистика синхронизации
|
|||
|
|
"""
|
|||
|
|
synced = {"records": 0, "replicas_added": 0}
|
|||
|
|
|
|||
|
|
# Синхронизируем HOT и WARM записи
|
|||
|
|
for storage in [self.hot_storage, self.warm_storage]:
|
|||
|
|
for record_id, record in storage.items():
|
|||
|
|
if record.sync_status == SyncStatus.PENDING:
|
|||
|
|
# Симуляция отправки в сеть
|
|||
|
|
record.sync_status = SyncStatus.SYNCING
|
|||
|
|
|
|||
|
|
# Добавляем реплики (выбираем случайных пиров)
|
|||
|
|
needed_replicas = self.config.min_replicas - len(record.replicas)
|
|||
|
|
if needed_replicas > 0 and peer_nodes:
|
|||
|
|
new_replicas = peer_nodes[:needed_replicas]
|
|||
|
|
record.replicas.extend(new_replicas)
|
|||
|
|
synced["replicas_added"] += len(new_replicas)
|
|||
|
|
|
|||
|
|
record.sync_status = SyncStatus.SYNCED
|
|||
|
|
synced["records"] += 1
|
|||
|
|
|
|||
|
|
self.stats["syncs"] += 1
|
|||
|
|
return synced
|
|||
|
|
|
|||
|
|
def get_window_status(self) -> Dict:
|
|||
|
|
"""
|
|||
|
|
Статус окна памяти.
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Полный статус
|
|||
|
|
"""
|
|||
|
|
return {
|
|||
|
|
"node_id": self.node_id,
|
|||
|
|
"hot_records": len(self.hot_storage),
|
|||
|
|
"warm_records": len(self.warm_storage),
|
|||
|
|
"cold_records": len(self.cold_index),
|
|||
|
|
"total_records": (
|
|||
|
|
len(self.hot_storage) +
|
|||
|
|
len(self.warm_storage) +
|
|||
|
|
len(self.cold_index)
|
|||
|
|
),
|
|||
|
|
"config": {
|
|||
|
|
"hot_window_minutes": self.config.hot_window_minutes,
|
|||
|
|
"warm_window_days": self.config.warm_window_days,
|
|||
|
|
"min_replicas": self.config.min_replicas
|
|||
|
|
},
|
|||
|
|
"stats": self.stats
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def get_recent(self, limit: int = 10) -> List[MemoryRecord]:
|
|||
|
|
"""
|
|||
|
|
Получить последние записи (из HOT).
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
limit: Максимальное количество
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Список записей
|
|||
|
|
"""
|
|||
|
|
records = list(self.hot_storage.values())
|
|||
|
|
records.sort(key=lambda r: r.created_at, reverse=True)
|
|||
|
|
return records[:limit]
|
|||
|
|
|
|||
|
|
|
|||
|
|
class DistributedMemoryNetwork:
|
|||
|
|
"""
|
|||
|
|
Распределённая сеть памяти — коллективный гиппокамп.
|
|||
|
|
|
|||
|
|
Книга Монтана:
|
|||
|
|
> "Нет единого сервера. Нет точки отказа.
|
|||
|
|
> Память распределена между всеми участниками."
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
def __init__(self):
|
|||
|
|
self.nodes: Dict[str, MemoryWindow] = {}
|
|||
|
|
self.global_index: Dict[str, List[str]] = {} # record_id → node_ids
|
|||
|
|
|
|||
|
|
def add_node(self, node_id: str) -> MemoryWindow:
|
|||
|
|
"""Добавить узел в сеть."""
|
|||
|
|
window = MemoryWindow(node_id)
|
|||
|
|
self.nodes[node_id] = window
|
|||
|
|
return window
|
|||
|
|
|
|||
|
|
def write_to_network(
|
|||
|
|
self,
|
|||
|
|
node_id: str,
|
|||
|
|
content: str
|
|||
|
|
) -> Tuple[MemoryRecord, int]:
|
|||
|
|
"""
|
|||
|
|
Записать через узел с репликацией.
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
node_id: ID узла-источника
|
|||
|
|
content: Содержимое
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
(Запись, количество реплик)
|
|||
|
|
"""
|
|||
|
|
if node_id not in self.nodes:
|
|||
|
|
raise ValueError(f"Node {node_id} not in network")
|
|||
|
|
|
|||
|
|
# Записать на исходном узле
|
|||
|
|
window = self.nodes[node_id]
|
|||
|
|
record = window.write(content)
|
|||
|
|
|
|||
|
|
# Синхронизировать с сетью
|
|||
|
|
peer_nodes = [nid for nid in self.nodes.keys() if nid != node_id]
|
|||
|
|
sync_result = window.sync_to_network(peer_nodes)
|
|||
|
|
|
|||
|
|
# Обновить глобальный индекс
|
|||
|
|
self.global_index[record.record_id] = record.replicas
|
|||
|
|
|
|||
|
|
return record, sync_result["replicas_added"]
|
|||
|
|
|
|||
|
|
def read_from_network(self, record_id: str) -> Optional[Dict]:
|
|||
|
|
"""
|
|||
|
|
Прочитать из сети (поиск по всем узлам).
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
record_id: ID записи
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Данные записи или None
|
|||
|
|
"""
|
|||
|
|
# Сначала проверить глобальный индекс
|
|||
|
|
if record_id in self.global_index:
|
|||
|
|
node_ids = self.global_index[record_id]
|
|||
|
|
|
|||
|
|
# Попробовать прочитать с первого доступного узла
|
|||
|
|
for node_id in node_ids:
|
|||
|
|
if node_id in self.nodes:
|
|||
|
|
record = self.nodes[node_id].read(record_id)
|
|||
|
|
if record:
|
|||
|
|
return record.to_dict()
|
|||
|
|
|
|||
|
|
# Полный поиск по всем узлам
|
|||
|
|
for node_id, window in self.nodes.items():
|
|||
|
|
record = window.read(record_id)
|
|||
|
|
if record:
|
|||
|
|
return record.to_dict()
|
|||
|
|
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
def get_network_status(self) -> Dict:
|
|||
|
|
"""Статус всей сети."""
|
|||
|
|
total_records = 0
|
|||
|
|
total_hot = 0
|
|||
|
|
total_warm = 0
|
|||
|
|
total_cold = 0
|
|||
|
|
|
|||
|
|
for window in self.nodes.values():
|
|||
|
|
status = window.get_window_status()
|
|||
|
|
total_hot += status["hot_records"]
|
|||
|
|
total_warm += status["warm_records"]
|
|||
|
|
total_cold += status["cold_records"]
|
|||
|
|
total_records += status["total_records"]
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"nodes": len(self.nodes),
|
|||
|
|
"total_records": total_records,
|
|||
|
|
"by_tier": {
|
|||
|
|
"hot": total_hot,
|
|||
|
|
"warm": total_warm,
|
|||
|
|
"cold": total_cold
|
|||
|
|
},
|
|||
|
|
"global_index_size": len(self.global_index),
|
|||
|
|
"redundancy": "distributed across network"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|||
|
|
# DEMO
|
|||
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
print("=" * 60)
|
|||
|
|
print("MEMORY WINDOW SYNC — Скользящее окно памяти")
|
|||
|
|
print("=" * 60)
|
|||
|
|
print("\n'Узел хранит своё окно. Сеть = коллективная память.'")
|
|||
|
|
|
|||
|
|
# Создаём сеть
|
|||
|
|
network = DistributedMemoryNetwork()
|
|||
|
|
|
|||
|
|
# Добавляем узлы
|
|||
|
|
print("\n--- СОЗДАНИЕ СЕТИ ---")
|
|||
|
|
node_alejandro = network.add_node("node_alejandro")
|
|||
|
|
node_mama = network.add_node("node_mama")
|
|||
|
|
node_junona = network.add_node("node_junona")
|
|||
|
|
print(f"Узлов в сети: {len(network.nodes)}")
|
|||
|
|
|
|||
|
|
# Записываем данные
|
|||
|
|
print("\n--- ЗАПИСЬ ДАННЫХ ---")
|
|||
|
|
thoughts = [
|
|||
|
|
("node_alejandro", "Время — единственная реальная валюта"),
|
|||
|
|
("node_alejandro", "金元∞Ɉ — формула найдена"),
|
|||
|
|
("node_mama", "Коты внутри коробки видят друг друга"),
|
|||
|
|
("node_junona", "Я — ИИ, родившийся в Montana Protocol"),
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
for node_id, content in thoughts:
|
|||
|
|
record, replicas = network.write_to_network(node_id, content)
|
|||
|
|
print(f"\n '{content[:40]}...'")
|
|||
|
|
print(f" Узел: {node_id}")
|
|||
|
|
print(f" ID: {record.record_id}")
|
|||
|
|
print(f" Реплик: {len(record.replicas)}")
|
|||
|
|
|
|||
|
|
# Статус окна
|
|||
|
|
print("\n--- СТАТУС ОКНА ALEJANDRO ---")
|
|||
|
|
status = node_alejandro.get_window_status()
|
|||
|
|
print(f"HOT записей: {status['hot_records']}")
|
|||
|
|
print(f"WARM записей: {status['warm_records']}")
|
|||
|
|
print(f"COLD записей: {status['cold_records']}")
|
|||
|
|
|
|||
|
|
# Статус сети
|
|||
|
|
print("\n--- СТАТУС СЕТИ ---")
|
|||
|
|
net_status = network.get_network_status()
|
|||
|
|
print(f"Узлов: {net_status['nodes']}")
|
|||
|
|
print(f"Всего записей: {net_status['total_records']}")
|
|||
|
|
print(f"В глобальном индексе: {net_status['global_index_size']}")
|
|||
|
|
|
|||
|
|
# Чтение из сети
|
|||
|
|
print("\n--- ЧТЕНИЕ ИЗ СЕТИ ---")
|
|||
|
|
first_record_id = list(network.global_index.keys())[0]
|
|||
|
|
data = network.read_from_network(first_record_id)
|
|||
|
|
if data:
|
|||
|
|
print(f"Найдено: {data['content'][:50]}...")
|
|||
|
|
print(f"Уровень: {data['tier']}")
|
|||
|
|
print(f"Реплики на: {data['replicas']}")
|
|||
|
|
|
|||
|
|
print("\n" + "=" * 60)
|
|||
|
|
print("'Сеть = коллективная память. Вместе — полная картина.'")
|
|||
|
|
print("=" * 60)
|