Configuration

Basic Failover Setup

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

const failoverOptions = {
  enabled: true,
  maxRetries: 3,
  retryDelay: 1000,
  preservePosition: true,
  resumePlayback: true,
  cooldownTime: 5000,
  maxFailoverAttempts: 5
};

Option Descriptions

  • enabled: Whether failover is active
  • maxRetries: Maximum retry attempts per failure
  • retryDelay: Delay between retries (milliseconds)
  • preservePosition: Keep track position during failover
  • resumePlayback: Automatically resume after failover
  • cooldownTime: Wait time before allowing new failover attempts
  • maxFailoverAttempts: Total failover attempts before giving up

Multiple Nodes Setup

Adding Backup Nodes

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

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

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

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

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

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

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

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

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

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

// 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

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

setInterval(async () => {
  const healthReport = await checkAllNodesHealth();
  const metricsReport = metrics.getReport();
  
  console.log('Health Report:', healthReport);
  console.log('Metrics Report:', metricsReport);
}, 30000);