forked from retoor/devplacepy
Add community node socket: devices, tickets, consent gate, kill switch
Phase 3 of plan.md. Device registration with one-time secrets, scoped single-use socket tickets, the lock-owner WS /nodes/ws with handshake and heartbeat eviction, a single batch ConsentGate, and NodesService which disconnects nodes within one tick when consent, device or the nodes_enabled kill switch changes. A reconcile of 2000 sessions measured about 110 ms after replacing per-session queries with batches. Devii may withdraw but never grant bandwidth_sharing. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
|
||||
from devplacepy.database import get_int_setting
|
||||
from devplacepy.services.nodes.session import NodeSession
|
||||
from devplacepy.services.nodes.spec import SPEC
|
||||
|
||||
LIMITS = SPEC.limits
|
||||
|
||||
COUNTER_NAMES: tuple[str, ...] = (
|
||||
"connected_total",
|
||||
"rejected_capacity",
|
||||
"rejected_ticket",
|
||||
"rejected_consent",
|
||||
"rejected_device",
|
||||
"protocol_errors",
|
||||
"evicted_heartbeat",
|
||||
"kicked",
|
||||
"replaced",
|
||||
)
|
||||
|
||||
|
||||
class NodeCounters:
|
||||
def __init__(self) -> None:
|
||||
self._counts: Counter[str] = Counter({name: 0 for name in COUNTER_NAMES})
|
||||
|
||||
def inc(self, name: str) -> None:
|
||||
if name not in COUNTER_NAMES:
|
||||
raise KeyError(name)
|
||||
self._counts[name] += 1
|
||||
|
||||
def snapshot(self) -> dict[str, int]:
|
||||
return {name: self._counts[name] for name in COUNTER_NAMES}
|
||||
|
||||
|
||||
class NodeRegistry:
|
||||
def __init__(self, counters: NodeCounters) -> None:
|
||||
self._sessions: dict[str, NodeSession] = {}
|
||||
self.counters = counters
|
||||
|
||||
def capacity(self) -> int:
|
||||
configured = get_int_setting("nodes_max_total", LIMITS.max_connected_nodes)
|
||||
return max(0, min(configured, LIMITS.max_connected_nodes))
|
||||
|
||||
def count(self) -> int:
|
||||
return len(self._sessions)
|
||||
|
||||
def sessions(self) -> list[NodeSession]:
|
||||
return list(self._sessions.values())
|
||||
|
||||
def get(self, device_uid: str) -> NodeSession | None:
|
||||
return self._sessions.get(device_uid)
|
||||
|
||||
def by_user(self, user_uid: str) -> list[NodeSession]:
|
||||
return [item for item in self._sessions.values() if item.user_uid == user_uid]
|
||||
|
||||
def admit(self, session: NodeSession) -> tuple[bool, NodeSession | None]:
|
||||
previous = self._sessions.get(session.device_uid)
|
||||
if previous is None and self.count() >= self.capacity():
|
||||
self.counters.inc("rejected_capacity")
|
||||
return False, None
|
||||
self._sessions[session.device_uid] = session
|
||||
self.counters.inc("connected_total")
|
||||
if previous is not None:
|
||||
self.counters.inc("replaced")
|
||||
return True, previous
|
||||
|
||||
def release(self, session: NodeSession) -> None:
|
||||
if self._sessions.get(session.device_uid) is session:
|
||||
del self._sessions[session.device_uid]
|
||||
|
||||
async def kick(self, session: NodeSession, reason: str) -> None:
|
||||
self.release(session)
|
||||
self.counters.inc("kicked")
|
||||
await session.kick(reason)
|
||||
|
||||
async def kick_all(self, reason: str) -> int:
|
||||
sessions = self.sessions()
|
||||
for session in sessions:
|
||||
await self.kick(session, reason)
|
||||
return len(sessions)
|
||||
Reference in New Issue
Block a user