feat: Add instance ID and implement Redis lock for global command registration to support multi-instance deployments.

This commit is contained in:
이정수 2026-03-27 18:29:46 +09:00
parent 031a8b3146
commit 5e41bea74e
3 changed files with 22 additions and 5 deletions

View File

@ -1,6 +1,11 @@
import { config } from 'dotenv'; import { config } from 'dotenv';
import { hostname } from 'os';
config(); config();
const generateInstanceId = () => {
return process.env.INSTANCE_ID || hostname() || `kord-${Math.random().toString(36).substring(2, 7)}`;
};
export const env = { export const env = {
NODE_ENV: process.env.NODE_ENV || 'development', NODE_ENV: process.env.NODE_ENV || 'development',
DISCORD_TOKEN: process.env.DISCORD_TOKEN || '', DISCORD_TOKEN: process.env.DISCORD_TOKEN || '',
@ -10,4 +15,5 @@ export const env = {
REDIS_PORT: parseInt(process.env.REDIS_PORT || '6379', 10), REDIS_PORT: parseInt(process.env.REDIS_PORT || '6379', 10),
VOICE_WAITING_ROOM_ID: process.env.VOICE_WAITING_ROOM_ID || '', VOICE_WAITING_ROOM_ID: process.env.VOICE_WAITING_ROOM_ID || '',
VOICE_CATEGORY_ID: process.env.VOICE_CATEGORY_ID || '', VOICE_CATEGORY_ID: process.env.VOICE_CATEGORY_ID || '',
INSTANCE_ID: generateInstanceId(),
}; };

View File

@ -5,6 +5,8 @@ import { InviteService } from '../services/InviteService';
import { VoiceService } from '../services/VoiceService'; import { VoiceService } from '../services/VoiceService';
import { PresenceService } from '../services/PresenceService'; import { PresenceService } from '../services/PresenceService';
import { auditLogService } from '../services/AuditLogService'; import { auditLogService } from '../services/AuditLogService';
import { redis } from '../cache';
import { env } from '../config/env';
export default { export default {
name: Events.ClientReady, name: Events.ClientReady,
@ -16,9 +18,17 @@ export default {
PresenceService.startActivePresence(client); PresenceService.startActivePresence(client);
try { try {
const commandsData = Array.from(client.commands.values()).map(c => c.data.toJSON()); const lockKey = 'commands:sync:lock';
await client.application?.commands.set(commandsData); // EX 300 = 5 minutes lock. Only one instance needs to do this per boot cycle.
logger.info(`Successfully registered ${commandsData.length} global application commands.`); const acquired = await redis.set(lockKey, '1', 'EX', 300, 'NX');
if (acquired) {
const commandsData = Array.from(client.commands.values()).map(c => c.data.toJSON());
await client.application?.commands.set(commandsData);
logger.info(`Successfully registered ${commandsData.length} global application commands.`);
} else {
logger.info('Global commands registration skipped (already handled by another instance).');
}
} catch (e) { } catch (e) {
logger.error('Failed to register global commands', e); logger.error('Failed to register global commands', e);
} }
@ -28,7 +38,7 @@ export default {
category: 'BOOT', category: 'BOOT',
severity: 'INFO', severity: 'INFO',
title: 'Bot Online', title: 'Bot Online',
description: `Kord has successfully started or reconnected.` description: `Kord instance **[${env.INSTANCE_ID}]** has successfully started or reconnected.`
}).catch(() => {}); }).catch(() => {});
}); });
}, },

View File

@ -1,5 +1,6 @@
import { Guild, EmbedBuilder, TextChannel, Colors } from 'discord.js'; import { Guild, EmbedBuilder, TextChannel, Colors } from 'discord.js';
import { prisma } from '../database'; import { prisma } from '../database';
import { env } from '../config/env';
export type AuditSeverity = 'INFO' | 'WARN' | 'ERROR'; export type AuditSeverity = 'INFO' | 'WARN' | 'ERROR';
export type AuditCategory = 'SYSTEM' | 'BOOT' | 'VOICE' | 'PERMISSION' | 'INVITE' | 'MIMIC'; export type AuditCategory = 'SYSTEM' | 'BOOT' | 'VOICE' | 'PERMISSION' | 'INVITE' | 'MIMIC';
@ -47,7 +48,7 @@ export class AuditLogService {
.setDescription(`**${payload.title}**\n\n${payload.description}`) .setDescription(`**${payload.title}**\n\n${payload.description}`)
.setColor(color) .setColor(color)
.setTimestamp() .setTimestamp()
.setFooter({ text: `${icon} ${payload.severity} · Kord System` }); .setFooter({ text: `${icon} ${payload.severity} · Kord System [${env.INSTANCE_ID}]` });
if (payload.fields && payload.fields.length > 0) { if (payload.fields && payload.fields.length > 0) {
embed.addFields(payload.fields); embed.addFields(payload.fields);