182 lines
5.9 KiB
Python
182 lines
5.9 KiB
Python
"""
|
||
D6 多服务器注册中心:登记 peer SDK 节点,聚合各节点 /api/v3/devices 发现结果。
|
||
|
||
环境变量(可选):
|
||
- WORKPHONE_SERVER_ID: 本机节点 ID(默认 hostname)
|
||
- WORKPHONE_PUBLIC_BASE_URL: 本机对外 Base,如 http://192.168.1.10:8899
|
||
- WORKPHONE_REGISTRY_TOKEN: 登记/心跳校验令牌(不设则开放)
|
||
- WORKPHONE_REGISTRY_PEERS: 静态 peer 列表,逗号分隔 Base URL(只读聚合,无需对方登记)
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import os
|
||
import time
|
||
from typing import Any, Dict, List, Optional
|
||
|
||
import httpx
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def _local_server_id() -> str:
|
||
return (os.getenv("WORKPHONE_SERVER_ID") or os.getenv("HOSTNAME") or "sdk-local").strip()
|
||
|
||
|
||
def _local_public_base() -> str:
|
||
return (os.getenv("WORKPHONE_PUBLIC_BASE_URL") or "http://127.0.0.1:8899").rstrip("/")
|
||
|
||
|
||
def _registry_token_expected() -> str:
|
||
return (os.getenv("WORKPHONE_REGISTRY_TOKEN") or "").strip()
|
||
|
||
|
||
def _static_peer_urls() -> List[str]:
|
||
raw = os.getenv("WORKPHONE_REGISTRY_PEERS", "")
|
||
return [u.strip().rstrip("/") for u in raw.split(",") if u.strip()]
|
||
|
||
|
||
def _check_token(token: Optional[str]) -> None:
|
||
exp = _registry_token_expected()
|
||
if exp and (token or "").strip() != exp:
|
||
raise PermissionError("WORKPHONE_REGISTRY_TOKEN 校验失败")
|
||
|
||
|
||
class RegistryClusterService:
|
||
"""内存登记 peer;生产可换 Redis/Mongo(当前满足 D6 契约与联调)。"""
|
||
|
||
def __init__(self) -> None:
|
||
self._nodes: Dict[str, Dict[str, Any]] = {}
|
||
|
||
async def register_peer(
|
||
self,
|
||
node_id: str,
|
||
base_url: str,
|
||
label: str = "",
|
||
token: Optional[str] = None,
|
||
version: str = "",
|
||
) -> Dict[str, Any]:
|
||
_check_token(token)
|
||
base_url = base_url.rstrip("/")
|
||
self._nodes[node_id] = {
|
||
"node_id": node_id,
|
||
"base_url": base_url,
|
||
"label": label or node_id,
|
||
"version": version or "",
|
||
"last_heartbeat": time.time(),
|
||
}
|
||
logger.info("registry: 登记节点 %s -> %s", node_id, base_url)
|
||
return {"registered": True, "node_id": node_id}
|
||
|
||
async def heartbeat_peer(
|
||
self,
|
||
node_id: str,
|
||
token: Optional[str] = None,
|
||
device_count: int = 0,
|
||
version: str = "",
|
||
) -> Dict[str, Any]:
|
||
_check_token(token)
|
||
if node_id not in self._nodes:
|
||
raise ValueError(f"未知节点: {node_id},请先 register")
|
||
n = self._nodes[node_id]
|
||
n["last_heartbeat"] = time.time()
|
||
n["device_count"] = device_count
|
||
if version:
|
||
n["version"] = version
|
||
return {"ok": True}
|
||
|
||
def list_peers(self, max_age_sec: float = 180.0) -> List[Dict[str, Any]]:
|
||
now = time.time()
|
||
out: List[Dict[str, Any]] = []
|
||
for n in self._nodes.values():
|
||
if now - float(n.get("last_heartbeat", 0)) <= max_age_sec:
|
||
out.append(dict(n))
|
||
return sorted(out, key=lambda x: x["node_id"])
|
||
|
||
async def unregister_peer(self, node_id: str, token: Optional[str] = None) -> Dict[str, Any]:
|
||
_check_token(token)
|
||
self._nodes.pop(node_id, None)
|
||
return {"removed": True, "node_id": node_id}
|
||
|
||
@staticmethod
|
||
async def fetch_devices_from_base(base_url: str) -> List[Dict[str, Any]]:
|
||
url = f"{base_url.rstrip('/')}/api/v3/devices"
|
||
try:
|
||
async with httpx.AsyncClient(timeout=12.0) as client:
|
||
r = await client.get(url)
|
||
r.raise_for_status()
|
||
j = r.json()
|
||
data = j.get("data")
|
||
if isinstance(data, list):
|
||
return data
|
||
if isinstance(data, dict) and isinstance(data.get("devices"), list):
|
||
return data["devices"]
|
||
return []
|
||
except Exception as e:
|
||
logger.warning("registry: 拉取 %s 失败: %s", url, e)
|
||
return []
|
||
|
||
async def aggregate_fleet(self) -> Dict[str, Any]:
|
||
from services.device_fleet import list_merged_local_devices
|
||
|
||
sid = _local_server_id()
|
||
local_base = _local_public_base()
|
||
local_list = await list_merged_local_devices()
|
||
|
||
merged: List[Dict[str, Any]] = []
|
||
for d in local_list:
|
||
merged.append(
|
||
{
|
||
**d,
|
||
"home_server_id": sid,
|
||
"home_base_url": local_base,
|
||
"_source": "local",
|
||
}
|
||
)
|
||
|
||
seen_bases = {local_base}
|
||
|
||
for peer in self.list_peers():
|
||
b = peer["base_url"]
|
||
if b in seen_bases:
|
||
continue
|
||
seen_bases.add(b)
|
||
remote = await self.fetch_devices_from_base(b)
|
||
for d in remote:
|
||
merged.append(
|
||
{
|
||
**d,
|
||
"home_server_id": peer["node_id"],
|
||
"home_base_url": b,
|
||
"_source": "remote_registry",
|
||
}
|
||
)
|
||
|
||
for b in _static_peer_urls():
|
||
if b in seen_bases:
|
||
continue
|
||
seen_bases.add(b)
|
||
remote = await self.fetch_devices_from_base(b)
|
||
nid = b.replace("http://", "").replace("https://", "").replace("/", "_")
|
||
for d in remote:
|
||
merged.append(
|
||
{
|
||
**d,
|
||
"home_server_id": f"static:{nid}",
|
||
"home_base_url": b,
|
||
"_source": "remote_static",
|
||
}
|
||
)
|
||
|
||
return {
|
||
"local_server_id": sid,
|
||
"local_public_base_url": local_base,
|
||
"registered_peers": self.list_peers(),
|
||
"devices": merged,
|
||
"device_count": len(merged),
|
||
}
|
||
|
||
|
||
registry_cluster = RegistryClusterService()
|