Configuration
Basic Failover Setup
Copy
const { Aqua } = require('aqualink');
const aqua = new Aqua(client, {
nodes: [
{
host: 'localhost',
port: 2333,
auth: 'youshallnotpass',
ssl: false,
name: 'main-node'
},
{
host: 'backup.server.com',
port: 2333,
auth: 'backup_password',
ssl: false,
name: 'backup-node'
}
],
defaultSearchPlatform: 'ytsearch',
nodeResolver: 'LeastLoad'
shouldDeleteMessage: false,
leaveOnEnd: true,
restVersion: 'v4',
plugins: [],
autoResume: false,
infiniteReconnects: false,
failoverOptions: {
enabled: true,
maxRetries: 3,
retryDelay: 1000,
preservePosition: true,
resumePlayback: true,
cooldownTime: 5000,
maxFailoverAttempts: 5
}
});
Failover Configuration Options
Available Options
Copy
const failoverOptions = {
enabled: true,
maxRetries: 3,
retryDelay: 1000,
preservePosition: true,
resumePlayback: true,
cooldownTime: 5000,
maxFailoverAttempts: 5
};
Option Descriptions
enabled
: Whether failover is activemaxRetries
: Maximum retry attempts per failureretryDelay
: Delay between retries (milliseconds)preservePosition
: Keep track position during failoverresumePlayback
: Automatically resume after failovercooldownTime
: Wait time before allowing new failover attemptsmaxFailoverAttempts
: Total failover attempts before giving up
Multiple Nodes Setup
Adding Backup Nodes
Copy
const aqua = new Aqua(client, {
nodes: [
{
host: 'primary.server.com',
port: 2333,
auth: 'primary_pass',
ssl: false,
name: 'primary-node'
},
{
host: 'secondary.server.com',
port: 2333,
auth: 'secondary_pass',
ssl: false,
name: 'secondary-node'
},
{
host: 'tertiary.server.com',
port: 2333,
auth: 'tertiary_pass',
ssl: false,
name: 'tertiary-node'
}
],
failoverOptions: {
enabled: true,
maxRetries: 5,
retryDelay: 2000,
preservePosition: true,
resumePlayback: true,
cooldownTime: 10000,
maxFailoverAttempts: 10
}
});
Event Handling
Failover Events
Copy
const { AqualinkEvents } = require('aqualink');
aqua.on(AqualinkEvents.NodeFailover, (node) => {
console.log(`Node ${node.name} is failing over...`);
});
aqua.on(AqualinkEvents.NodeFailoverComplete, (node, successful, failed) => {
console.log(`Node ${node.name} failover complete. Migrated ${successful} players, ${failed} failed.`);
});
aqua.on(AqualinkEvents.PlayerMigrated, (oldPlayer, newPlayer, newNode) => {
console.log(`Player for guild ${oldPlayer.guildId} moved from ${oldPlayer.nodes.name} to ${newNode.name}`);
});
aqua.on(AqualinkEvents.NodeDisconnect, (node, { code, reason }) => {
console.log(`Node ${node.name} disconnected: ${reason} (code: ${code})`);
});
aqua.on(AqualinkEvents.NodeError, (node, error) => {
console.error(`Node ${node.name} error:`, error);
});
Node Management
Check Node Status
Copy
function getNodeStatus() {
const nodes = aqua.nodeMap;
const status = {};
for (const [name, node] of nodes) {
status[name] = {
connected: node.connected,
players: node.players.size,
stats: node.stats
};
}
return status;
}
console.log('Node Status:', getNodeStatus());
Get Available Nodes
Copy
function getAvailableNodes() {
return Array.from(aqua.nodeMap.values())
.filter(node => node.connected)
.map(node => ({
name: node.name,
players: node.players.size,
connected: node.connected
}));
}
Player State Preservation
State Backup During Failover
Copy
class PlayerStateManager {
constructor() {
this.states = new Map();
}
backup(player) {
const state = {
guildId: player.guildId,
voiceChannel: player.voiceChannel,
textChannel: player.textChannel,
queue: player.queue.slice(),
currentTrack: player.current,
position: player.position,
volume: player.volume,
paused: player.paused,
timestamp: Date.now()
};
this.states.set(player.guildId, state);
return state;
}
restore(player, state) {
if (!state) return false;
try {
if (state.queue && state.queue.length > 0) {
player.queue.push(...state.queue);
}
if (state.currentTrack) {
player.queue.unshift(state.currentTrack);
player.seek(state.position);
}
if (state.volume !== player.volume) {
player.setVolume(state.volume);
}
if (state.paused) {
player.pause(true);
}
return true;
} catch (error) {
console.error('Failed to restore player state:', error);
return false;
}
}
}
const stateManager = new PlayerStateManager();
Manual Node Operations
Force Node Switch
Copy
async function movePlayerToNode(guildId, targetNodeName) {
const player = aqua.players.get(guildId);
if (!player) {
console.log('Player not found');
return false;
}
const targetNode = aqua.nodeMap.get(targetNodeName);
if (!targetNode || !targetNode.connected) {
console.log('Target node not available');
return false;
}
const state = stateManager.backup(player);
try {
await player.destroy();
const newPlayer = aqua.createConnection({
guildId: guildId,
voiceChannel: state.voiceChannel,
textChannel: state.textChannel
});
stateManager.restore(newPlayer, state);
console.log(`Player moved to ${targetNodeName}`);
return true;
} catch (error) {
console.error('Manual node switch failed:', error);
return false;
}
}
Node Health Check
Copy
async function checkNodeHealth(nodeName) {
const node = aqua.nodeMap.get(nodeName);
if (!node) {
return { healthy: false, error: 'Node not found' };
}
return {
healthy: node.connected,
players: node.players.size,
uptime: node.stats.uptime,
stats: node.stats
};
}
async function checkAllNodesHealth() {
const results = {};
for (const [name, node] of aqua.nodeMap) {
results[name] = await checkNodeHealth(name);
}
return results;
}
Monitoring
Failover Metrics
Copy
class FailoverMetrics {
constructor() {
this.metrics = {
totalFailovers: 0,
successfulFailovers: 0,
failedFailovers: 0,
nodeFailures: new Map(),
lastFailover: null
};
this.setupEventListeners();
}
setupEventListeners() {
aqua.on('nodeDisconnect', (node) => {
this.recordNodeFailure(node.name);
});
aqua.on('playerMigrated', (player, newPlayer, newNode) => {
this.recordFailover(true, player.nodes.name, newNode.name);
});
}
recordNodeFailure(nodeName) {
const current = this.metrics.nodeFailures.get(nodeName) || 0;
this.metrics.nodeFailures.set(nodeName, current + 1);
}
recordFailover(success, fromNode, toNode) {
this.metrics.totalFailovers++;
if (success) {
this.metrics.successfulFailovers++;
} else {
this.metrics.failedFailovers++;
}
this.metrics.lastFailover = {
timestamp: Date.now(),
success,
fromNode,
toNode
};
}
getReport() {
const successRate = this.metrics.totalFailovers > 0
? (this.metrics.successfulFailovers / this.metrics.totalFailovers) * 100
: 0;
return {
...this.metrics,
successRate: successRate.toFixed(2) + '%'
};
}
}
const metrics = new FailoverMetrics();
Testing
Simulate Node Failure
Copy
async function simulateNodeFailure(nodeName) {
const node = aqua.nodeMap.get(nodeName);
if (!node) {
console.log('Node not found');
return;
}
console.log(`Simulating failure for node: ${nodeName}`);
const affectedPlayers = Array.from(aqua.players.values())
.filter(player => player.nodes.name === nodeName);
console.log(`${affectedPlayers.length} players will be affected`);
if (node.ws) {
node.ws.close(1006, 'Simulating node failure');
}
setTimeout(() => {
console.log('Attempting to reconnect node...');
node.connect();
}, 10000);
}
Failover Test
Copy
async function testFailover() {
console.log('Starting failover test...');
const initialNodes = getAvailableNodes();
console.log('Initial nodes:', initialNodes);
if (initialNodes.length < 2) {
console.log('Need at least 2 nodes for failover test');
return;
}
const primaryNode = initialNodes[0].name;
await simulateNodeFailure(primaryNode);
setTimeout(() => {
const remainingNodes = getAvailableNodes();
console.log('Nodes after failure:', remainingNodes);
const activePlayerCount = aqua.players.size;
console.log(`Active players after failover: ${activePlayerCount}`);
}, 5000);
}
Error Handling
Failover Error Recovery
Copy
// Make sure to import AqualinkEvents from your aqualink package
// const { AqualinkEvents } = require('aqualink');
aqua.on(AqualinkEvents.NodeError, async (node, error) => {
console.error(`Node ${node.name} encountered error:`, error.message);
if (error.code === 'ECONNREFUSED') {
console.log('Connection refused, marking node as unavailable');
}
const availableNodes = getAvailableNodes();
if (availableNodes.length === 0) {
console.error('No available nodes for failover!');
await notifyAdmins('Critical: All music nodes are down');
}
});
async function notifyAdmins(message) {
console.error('ADMIN NOTIFICATION:', message);
}
Best Practices
Optimal Configuration
Copy
const aqua = new Aqua(client, {
nodes: [
{ host: 'primary.domain.com', port: 2333, auth: 'pass1', name: 'primary', ssl: false },
{ host: 'backup.domain.com', port: 2333, auth: 'pass2', name: 'backup', ssl: false }
],
failoverOptions: {
enabled: true,
maxRetries: 3,
retryDelay: 1500,
preservePosition: true,
resumePlayback: true,
cooldownTime: 10000,
maxFailoverAttempts: 5
}
});
Monitoring Setup
Copy
setInterval(async () => {
const healthReport = await checkAllNodesHealth();
const metricsReport = metrics.getReport();
console.log('Health Report:', healthReport);
console.log('Metrics Report:', metricsReport);
}, 30000);