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)
|