Skip to content

Commit d05272a

Browse files
committed
feat: add LavalinkNode class and NodeLink event/health types.
1 parent b069f86 commit d05272a

File tree

2 files changed

+78
-81
lines changed

2 files changed

+78
-81
lines changed

src/structures/Node.ts

Lines changed: 58 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import type { NodeManager } from "./NodeManager";
2121
import type {
2222
BaseNodeStats, LavalinkInfo, LavalinkNodeOptions, LyricsResult, ModifyRequest, NodeLinkConnectionMetrics, NodeStats, SponsorBlockSegment
2323
} from "./Types/Node";
24-
import { NodeLinkEventPayload, NodeLinkEventTypes, HealthStatusThreshold, HealthStatusKeys, HealthPerformanceKeys, NodeMetricSummary } from "./Types/NodeLink";
24+
import { NodeLinkEventPayload, NodeLinkEventTypes, HealthStatusThreshold, HealthStatusKeys, HealthPerformanceKeys, NodeMetricSummary, HealthStatusObject, HealthStatusThresholdOptions } from "./Types/NodeLink";
2525
/**
2626
* Lavalink Node creator class
2727
*/
@@ -181,14 +181,6 @@ export class LavalinkNode {
181181
...options
182182
};
183183

184-
// Allow custom health/capacity thresholds via options.healthThresholds
185-
this.healthThresholds = options.healthThresholds || {
186-
cpu: { excellent: 0.3, good: 0.5, fair: 0.7, poor: 0.85 },
187-
memory: { excellent: 60, good: 75, fair: 85, poor: 95 },
188-
ping: { excellent: 50, good: 100, fair: 200, poor: 300 },
189-
frameDeficit: { overload: 100, critical: 500 }
190-
};
191-
192184
this.NodeManager = manager;
193185
this.validate();
194186
if (this.options.secure && this.options.port !== 443) throw new SyntaxError("If secure is true, then the port must be 443");
@@ -933,25 +925,29 @@ export class LavalinkNode {
933925
return await this.request(`/info`) as LavalinkInfo;
934926
}
935927

