From 5e41bea74e9936783a39e87c0b2d5f64c99d2dbb Mon Sep 17 00:00:00 2001 From: artbiit Date: Fri, 27 Mar 2026 18:29:46 +0900 Subject: [PATCH] feat: Add instance ID and implement Redis lock for global command registration to support multi-instance deployments. --- src/config/env.ts | 6 ++++++ src/events/ready.ts | 18 ++++++++++++++---- src/services/AuditLogService.ts | 3 ++- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/config/env.ts b/src/config/env.ts index 52eb097..dbb2300 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -1,6 +1,11 @@ import { config } from 'dotenv'; +import { hostname } from 'os'; config(); +const generateInstanceId = () => { + return process.env.INSTANCE_ID || hostname() || `kord-${Math.random().toString(36).substring(2, 7)}`; +}; + export const env = { NODE_ENV: process.env.NODE_ENV || 'development', DISCORD_TOKEN: process.env.DISCORD_TOKEN || '', @@ -10,4 +15,5 @@ export const env = { REDIS_PORT: parseInt(process.env.REDIS_PORT || '6379', 10), VOICE_WAITING_ROOM_ID: process.env.VOICE_WAITING_ROOM_ID || '', VOICE_CATEGORY_ID: process.env.VOICE_CATEGORY_ID || '', + INSTANCE_ID: generateInstanceId(), }; diff --git a/src/events/ready.ts b/src/events/ready.ts index 5342873..de47035 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -5,6 +5,8 @@ import { InviteService } from '../services/InviteService'; import { VoiceService } from '../services/VoiceService'; import { PresenceService } from '../services/PresenceService'; import { auditLogService } from '../services/AuditLogService'; +import { redis } from '../cache'; +import { env } from '../config/env'; export default { name: Events.ClientReady, @@ -16,9 +18,17 @@ export default { PresenceService.startActivePresence(client); try { - 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.`); + const lockKey = 'commands:sync:lock'; + // EX 300 = 5 minutes lock. Only one instance needs to do this per boot cycle. + 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) { logger.error('Failed to register global commands', e); } @@ -28,7 +38,7 @@ export default { category: 'BOOT', severity: 'INFO', title: 'Bot Online', - description: `Kord has successfully started or reconnected.` + description: `Kord instance **[${env.INSTANCE_ID}]** has successfully started or reconnected.` }).catch(() => {}); }); }, diff --git a/src/services/AuditLogService.ts b/src/services/AuditLogService.ts index 79c258e..9352bdd 100644 --- a/src/services/AuditLogService.ts +++ b/src/services/AuditLogService.ts @@ -1,5 +1,6 @@ import { Guild, EmbedBuilder, TextChannel, Colors } from 'discord.js'; import { prisma } from '../database'; +import { env } from '../config/env'; export type AuditSeverity = 'INFO' | 'WARN' | 'ERROR'; export type AuditCategory = 'SYSTEM' | 'BOOT' | 'VOICE' | 'PERMISSION' | 'INVITE' | 'MIMIC'; @@ -47,7 +48,7 @@ export class AuditLogService { .setDescription(`**${payload.title}**\n\n${payload.description}`) .setColor(color) .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) { embed.addFields(payload.fields);