montana/Русский/Сеть/five_nodes.py

314 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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)