936-
928+
/**
929+
* Returns the metric summary of the node
930+
* @returns the metric summary of the node
931+
*/
937932
public nodeMetricSummary(): NodeMetricSummary {
938933
if (!this.connected || !this.isAlive) return { systemLoad: 0, cpuLoad: 0, memoryUsage: 0, players: 0, playingPlayers: 0, uptime: 0, ping: 0, frameDeficit: 0 }
939-
const cpuLoad = this.stats.cpu.lavalinkLoad;
940-
const systemLoad = this.stats.cpu.systemLoad;
941934
const _memoryUsed = this.stats.memory.used;
942935
const _memoryAllocated = this.stats.memory.allocated;
943-
const memoryUsage = _memoryAllocated > 0 ? (_memoryUsed / _memoryAllocated) * 100 : 0;
944-
const players = this.stats.players;
945-
const playingPlayers = this.stats.playingPlayers;
946-
const frameDeficit = this.stats.frameStats?.deficit || 0;
947-
const ping = this.heartBeatPing;
948-
const uptime = this.stats.uptime;
949-
return { systemLoad, cpuLoad, memoryUsage, players, playingPlayers, uptime, ping, frameDeficit }
936+
return {
937+
systemLoad: this.stats.cpu.systemLoad,
938+
cpuLoad: this.stats.cpu.lavalinkLoad,
939+
memoryUsage: _memoryAllocated > 0 ? (_memoryUsed / _memoryAllocated) * 100 : 0,
940+
players: this.stats.players,
941+
playingPlayers: this.stats.playingPlayers,
942+
uptime: this.stats.uptime,
943+
ping: this.heartBeatPing,
944+
frameDeficit: this.stats.frameStats?.deficit || 0
945+
}
950946
}
951947
/**
952948
* Get the node's health status with performance assessment.
953949
* @returns Object containing health status, performance rating, load balancing info, and recommendations
954-
*
950+
*
955951
* @example
956952
* ```ts
957953
* const health = node.getHealthStatus();
@@ -966,31 +962,13 @@ export class LavalinkNode {
966962
* }
967963
* ```
968964
*/
969-
public getHealthStatus(thresholds?: { cpu: Partial<HealthStatusThreshold>, memory: Partial<HealthStatusThreshold>, ping: Partial<HealthStatusThreshold> }): {
970-
status: HealthStatusKeys;
971-
performance: HealthPerformanceKeys;
972-
isOverloaded: boolean;
973-
needsRestart: boolean;
974-
penaltyScore: number;
975-
estimatedRemainingCapacity: number;
976-
recommendations: string[];
977-
metrics: {
978-
cpuLoad: number;
979-
memoryUsage: number;
980-
players: number;
981-
playingPlayers: number;
982-
uptime: number;
983-
ping: number;
984-
frameDeficit: number;
985-
};
986-
} {
965+
public getHealthStatus(thresholds?: HealthStatusThresholdOptions): HealthStatusObject {
987966
const cpuThresholds: HealthStatusThreshold = { excellent: 0.3, good: 0.5, fair: 0.7, poor: 0.85, ...thresholds?.cpu };
988967
const memoryThresholds: HealthStatusThreshold = { excellent: 60, good: 75, fair: 85, poor: 95, ...thresholds?.memory };
989968
const pingThresholds: HealthStatusThreshold = { excellent: 50, good: 100, fair: 200, poor: 300, ...thresholds?.ping };
990969
const recommendations: string[] = [];
991970
const metrics = this.nodeMetricSummary();
992-
const { systemLoad, cpuLoad, memoryUsage, players, playingPlayers, uptime, ping, frameDeficit } = metrics;
993-
971+
994972
// Check if node is offline
995973
if (!this.connected || !this.isAlive) {
996974
return {
@@ -1000,32 +978,32 @@ export class LavalinkNode {
1000978
needsRestart: true,
1001979
penaltyScore: 999999, // Maximum penalty for offline nodes
1002980
estimatedRemainingCapacity: 0,
1003-
recommendations: [ RecommendationsStrings.nodeOffline, RecommendationsStrings.checkConnectivity ],
981+
recommendations: [RecommendationsStrings.nodeOffline, RecommendationsStrings.checkConnectivity],
1004982
metrics
1005983
};
1006984
}
1007985

1008986

1009987
// Assess CPU performance
1010988
let cpuScore = 0;
1011-
if (cpuLoad < cpuThresholds.excellent) cpuScore = 4;
1012-
else if (cpuLoad < cpuThresholds.good) cpuScore = 3;
1013-
else if (cpuLoad < cpuThresholds.fair) cpuScore = 2;
1014-
else if (cpuLoad < cpuThresholds.poor) cpuScore = 1;
989+
if (metrics.cpuLoad < cpuThresholds.excellent) cpuScore = 4;
990+
else if (metrics.cpuLoad < cpuThresholds.good) cpuScore = 3;
991+
else if (metrics.cpuLoad < cpuThresholds.fair) cpuScore = 2;
992+
else if (metrics.cpuLoad < cpuThresholds.poor) cpuScore = 1;
1015993

1016994
// Assess memory performance
1017995
let memoryScore = 0;
1018-
if (memoryUsage < memoryThresholds.excellent) memoryScore = 4;
1019-
else if (memoryUsage < memoryThresholds.good) memoryScore = 3;
1020-
else if (memoryUsage < memoryThresholds.fair) memoryScore = 2;
1021-
else if (memoryUsage < memoryThresholds.poor) memoryScore = 1;
996+
if (metrics.memoryUsage < memoryThresholds.excellent) memoryScore = 4;
997+
else if (metrics.memoryUsage < memoryThresholds.good) memoryScore = 3;
998+
else if (metrics.memoryUsage < memoryThresholds.fair) memoryScore = 2;
999+
else if (metrics.memoryUsage < memoryThresholds.poor) memoryScore = 1;
10221000

10231001
// Assess ping performance
10241002
let pingScore = 0;
1025-
if (ping < pingThresholds.excellent) pingScore = 4;
1026-
else if (ping < pingThresholds.good) pingScore = 3;
1027-
else if (ping < pingThresholds.fair) pingScore = 2;
1028-
else if (ping < pingThresholds.poor) pingScore = 1;
1003+
if (metrics.ping < pingThresholds.excellent) pingScore = 4;
1004+
else if (metrics.ping < pingThresholds.good) pingScore = 3;
1005+
else if (metrics.ping < pingThresholds.fair) pingScore = 2;
1006+
else if (metrics.ping < pingThresholds.poor) pingScore = 1;
10291007

10301008
// Overall performance rating (average of scores)
10311009
const avgScore = (cpuScore + memoryScore + pingScore) / 3;
@@ -1035,36 +1013,36 @@ export class LavalinkNode {
10351013
else if (avgScore >= 1.5) performance = "fair";
10361014

10371015
// Check if overloaded
1038-
const isOverloaded = cpuLoad > cpuThresholds.fair || memoryUsage > memoryThresholds.fair || frameDeficit > 100;
1039-
const isCritical = cpuLoad > cpuThresholds.poor || memoryUsage > memoryThresholds.poor || frameDeficit > 500;
1016+
const isOverloaded = metrics.cpuLoad > cpuThresholds.fair || metrics.memoryUsage > memoryThresholds.fair || metrics.frameDeficit > 100;
1017+
const isCritical = metrics.cpuLoad > cpuThresholds.poor || metrics.memoryUsage > memoryThresholds.poor || metrics.frameDeficit > 500;
10401018
// Determine status
10411019
const status: HealthStatusKeys = isCritical ? "critical" : isOverloaded ? "degraded" : "healthy";
10421020

10431021
// Check if restart is needed
1044-
const needsRestart = status === "critical" ||
1045-
(isOverloaded && memoryUsage > 90) ||
1046-
frameDeficit > 1000 ||
1047-
(this.reconnectionAttemptCount > 0 && this.reconnectionAttemptCount >= this.options.retryAmount / 2);
1022+
const needsRestart = status === "critical" ||
1023+
(isOverloaded && metrics.memoryUsage > 90) ||
1024+
metrics.frameDeficit > 1000 ||
1025+
(this.reconnectionAttemptCount > 0 && this.reconnectionAttemptCount >= this.options.retryAmount / 2);
10481026

10491027
// Generate recommendations
1050-
if (cpuLoad > cpuThresholds.fair) recommendations.push(RecommendationsStrings.highCPULoad(cpuLoad));
1051-
if (systemLoad > 0.8) recommendations.push(RecommendationsStrings.highSystemLoad(systemLoad));
1052-
if (memoryUsage > memoryThresholds.fair) recommendations.push(RecommendationsStrings.highMemoryUsage(memoryUsage));
1053-
if (frameDeficit > 100) recommendations.push(RecommendationsStrings.frameDeficit(frameDeficit));
1054-
if (ping > pingThresholds.fair) recommendations.push(RecommendationsStrings.highLatency(ping));
1028+
if (metrics.cpuLoad > cpuThresholds.fair) recommendations.push(RecommendationsStrings.highCPULoad(metrics.cpuLoad));
1029+
if (metrics.systemLoad > 0.8) recommendations.push(RecommendationsStrings.highSystemLoad(metrics.systemLoad));
1030+
if (metrics.memoryUsage > memoryThresholds.fair) recommendations.push(RecommendationsStrings.highMemoryUsage(metrics.memoryUsage));
1031+
if (metrics.frameDeficit > 100) recommendations.push(RecommendationsStrings.frameDeficit(metrics.frameDeficit));
1032+
if (metrics.ping > pingThresholds.fair) recommendations.push(RecommendationsStrings.highLatency(metrics.ping));
10551033
if (needsRestart) recommendations.push(RecommendationsStrings.nodeRestart);
1056-
if (players > 500) recommendations.push(RecommendationsStrings.highPlayercount(players));
1034+
if (metrics.players > 500) recommendations.push(RecommendationsStrings.highPlayercount(metrics.players));
10571035

10581036
// Calculate penalty score for load balancing (lower is better)
10591037
// Based on Lavalink's penalty system but customized for health
10601038
const nullFrames = this.stats.frameStats?.nulled || 0;
1061-
const penaltyScore = players // Player count penalty (each player adds base penalty)
1062-
+ Math.pow(cpuLoad * 100, 2) // CPU penalty (exponential - heavily penalize high CPU)
1063-
+ Math.pow(memoryUsage, 1.5) // Memory penalty (exponential - heavily penalize high memory)
1064-
+ ping * 2 // Latency penalty
1065-
+ frameDeficit * 10 // Frame deficit penalty (critical for audio quality)
1039+
let penaltyScore = metrics.players // Player count penalty (each player adds base penalty)
1040+
+ Math.pow(metrics.cpuLoad * 100, 2) // CPU penalty (exponential - heavily penalize high CPU)
1041+
+ Math.pow(metrics.memoryUsage, 1.5) // Memory penalty (exponential - heavily penalize high memory)
1042+
+ metrics.ping * 2 // Latency penalty
1043+
+ metrics.frameDeficit * 10 // Frame deficit penalty (critical for audio quality)
10661044
+ nullFrames * 5; // Null frame penalty (if available)
1067-
1045+
10681046
// Status penalties
10691047
if (status === "critical") penaltyScore += 10000;
10701048
else if (status === "degraded") penaltyScore += 5000;
@@ -1076,19 +1054,19 @@ export class LavalinkNode {
10761054

10771055
// Estimate remaining capacity
10781056
let estimatedRemainingCapacity = 0;
1079-
1057+
10801058
// Base capacity estimation on current resource usage
10811059
// Assume a healthy node can handle ~100 players at 50% CPU, ~200 at 70% CPU
1082-
if (status !== "critical" && status !== "offline") {
1083-
const cpuCapacity = players === 0
1060+
if (status !== "critical") {
1061+
const cpuCapacity = metrics.players === 0
10841062
? 200
1085-
: cpuLoad > 0
1086-
? Math.max(0, Math.floor((cpuThresholds.fair - cpuLoad) / cpuLoad * players))
1063+
: metrics.cpuLoad > 0
1064+
? Math.max(0, Math.floor((cpuThresholds.fair - metrics.cpuLoad) / metrics.cpuLoad * metrics.players))
10871065
: 200;
1088-
const memoryCapacity = players === 0
1066+
const memoryCapacity = metrics.players === 0
10891067
? 200
1090-
: memoryUsage > 0
1091-
? Math.max(0, Math.floor((memoryThresholds.fair - memoryUsage) / memoryUsage * players))
1068+
: metrics.memoryUsage > 0
1069+
? Math.max(0, Math.floor((memoryThresholds.fair - metrics.memoryUsage) / metrics.memoryUsage * metrics.players))
10921070
: 200;
10931071

10941072
// Use the more conservative estimate, capped at a reasonable maximum
@@ -1364,7 +1342,7 @@ export class LavalinkNode {
13641342
if (this.options.enablePingOnStatsCheck) this.heartBeat();
13651343

13661344
if (this.heartBeatInterval) clearInterval(this.heartBeatInterval);
1367-
1345+
13681346
if (this.options.heartBeatInterval > 0) {
13691347
// everytime a pong happens, set this.isAlive to true
13701348
this.socket.on("pong", () => {

src/structures/Types/NodeLink.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export type NodeLinkEventPayload<T extends NodeLinkEventTypes> =
9898
never;
9999

100100
export type HealthStatusThreshold = { excellent: number, good: number, fair: number, poor: number }
101+
export type HealthStatusThresholdOptions = { cpu: Partial<HealthStatusThreshold>, memory: Partial<HealthStatusThreshold>, ping: Partial<HealthStatusThreshold> };
101102
export type NodeMetricSummary = {
102103
cpuLoad: number;
103104
systemLoad: number;
@@ -108,6 +109,24 @@ export type NodeMetricSummary = {
108109
ping: number;
109110
frameDeficit: number;
110111
}
112+
export type HealthStatusObject = {
113+
status: HealthStatusKeys;
114+
performance: HealthPerformanceKeys;
115+
isOverloaded: boolean;
116+
needsRestart: boolean;
117+
penaltyScore: number;
118+
estimatedRemainingCapacity: number;
119+
recommendations: string[];
120+
metrics: {
121+
cpuLoad: number;
122+
memoryUsage: number;
123+
players: number;
124+
playingPlayers: number;
125+
uptime: number;
126+
ping: number;
127+
frameDeficit: number;
128+
};
129+
}
111130

112131
export type HealthPerformanceKeys = "excellent" | "good" | "fair" | "poor";
113-
export type HealthStatusKeys = "healthy" | "degraded" | "critical" | "offline";
132+
export type HealthStatusKeys = "healthy" | "degraded" | "critical" | "offline";

0 commit comments

Comments
 (0)