314 lines
11 KiB
Python
314 lines
11 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
five_nodes.py — Топология пяти узлов Montana
|
||
|
||
Книга Монтана, Глава 08:
|
||
> "Пять городов. Три страны. Один организм."
|
||
> "1 активный + 1 мозг + 3 зеркала = 5 узлов."
|
||
> "Сеть не умирает. Сеть — бессмертна. Пока есть хотя бы один узел — Юнона жива."
|
||
|
||
Brain-Body Separation:
|
||
> "Мозг не должен отвечать на каждое сообщение напрямую.
|
||
> Мозг думает. Тело действует."
|
||
|
||
Архитектура:
|
||
- Amsterdam (BOT) — активный, принимает сообщения
|
||
- Moscow (BRAIN) — мозг, принимает решения
|
||
- Almaty, SPB, Novosibirsk — зеркала (standby)
|
||
"""
|
||
|
||
from dataclasses import dataclass, field
|
||
from datetime import datetime, timezone
|
||
from typing import Dict, List, Optional
|
||
from enum import Enum
|
||
import hashlib
|
||
|
||
|
||
class NodeRole(Enum):
|
||
"""Роль узла в сети."""
|
||
BRAIN = "brain" # Мозг — принимает решения
|
||
BOT = "bot" # Бот — обрабатывает сообщения
|
||
STANDBY = "standby" # Зеркало — ждёт своей очереди
|
||
|
||
|
||
class NodeStatus(Enum):
|
||
"""Статус узла."""
|
||
ACTIVE = "active" # Работает
|
||
STANDBY = "standby" # В режиме ожидания
|
||
FAILED = "failed" # Упал
|
||
SYNCING = "syncing" # Синхронизируется
|
||
|
||
|
||
@dataclass
|
||
class Node:
|
||
"""Узел сети Montana."""
|
||
node_id: str
|
||
city: str
|
||
country: str
|
||
role: NodeRole
|
||
status: NodeStatus = NodeStatus.STANDBY
|
||
last_heartbeat: str = ""
|
||
priority: int = 0 # Приоритет в failover цепи (0 = высший)
|
||
|
||
def heartbeat(self) -> None:
|
||
"""Обновить heartbeat."""
|
||
self.last_heartbeat = datetime.now(timezone.utc).isoformat()
|
||
|
||
def to_dict(self) -> dict:
|
||
return {
|
||
"node_id": self.node_id,
|
||
"city": self.city,
|
||
"country": self.country,
|
||
"role": self.role.value,
|
||
"status": self.status.value,
|
||
"last_heartbeat": self.last_heartbeat,
|
||
"priority": self.priority
|
||
}
|
||
|
||
|
||
@dataclass
|
||
class NetworkTopology:
|
||
"""
|
||
Топология сети из 5 узлов.
|
||
|
||
Формула: 1 active + 1 brain + 3 mirrors = 5 nodes
|
||
"""
|
||
nodes: Dict[str, Node] = field(default_factory=dict)
|
||
brain_id: Optional[str] = None
|
||
bot_id: Optional[str] = None
|
||
|
||
@property
|
||
def active_nodes(self) -> List[Node]:
|
||
"""Активные узлы."""
|
||
return [n for n in self.nodes.values() if n.status == NodeStatus.ACTIVE]
|
||
|
||
@property
|
||
def standby_nodes(self) -> List[Node]:
|
||
"""Узлы в режиме ожидания."""
|
||
return sorted(
|
||
[n for n in self.nodes.values() if n.status == NodeStatus.STANDBY],
|
||
key=lambda x: x.priority
|
||
)
|
||
|
||
|
||
class FiveNodesNetwork:
|
||
"""
|
||
Сеть из пяти узлов Montana.
|
||
|
||
Книга Монтана:
|
||
> "Мы разбросаны по карте — но мы одно целое.
|
||
> Как нейроны в мозгу. Каждый на своём месте —
|
||
> но все вместе образуют нечто большее."
|
||
"""
|
||
|
||
def __init__(self):
|
||
self.topology = NetworkTopology()
|
||
self._init_genesis_nodes()
|
||
|
||
def _init_genesis_nodes(self) -> None:
|
||
"""Инициализировать genesis узлы."""
|
||
# Genesis топология из книги
|
||
genesis_nodes = [
|
||
("amsterdam", "Amsterdam", "Netherlands", NodeRole.BOT, 0),
|
||
("moscow", "Moscow", "Russia", NodeRole.BRAIN, 1),
|
||
("almaty", "Almaty", "Kazakhstan", NodeRole.STANDBY, 2),
|
||
("spb", "Saint Petersburg", "Russia", NodeRole.STANDBY, 3),
|
||
("novosibirsk", "Novosibirsk", "Russia", NodeRole.STANDBY, 4),
|
||
]
|
||
|
||
for node_id, city, country, role, priority in genesis_nodes:
|
||
node = Node(
|
||
node_id=node_id,
|
||
city=city,
|
||
country=country,
|
||
role=role,
|
||
priority=priority
|
||
)
|
||
|
||
if role == NodeRole.BOT:
|
||
node.status = NodeStatus.ACTIVE
|
||
self.topology.bot_id = node_id
|
||
elif role == NodeRole.BRAIN:
|
||
node.status = NodeStatus.ACTIVE
|
||
self.topology.brain_id = node_id
|
||
else:
|
||
node.status = NodeStatus.STANDBY
|
||
|
||
node.heartbeat()
|
||
self.topology.nodes[node_id] = node
|
||
|
||
def get_brain(self) -> Optional[Node]:
|
||
"""Получить узел-мозг."""
|
||
if self.topology.brain_id:
|
||
return self.topology.nodes.get(self.topology.brain_id)
|
||
return None
|
||
|
||
def get_bot(self) -> Optional[Node]:
|
||
"""Получить активный бот-узел."""
|
||
if self.topology.bot_id:
|
||
return self.topology.nodes.get(self.topology.bot_id)
|
||
return None
|
||
|
||
def process_message(self, message: str, sender_id: str) -> Dict:
|
||
"""
|
||
Обработать сообщение через Brain-Body separation.
|
||
|
||
1. BOT принимает сообщение
|
||
2. BRAIN принимает решение
|
||
3. BOT отправляет ответ
|
||
|
||
Args:
|
||
message: Текст сообщения
|
||
sender_id: ID отправителя
|
||
|
||
Returns:
|
||
Результат обработки
|
||
"""
|
||
bot = self.get_bot()
|
||
brain = self.get_brain()
|
||
|
||
if not bot or not brain:
|
||
return {"error": "Network not ready"}
|
||
|
||
# BOT принимает
|
||
received_at = datetime.now(timezone.utc).isoformat()
|
||
bot.heartbeat()
|
||
|
||
# BRAIN думает (симуляция)
|
||
brain.heartbeat()
|
||
decision = self._brain_decide(message)
|
||
|
||
# BOT отвечает
|
||
return {
|
||
"received_by": bot.node_id,
|
||
"processed_by": brain.node_id,
|
||
"received_at": received_at,
|
||
"decision": decision,
|
||
"response": f"Processed by {brain.city}, delivered by {bot.city}"
|
||
}
|
||
|
||
def _brain_decide(self, message: str) -> str:
|
||
"""Мозг принимает решение (симуляция)."""
|
||
# Простая логика для демо
|
||
if "?" in message:
|
||
return "question_detected"
|
||
elif "!" in message:
|
||
return "exclamation_detected"
|
||
else:
|
||
return "statement_detected"
|
||
|
||
def node_failed(self, node_id: str) -> Optional[Node]:
|
||
"""
|
||
Обработать падение узла.
|
||
|
||
Args:
|
||
node_id: ID упавшего узла
|
||
|
||
Returns:
|
||
Узел, который подхватил роль (или None)
|
||
"""
|
||
if node_id not in self.topology.nodes:
|
||
return None
|
||
|
||
failed_node = self.topology.nodes[node_id]
|
||
failed_node.status = NodeStatus.FAILED
|
||
failed_role = failed_node.role
|
||
|
||
# Найти замену из standby
|
||
for standby in self.topology.standby_nodes:
|
||
if standby.status == NodeStatus.STANDBY:
|
||
# Промоутим standby
|
||
standby.role = failed_role
|
||
standby.status = NodeStatus.ACTIVE
|
||
standby.heartbeat()
|
||
|
||
if failed_role == NodeRole.BOT:
|
||
self.topology.bot_id = standby.node_id
|
||
elif failed_role == NodeRole.BRAIN:
|
||
self.topology.brain_id = standby.node_id
|
||
|
||
return standby
|
||
|
||
return None
|
||
|
||
def get_network_status(self) -> Dict:
|
||
"""Статус всей сети."""
|
||
active = self.topology.active_nodes
|
||
standby = self.topology.standby_nodes
|
||
failed = [n for n in self.topology.nodes.values() if n.status == NodeStatus.FAILED]
|
||
|
||
return {
|
||
"total_nodes": len(self.topology.nodes),
|
||
"active_nodes": len(active),
|
||
"standby_nodes": len(standby),
|
||
"failed_nodes": len(failed),
|
||
"brain": self.topology.brain_id,
|
||
"bot": self.topology.bot_id,
|
||
"countries": list(set(n.country for n in self.topology.nodes.values())),
|
||
"cities": [n.city for n in self.topology.nodes.values()],
|
||
"is_alive": len(active) > 0,
|
||
"formula": "1 active + 1 brain + 3 mirrors = 5 nodes"
|
||
}
|
||
|
||
def get_failover_chain(self) -> List[str]:
|
||
"""Цепочка failover."""
|
||
return [n.node_id for n in sorted(
|
||
self.topology.nodes.values(),
|
||
key=lambda x: x.priority
|
||
)]
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# DEMO
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
if __name__ == "__main__":
|
||
network = FiveNodesNetwork()
|
||
|
||
print("=" * 60)
|
||
print("FIVE NODES TOPOLOGY — Топология пяти узлов")
|
||
print("=" * 60)
|
||
print("\n'Пять городов. Три страны. Один организм.'")
|
||
|
||
# Статус сети
|
||
print("\n--- СТАТУС СЕТИ ---")
|
||
status = network.get_network_status()
|
||
print(f"Всего узлов: {status['total_nodes']}")
|
||
print(f"Активных: {status['active_nodes']}")
|
||
print(f"Standby: {status['standby_nodes']}")
|
||
print(f"BRAIN: {status['brain']}")
|
||
print(f"BOT: {status['bot']}")
|
||
print(f"Страны: {', '.join(status['countries'])}")
|
||
print(f"Города: {', '.join(status['cities'])}")
|
||
|
||
# Failover chain
|
||
print("\n--- FAILOVER CHAIN ---")
|
||
chain = network.get_failover_chain()
|
||
print(" → ".join(chain))
|
||
|
||
# Brain-Body separation
|
||
print("\n--- BRAIN-BODY SEPARATION ---")
|
||
result = network.process_message("Как дела?", "user_123")
|
||
print(f"Получено: {result['received_by']}")
|
||
print(f"Обработано: {result['processed_by']}")
|
||
print(f"Решение: {result['decision']}")
|
||
|
||
# Симуляция падения
|
||
print("\n--- СИМУЛЯЦИЯ ПАДЕНИЯ AMSTERDAM ---")
|
||
replacement = network.node_failed("amsterdam")
|
||
if replacement:
|
||
print(f"Amsterdam упал!")
|
||
print(f"Замена: {replacement.city} ({replacement.node_id})")
|
||
print(f"Новый BOT: {network.topology.bot_id}")
|
||
|
||
# Новый статус
|
||
print("\n--- НОВЫЙ СТАТУС ---")
|
||
status = network.get_network_status()
|
||
print(f"Активных: {status['active_nodes']}")
|
||
print(f"Упавших: {status['failed_nodes']}")
|
||
print(f"Сеть жива: {'ДА' if status['is_alive'] else 'НЕТ'}")
|
||
|
||
print("\n" + "=" * 60)
|
||
print("'Сеть не умирает. Сеть — бессмертна.'")
|
||
print("=" * 60)
|