feat: add server liveness sweep and rule registry
This commit is contained in:
@@ -8,8 +8,10 @@ import {
|
||||
YONEXUS_PROTOCOL_VERSION,
|
||||
buildAuthFailed,
|
||||
buildAuthSuccess,
|
||||
buildDisconnectNotice,
|
||||
buildError,
|
||||
buildHeartbeatAck,
|
||||
buildStatusUpdate,
|
||||
buildHelloAck,
|
||||
buildPairFailed,
|
||||
buildPairRequest,
|
||||
@@ -47,6 +49,7 @@ export interface YonexusServerRuntimeOptions {
|
||||
store: YonexusServerStore;
|
||||
transport: ServerTransport;
|
||||
now?: () => number;
|
||||
sweepIntervalMs?: number;
|
||||
}
|
||||
|
||||
export interface ServerLifecycleState {
|
||||
@@ -60,6 +63,8 @@ export class YonexusServerRuntime {
|
||||
private readonly registry: ServerRegistry;
|
||||
private readonly pairingService: PairingService;
|
||||
private readonly notificationService: DiscordNotificationService;
|
||||
private readonly sweepIntervalMs: number;
|
||||
private sweepTimer: NodeJS.Timeout | null = null;
|
||||
private started = false;
|
||||
|
||||
constructor(options: YonexusServerRuntimeOptions) {
|
||||
@@ -69,6 +74,7 @@ export class YonexusServerRuntime {
|
||||
clients: new Map(),
|
||||
sessions: new Map()
|
||||
};
|
||||
this.sweepIntervalMs = options.sweepIntervalMs ?? 30_000;
|
||||
this.pairingService = createPairingService({ now: this.now });
|
||||
this.notificationService = createDiscordNotificationService({
|
||||
botToken: options.config.notifyBotToken,
|
||||
@@ -100,6 +106,7 @@ export class YonexusServerRuntime {
|
||||
}
|
||||
|
||||
await this.options.transport.start();
|
||||
this.startSweepTimer();
|
||||
this.started = true;
|
||||
}
|
||||
|
||||
@@ -108,6 +115,7 @@ export class YonexusServerRuntime {
|
||||
return;
|
||||
}
|
||||
|
||||
this.stopSweepTimer();
|
||||
await this.persist();
|
||||
this.registry.sessions.clear();
|
||||
await this.options.transport.stop();
|
||||
@@ -669,6 +677,97 @@ export class YonexusServerRuntime {
|
||||
await this.persist();
|
||||
}
|
||||
|
||||
private startSweepTimer(): void {
|
||||
this.stopSweepTimer();
|
||||
this.sweepTimer = setInterval(() => {
|
||||
void this.runLivenessSweep();
|
||||
}, this.sweepIntervalMs);
|
||||
}
|
||||
|
||||
private stopSweepTimer(): void {
|
||||
if (!this.sweepTimer) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearInterval(this.sweepTimer);
|
||||
this.sweepTimer = null;
|
||||
}
|
||||
|
||||
private async runLivenessSweep(): Promise<void> {
|
||||
const now = this.now();
|
||||
let hasChanges = false;
|
||||
|
||||
for (const record of this.registry.clients.values()) {
|
||||
const nextStatus = this.getLivenessStatus(record, now);
|
||||
if (!nextStatus || nextStatus === record.status) {
|
||||
continue;
|
||||
}
|
||||
|
||||
record.status = nextStatus;
|
||||
record.updatedAt = now;
|
||||
hasChanges = true;
|
||||
|
||||
if (nextStatus === "unstable") {
|
||||
this.options.transport.send(
|
||||
record.identifier,
|
||||
encodeBuiltin(
|
||||
buildStatusUpdate(
|
||||
{
|
||||
identifier: record.identifier,
|
||||
status: "unstable",
|
||||
reason: "heartbeat_timeout_7m"
|
||||
},
|
||||
{ timestamp: now }
|
||||
)
|
||||
)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nextStatus === "offline") {
|
||||
this.options.transport.send(
|
||||
record.identifier,
|
||||
encodeBuiltin(
|
||||
buildDisconnectNotice(
|
||||
{
|
||||
identifier: record.identifier,
|
||||
reason: "heartbeat_timeout_11m"
|
||||
},
|
||||
{ timestamp: now }
|
||||
)
|
||||
)
|
||||
);
|
||||
this.options.transport.closeConnection(record.identifier, 1001, "Heartbeat timeout");
|
||||
this.registry.sessions.delete(record.identifier);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
await this.persist();
|
||||
}
|
||||
}
|
||||
|
||||
private getLivenessStatus(
|
||||
record: ClientRecord,
|
||||
now: number
|
||||
): "online" | "unstable" | "offline" | null {
|
||||
const session = this.registry.sessions.get(record.identifier);
|
||||
if (!session || !session.isAuthenticated || !record.lastHeartbeatAt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const silenceSeconds = now - record.lastHeartbeatAt;
|
||||
if (silenceSeconds >= 11 * 60) {
|
||||
return "offline";
|
||||
}
|
||||
|
||||
if (silenceSeconds >= 7 * 60) {
|
||||
return "unstable";
|
||||
}
|
||||
|
||||
return "online";
|
||||
}
|
||||
|
||||
private async triggerRePairRequired(
|
||||
connection: ClientConnection,
|
||||
record: ClientRecord,
|
||||
|
||||
Reference in New Issue
Block a user