lavalink-client 2.10.3 → 2.10.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +282 -48
- package/dist/index.d.cts +36 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +282 -48
- package/dist/index.mjs +282 -48
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1270,6 +1270,11 @@ function safeStringify(obj, padding = 0) {
|
|
|
1270
1270
|
}
|
|
1271
1271
|
|
|
1272
1272
|
// src/structures/Node.ts
|
|
1273
|
+
var normalizeInt = (value, fallback, min, max) => {
|
|
1274
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
1275
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
1276
|
+
return Math.min(max, Math.max(min, Math.floor(parsed)));
|
|
1277
|
+
};
|
|
1273
1278
|
var LavalinkNode = class _LavalinkNode {
|
|
1274
1279
|
heartBeatPingTimestamp = 0;
|
|
1275
1280
|
heartBeatPongTimestamp = 0;
|
|
@@ -1704,7 +1709,12 @@ var LavalinkNode = class _LavalinkNode {
|
|
|
1704
1709
|
{ headers }
|
|
1705
1710
|
);
|
|
1706
1711
|
this.socket.on("open", this.open.bind(this));
|
|
1707
|
-
this.socket.on(
|
|
1712
|
+
this.socket.on(
|
|
1713
|
+
"close",
|
|
1714
|
+
(code, reason) => void this.close(code, reason?.toString()).catch((error) => {
|
|
1715
|
+
this.NodeManager.emit("error", this, error);
|
|
1716
|
+
})
|
|
1717
|
+
);
|
|
1708
1718
|
this.socket.on("message", this.message.bind(this));
|
|
1709
1719
|
this.socket.on("error", this.error.bind(this));
|
|
1710
1720
|
}
|
|
@@ -1796,43 +1806,7 @@ var LavalinkNode = class _LavalinkNode {
|
|
|
1796
1806
|
)
|
|
1797
1807
|
);
|
|
1798
1808
|
}
|
|
1799
|
-
|
|
1800
|
-
(n) => n.connected && n.options.id !== this.id
|
|
1801
|
-
);
|
|
1802
|
-
if (!nodeToMove) {
|
|
1803
|
-
return Promise.allSettled(
|
|
1804
|
-
Array.from(players.values()).map(
|
|
1805
|
-
(player) => player.destroy("PlayerChangeNodeFailNoEligibleNode" /* PlayerChangeNodeFailNoEligibleNode */).catch((error) => {
|
|
1806
|
-
this._emitDebugEvent("PlayerChangeNodeFailNoEligibleNode" /* PlayerChangeNodeFailNoEligibleNode */, {
|
|
1807
|
-
state: "error",
|
|
1808
|
-
message: `Failed to destroy player ${player.guildId}: ${error.message}`,
|
|
1809
|
-
error,
|
|
1810
|
-
functionLayer: "Node > destroy() > movePlayers"
|
|
1811
|
-
});
|
|
1812
|
-
})
|
|
1813
|
-
)
|
|
1814
|
-
);
|
|
1815
|
-
}
|
|
1816
|
-
return Promise.allSettled(
|
|
1817
|
-
Array.from(players.values()).map(
|
|
1818
|
-
(player) => player.changeNode(nodeToMove.options.id).catch((error) => {
|
|
1819
|
-
this._emitDebugEvent("PlayerChangeNodeFail" /* PlayerChangeNodeFail */, {
|
|
1820
|
-
state: "error",
|
|
1821
|
-
message: `Failed to move player ${player.guildId}: ${error.message}`,
|
|
1822
|
-
error,
|
|
1823
|
-
functionLayer: "Node > destroy() > movePlayers"
|
|
1824
|
-
});
|
|
1825
|
-
return player.destroy(error.message ?? "PlayerChangeNodeFail" /* PlayerChangeNodeFail */).catch((destroyError) => {
|
|
1826
|
-
this._emitDebugEvent("PlayerDestroyFail" /* PlayerDestroyFail */, {
|
|
1827
|
-
state: "error",
|
|
1828
|
-
message: `Failed to destroy player ${player.guildId} after move failure: ${destroyError.message}`,
|
|
1829
|
-
error: destroyError,
|
|
1830
|
-
functionLayer: "Node > destroy() > movePlayers"
|
|
1831
|
-
});
|
|
1832
|
-
});
|
|
1833
|
-
})
|
|
1834
|
-
)
|
|
1835
|
-
);
|
|
1809
|
+
return this.movePlayersToReplacementNode(Array.from(players.values()), "destroy");
|
|
1836
1810
|
};
|
|
1837
1811
|
return void handlePlayerOperations().finally(() => {
|
|
1838
1812
|
this.socket?.close(1e3, "Node-Destroy");
|
|
@@ -1871,6 +1845,247 @@ var LavalinkNode = class _LavalinkNode {
|
|
|
1871
1845
|
this.resetReconnectionAttempts();
|
|
1872
1846
|
this.NodeManager.emit("disconnect", this, { code: 1e3, reason: disconnectReason });
|
|
1873
1847
|
}
|
|
1848
|
+
/**
|
|
1849
|
+
* Pick the best ready node to move players to.
|
|
1850
|
+
* A node must be connected and have a sessionId before a player can be migrated to it.
|
|
1851
|
+
*/
|
|
1852
|
+
getReadyMigrationNode(excludeNodeId) {
|
|
1853
|
+
return this.selectMigrationTarget(/* @__PURE__ */ new Set([excludeNodeId]), Number.MAX_SAFE_INTEGER);
|
|
1854
|
+
}
|
|
1855
|
+
getMigrationErrorMessage(error) {
|
|
1856
|
+
if (error instanceof Error) return error.message;
|
|
1857
|
+
if (typeof error === "string") return error;
|
|
1858
|
+
try {
|
|
1859
|
+
return safeStringify(error);
|
|
1860
|
+
} catch {
|
|
1861
|
+
return String(error);
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
getMigrationConfig() {
|
|
1865
|
+
const migration = this._LManager.options?.advancedOptions?.playerMigration;
|
|
1866
|
+
return {
|
|
1867
|
+
concurrency: normalizeInt(migration?.concurrency, 5, 1, 32),
|
|
1868
|
+
perTargetConcurrency: normalizeInt(migration?.perTargetConcurrency, 2, 1, 8),
|
|
1869
|
+
backpressureDelayMs: normalizeInt(migration?.backpressureDelayMs, 50, 0, 1e3)
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
getMigrationReservationCount(nodeId) {
|
|
1873
|
+
return this.NodeManager.playerMigration.reservations.get(nodeId) || 0;
|
|
1874
|
+
}
|
|
1875
|
+
reserveMigrationTarget(nodeId) {
|
|
1876
|
+
this.NodeManager.playerMigration.reservations.set(nodeId, this.getMigrationReservationCount(nodeId) + 1);
|
|
1877
|
+
}
|
|
1878
|
+
releaseMigrationTarget(nodeId) {
|
|
1879
|
+
const current = this.getMigrationReservationCount(nodeId);
|
|
1880
|
+
if (current <= 1) this.NodeManager.playerMigration.reservations.delete(nodeId);
|
|
1881
|
+
else this.NodeManager.playerMigration.reservations.set(nodeId, current - 1);
|
|
1882
|
+
this.NodeManager.signalPlayerMigrationCapacityChange();
|
|
1883
|
+
}
|
|
1884
|
+
getEffectiveMigrationLoad(node) {
|
|
1885
|
+
return (node.stats?.players || 0) + this.getMigrationReservationCount(node.id);
|
|
1886
|
+
}
|
|
1887
|
+
selectMigrationTarget(excludeNodeIds, perTargetConcurrency) {
|
|
1888
|
+
let selectedNode = null;
|
|
1889
|
+
let selectedEffectiveLoad = Number.POSITIVE_INFINITY;
|
|
1890
|
+
let selectedLiveLoad = Number.POSITIVE_INFINITY;
|
|
1891
|
+
for (const node of this.NodeManager.nodes.values()) {
|
|
1892
|
+
if (!node.connected || !node.sessionId) continue;
|
|
1893
|
+
if (excludeNodeIds.has(node.id)) continue;
|
|
1894
|
+
const reservationCount = this.getMigrationReservationCount(node.id);
|
|
1895
|
+
if (reservationCount >= perTargetConcurrency) continue;
|
|
1896
|
+
const liveLoad = node.stats?.players || 0;
|
|
1897
|
+
const effectiveLoad = this.getEffectiveMigrationLoad(node);
|
|
1898
|
+
if (!selectedNode || effectiveLoad < selectedEffectiveLoad || effectiveLoad === selectedEffectiveLoad && (liveLoad < selectedLiveLoad || liveLoad === selectedLiveLoad && node.id.localeCompare(selectedNode.id) < 0)) {
|
|
1899
|
+
selectedNode = node;
|
|
1900
|
+
selectedEffectiveLoad = effectiveLoad;
|
|
1901
|
+
selectedLiveLoad = liveLoad;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
return selectedNode;
|
|
1905
|
+
}
|
|
1906
|
+
hasAnyHealthyMigrationNode(excludeNodeIds) {
|
|
1907
|
+
for (const node of this.NodeManager.nodes.values()) {
|
|
1908
|
+
if (!node.connected || !node.sessionId) continue;
|
|
1909
|
+
if (excludeNodeIds.has(node.id)) continue;
|
|
1910
|
+
return true;
|
|
1911
|
+
}
|
|
1912
|
+
return false;
|
|
1913
|
+
}
|
|
1914
|
+
isMigrationNodeHealthy(nodeId) {
|
|
1915
|
+
const node = this.NodeManager.nodes.get(nodeId);
|
|
1916
|
+
return !!node?.connected && !!node.sessionId;
|
|
1917
|
+
}
|
|
1918
|
+
async waitForMigrationCapacity(excludeNodeIds, perTargetConcurrency, timeoutMs) {
|
|
1919
|
+
if (this.selectMigrationTarget(excludeNodeIds, perTargetConcurrency)) return;
|
|
1920
|
+
if (timeoutMs <= 0) {
|
|
1921
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
await new Promise((resolve) => {
|
|
1925
|
+
let settled = false;
|
|
1926
|
+
let timer;
|
|
1927
|
+
const onSignal = () => {
|
|
1928
|
+
if (settled) return;
|
|
1929
|
+
settled = true;
|
|
1930
|
+
if (timer) clearTimeout(timer);
|
|
1931
|
+
this.NodeManager.playerMigration.capacityWaiters.delete(onSignal);
|
|
1932
|
+
resolve();
|
|
1933
|
+
};
|
|
1934
|
+
timer = setTimeout(() => {
|
|
1935
|
+
if (settled) return;
|
|
1936
|
+
settled = true;
|
|
1937
|
+
this.NodeManager.playerMigration.capacityWaiters.delete(onSignal);
|
|
1938
|
+
resolve();
|
|
1939
|
+
}, timeoutMs);
|
|
1940
|
+
this.NodeManager.playerMigration.capacityWaiters.add(onSignal);
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
/**
|
|
1944
|
+
* Move a batch of players to a ready node with bounded concurrency.
|
|
1945
|
+
* This keeps migration responsive without flooding the replacement node with requests.
|
|
1946
|
+
*/
|
|
1947
|
+
async movePlayersToReplacementNode(players, onNoEligibleNode) {
|
|
1948
|
+
const migrationQueue = players.filter((player) => {
|
|
1949
|
+
if (player.getData("internal_nodeMigrating") === true) return false;
|
|
1950
|
+
if (player.getData("internal_destroystatus") === true) return false;
|
|
1951
|
+
player.setData("internal_nodeMigrating", true);
|
|
1952
|
+
return true;
|
|
1953
|
+
});
|
|
1954
|
+
const { concurrency, perTargetConcurrency, backpressureDelayMs } = this.getMigrationConfig();
|
|
1955
|
+
let nextQueueIndex = 0;
|
|
1956
|
+
const workerCount = Math.min(concurrency, migrationQueue.length);
|
|
1957
|
+
const migratePlayer = async (player) => {
|
|
1958
|
+
const excludedNodes = /* @__PURE__ */ new Set();
|
|
1959
|
+
try {
|
|
1960
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1961
|
+
if (player.getData("internal_destroystatus") === true) return;
|
|
1962
|
+
if (player.node?.options?.id !== this.id) return;
|
|
1963
|
+
let targetNode = null;
|
|
1964
|
+
while (!(targetNode = this.selectMigrationTarget(excludedNodes, perTargetConcurrency))) {
|
|
1965
|
+
if (!this.hasAnyHealthyMigrationNode(excludedNodes)) {
|
|
1966
|
+
this._emitDebugEvent("PlayerChangeNodeFailNoEligibleNode" /* PlayerChangeNodeFailNoEligibleNode */, {
|
|
1967
|
+
state: "warn",
|
|
1968
|
+
message: `No eligible ready node was available for player ${player.guildId}`,
|
|
1969
|
+
functionLayer: "Node > movePlayersToReplacementNode()"
|
|
1970
|
+
});
|
|
1971
|
+
if (onNoEligibleNode === "destroy") {
|
|
1972
|
+
await player.destroy("PlayerChangeNodeFailNoEligibleNode" /* PlayerChangeNodeFailNoEligibleNode */).catch((destroyError) => {
|
|
1973
|
+
this._emitDebugEvent("PlayerDestroyFail" /* PlayerDestroyFail */, {
|
|
1974
|
+
state: "error",
|
|
1975
|
+
message: `Failed to destroy player ${player.guildId}: ${this.getMigrationErrorMessage(destroyError)}`,
|
|
1976
|
+
error: destroyError,
|
|
1977
|
+
functionLayer: "Node > movePlayersToReplacementNode()"
|
|
1978
|
+
});
|
|
1979
|
+
});
|
|
1980
|
+
} else {
|
|
1981
|
+
player.playing = false;
|
|
1982
|
+
}
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
await this.waitForMigrationCapacity(excludedNodes, perTargetConcurrency, backpressureDelayMs);
|
|
1986
|
+
}
|
|
1987
|
+
const targetNodeId = targetNode.id;
|
|
1988
|
+
if (player.getData("internal_destroystatus") === true) return;
|
|
1989
|
+
if (player.node?.options?.id !== this.id) return;
|
|
1990
|
+
this.reserveMigrationTarget(targetNodeId);
|
|
1991
|
+
try {
|
|
1992
|
+
await player.changeNode(targetNodeId);
|
|
1993
|
+
return;
|
|
1994
|
+
} catch (error) {
|
|
1995
|
+
const errorMessage = this.getMigrationErrorMessage(error);
|
|
1996
|
+
this._emitDebugEvent("PlayerChangeNodeFail" /* PlayerChangeNodeFail */, {
|
|
1997
|
+
state: "error",
|
|
1998
|
+
message: `Failed to move player ${player.guildId}: ${errorMessage}`,
|
|
1999
|
+
error,
|
|
2000
|
+
functionLayer: "Node > movePlayersToReplacementNode()"
|
|
2001
|
+
});
|
|
2002
|
+
const targetStillHealthy = this.isMigrationNodeHealthy(targetNodeId);
|
|
2003
|
+
excludedNodes.add(targetNodeId);
|
|
2004
|
+
if (attempt === 0 && !targetStillHealthy) {
|
|
2005
|
+
this._emitDebugEvent("PlayerChangeNodeFail" /* PlayerChangeNodeFail */, {
|
|
2006
|
+
state: "warn",
|
|
2007
|
+
message: `Migration target ${targetNodeId} became unavailable while moving player ${player.guildId}; retrying on another ready node`,
|
|
2008
|
+
error,
|
|
2009
|
+
functionLayer: "Node > movePlayersToReplacementNode()"
|
|
2010
|
+
});
|
|
2011
|
+
if (player.node?.options?.id === targetNodeId) player.node = this;
|
|
2012
|
+
continue;
|
|
2013
|
+
}
|
|
2014
|
+
if (attempt === 0 && this.hasAnyHealthyMigrationNode(excludedNodes)) {
|
|
2015
|
+
this._emitDebugEvent("PlayerChangeNodeFail" /* PlayerChangeNodeFail */, {
|
|
2016
|
+
state: "warn",
|
|
2017
|
+
message: `Retrying migration for player ${player.guildId} on a different ready node`,
|
|
2018
|
+
error,
|
|
2019
|
+
functionLayer: "Node > movePlayersToReplacementNode()"
|
|
2020
|
+
});
|
|
2021
|
+
if (player.node?.options?.id === targetNodeId) player.node = this;
|
|
2022
|
+
continue;
|
|
2023
|
+
}
|
|
2024
|
+
if (onNoEligibleNode === "destroy") {
|
|
2025
|
+
await player.destroy("PlayerChangeNodeFail" /* PlayerChangeNodeFail */).catch((destroyError) => {
|
|
2026
|
+
this._emitDebugEvent("PlayerDestroyFail" /* PlayerDestroyFail */, {
|
|
2027
|
+
state: "error",
|
|
2028
|
+
message: `Failed to destroy player ${player.guildId} after unexpected migration failure: ${this.getMigrationErrorMessage(destroyError)}`,
|
|
2029
|
+
error: destroyError,
|
|
2030
|
+
functionLayer: "Node > movePlayersToReplacementNode()"
|
|
2031
|
+
});
|
|
2032
|
+
});
|
|
2033
|
+
} else {
|
|
2034
|
+
player.playing = false;
|
|
2035
|
+
}
|
|
2036
|
+
return;
|
|
2037
|
+
} finally {
|
|
2038
|
+
this.releaseMigrationTarget(targetNodeId);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
} catch (error) {
|
|
2042
|
+
this._emitDebugEvent("PlayerChangeNodeFail" /* PlayerChangeNodeFail */, {
|
|
2043
|
+
state: "error",
|
|
2044
|
+
message: `Unexpected error while migrating player ${player.guildId}: ${this.getMigrationErrorMessage(error)}`,
|
|
2045
|
+
error,
|
|
2046
|
+
functionLayer: "Node > movePlayersToReplacementNode()"
|
|
2047
|
+
});
|
|
2048
|
+
if (onNoEligibleNode === "destroy") {
|
|
2049
|
+
await player.destroy("PlayerChangeNodeFail" /* PlayerChangeNodeFail */).catch((destroyError) => {
|
|
2050
|
+
this._emitDebugEvent("PlayerDestroyFail" /* PlayerDestroyFail */, {
|
|
2051
|
+
state: "error",
|
|
2052
|
+
message: `Failed to destroy player ${player.guildId} after unexpected migration failure: ${this.getMigrationErrorMessage(destroyError)}`,
|
|
2053
|
+
error: destroyError,
|
|
2054
|
+
functionLayer: "Node > movePlayersToReplacementNode()"
|
|
2055
|
+
});
|
|
2056
|
+
});
|
|
2057
|
+
} else {
|
|
2058
|
+
player.playing = false;
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
};
|
|
2062
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
2063
|
+
while (true) {
|
|
2064
|
+
const queueIndex = nextQueueIndex++;
|
|
2065
|
+
if (queueIndex >= migrationQueue.length) return;
|
|
2066
|
+
const player = migrationQueue[queueIndex];
|
|
2067
|
+
if (!player) continue;
|
|
2068
|
+
try {
|
|
2069
|
+
if (player.getData("internal_nodeMigrating") !== true) {
|
|
2070
|
+
player.setData("internal_nodeMigrating", true);
|
|
2071
|
+
}
|
|
2072
|
+
if (player.getData("internal_destroystatus") === true) continue;
|
|
2073
|
+
if (player.node?.options?.id !== this.id) continue;
|
|
2074
|
+
await migratePlayer(player);
|
|
2075
|
+
} catch (error) {
|
|
2076
|
+
this._emitDebugEvent("PlayerChangeNodeFail" /* PlayerChangeNodeFail */, {
|
|
2077
|
+
state: "error",
|
|
2078
|
+
message: `Unexpected worker error while migrating player ${player.guildId}: ${this.getMigrationErrorMessage(error)}`,
|
|
2079
|
+
error,
|
|
2080
|
+
functionLayer: "Node > movePlayersToReplacementNode()"
|
|
2081
|
+
});
|
|
2082
|
+
} finally {
|
|
2083
|
+
player.setData("internal_nodeMigrating", void 0);
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
});
|
|
2087
|
+
await Promise.all(workers);
|
|
2088
|
+
}
|
|
1874
2089
|
/**
|
|
1875
2090
|
* Gets all Players of a Node
|
|
1876
2091
|
* @returns array of players inside of lavalink
|
|
@@ -2583,9 +2798,10 @@ var LavalinkNode = class _LavalinkNode {
|
|
|
2583
2798
|
}
|
|
2584
2799
|
this.info.isNodelink = !!this.info.isNodelink;
|
|
2585
2800
|
this.NodeManager.emit("connect", this);
|
|
2801
|
+
this.NodeManager.signalPlayerMigrationCapacityChange();
|
|
2586
2802
|
}
|
|
2587
2803
|
/** @private util function for handling closing events from websocket */
|
|
2588
|
-
close(code, reason) {
|
|
2804
|
+
async close(code, reason) {
|
|
2589
2805
|
this.resetAckTimeouts(true, true);
|
|
2590
2806
|
try {
|
|
2591
2807
|
if (this.socket) {
|
|
@@ -2604,19 +2820,22 @@ var LavalinkNode = class _LavalinkNode {
|
|
|
2604
2820
|
this.isAlive = false;
|
|
2605
2821
|
if (code === 1006 && !reason) reason = "Socket got terminated due to no ping connection";
|
|
2606
2822
|
if (code === 1e3 && reason === "Node-Disconnect") return;
|
|
2823
|
+
const players = Array.from(
|
|
2824
|
+
this._LManager.players.filter((p) => p?.node?.options?.id === this?.options?.id).values()
|
|
2825
|
+
);
|
|
2607
2826
|
this.NodeManager.emit("disconnect", this, { code, reason });
|
|
2608
2827
|
if (code !== 1e3 || reason !== "Node-Destroy") {
|
|
2609
2828
|
if (this.NodeManager.nodes.has(this.id)) {
|
|
2610
2829
|
this.reconnect();
|
|
2611
2830
|
}
|
|
2612
2831
|
}
|
|
2613
|
-
this._LManager.
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
}
|
|
2832
|
+
if (this._LManager.options.autoMove && players.length) {
|
|
2833
|
+
await this.movePlayersToReplacementNode(players, "freeze");
|
|
2834
|
+
} else if (!this._LManager.options.autoMove) {
|
|
2835
|
+
players.forEach((p) => p.playing = false);
|
|
2836
|
+
} else if (this.NodeManager.nodes.filter((n) => n.connected && !!n.sessionId).size === 0) {
|
|
2837
|
+
players.forEach((p) => p.playing = false);
|
|
2838
|
+
}
|
|
2620
2839
|
}
|
|
2621
2840
|
/** @private util function for handling error events from websocket */
|
|
2622
2841
|
error(error) {
|
|
@@ -3694,6 +3913,16 @@ var NodeManager = class extends import_node_events.EventEmitter {
|
|
|
3694
3913
|
* A map of all nodes in the nodeManager
|
|
3695
3914
|
*/
|
|
3696
3915
|
nodes = new MiniMap();
|
|
3916
|
+
/** @internal Coordinates player migrations between nodes owned by this manager. */
|
|
3917
|
+
playerMigration = {
|
|
3918
|
+
reservations: /* @__PURE__ */ new Map(),
|
|
3919
|
+
capacityWaiters: /* @__PURE__ */ new Set()
|
|
3920
|
+
};
|
|
3921
|
+
/** @internal Wakes migrations waiting for capacity on one of this manager's nodes. */
|
|
3922
|
+
signalPlayerMigrationCapacityChange() {
|
|
3923
|
+
for (const resolve of this.playerMigration.capacityWaiters) resolve();
|
|
3924
|
+
this.playerMigration.capacityWaiters.clear();
|
|
3925
|
+
}
|
|
3697
3926
|
/**
|
|
3698
3927
|
* @param LavalinkManager The LavalinkManager that created this NodeManager
|
|
3699
3928
|
*/
|
|
@@ -6345,8 +6574,8 @@ var Player = class {
|
|
|
6345
6574
|
try {
|
|
6346
6575
|
if (!node)
|
|
6347
6576
|
node = Array.from(this.LavalinkManager.nodeManager.leastUsedNodes("playingPlayers")).find(
|
|
6348
|
-
(n) => n.connected && n.options.id !== this.node.options.id
|
|
6349
|
-
)
|
|
6577
|
+
(n) => n.connected && !!n.sessionId && n.options.id !== this.node.options.id
|
|
6578
|
+
)?.id;
|
|
6350
6579
|
if (!node || !this.LavalinkManager.nodeManager.nodes.get(node))
|
|
6351
6580
|
throw new RangeError("No nodes are available.");
|
|
6352
6581
|
if (this.node.options.id === node) return this;
|
|
@@ -6501,6 +6730,11 @@ var LavalinkManager = class _LavalinkManager extends import_node_events2.EventEm
|
|
|
6501
6730
|
advancedOptions: {
|
|
6502
6731
|
enableDebugEvents: options?.advancedOptions?.enableDebugEvents ?? false,
|
|
6503
6732
|
maxFilterFixDuration: options?.advancedOptions?.maxFilterFixDuration ?? 6e5,
|
|
6733
|
+
playerMigration: {
|
|
6734
|
+
concurrency: options?.advancedOptions?.playerMigration?.concurrency ?? 5,
|
|
6735
|
+
perTargetConcurrency: options?.advancedOptions?.playerMigration?.perTargetConcurrency ?? 2,
|
|
6736
|
+
backpressureDelayMs: options?.advancedOptions?.playerMigration?.backpressureDelayMs ?? 50
|
|
6737
|
+
},
|
|
6504
6738
|
debugOptions: {
|
|
6505
6739
|
logCustomSearches: options?.advancedOptions?.debugOptions?.logCustomSearches ?? false,
|
|
6506
6740
|
noAudio: options?.advancedOptions?.debugOptions?.noAudio ?? false,
|
package/dist/index.d.cts
CHANGED
|
@@ -3306,6 +3306,26 @@ declare class LavalinkNode {
|
|
|
3306
3306
|
* ```
|
|
3307
3307
|
*/
|
|
3308
3308
|
disconnect(disconnectReason?: DisconnectReasonsType): void;
|
|
3309
|
+
/**
|
|
3310
|
+
* Pick the best ready node to move players to.
|
|
3311
|
+
* A node must be connected and have a sessionId before a player can be migrated to it.
|
|
3312
|
+
*/
|
|
3313
|
+
private getReadyMigrationNode;
|
|
3314
|
+
private getMigrationErrorMessage;
|
|
3315
|
+
private getMigrationConfig;
|
|
3316
|
+
private getMigrationReservationCount;
|
|
3317
|
+
private reserveMigrationTarget;
|
|
3318
|
+
private releaseMigrationTarget;
|
|
3319
|
+
private getEffectiveMigrationLoad;
|
|
3320
|
+
private selectMigrationTarget;
|
|
3321
|
+
private hasAnyHealthyMigrationNode;
|
|
3322
|
+
private isMigrationNodeHealthy;
|
|
3323
|
+
private waitForMigrationCapacity;
|
|
3324
|
+
/**
|
|
3325
|
+
* Move a batch of players to a ready node with bounded concurrency.
|
|
3326
|
+
* This keeps migration responsive without flooding the replacement node with requests.
|
|
3327
|
+
*/
|
|
3328
|
+
private movePlayersToReplacementNode;
|
|
3309
3329
|
/**
|
|
3310
3330
|
* Gets all Players of a Node
|
|
3311
3331
|
* @returns array of players inside of lavalink
|
|
@@ -3741,6 +3761,13 @@ declare class NodeManager extends EventEmitter {
|
|
|
3741
3761
|
* A map of all nodes in the nodeManager
|
|
3742
3762
|
*/
|
|
3743
3763
|
nodes: MiniMap<string, LavalinkNode | NodeLinkNode>;
|
|
3764
|
+
/** @internal Coordinates player migrations between nodes owned by this manager. */
|
|
3765
|
+
readonly playerMigration: {
|
|
3766
|
+
reservations: Map<string, number>;
|
|
3767
|
+
capacityWaiters: Set<() => void>;
|
|
3768
|
+
};
|
|
3769
|
+
/** @internal Wakes migrations waiting for capacity on one of this manager's nodes. */
|
|
3770
|
+
signalPlayerMigrationCapacityChange(): void;
|
|
3744
3771
|
/**
|
|
3745
3772
|
* @param LavalinkManager The LavalinkManager that created this NodeManager
|
|
3746
3773
|
*/
|
|
@@ -4103,6 +4130,15 @@ interface ManagerOptions<CustomPlayerT extends Player = Player> {
|
|
|
4103
4130
|
dontThrowError?: boolean;
|
|
4104
4131
|
};
|
|
4105
4132
|
};
|
|
4133
|
+
/** Player migration configuration */
|
|
4134
|
+
playerMigration?: {
|
|
4135
|
+
/** Maximum number of players migrated concurrently. Default: 5 */
|
|
4136
|
+
concurrency?: number;
|
|
4137
|
+
/** Maximum number of concurrent migrations to a single target node. Default: 2 */
|
|
4138
|
+
perTargetConcurrency?: number;
|
|
4139
|
+
/** Maximum time to wait for migration capacity before retrying. Default: 50ms */
|
|
4140
|
+
backpressureDelayMs?: number;
|
|
4141
|
+
};
|
|
4106
4142
|
};
|
|
4107
4143
|
}
|
|
4108
4144
|
|
package/dist/index.d.ts
CHANGED
|
@@ -3306,6 +3306,26 @@ declare class LavalinkNode {
|
|
|
3306
3306
|
* ```
|
|
3307
3307
|
*/
|
|
3308
3308
|
disconnect(disconnectReason?: DisconnectReasonsType): void;
|
|
3309
|
+
/**
|
|
3310
|
+
* Pick the best ready node to move players to.
|
|
3311
|
+
* A node must be connected and have a sessionId before a player can be migrated to it.
|
|
3312
|
+
*/
|
|
3313
|
+
private getReadyMigrationNode;
|
|
3314
|
+
private getMigrationErrorMessage;
|
|
3315
|
+
private getMigrationConfig;
|
|
3316
|
+
private getMigrationReservationCount;
|
|
3317
|
+
private reserveMigrationTarget;
|
|
3318
|
+
private releaseMigrationTarget;
|
|
3319
|
+
private getEffectiveMigrationLoad;
|
|
3320
|
+
private selectMigrationTarget;
|
|
3321
|
+
private hasAnyHealthyMigrationNode;
|
|
3322
|
+
private isMigrationNodeHealthy;
|
|
3323
|
+
private waitForMigrationCapacity;
|
|
3324
|
+
/**
|
|
3325
|
+
* Move a batch of players to a ready node with bounded concurrency.
|
|
3326
|
+
* This keeps migration responsive without flooding the replacement node with requests.
|
|
3327
|
+
*/
|
|
3328
|
+
private movePlayersToReplacementNode;
|
|
3309
3329
|
/**
|
|
3310
3330
|
* Gets all Players of a Node
|
|
3311
3331
|
* @returns array of players inside of lavalink
|
|
@@ -3741,6 +3761,13 @@ declare class NodeManager extends EventEmitter {
|
|
|
3741
3761
|
* A map of all nodes in the nodeManager
|
|
3742
3762
|
*/
|
|
3743
3763
|
nodes: MiniMap<string, LavalinkNode | NodeLinkNode>;
|
|
3764
|
+
/** @internal Coordinates player migrations between nodes owned by this manager. */
|
|
3765
|
+
readonly playerMigration: {
|
|
3766
|
+
reservations: Map<string, number>;
|
|
3767
|
+
capacityWaiters: Set<() => void>;
|
|
3768
|
+
};
|
|
3769
|
+
/** @internal Wakes migrations waiting for capacity on one of this manager's nodes. */
|
|
3770
|
+
signalPlayerMigrationCapacityChange(): void;
|
|
3744
3771
|
/**
|
|
3745
3772
|
* @param LavalinkManager The LavalinkManager that created this NodeManager
|
|
3746
3773
|
*/
|
|
@@ -4103,6 +4130,15 @@ interface ManagerOptions<CustomPlayerT extends Player = Player> {
|
|
|
4103
4130
|
dontThrowError?: boolean;
|
|
4104
4131
|
};
|
|
4105
4132
|
};
|
|
4133
|
+
/** Player migration configuration */
|
|
4134
|
+
playerMigration?: {
|
|
4135
|
+
/** Maximum number of players migrated concurrently. Default: 5 */
|
|
4136
|
+
concurrency?: number;
|
|
4137
|
+
/** Maximum number of concurrent migrations to a single target node. Default: 2 */
|
|
4138
|
+
perTargetConcurrency?: number;
|
|
4139
|
+
/** Maximum time to wait for migration capacity before retrying. Default: 50ms */
|
|
4140
|
+
backpressureDelayMs?: number;
|
|
4141
|
+
};
|
|
4106
4142
|
};
|
|
4107
4143
|
}
|
|
4108
4144
|
|
package/dist/index.js
CHANGED
|
@@ -1204,6 +1204,11 @@ function safeStringify(obj, padding = 0) {
|
|
|
1204
1204
|
}
|
|
1205
1205
|
|
|
1206
1206
|
// src/structures/Node.ts
|
|
1207
|
+
var normalizeInt = (value, fallback, min, max) => {
|
|
1208
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
1209
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
1210
|
+
return Math.min(max, Math.max(min, Math.floor(parsed)));
|
|
1211
|
+
};
|
|
1207
1212
|
var LavalinkNode = class _LavalinkNode {
|
|
1208
1213
|
heartBeatPingTimestamp = 0;
|
|
1209
1214
|
heartBeatPongTimestamp = 0;
|
|
@@ -1638,7 +1643,12 @@ var LavalinkNode = class _LavalinkNode {
|
|
|
1638
1643
|
{ headers }
|
|
1639
1644
|
);
|
|
1640
1645
|
this.socket.on("open", this.open.bind(this));
|
|
1641
|
-
this.socket.on(
|
|
1646
|
+
this.socket.on(
|
|
1647
|
+
"close",
|
|
1648
|
+
(code, reason) => void this.close(code, reason?.toString()).catch((error) => {
|
|
1649
|
+
this.NodeManager.emit("error", this, error);
|
|
1650
|
+
})
|
|
1651
|
+
);
|
|
1642
1652
|
this.socket.on("message", this.message.bind(this));
|
|
1643
1653
|
this.socket.on("error", this.error.bind(this));
|
|
1644
1654
|
}
|
|
@@ -1730,43 +1740,7 @@ var LavalinkNode = class _LavalinkNode {
|
|
|
1730
1740
|
)
|
|
1731
1741
|
);
|
|
1732
1742
|
}
|
|
1733
|
-
|
|
1734
|
-
(n) => n.connected && n.options.id !== this.id
|
|
1735
|
-
);
|
|
1736
|
-
if (!nodeToMove) {
|
|
1737
|
-
return Promise.allSettled(
|
|
1738
|
-
Array.from(players.values()).map(
|
|
1739
|
-
(player) => player.destroy("PlayerChangeNodeFailNoEligibleNode" /* PlayerChangeNodeFailNoEligibleNode */).catch((error) => {
|
|
1740
|
-
this._emitDebugEvent("PlayerChangeNodeFailNoEligibleNode" /* PlayerChangeNodeFailNoEligibleNode */, {
|
|
1741
|
-
state: "error",
|
|
1742
|
-
message: `Failed to destroy player ${player.guildId}: ${error.message}`,
|
|
1743
|
-
error,
|
|
1744
|
-
functionLayer: "Node > destroy() > movePlayers"
|
|
1745
|
-
});
|
|
1746
|
-
})
|
|
1747
|
-
)
|
|
1748
|
-
);
|
|
1749
|
-
}
|
|
1750
|
-
return Promise.allSettled(
|
|
1751
|
-
Array.from(players.values()).map(
|
|
1752
|
-
(player) => player.changeNode(nodeToMove.options.id).catch((error) => {
|
|
1753
|
-
this._emitDebugEvent("PlayerChangeNodeFail" /* PlayerChangeNodeFail */, {
|
|
1754
|
-
state: "error",
|
|
1755
|
-
message: `Failed to move player ${player.guildId}: ${error.message}`,
|
|
1756
|
-
error,
|
|
1757
|
-
functionLayer: "Node > destroy() > movePlayers"
|
|
1758
|
-
});
|
|
1759
|
-
return player.destroy(error.message ?? "PlayerChangeNodeFail" /* PlayerChangeNodeFail */).catch((destroyError) => {
|
|
1760
|
-
this._emitDebugEvent("PlayerDestroyFail" /* PlayerDestroyFail */, {
|
|
1761
|
-
state: "error",
|
|
1762
|
-
message: `Failed to destroy player ${player.guildId} after move failure: ${destroyError.message}`,
|
|
1763
|
-
error: destroyError,
|
|
1764
|
-
functionLayer: "Node > destroy() > movePlayers"
|
|
1765
|
-
});
|
|
1766
|
-
});
|
|
1767
|
-
})
|
|
1768
|
-
)
|
|
1769
|
-
);
|
|
1743
|
+
return this.movePlayersToReplacementNode(Array.from(players.values()), "destroy");
|
|
1770
1744
|
};
|
|
1771
1745
|
return void handlePlayerOperations().finally(() => {
|
|
1772
1746
|
this.socket?.close(1e3, "Node-Destroy");
|
|
@@ -1805,6 +1779,247 @@ var LavalinkNode = class _LavalinkNode {
|
|
|
1805
1779
|
this.resetReconnectionAttempts();
|
|
1806
1780
|
this.NodeManager.emit("disconnect", this, { code: 1e3, reason: disconnectReason });
|
|
1807
1781
|
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Pick the best ready node to move players to.
|
|
1784
|
+
* A node must be connected and have a sessionId before a player can be migrated to it.
|
|
1785
|
+
*/
|
|
1786
|
+
getReadyMigrationNode(excludeNodeId) {
|
|
1787
|
+
return this.selectMigrationTarget(/* @__PURE__ */ new Set([excludeNodeId]), Number.MAX_SAFE_INTEGER);
|
|
1788
|
+
}
|
|
1789
|
+
getMigrationErrorMessage(error) {
|
|
1790
|
+
if (error instanceof Error) return error.message;
|
|
1791
|
+
if (typeof error === "string") return error;
|
|
1792
|
+
try {
|
|
1793
|
+
return safeStringify(error);
|
|
1794
|
+
} catch {
|
|
1795
|
+
return String(error);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
getMigrationConfig() {
|
|
1799
|
+
const migration = this._LManager.options?.advancedOptions?.playerMigration;
|
|
1800
|
+
return {
|
|
1801
|
+
concurrency: normalizeInt(migration?.concurrency, 5, 1, 32),
|
|
1802
|
+
perTargetConcurrency: normalizeInt(migration?.perTargetConcurrency, 2, 1, 8),
|
|
1803
|
+
backpressureDelayMs: normalizeInt(migration?.backpressureDelayMs, 50, 0, 1e3)
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
getMigrationReservationCount(nodeId) {
|
|
1807
|
+
return this.NodeManager.playerMigration.reservations.get(nodeId) || 0;
|
|
1808
|
+
}
|
|
1809
|
+
reserveMigrationTarget(nodeId) {
|
|
1810
|
+
this.NodeManager.playerMigration.reservations.set(nodeId, this.getMigrationReservationCount(nodeId) + 1);
|
|
1811
|
+
}
|
|
1812
|
+
releaseMigrationTarget(nodeId) {
|
|
1813
|
+
const current = this.getMigrationReservationCount(nodeId);
|
|
1814
|
+
if (current <= 1) this.NodeManager.playerMigration.reservations.delete(nodeId);
|
|
1815
|
+
else this.NodeManager.playerMigration.reservations.set(nodeId, current - 1);
|
|
1816
|
+
this.NodeManager.signalPlayerMigrationCapacityChange();
|
|
1817
|
+
}
|
|
1818
|
+
getEffectiveMigrationLoad(node) {
|
|
1819
|
+
return (node.stats?.players || 0) + this.getMigrationReservationCount(node.id);
|
|
1820
|
+
}
|
|
1821
|
+
selectMigrationTarget(excludeNodeIds, perTargetConcurrency) {
|
|
1822
|
+
let selectedNode = null;
|
|
1823
|
+
let selectedEffectiveLoad = Number.POSITIVE_INFINITY;
|
|
1824
|
+
let selectedLiveLoad = Number.POSITIVE_INFINITY;
|
|
1825
|
+
for (const node of this.NodeManager.nodes.values()) {
|
|
1826
|
+
if (!node.connected || !node.sessionId) continue;
|
|
1827
|
+
if (excludeNodeIds.has(node.id)) continue;
|
|
1828
|
+
const reservationCount = this.getMigrationReservationCount(node.id);
|
|
1829
|
+
if (reservationCount >= perTargetConcurrency) continue;
|
|
1830
|
+
const liveLoad = node.stats?.players || 0;
|
|
1831
|
+
const effectiveLoad = this.getEffectiveMigrationLoad(node);
|
|
1832
|
+
if (!selectedNode || effectiveLoad < selectedEffectiveLoad || effectiveLoad === selectedEffectiveLoad && (liveLoad < selectedLiveLoad || liveLoad === selectedLiveLoad && node.id.localeCompare(selectedNode.id) < 0)) {
|
|
1833
|
+
selectedNode = node;
|
|
1834
|
+
selectedEffectiveLoad = effectiveLoad;
|
|
1835
|
+
selectedLiveLoad = liveLoad;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
return selectedNode;
|
|
1839
|
+
}
|
|
1840
|
+
hasAnyHealthyMigrationNode(excludeNodeIds) {
|
|
1841
|
+
for (const node of this.NodeManager.nodes.values()) {
|
|
1842
|
+
if (!node.connected || !node.sessionId) continue;
|
|
1843
|
+
if (excludeNodeIds.has(node.id)) continue;
|
|
1844
|
+
return true;
|
|
1845
|
+
}
|
|
1846
|
+
return false;
|
|
1847
|
+
}
|
|
1848
|
+
isMigrationNodeHealthy(nodeId) {
|
|
1849
|
+
const node = this.NodeManager.nodes.get(nodeId);
|
|
1850
|
+
return !!node?.connected && !!node.sessionId;
|
|
1851
|
+
}
|
|
1852
|
+
async waitForMigrationCapacity(excludeNodeIds, perTargetConcurrency, timeoutMs) {
|
|
1853
|
+
if (this.selectMigrationTarget(excludeNodeIds, perTargetConcurrency)) return;
|
|
1854
|
+
if (timeoutMs <= 0) {
|
|
1855
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
1856
|
+
return;
|
|
1857
|
+
}
|
|
1858
|
+
await new Promise((resolve) => {
|
|
1859
|
+
let settled = false;
|
|
1860
|
+
let timer;
|
|
1861
|
+
const onSignal = () => {
|
|
1862
|
+
if (settled) return;
|
|
1863
|
+
settled = true;
|
|
1864
|
+
if (timer) clearTimeout(timer);
|
|
1865
|
+
this.NodeManager.playerMigration.capacityWaiters.delete(onSignal);
|
|
1866
|
+
resolve();
|
|
1867
|
+
};
|
|
1868
|
+
timer = setTimeout(() => {
|
|
1869
|
+
if (settled) return;
|
|
1870
|
+
settled = true;
|
|
1871
|
+
this.NodeManager.playerMigration.capacityWaiters.delete(onSignal);
|
|
1872
|
+
resolve();
|
|
1873
|
+
}, timeoutMs);
|
|
1874
|
+
this.NodeManager.playerMigration.capacityWaiters.add(onSignal);
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
/**
|
|
1878
|
+
* Move a batch of players to a ready node with bounded concurrency.
|
|
1879
|
+
* This keeps migration responsive without flooding the replacement node with requests.
|
|
1880
|
+
*/
|
|
1881
|
+
async movePlayersToReplacementNode(players, onNoEligibleNode) {
|
|
1882
|
+
const migrationQueue = players.filter((player) => {
|
|
1883
|
+
if (player.getData("internal_nodeMigrating") === true) return false;
|
|
1884
|
+
if (player.getData("internal_destroystatus") === true) return false;
|
|
1885
|
+
player.setData("internal_nodeMigrating", true);
|
|
1886
|
+
return true;
|
|
1887
|
+
});
|
|
1888
|
+
const { concurrency, perTargetConcurrency, backpressureDelayMs } = this.getMigrationConfig();
|
|
1889
|
+
let nextQueueIndex = 0;
|
|
1890
|
+
const workerCount = Math.min(concurrency, migrationQueue.length);
|
|
1891
|
+
const migratePlayer = async (player) => {
|
|
1892
|
+
const excludedNodes = /* @__PURE__ */ new Set();
|
|
1893
|
+
try {
|
|
1894
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1895
|
+
if (player.getData("internal_destroystatus") === true) return;
|
|
1896
|
+
if (player.node?.options?.id !== this.id) return;
|
|
1897
|
+
let targetNode = null;
|
|
1898
|
+
while (!(targetNode = this.selectMigrationTarget(excludedNodes, perTargetConcurrency))) {
|
|
1899
|
+
if (!this.hasAnyHealthyMigrationNode(excludedNodes)) {
|
|
1900
|
+
this._emitDebugEvent("PlayerChangeNodeFailNoEligibleNode" /* PlayerChangeNodeFailNoEligibleNode */, {
|
|
1901
|
+
state: "warn",
|
|
1902
|
+
message: `No eligible ready node was available for player ${player.guildId}`,
|
|
1903
|
+
functionLayer: "Node > movePlayersToReplacementNode()"
|
|
1904
|
+
});
|
|
1905
|
+
if (onNoEligibleNode === "destroy") {
|
|
1906
|
+
await player.destroy("PlayerChangeNodeFailNoEligibleNode" /* PlayerChangeNodeFailNoEligibleNode */).catch((destroyError) => {
|
|
1907
|
+
this._emitDebugEvent("PlayerDestroyFail" /* PlayerDestroyFail */, {
|
|
1908
|
+
state: "error",
|
|
1909
|
+
message: `Failed to destroy player ${player.guildId}: ${this.getMigrationErrorMessage(destroyError)}`,
|
|
1910
|
+
error: destroyError,
|
|
1911
|
+
functionLayer: "Node > movePlayersToReplacementNode()"
|
|
1912
|
+
});
|
|
1913
|
+
});
|
|
1914
|
+
} else {
|
|
1915
|
+
player.playing = false;
|
|
1916
|
+
}
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
await this.waitForMigrationCapacity(excludedNodes, perTargetConcurrency, backpressureDelayMs);
|
|
1920
|
+
}
|
|
1921
|
+
const targetNodeId = targetNode.id;
|
|
1922
|
+
if (player.getData("internal_destroystatus") === true) return;
|
|
1923
|
+
if (player.node?.options?.id !== this.id) return;
|
|
1924
|
+
this.reserveMigrationTarget(targetNodeId);
|
|
1925
|
+
try {
|
|
1926
|
+
await player.changeNode(targetNodeId);
|
|
1927
|
+
return;
|
|
1928
|
+
} catch (error) {
|
|
1929
|
+
const errorMessage = this.getMigrationErrorMessage(error);
|
|
1930
|
+
this._emitDebugEvent("PlayerChangeNodeFail" /* PlayerChangeNodeFail */, {
|
|
1931
|
+
state: "error",
|
|
1932
|
+
message: `Failed to move player ${player.guildId}: ${errorMessage}`,
|
|
1933
|
+
error,
|
|
1934
|
+
functionLayer: "Node > movePlayersToReplacementNode()"
|
|
1935
|
+
});
|
|
1936
|
+
const targetStillHealthy = this.isMigrationNodeHealthy(targetNodeId);
|
|
1937
|
+
excludedNodes.add(targetNodeId);
|
|
1938
|
+
if (attempt === 0 && !targetStillHealthy) {
|
|
1939
|
+
this._emitDebugEvent("PlayerChangeNodeFail" /* PlayerChangeNodeFail */, {
|
|
1940
|
+
state: "warn",
|
|
1941
|
+
message: `Migration target ${targetNodeId} became unavailable while moving player ${player.guildId}; retrying on another ready node`,
|
|
1942
|
+
error,
|
|
1943
|
+
functionLayer: "Node > movePlayersToReplacementNode()"
|
|
1944
|
+
});
|
|
1945
|
+
if (player.node?.options?.id === targetNodeId) player.node = this;
|
|
1946
|
+
continue;
|
|
1947
|
+
}
|
|
1948
|
+
if (attempt === 0 && this.hasAnyHealthyMigrationNode(excludedNodes)) {
|
|
1949
|
+
this._emitDebugEvent("PlayerChangeNodeFail" /* PlayerChangeNodeFail */, {
|
|
1950
|
+
state: "warn",
|
|
1951
|
+
message: `Retrying migration for player ${player.guildId} on a different ready node`,
|
|
1952
|
+
error,
|
|
1953
|
+
functionLayer: "Node > movePlayersToReplacementNode()"
|
|
1954
|
+
});
|
|
1955
|
+
if (player.node?.options?.id === targetNodeId) player.node = this;
|
|
1956
|
+
continue;
|
|
1957
|
+
}
|
|
1958
|
+
if (onNoEligibleNode === "destroy") {
|
|
1959
|
+
await player.destroy("PlayerChangeNodeFail" /* PlayerChangeNodeFail */).catch((destroyError) => {
|
|
1960
|
+
this._emitDebugEvent("PlayerDestroyFail" /* PlayerDestroyFail */, {
|
|
1961
|
+
state: "error",
|
|
1962
|
+
message: `Failed to destroy player ${player.guildId} after unexpected migration failure: ${this.getMigrationErrorMessage(destroyError)}`,
|
|
1963
|
+
error: destroyError,
|
|
1964
|
+
functionLayer: "Node > movePlayersToReplacementNode()"
|
|
1965
|
+
});
|
|
1966
|
+
});
|
|
1967
|
+
} else {
|
|
1968
|
+
player.playing = false;
|
|
1969
|
+
}
|
|
1970
|
+
return;
|
|
1971
|
+
} finally {
|
|
1972
|
+
this.releaseMigrationTarget(targetNodeId);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
} catch (error) {
|
|
1976
|
+
this._emitDebugEvent("PlayerChangeNodeFail" /* PlayerChangeNodeFail */, {
|
|
1977
|
+
state: "error",
|
|
1978
|
+
message: `Unexpected error while migrating player ${player.guildId}: ${this.getMigrationErrorMessage(error)}`,
|
|
1979
|
+
error,
|
|
1980
|
+
functionLayer: "Node > movePlayersToReplacementNode()"
|
|
1981
|
+
});
|
|
1982
|
+
if (onNoEligibleNode === "destroy") {
|
|
1983
|
+
await player.destroy("PlayerChangeNodeFail" /* PlayerChangeNodeFail */).catch((destroyError) => {
|
|
1984
|
+
this._emitDebugEvent("PlayerDestroyFail" /* PlayerDestroyFail */, {
|
|
1985
|
+
state: "error",
|
|
1986
|
+
message: `Failed to destroy player ${player.guildId} after unexpected migration failure: ${this.getMigrationErrorMessage(destroyError)}`,
|
|
1987
|
+
error: destroyError,
|
|
1988
|
+
functionLayer: "Node > movePlayersToReplacementNode()"
|
|
1989
|
+
});
|
|
1990
|
+
});
|
|
1991
|
+
} else {
|
|
1992
|
+
player.playing = false;
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
};
|
|
1996
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
1997
|
+
while (true) {
|
|
1998
|
+
const queueIndex = nextQueueIndex++;
|
|
1999
|
+
if (queueIndex >= migrationQueue.length) return;
|
|
2000
|
+
const player = migrationQueue[queueIndex];
|
|
2001
|
+
if (!player) continue;
|
|
2002
|
+
try {
|
|
2003
|
+
if (player.getData("internal_nodeMigrating") !== true) {
|
|
2004
|
+
player.setData("internal_nodeMigrating", true);
|
|
2005
|
+
}
|
|
2006
|
+
if (player.getData("internal_destroystatus") === true) continue;
|
|
2007
|
+
if (player.node?.options?.id !== this.id) continue;
|
|
2008
|
+
await migratePlayer(player);
|
|
2009
|
+
} catch (error) {
|
|
2010
|
+
this._emitDebugEvent("PlayerChangeNodeFail" /* PlayerChangeNodeFail */, {
|
|
2011
|
+
state: "error",
|
|
2012
|
+
message: `Unexpected worker error while migrating player ${player.guildId}: ${this.getMigrationErrorMessage(error)}`,
|
|
2013
|
+
error,
|
|
2014
|
+
functionLayer: "Node > movePlayersToReplacementNode()"
|
|
2015
|
+
});
|
|
2016
|
+
} finally {
|
|
2017
|
+
player.setData("internal_nodeMigrating", void 0);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
});
|
|
2021
|
+
await Promise.all(workers);
|
|
2022
|
+
}
|
|
1808
2023
|
/**
|
|
1809
2024
|
* Gets all Players of a Node
|
|
1810
2025
|
* @returns array of players inside of lavalink
|
|
@@ -2517,9 +2732,10 @@ var LavalinkNode = class _LavalinkNode {
|
|
|
2517
2732
|
}
|
|
2518
2733
|
this.info.isNodelink = !!this.info.isNodelink;
|
|
2519
2734
|
this.NodeManager.emit("connect", this);
|
|
2735
|
+
this.NodeManager.signalPlayerMigrationCapacityChange();
|
|
2520
2736
|
}
|
|
2521
2737
|
/** @private util function for handling closing events from websocket */
|
|
2522
|
-
close(code, reason) {
|
|
2738
|
+
async close(code, reason) {
|
|
2523
2739
|
this.resetAckTimeouts(true, true);
|
|
2524
2740
|
try {
|
|
2525
2741
|
if (this.socket) {
|
|
@@ -2538,19 +2754,22 @@ var LavalinkNode = class _LavalinkNode {
|
|
|
2538
2754
|
this.isAlive = false;
|
|
2539
2755
|
if (code === 1006 && !reason) reason = "Socket got terminated due to no ping connection";
|
|
2540
2756
|
if (code === 1e3 && reason === "Node-Disconnect") return;
|
|
2757
|
+
const players = Array.from(
|
|
2758
|
+
this._LManager.players.filter((p) => p?.node?.options?.id === this?.options?.id).values()
|
|
2759
|
+
);
|
|
2541
2760
|
this.NodeManager.emit("disconnect", this, { code, reason });
|
|
2542
2761
|
if (code !== 1e3 || reason !== "Node-Destroy") {
|
|
2543
2762
|
if (this.NodeManager.nodes.has(this.id)) {
|
|
2544
2763
|
this.reconnect();
|
|
2545
2764
|
}
|
|
2546
2765
|
}
|
|
2547
|
-
this._LManager.
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
}
|
|
2766
|
+
if (this._LManager.options.autoMove && players.length) {
|
|
2767
|
+
await this.movePlayersToReplacementNode(players, "freeze");
|
|
2768
|
+
} else if (!this._LManager.options.autoMove) {
|
|
2769
|
+
players.forEach((p) => p.playing = false);
|
|
2770
|
+
} else if (this.NodeManager.nodes.filter((n) => n.connected && !!n.sessionId).size === 0) {
|
|
2771
|
+
players.forEach((p) => p.playing = false);
|
|
2772
|
+
}
|
|
2554
2773
|
}
|
|
2555
2774
|
/** @private util function for handling error events from websocket */
|
|
2556
2775
|
error(error) {
|
|
@@ -3628,6 +3847,16 @@ var NodeManager = class extends EventEmitter {
|
|
|
3628
3847
|
* A map of all nodes in the nodeManager
|
|
3629
3848
|
*/
|
|
3630
3849
|
nodes = new MiniMap();
|
|
3850
|
+
/** @internal Coordinates player migrations between nodes owned by this manager. */
|
|
3851
|
+
playerMigration = {
|
|
3852
|
+
reservations: /* @__PURE__ */ new Map(),
|
|
3853
|
+
capacityWaiters: /* @__PURE__ */ new Set()
|
|
3854
|
+
};
|
|
3855
|
+
/** @internal Wakes migrations waiting for capacity on one of this manager's nodes. */
|
|
3856
|
+
signalPlayerMigrationCapacityChange() {
|
|
3857
|
+
for (const resolve of this.playerMigration.capacityWaiters) resolve();
|
|
3858
|
+
this.playerMigration.capacityWaiters.clear();
|
|
3859
|
+
}
|
|
3631
3860
|
/**
|
|
3632
3861
|
* @param LavalinkManager The LavalinkManager that created this NodeManager
|
|
3633
3862
|
*/
|
|
@@ -6279,8 +6508,8 @@ var Player = class {
|
|
|
6279
6508
|
try {
|
|
6280
6509
|
if (!node)
|
|
6281
6510
|
node = Array.from(this.LavalinkManager.nodeManager.leastUsedNodes("playingPlayers")).find(
|
|
6282
|
-
(n) => n.connected && n.options.id !== this.node.options.id
|
|
6283
|
-
)
|
|
6511
|
+
(n) => n.connected && !!n.sessionId && n.options.id !== this.node.options.id
|
|
6512
|
+
)?.id;
|
|
6284
6513
|
if (!node || !this.LavalinkManager.nodeManager.nodes.get(node))
|
|
6285
6514
|
throw new RangeError("No nodes are available.");
|
|
6286
6515
|
if (this.node.options.id === node) return this;
|
|
@@ -6435,6 +6664,11 @@ var LavalinkManager = class _LavalinkManager extends EventEmitter2 {
|
|
|
6435
6664
|
advancedOptions: {
|
|
6436
6665
|
enableDebugEvents: options?.advancedOptions?.enableDebugEvents ?? false,
|
|
6437
6666
|
maxFilterFixDuration: options?.advancedOptions?.maxFilterFixDuration ?? 6e5,
|
|
6667
|
+
playerMigration: {
|
|
6668
|
+
concurrency: options?.advancedOptions?.playerMigration?.concurrency ?? 5,
|
|
6669
|
+
perTargetConcurrency: options?.advancedOptions?.playerMigration?.perTargetConcurrency ?? 2,
|
|
6670
|
+
backpressureDelayMs: options?.advancedOptions?.playerMigration?.backpressureDelayMs ?? 50
|
|
6671
|
+
},
|
|
6438
6672
|
debugOptions: {
|
|
6439
6673
|
logCustomSearches: options?.advancedOptions?.debugOptions?.logCustomSearches ?? false,
|
|
6440
6674
|
noAudio: options?.advancedOptions?.debugOptions?.noAudio ?? false,
|
package/dist/index.mjs
CHANGED
|
@@ -1204,6 +1204,11 @@ function safeStringify(obj, padding = 0) {
|
|
|
1204
1204
|
}
|
|
1205
1205
|
|
|
1206
1206
|
// src/structures/Node.ts
|
|
1207
|
+
var normalizeInt = (value, fallback, min, max) => {
|
|
1208
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
1209
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
1210
|
+
return Math.min(max, Math.max(min, Math.floor(parsed)));
|
|
1211
|
+
};
|
|
1207
1212
|
var LavalinkNode = class _LavalinkNode {
|
|
1208
1213
|
heartBeatPingTimestamp = 0;
|
|
1209
1214
|
heartBeatPongTimestamp = 0;
|
|
@@ -1638,7 +1643,12 @@ var LavalinkNode = class _LavalinkNode {
|
|
|
1638
1643
|
{ headers }
|
|
1639
1644
|
);
|
|
1640
1645
|
this.socket.on("open", this.open.bind(this));
|
|
1641
|
-
this.socket.on(
|
|
1646
|
+
this.socket.on(
|
|
1647
|
+
"close",
|
|
1648
|
+
(code, reason) => void this.close(code, reason?.toString()).catch((error) => {
|
|
1649
|
+
this.NodeManager.emit("error", this, error);
|
|
1650
|
+
})
|
|
1651
|
+
);
|
|
1642
1652
|
this.socket.on("message", this.message.bind(this));
|
|
1643
1653
|
this.socket.on("error", this.error.bind(this));
|
|
1644
1654
|
}
|
|
@@ -1730,43 +1740,7 @@ var LavalinkNode = class _LavalinkNode {
|
|
|
1730
1740
|
)
|
|
1731
1741
|
);
|
|
1732
1742
|
}
|
|
1733
|
-
|
|
1734
|
-
(n) => n.connected && n.options.id !== this.id
|
|
1735
|
-
);
|
|
1736
|
-
if (!nodeToMove) {
|
|
1737
|
-
return Promise.allSettled(
|
|
1738
|
-
Array.from(players.values()).map(
|
|
1739
|
-
(player) => player.destroy("PlayerChangeNodeFailNoEligibleNode" /* PlayerChangeNodeFailNoEligibleNode */).catch((error) => {
|
|
1740
|
-
this._emitDebugEvent("PlayerChangeNodeFailNoEligibleNode" /* PlayerChangeNodeFailNoEligibleNode */, {
|
|
1741
|
-
state: "error",
|
|
1742
|
-
message: `Failed to destroy player ${player.guildId}: ${error.message}`,
|
|
1743
|
-
error,
|
|
1744
|
-
functionLayer: "Node > destroy() > movePlayers"
|
|
1745
|
-
});
|
|
1746
|
-
})
|
|
1747
|
-
)
|
|
1748
|
-
);
|
|
1749
|
-
}
|
|
1750
|
-
return Promise.allSettled(
|
|
1751
|
-
Array.from(players.values()).map(
|
|
1752
|
-
(player) => player.changeNode(nodeToMove.options.id).catch((error) => {
|
|
1753
|
-
this._emitDebugEvent("PlayerChangeNodeFail" /* PlayerChangeNodeFail */, {
|
|
1754
|
-
state: "error",
|
|
1755
|
-
message: `Failed to move player ${player.guildId}: ${error.message}`,
|
|
1756
|
-
error,
|
|
1757
|
-
functionLayer: "Node > destroy() > movePlayers"
|
|
1758
|
-
});
|
|
1759
|
-
return player.destroy(error.message ?? "PlayerChangeNodeFail" /* PlayerChangeNodeFail */).catch((destroyError) => {
|
|
1760
|
-
this._emitDebugEvent("PlayerDestroyFail" /* PlayerDestroyFail */, {
|
|
1761
|
-
state: "error",
|
|
1762
|
-
message: `Failed to destroy player ${player.guildId} after move failure: ${destroyError.message}`,
|
|
1763
|
-
error: destroyError,
|
|
1764
|
-
functionLayer: "Node > destroy() > movePlayers"
|
|
1765
|
-
});
|
|
1766
|
-
});
|
|
1767
|
-
})
|
|
1768
|
-
)
|
|
1769
|
-
);
|
|
1743
|
+
return this.movePlayersToReplacementNode(Array.from(players.values()), "destroy");
|
|
1770
1744
|
};
|
|
1771
1745
|
return void handlePlayerOperations().finally(() => {
|
|
1772
1746
|
this.socket?.close(1e3, "Node-Destroy");
|
|
@@ -1805,6 +1779,247 @@ var LavalinkNode = class _LavalinkNode {
|
|
|
1805
1779
|
this.resetReconnectionAttempts();
|
|
1806
1780
|
this.NodeManager.emit("disconnect", this, { code: 1e3, reason: disconnectReason });
|
|
1807
1781
|
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Pick the best ready node to move players to.
|
|
1784
|
+
* A node must be connected and have a sessionId before a player can be migrated to it.
|
|
1785
|
+
*/
|
|
1786
|
+
getReadyMigrationNode(excludeNodeId) {
|
|
1787
|
+
return this.selectMigrationTarget(/* @__PURE__ */ new Set([excludeNodeId]), Number.MAX_SAFE_INTEGER);
|
|
1788
|
+
}
|
|
1789
|
+
getMigrationErrorMessage(error) {
|
|
1790
|
+
if (error instanceof Error) return error.message;
|
|
1791
|
+
if (typeof error === "string") return error;
|
|
1792
|
+
try {
|
|
1793
|
+
return safeStringify(error);
|
|
1794
|
+
} catch {
|
|
1795
|
+
return String(error);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
getMigrationConfig() {
|
|
1799
|
+
const migration = this._LManager.options?.advancedOptions?.playerMigration;
|
|
1800
|
+
return {
|
|
1801
|
+
concurrency: normalizeInt(migration?.concurrency, 5, 1, 32),
|
|
1802
|
+
perTargetConcurrency: normalizeInt(migration?.perTargetConcurrency, 2, 1, 8),
|
|
1803
|
+
backpressureDelayMs: normalizeInt(migration?.backpressureDelayMs, 50, 0, 1e3)
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
getMigrationReservationCount(nodeId) {
|
|
1807
|
+
return this.NodeManager.playerMigration.reservations.get(nodeId) || 0;
|
|
1808
|
+
}
|
|
1809
|
+
reserveMigrationTarget(nodeId) {
|
|
1810
|
+
this.NodeManager.playerMigration.reservations.set(nodeId, this.getMigrationReservationCount(nodeId) + 1);
|
|
1811
|
+
}
|
|
1812
|
+
releaseMigrationTarget(nodeId) {
|
|
1813
|
+
const current = this.getMigrationReservationCount(nodeId);
|
|
1814
|
+
if (current <= 1) this.NodeManager.playerMigration.reservations.delete(nodeId);
|
|
1815
|
+
else this.NodeManager.playerMigration.reservations.set(nodeId, current - 1);
|
|
1816
|
+
this.NodeManager.signalPlayerMigrationCapacityChange();
|
|
1817
|
+
}
|
|
1818
|
+
getEffectiveMigrationLoad(node) {
|
|
1819
|
+
return (node.stats?.players || 0) + this.getMigrationReservationCount(node.id);
|
|
1820
|
+
}
|
|
1821
|
+
selectMigrationTarget(excludeNodeIds, perTargetConcurrency) {
|
|
1822
|
+
let selectedNode = null;
|
|
1823
|
+
let selectedEffectiveLoad = Number.POSITIVE_INFINITY;
|
|
1824
|
+
let selectedLiveLoad = Number.POSITIVE_INFINITY;
|
|
1825
|
+
for (const node of this.NodeManager.nodes.values()) {
|
|
1826
|
+
if (!node.connected || !node.sessionId) continue;
|
|
1827
|
+
if (excludeNodeIds.has(node.id)) continue;
|
|
1828
|
+
const reservationCount = this.getMigrationReservationCount(node.id);
|
|
1829
|
+
if (reservationCount >= perTargetConcurrency) continue;
|
|
1830
|
+
const liveLoad = node.stats?.players || 0;
|
|
1831
|
+
const effectiveLoad = this.getEffectiveMigrationLoad(node);
|
|
1832
|
+
if (!selectedNode || effectiveLoad < selectedEffectiveLoad || effectiveLoad === selectedEffectiveLoad && (liveLoad < selectedLiveLoad || liveLoad === selectedLiveLoad && node.id.localeCompare(selectedNode.id) < 0)) {
|
|
1833
|
+
selectedNode = node;
|
|
1834
|
+
selectedEffectiveLoad = effectiveLoad;
|
|
1835
|
+
selectedLiveLoad = liveLoad;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
return selectedNode;
|
|
1839
|
+
}
|
|
1840
|
+
hasAnyHealthyMigrationNode(excludeNodeIds) {
|
|
1841
|
+
for (const node of this.NodeManager.nodes.values()) {
|
|
1842
|
+
if (!node.connected || !node.sessionId) continue;
|
|
1843
|
+
if (excludeNodeIds.has(node.id)) continue;
|
|
1844
|
+
return true;
|
|
1845
|
+
}
|
|
1846
|
+
return false;
|
|
1847
|
+
}
|
|
1848
|
+
isMigrationNodeHealthy(nodeId) {
|
|
1849
|
+
const node = this.NodeManager.nodes.get(nodeId);
|
|
1850
|
+
return !!node?.connected && !!node.sessionId;
|
|
1851
|
+
}
|
|
1852
|
+
async waitForMigrationCapacity(excludeNodeIds, perTargetConcurrency, timeoutMs) {
|
|
1853
|
+
if (this.selectMigrationTarget(excludeNodeIds, perTargetConcurrency)) return;
|
|
1854
|
+
if (timeoutMs <= 0) {
|
|
1855
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
1856
|
+
return;
|
|
1857
|
+
}
|
|
1858
|
+
await new Promise((resolve) => {
|
|
1859
|
+
let settled = false;
|
|
1860
|
+
let timer;
|
|
1861
|
+
const onSignal = () => {
|
|
1862
|
+
if (settled) return;
|
|
1863
|
+
settled = true;
|
|
1864
|
+
if (timer) clearTimeout(timer);
|
|
1865
|
+
this.NodeManager.playerMigration.capacityWaiters.delete(onSignal);
|
|
1866
|
+
resolve();
|
|
1867
|
+
};
|
|
1868
|
+
timer = setTimeout(() => {
|
|
1869
|
+
if (settled) return;
|
|
1870
|
+
settled = true;
|
|
1871
|
+
this.NodeManager.playerMigration.capacityWaiters.delete(onSignal);
|
|
1872
|
+
resolve();
|
|
1873
|
+
}, timeoutMs);
|
|
1874
|
+
this.NodeManager.playerMigration.capacityWaiters.add(onSignal);
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
/**
|
|
1878
|
+
* Move a batch of players to a ready node with bounded concurrency.
|
|
1879
|
+
* This keeps migration responsive without flooding the replacement node with requests.
|
|
1880
|
+
*/
|
|
1881
|
+
async movePlayersToReplacementNode(players, onNoEligibleNode) {
|
|
1882
|
+
const migrationQueue = players.filter((player) => {
|
|
1883
|
+
if (player.getData("internal_nodeMigrating") === true) return false;
|
|
1884
|
+
if (player.getData("internal_destroystatus") === true) return false;
|
|
1885
|
+
player.setData("internal_nodeMigrating", true);
|
|
1886
|
+
return true;
|
|
1887
|
+
});
|
|
1888
|
+
const { concurrency, perTargetConcurrency, backpressureDelayMs } = this.getMigrationConfig();
|
|
1889
|
+
let nextQueueIndex = 0;
|
|
1890
|
+
const workerCount = Math.min(concurrency, migrationQueue.length);
|
|
1891
|
+
const migratePlayer = async (player) => {
|
|
1892
|
+
const excludedNodes = /* @__PURE__ */ new Set();
|
|
1893
|
+
try {
|
|
1894
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1895
|
+
if (player.getData("internal_destroystatus") === true) return;
|
|
1896
|
+
if (player.node?.options?.id !== this.id) return;
|
|
1897
|
+
let targetNode = null;
|
|
1898
|
+
while (!(targetNode = this.selectMigrationTarget(excludedNodes, perTargetConcurrency))) {
|
|
1899
|
+
if (!this.hasAnyHealthyMigrationNode(excludedNodes)) {
|
|
1900
|
+
this._emitDebugEvent("PlayerChangeNodeFailNoEligibleNode" /* PlayerChangeNodeFailNoEligibleNode */, {
|
|
1901
|
+
state: "warn",
|
|
1902
|
+
message: `No eligible ready node was available for player ${player.guildId}`,
|
|
1903
|
+
functionLayer: "Node > movePlayersToReplacementNode()"
|
|
1904
|
+
});
|
|
1905
|
+
if (onNoEligibleNode === "destroy") {
|
|
1906
|
+
await player.destroy("PlayerChangeNodeFailNoEligibleNode" /* PlayerChangeNodeFailNoEligibleNode */).catch((destroyError) => {
|
|
1907
|
+
this._emitDebugEvent("PlayerDestroyFail" /* PlayerDestroyFail */, {
|
|
1908
|
+
state: "error",
|
|
1909
|
+
message: `Failed to destroy player ${player.guildId}: ${this.getMigrationErrorMessage(destroyError)}`,
|
|
1910
|
+
error: destroyError,
|
|
1911
|
+
functionLayer: "Node > movePlayersToReplacementNode()"
|
|
1912
|
+
});
|
|
1913
|
+
});
|
|
1914
|
+
} else {
|
|
1915
|
+
player.playing = false;
|
|
1916
|
+
}
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
await this.waitForMigrationCapacity(excludedNodes, perTargetConcurrency, backpressureDelayMs);
|
|
1920
|
+
}
|
|
1921
|
+
const targetNodeId = targetNode.id;
|
|
1922
|
+
if (player.getData("internal_destroystatus") === true) return;
|
|
1923
|
+
if (player.node?.options?.id !== this.id) return;
|
|
1924
|
+
this.reserveMigrationTarget(targetNodeId);
|
|
1925
|
+
try {
|
|
1926
|
+
await player.changeNode(targetNodeId);
|
|
1927
|
+
return;
|
|
1928
|
+
} catch (error) {
|
|
1929
|
+
const errorMessage = this.getMigrationErrorMessage(error);
|
|
1930
|
+
this._emitDebugEvent("PlayerChangeNodeFail" /* PlayerChangeNodeFail */, {
|
|
1931
|
+
state: "error",
|
|
1932
|
+
message: `Failed to move player ${player.guildId}: ${errorMessage}`,
|
|
1933
|
+
error,
|
|
1934
|
+
functionLayer: "Node > movePlayersToReplacementNode()"
|
|
1935
|
+
});
|
|
1936
|
+
const targetStillHealthy = this.isMigrationNodeHealthy(targetNodeId);
|
|
1937
|
+
excludedNodes.add(targetNodeId);
|
|
1938
|
+
if (attempt === 0 && !targetStillHealthy) {
|
|
1939
|
+
this._emitDebugEvent("PlayerChangeNodeFail" /* PlayerChangeNodeFail */, {
|
|
1940
|
+
state: "warn",
|
|
1941
|
+
message: `Migration target ${targetNodeId} became unavailable while moving player ${player.guildId}; retrying on another ready node`,
|
|
1942
|
+
error,
|
|
1943
|
+
functionLayer: "Node > movePlayersToReplacementNode()"
|
|
1944
|
+
});
|
|
1945
|
+
if (player.node?.options?.id === targetNodeId) player.node = this;
|
|
1946
|
+
continue;
|
|
1947
|
+
}
|
|
1948
|
+
if (attempt === 0 && this.hasAnyHealthyMigrationNode(excludedNodes)) {
|
|
1949
|
+
this._emitDebugEvent("PlayerChangeNodeFail" /* PlayerChangeNodeFail */, {
|
|
1950
|
+
state: "warn",
|
|
1951
|
+
message: `Retrying migration for player ${player.guildId} on a different ready node`,
|
|
1952
|
+
error,
|
|
1953
|
+
functionLayer: "Node > movePlayersToReplacementNode()"
|
|
1954
|
+
});
|
|
1955
|
+
if (player.node?.options?.id === targetNodeId) player.node = this;
|
|
1956
|
+
continue;
|
|
1957
|
+
}
|
|
1958
|
+
if (onNoEligibleNode === "destroy") {
|
|
1959
|
+
await player.destroy("PlayerChangeNodeFail" /* PlayerChangeNodeFail */).catch((destroyError) => {
|
|
1960
|
+
this._emitDebugEvent("PlayerDestroyFail" /* PlayerDestroyFail */, {
|
|
1961
|
+
state: "error",
|
|
1962
|
+
message: `Failed to destroy player ${player.guildId} after unexpected migration failure: ${this.getMigrationErrorMessage(destroyError)}`,
|
|
1963
|
+
error: destroyError,
|
|
1964
|
+
functionLayer: "Node > movePlayersToReplacementNode()"
|
|
1965
|
+
});
|
|
1966
|
+
});
|
|
1967
|
+
} else {
|
|
1968
|
+
player.playing = false;
|
|
1969
|
+
}
|
|
1970
|
+
return;
|
|
1971
|
+
} finally {
|
|
1972
|
+
this.releaseMigrationTarget(targetNodeId);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
} catch (error) {
|
|
1976
|
+
this._emitDebugEvent("PlayerChangeNodeFail" /* PlayerChangeNodeFail */, {
|
|
1977
|
+
state: "error",
|
|
1978
|
+
message: `Unexpected error while migrating player ${player.guildId}: ${this.getMigrationErrorMessage(error)}`,
|
|
1979
|
+
error,
|
|
1980
|
+
functionLayer: "Node > movePlayersToReplacementNode()"
|
|
1981
|
+
});
|
|
1982
|
+
if (onNoEligibleNode === "destroy") {
|
|
1983
|
+
await player.destroy("PlayerChangeNodeFail" /* PlayerChangeNodeFail */).catch((destroyError) => {
|
|
1984
|
+
this._emitDebugEvent("PlayerDestroyFail" /* PlayerDestroyFail */, {
|
|
1985
|
+
state: "error",
|
|
1986
|
+
message: `Failed to destroy player ${player.guildId} after unexpected migration failure: ${this.getMigrationErrorMessage(destroyError)}`,
|
|
1987
|
+
error: destroyError,
|
|
1988
|
+
functionLayer: "Node > movePlayersToReplacementNode()"
|
|
1989
|
+
});
|
|
1990
|
+
});
|
|
1991
|
+
} else {
|
|
1992
|
+
player.playing = false;
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
};
|
|
1996
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
1997
|
+
while (true) {
|
|
1998
|
+
const queueIndex = nextQueueIndex++;
|
|
1999
|
+
if (queueIndex >= migrationQueue.length) return;
|
|
2000
|
+
const player = migrationQueue[queueIndex];
|
|
2001
|
+
if (!player) continue;
|
|
2002
|
+
try {
|
|
2003
|
+
if (player.getData("internal_nodeMigrating") !== true) {
|
|
2004
|
+
player.setData("internal_nodeMigrating", true);
|
|
2005
|
+
}
|
|
2006
|
+
if (player.getData("internal_destroystatus") === true) continue;
|
|
2007
|
+
if (player.node?.options?.id !== this.id) continue;
|
|
2008
|
+
await migratePlayer(player);
|
|
2009
|
+
} catch (error) {
|
|
2010
|
+
this._emitDebugEvent("PlayerChangeNodeFail" /* PlayerChangeNodeFail */, {
|
|
2011
|
+
state: "error",
|
|
2012
|
+
message: `Unexpected worker error while migrating player ${player.guildId}: ${this.getMigrationErrorMessage(error)}`,
|
|
2013
|
+
error,
|
|
2014
|
+
functionLayer: "Node > movePlayersToReplacementNode()"
|
|
2015
|
+
});
|
|
2016
|
+
} finally {
|
|
2017
|
+
player.setData("internal_nodeMigrating", void 0);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
});
|
|
2021
|
+
await Promise.all(workers);
|
|
2022
|
+
}
|
|
1808
2023
|
/**
|
|
1809
2024
|
* Gets all Players of a Node
|
|
1810
2025
|
* @returns array of players inside of lavalink
|
|
@@ -2517,9 +2732,10 @@ var LavalinkNode = class _LavalinkNode {
|
|
|
2517
2732
|
}
|
|
2518
2733
|
this.info.isNodelink = !!this.info.isNodelink;
|
|
2519
2734
|
this.NodeManager.emit("connect", this);
|
|
2735
|
+
this.NodeManager.signalPlayerMigrationCapacityChange();
|
|
2520
2736
|
}
|
|
2521
2737
|
/** @private util function for handling closing events from websocket */
|
|
2522
|
-
close(code, reason) {
|
|
2738
|
+
async close(code, reason) {
|
|
2523
2739
|
this.resetAckTimeouts(true, true);
|
|
2524
2740
|
try {
|
|
2525
2741
|
if (this.socket) {
|
|
@@ -2538,19 +2754,22 @@ var LavalinkNode = class _LavalinkNode {
|
|
|
2538
2754
|
this.isAlive = false;
|
|
2539
2755
|
if (code === 1006 && !reason) reason = "Socket got terminated due to no ping connection";
|
|
2540
2756
|
if (code === 1e3 && reason === "Node-Disconnect") return;
|
|
2757
|
+
const players = Array.from(
|
|
2758
|
+
this._LManager.players.filter((p) => p?.node?.options?.id === this?.options?.id).values()
|
|
2759
|
+
);
|
|
2541
2760
|
this.NodeManager.emit("disconnect", this, { code, reason });
|
|
2542
2761
|
if (code !== 1e3 || reason !== "Node-Destroy") {
|
|
2543
2762
|
if (this.NodeManager.nodes.has(this.id)) {
|
|
2544
2763
|
this.reconnect();
|
|
2545
2764
|
}
|
|
2546
2765
|
}
|
|
2547
|
-
this._LManager.
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
}
|
|
2766
|
+
if (this._LManager.options.autoMove && players.length) {
|
|
2767
|
+
await this.movePlayersToReplacementNode(players, "freeze");
|
|
2768
|
+
} else if (!this._LManager.options.autoMove) {
|
|
2769
|
+
players.forEach((p) => p.playing = false);
|
|
2770
|
+
} else if (this.NodeManager.nodes.filter((n) => n.connected && !!n.sessionId).size === 0) {
|
|
2771
|
+
players.forEach((p) => p.playing = false);
|
|
2772
|
+
}
|
|
2554
2773
|
}
|
|
2555
2774
|
/** @private util function for handling error events from websocket */
|
|
2556
2775
|
error(error) {
|
|
@@ -3628,6 +3847,16 @@ var NodeManager = class extends EventEmitter {
|
|
|
3628
3847
|
* A map of all nodes in the nodeManager
|
|
3629
3848
|
*/
|
|
3630
3849
|
nodes = new MiniMap();
|
|
3850
|
+
/** @internal Coordinates player migrations between nodes owned by this manager. */
|
|
3851
|
+
playerMigration = {
|
|
3852
|
+
reservations: /* @__PURE__ */ new Map(),
|
|
3853
|
+
capacityWaiters: /* @__PURE__ */ new Set()
|
|
3854
|
+
};
|
|
3855
|
+
/** @internal Wakes migrations waiting for capacity on one of this manager's nodes. */
|
|
3856
|
+
signalPlayerMigrationCapacityChange() {
|
|
3857
|
+
for (const resolve of this.playerMigration.capacityWaiters) resolve();
|
|
3858
|
+
this.playerMigration.capacityWaiters.clear();
|
|
3859
|
+
}
|
|
3631
3860
|
/**
|
|
3632
3861
|
* @param LavalinkManager The LavalinkManager that created this NodeManager
|
|
3633
3862
|
*/
|
|
@@ -6279,8 +6508,8 @@ var Player = class {
|
|
|
6279
6508
|
try {
|
|
6280
6509
|
if (!node)
|
|
6281
6510
|
node = Array.from(this.LavalinkManager.nodeManager.leastUsedNodes("playingPlayers")).find(
|
|
6282
|
-
(n) => n.connected && n.options.id !== this.node.options.id
|
|
6283
|
-
)
|
|
6511
|
+
(n) => n.connected && !!n.sessionId && n.options.id !== this.node.options.id
|
|
6512
|
+
)?.id;
|
|
6284
6513
|
if (!node || !this.LavalinkManager.nodeManager.nodes.get(node))
|
|
6285
6514
|
throw new RangeError("No nodes are available.");
|
|
6286
6515
|
if (this.node.options.id === node) return this;
|
|
@@ -6435,6 +6664,11 @@ var LavalinkManager = class _LavalinkManager extends EventEmitter2 {
|
|
|
6435
6664
|
advancedOptions: {
|
|
6436
6665
|
enableDebugEvents: options?.advancedOptions?.enableDebugEvents ?? false,
|
|
6437
6666
|
maxFilterFixDuration: options?.advancedOptions?.maxFilterFixDuration ?? 6e5,
|
|
6667
|
+
playerMigration: {
|
|
6668
|
+
concurrency: options?.advancedOptions?.playerMigration?.concurrency ?? 5,
|
|
6669
|
+
perTargetConcurrency: options?.advancedOptions?.playerMigration?.perTargetConcurrency ?? 2,
|
|
6670
|
+
backpressureDelayMs: options?.advancedOptions?.playerMigration?.backpressureDelayMs ?? 50
|
|
6671
|
+
},
|
|
6438
6672
|
debugOptions: {
|
|
6439
6673
|
logCustomSearches: options?.advancedOptions?.debugOptions?.logCustomSearches ?? false,
|
|
6440
6674
|
noAudio: options?.advancedOptions?.debugOptions?.noAudio ?? false,
|
package/package.json
CHANGED