fix(api): various subtle memory leaks
(and some not so subtle ones, *cough* ReportService *cough*)
This commit is contained in:
parent
848269a4d4
commit
7b1aa6ff2e
@ -25,7 +25,7 @@ import {KVAccountDeletionQueueService} from '@fluxer/api/src/infrastructure/KVAc
|
|||||||
import {initializeMetricsService} from '@fluxer/api/src/infrastructure/MetricsService';
|
import {initializeMetricsService} from '@fluxer/api/src/infrastructure/MetricsService';
|
||||||
import {InstanceConfigRepository} from '@fluxer/api/src/instance/InstanceConfigRepository';
|
import {InstanceConfigRepository} from '@fluxer/api/src/instance/InstanceConfigRepository';
|
||||||
import {ipBanCache} from '@fluxer/api/src/middleware/IpBanMiddleware';
|
import {ipBanCache} from '@fluxer/api/src/middleware/IpBanMiddleware';
|
||||||
import {initializeServiceSingletons} from '@fluxer/api/src/middleware/ServiceMiddleware';
|
import {initializeServiceSingletons, shutdownReportService} from '@fluxer/api/src/middleware/ServiceMiddleware';
|
||||||
import {
|
import {
|
||||||
ensureVoiceResourcesInitialized,
|
ensureVoiceResourcesInitialized,
|
||||||
getKVClient,
|
getKVClient,
|
||||||
@ -207,6 +207,13 @@ export function createShutdown(logger: ILogger): () => Promise<void> {
|
|||||||
logger.error({error}, 'Error shutting down IP ban cache');
|
logger.error({error}, 'Error shutting down IP ban cache');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
shutdownReportService();
|
||||||
|
logger.info('Report service shut down');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error({error}, 'Error shutting down report service');
|
||||||
|
}
|
||||||
|
|
||||||
logger.info('API service shutdown complete');
|
logger.info('API service shutdown complete');
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,8 +42,17 @@ export class ClamAV {
|
|||||||
const socket = createConnection(this.port, this.host);
|
const socket = createConnection(this.port, this.host);
|
||||||
let response = '';
|
let response = '';
|
||||||
let isResolved = false;
|
let isResolved = false;
|
||||||
|
const MAX_RESPONSE_SIZE = 10 * 1024 * 1024;
|
||||||
|
const CONNECT_TIMEOUT_MS = 5000;
|
||||||
|
|
||||||
|
const connectTimeout = setTimeout(() => {
|
||||||
|
if (!isResolved) {
|
||||||
|
doReject(new Error('ClamAV connection timeout (5s)'));
|
||||||
|
}
|
||||||
|
}, CONNECT_TIMEOUT_MS);
|
||||||
|
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
|
clearTimeout(connectTimeout);
|
||||||
if (!socket.destroyed) {
|
if (!socket.destroyed) {
|
||||||
socket.destroy();
|
socket.destroy();
|
||||||
}
|
}
|
||||||
@ -64,6 +73,7 @@ export class ClamAV {
|
|||||||
};
|
};
|
||||||
|
|
||||||
socket.on('connect', () => {
|
socket.on('connect', () => {
|
||||||
|
clearTimeout(connectTimeout);
|
||||||
try {
|
try {
|
||||||
socket.write('zINSTREAM\0');
|
socket.write('zINSTREAM\0');
|
||||||
|
|
||||||
@ -92,6 +102,9 @@ export class ClamAV {
|
|||||||
|
|
||||||
socket.on('data', (data) => {
|
socket.on('data', (data) => {
|
||||||
response += data.toString();
|
response += data.toString();
|
||||||
|
if (response.length > MAX_RESPONSE_SIZE) {
|
||||||
|
doReject(new Error(`ClamAV response exceeded ${(MAX_RESPONSE_SIZE / 1024 / 1024).toFixed(0)} MB limit`));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('end', () => {
|
socket.on('end', () => {
|
||||||
|
|||||||
@ -246,6 +246,7 @@ export class GatewayService {
|
|||||||
private circuitBreakerOpenUntilMs = 0;
|
private circuitBreakerOpenUntilMs = 0;
|
||||||
private readonly CIRCUIT_BREAKER_FAILURE_THRESHOLD = 5;
|
private readonly CIRCUIT_BREAKER_FAILURE_THRESHOLD = 5;
|
||||||
private readonly CIRCUIT_BREAKER_COOLDOWN_MS = ms('10 seconds');
|
private readonly CIRCUIT_BREAKER_COOLDOWN_MS = ms('10 seconds');
|
||||||
|
private readonly PENDING_REQUEST_TIMEOUT_MS = ms('30 seconds');
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.rpcClient = GatewayRpcClient.getInstance();
|
this.rpcClient = GatewayRpcClient.getInstance();
|
||||||
@ -260,9 +261,29 @@ export class GatewayService {
|
|||||||
this.circuitBreakerOpenUntilMs = 0;
|
this.circuitBreakerOpenUntilMs = 0;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
this.rejectAllPendingRequests(new ServiceUnavailableError('Gateway circuit breaker open'));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private rejectAllPendingRequests(error: Error): void {
|
||||||
|
this.pendingGuildDataRequests.forEach((requests) => {
|
||||||
|
requests.forEach((req) => req.reject(error));
|
||||||
|
});
|
||||||
|
this.pendingGuildDataRequests.clear();
|
||||||
|
|
||||||
|
this.pendingGuildMemberRequests.forEach((requests) => {
|
||||||
|
requests.forEach((req) => req.reject(error));
|
||||||
|
});
|
||||||
|
this.pendingGuildMemberRequests.clear();
|
||||||
|
|
||||||
|
this.pendingPermissionRequests.forEach((requests) => {
|
||||||
|
requests.forEach((req) => req.reject(error));
|
||||||
|
});
|
||||||
|
this.pendingPermissionRequests.clear();
|
||||||
|
|
||||||
|
this.pendingBatchRequestCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
private recordCircuitBreakerSuccess(): void {
|
private recordCircuitBreakerSuccess(): void {
|
||||||
this.circuitBreakerConsecutiveFailures = 0;
|
this.circuitBreakerConsecutiveFailures = 0;
|
||||||
}
|
}
|
||||||
@ -626,8 +647,25 @@ export class GatewayService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let timeoutId: NodeJS.Timeout | null = setTimeout(() => {
|
||||||
|
reject(new GatewayTimeoutError());
|
||||||
|
this.removePendingRequest(this.pendingGuildDataRequests, key, wrappedResolve, wrappedReject);
|
||||||
|
}, this.PENDING_REQUEST_TIMEOUT_MS);
|
||||||
|
|
||||||
|
const wrappedResolve = (value: GuildResponse) => {
|
||||||
|
if (timeoutId) clearTimeout(timeoutId);
|
||||||
|
timeoutId = null;
|
||||||
|
resolve(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrappedReject = (error: Error) => {
|
||||||
|
if (timeoutId) clearTimeout(timeoutId);
|
||||||
|
timeoutId = null;
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
|
||||||
const pending = this.pendingGuildDataRequests.get(key) || [];
|
const pending = this.pendingGuildDataRequests.get(key) || [];
|
||||||
pending.push({resolve, reject});
|
pending.push({resolve: wrappedResolve, reject: wrappedReject});
|
||||||
this.pendingGuildDataRequests.set(key, pending);
|
this.pendingGuildDataRequests.set(key, pending);
|
||||||
this.pendingBatchRequestCount += 1;
|
this.pendingBatchRequestCount += 1;
|
||||||
|
|
||||||
@ -651,8 +689,25 @@ export class GatewayService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let timeoutId: NodeJS.Timeout | null = setTimeout(() => {
|
||||||
|
reject(new GatewayTimeoutError());
|
||||||
|
this.removePendingRequest(this.pendingGuildMemberRequests, key, wrappedResolve, wrappedReject);
|
||||||
|
}, this.PENDING_REQUEST_TIMEOUT_MS);
|
||||||
|
|
||||||
|
const wrappedResolve = (value: {success: boolean; memberData?: GuildMemberResponse}) => {
|
||||||
|
if (timeoutId) clearTimeout(timeoutId);
|
||||||
|
timeoutId = null;
|
||||||
|
resolve(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrappedReject = (error: Error) => {
|
||||||
|
if (timeoutId) clearTimeout(timeoutId);
|
||||||
|
timeoutId = null;
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
|
||||||
const pending = this.pendingGuildMemberRequests.get(key) || [];
|
const pending = this.pendingGuildMemberRequests.get(key) || [];
|
||||||
pending.push({resolve, reject});
|
pending.push({resolve: wrappedResolve, reject: wrappedReject});
|
||||||
this.pendingGuildMemberRequests.set(key, pending);
|
this.pendingGuildMemberRequests.set(key, pending);
|
||||||
this.pendingBatchRequestCount += 1;
|
this.pendingBatchRequestCount += 1;
|
||||||
|
|
||||||
@ -804,8 +859,25 @@ export class GatewayService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let timeoutId: NodeJS.Timeout | null = setTimeout(() => {
|
||||||
|
reject(new GatewayTimeoutError());
|
||||||
|
this.removePendingRequest(this.pendingPermissionRequests, key, wrappedResolve, wrappedReject);
|
||||||
|
}, this.PENDING_REQUEST_TIMEOUT_MS);
|
||||||
|
|
||||||
|
const wrappedResolve = (value: boolean) => {
|
||||||
|
if (timeoutId) clearTimeout(timeoutId);
|
||||||
|
timeoutId = null;
|
||||||
|
resolve(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrappedReject = (error: Error) => {
|
||||||
|
if (timeoutId) clearTimeout(timeoutId);
|
||||||
|
timeoutId = null;
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
|
||||||
const pending = this.pendingPermissionRequests.get(key) || [];
|
const pending = this.pendingPermissionRequests.get(key) || [];
|
||||||
pending.push({resolve, reject});
|
pending.push({resolve: wrappedResolve, reject: wrappedReject});
|
||||||
this.pendingPermissionRequests.set(key, pending);
|
this.pendingPermissionRequests.set(key, pending);
|
||||||
this.pendingBatchRequestCount += 1;
|
this.pendingBatchRequestCount += 1;
|
||||||
|
|
||||||
@ -817,6 +889,25 @@ export class GatewayService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private removePendingRequest<T>(
|
||||||
|
map: Map<string, Array<PendingRequest<T>>>,
|
||||||
|
key: string,
|
||||||
|
resolve: (value: T) => void,
|
||||||
|
reject: (error: Error) => void,
|
||||||
|
): void {
|
||||||
|
const pending = map.get(key);
|
||||||
|
if (pending) {
|
||||||
|
const index = pending.findIndex((r) => r.resolve === resolve || r.reject === reject);
|
||||||
|
if (index >= 0) {
|
||||||
|
pending.splice(index, 1);
|
||||||
|
this.pendingBatchRequestCount--;
|
||||||
|
if (pending.length === 0) {
|
||||||
|
map.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async canManageRoles({guildId, userId, targetUserId, roleId}: CanManageRolesParams): Promise<boolean> {
|
async canManageRoles({guildId, userId, targetUserId, roleId}: CanManageRolesParams): Promise<boolean> {
|
||||||
const result = await this.call<{can_manage: boolean}>('guild.can_manage_roles', {
|
const result = await this.call<{can_manage: boolean}>('guild.can_manage_roles', {
|
||||||
guild_id: guildId.toString(),
|
guild_id: guildId.toString(),
|
||||||
|
|||||||
@ -30,6 +30,7 @@ export class SnowflakeReservationService {
|
|||||||
private initialized = false;
|
private initialized = false;
|
||||||
private reloadPromise: Promise<void> | null = null;
|
private reloadPromise: Promise<void> | null = null;
|
||||||
private kvSubscription: IKVSubscription | null = null;
|
private kvSubscription: IKVSubscription | null = null;
|
||||||
|
private messageHandler: ((channel: string) => void) | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private repository: SnowflakeReservationRepository,
|
private repository: SnowflakeReservationRepository,
|
||||||
@ -50,13 +51,14 @@ export class SnowflakeReservationService {
|
|||||||
this.kvSubscription = subscription;
|
this.kvSubscription = subscription;
|
||||||
await subscription.connect();
|
await subscription.connect();
|
||||||
await subscription.subscribe(SNOWFLAKE_RESERVATION_REFRESH_CHANNEL);
|
await subscription.subscribe(SNOWFLAKE_RESERVATION_REFRESH_CHANNEL);
|
||||||
subscription.on('message', (channel) => {
|
this.messageHandler = (channel: string) => {
|
||||||
if (channel === SNOWFLAKE_RESERVATION_REFRESH_CHANNEL) {
|
if (channel === SNOWFLAKE_RESERVATION_REFRESH_CHANNEL) {
|
||||||
this.reload().catch((error) => {
|
this.reload().catch((error) => {
|
||||||
Logger.error({error}, 'Failed to reload snowflake reservations');
|
Logger.error({error}, 'Failed to reload snowflake reservations');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
subscription.on('message', this.messageHandler);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error({error}, 'Failed to subscribe to snowflake reservation refresh channel');
|
Logger.error({error}, 'Failed to subscribe to snowflake reservation refresh channel');
|
||||||
}
|
}
|
||||||
@ -99,9 +101,13 @@ export class SnowflakeReservationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
shutdown(): void {
|
shutdown(): void {
|
||||||
|
if (this.kvSubscription && this.messageHandler) {
|
||||||
|
this.kvSubscription.removeAllListeners('message');
|
||||||
|
}
|
||||||
if (this.kvSubscription) {
|
if (this.kvSubscription) {
|
||||||
this.kvSubscription.disconnect();
|
this.kvSubscription.disconnect();
|
||||||
this.kvSubscription = null;
|
this.kvSubscription = null;
|
||||||
}
|
}
|
||||||
|
this.messageHandler = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,6 +45,7 @@ export class LimitConfigService {
|
|||||||
private kvSubscription: IKVSubscription | null = null;
|
private kvSubscription: IKVSubscription | null = null;
|
||||||
private subscriberInitialized = false;
|
private subscriberInitialized = false;
|
||||||
private readonly cacheKey: string;
|
private readonly cacheKey: string;
|
||||||
|
private messageHandler: ((channel: string) => void) | null = null;
|
||||||
|
|
||||||
constructor(repository: InstanceConfigRepository, cacheService: ICacheService, kvClient: IKVProvider | null = null) {
|
constructor(repository: InstanceConfigRepository, cacheService: ICacheService, kvClient: IKVProvider | null = null) {
|
||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
@ -144,17 +145,21 @@ export class LimitConfigService {
|
|||||||
const subscription = this.kvClient.duplicate();
|
const subscription = this.kvClient.duplicate();
|
||||||
this.kvSubscription = subscription;
|
this.kvSubscription = subscription;
|
||||||
|
|
||||||
|
this.messageHandler = (channel: string) => {
|
||||||
|
if (channel === LIMIT_CONFIG_REFRESH_CHANNEL) {
|
||||||
|
this.refreshCache().catch((err) => {
|
||||||
|
Logger.error({err}, 'Failed to refresh limit config from pubsub');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
subscription
|
subscription
|
||||||
.connect()
|
.connect()
|
||||||
.then(() => subscription.subscribe(LIMIT_CONFIG_REFRESH_CHANNEL))
|
.then(() => subscription.subscribe(LIMIT_CONFIG_REFRESH_CHANNEL))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
subscription.on('message', (channel) => {
|
if (this.messageHandler) {
|
||||||
if (channel === LIMIT_CONFIG_REFRESH_CHANNEL) {
|
subscription.on('message', this.messageHandler);
|
||||||
this.refreshCache().catch((err) => {
|
}
|
||||||
Logger.error({err}, 'Failed to refresh limit config from pubsub');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
Logger.error({error}, 'Failed to subscribe to limit config refresh channel');
|
Logger.error({error}, 'Failed to subscribe to limit config refresh channel');
|
||||||
@ -164,12 +169,16 @@ export class LimitConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
shutdown(): void {
|
shutdown(): void {
|
||||||
|
if (this.kvSubscription && this.messageHandler) {
|
||||||
|
this.kvSubscription.removeAllListeners('message');
|
||||||
|
}
|
||||||
if (this.kvSubscription) {
|
if (this.kvSubscription) {
|
||||||
this.kvSubscription.quit().catch((err) => {
|
this.kvSubscription.quit().catch((err) => {
|
||||||
Logger.error({err}, 'Failed to close KV subscription');
|
Logger.error({err}, 'Failed to close KV subscription');
|
||||||
});
|
});
|
||||||
this.kvSubscription = null;
|
this.kvSubscription = null;
|
||||||
}
|
}
|
||||||
|
this.messageHandler = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -52,6 +52,7 @@ class IpBanCache {
|
|||||||
private kvClient: IKVProvider | null = null;
|
private kvClient: IKVProvider | null = null;
|
||||||
private kvSubscription: IKVSubscription | null = null;
|
private kvSubscription: IKVSubscription | null = null;
|
||||||
private subscriberInitialized = false;
|
private subscriberInitialized = false;
|
||||||
|
private messageHandler: ((channel: string) => void) | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.singleIpBans = this.createFamilyMaps();
|
this.singleIpBans = this.createFamilyMaps();
|
||||||
@ -78,23 +79,27 @@ class IpBanCache {
|
|||||||
const subscription = this.kvClient.duplicate();
|
const subscription = this.kvClient.duplicate();
|
||||||
this.kvSubscription = subscription;
|
this.kvSubscription = subscription;
|
||||||
|
|
||||||
|
this.messageHandler = (channel: string) => {
|
||||||
|
if (channel === IP_BAN_REFRESH_CHANNEL) {
|
||||||
|
this.refresh().catch((err) => {
|
||||||
|
this.consecutiveFailures++;
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
if (this.consecutiveFailures >= this.maxConsecutiveFailures) {
|
||||||
|
Logger.error({error: message}, 'Failed to refresh IP ban cache after notification');
|
||||||
|
} else {
|
||||||
|
Logger.warn({error: message}, 'Failed to refresh IP ban cache after notification');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
subscription
|
subscription
|
||||||
.connect()
|
.connect()
|
||||||
.then(() => subscription.subscribe(IP_BAN_REFRESH_CHANNEL))
|
.then(() => subscription.subscribe(IP_BAN_REFRESH_CHANNEL))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
subscription.on('message', (channel) => {
|
if (this.messageHandler) {
|
||||||
if (channel === IP_BAN_REFRESH_CHANNEL) {
|
subscription.on('message', this.messageHandler);
|
||||||
this.refresh().catch((err) => {
|
}
|
||||||
this.consecutiveFailures++;
|
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
|
||||||
if (this.consecutiveFailures >= this.maxConsecutiveFailures) {
|
|
||||||
Logger.error({error: message}, 'Failed to refresh IP ban cache after notification');
|
|
||||||
} else {
|
|
||||||
Logger.warn({error: message}, 'Failed to refresh IP ban cache after notification');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
Logger.error({error}, 'Failed to subscribe to IP ban refresh channel');
|
Logger.error({error}, 'Failed to subscribe to IP ban refresh channel');
|
||||||
@ -203,10 +208,14 @@ class IpBanCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
shutdown(): void {
|
shutdown(): void {
|
||||||
|
if (this.kvSubscription && this.messageHandler) {
|
||||||
|
this.kvSubscription.removeAllListeners('message');
|
||||||
|
}
|
||||||
if (this.kvSubscription) {
|
if (this.kvSubscription) {
|
||||||
this.kvSubscription.disconnect();
|
this.kvSubscription.disconnect();
|
||||||
this.kvSubscription = null;
|
this.kvSubscription = null;
|
||||||
}
|
}
|
||||||
|
this.messageHandler = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -173,6 +173,15 @@ import {createMiddleware} from 'hono/factory';
|
|||||||
|
|
||||||
const errorI18nService = new ErrorI18nService();
|
const errorI18nService = new ErrorI18nService();
|
||||||
|
|
||||||
|
let _reportService: ReportService | null = null;
|
||||||
|
|
||||||
|
export function shutdownReportService(): void {
|
||||||
|
if (_reportService) {
|
||||||
|
_reportService.shutdown();
|
||||||
|
_reportService = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let _testEmailService: TestEmailService | null = null;
|
let _testEmailService: TestEmailService | null = null;
|
||||||
function getTestEmailService(): TestEmailService {
|
function getTestEmailService(): TestEmailService {
|
||||||
if (!_testEmailService) {
|
if (!_testEmailService) {
|
||||||
@ -617,19 +626,21 @@ export const ServiceMiddleware = createMiddleware<HonoEnv>(async (ctx, next) =>
|
|||||||
const desktopHandoffService = new DesktopHandoffService(cacheService);
|
const desktopHandoffService = new DesktopHandoffService(cacheService);
|
||||||
const authRequestService = new AuthRequestService(authService, ssoService, cacheService, desktopHandoffService);
|
const authRequestService = new AuthRequestService(authService, ssoService, cacheService, desktopHandoffService);
|
||||||
|
|
||||||
const reportSearchService = getReportSearchService();
|
if (!_reportService) {
|
||||||
const reportService = new ReportService(
|
_reportService = new ReportService(
|
||||||
reportRepository,
|
reportRepository,
|
||||||
channelRepository,
|
channelRepository,
|
||||||
guildRepository,
|
guildRepository,
|
||||||
userRepository,
|
userRepository,
|
||||||
inviteRepository,
|
inviteRepository,
|
||||||
emailService,
|
emailService,
|
||||||
emailDnsValidationService,
|
emailDnsValidationService,
|
||||||
snowflakeService,
|
snowflakeService,
|
||||||
storageService,
|
storageService,
|
||||||
reportSearchService,
|
getReportSearchService(),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
const reportService = _reportService;
|
||||||
const reportRequestService = new ReportRequestService(reportService);
|
const reportRequestService = new ReportRequestService(reportService);
|
||||||
|
|
||||||
const adminService = new AdminService(
|
const adminService = new AdminService(
|
||||||
|
|||||||
@ -34,6 +34,7 @@ export class VoiceTopology {
|
|||||||
private subscribers: Set<Subscriber> = new Set();
|
private subscribers: Set<Subscriber> = new Set();
|
||||||
private serverRotationIndex: Map<string, number> = new Map();
|
private serverRotationIndex: Map<string, number> = new Map();
|
||||||
private kvSubscription: IKVSubscription | null = null;
|
private kvSubscription: IKVSubscription | null = null;
|
||||||
|
private messageHandler: ((channel: string) => void) | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private voiceRepository: IVoiceRepository,
|
private voiceRepository: IVoiceRepository,
|
||||||
@ -53,13 +54,14 @@ export class VoiceTopology {
|
|||||||
this.kvSubscription = subscription;
|
this.kvSubscription = subscription;
|
||||||
await subscription.connect();
|
await subscription.connect();
|
||||||
await subscription.subscribe(VOICE_CONFIGURATION_CHANNEL);
|
await subscription.subscribe(VOICE_CONFIGURATION_CHANNEL);
|
||||||
subscription.on('message', (channel) => {
|
this.messageHandler = (channel: string) => {
|
||||||
if (channel === VOICE_CONFIGURATION_CHANNEL) {
|
if (channel === VOICE_CONFIGURATION_CHANNEL) {
|
||||||
this.reload().catch((error) => {
|
this.reload().catch((error) => {
|
||||||
Logger.error({error}, 'Failed to reload voice topology from KV notification');
|
Logger.error({error}, 'Failed to reload voice topology from KV notification');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
subscription.on('message', this.messageHandler);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error({error}, 'Failed to subscribe to voice configuration channel');
|
Logger.error({error}, 'Failed to subscribe to voice configuration channel');
|
||||||
}
|
}
|
||||||
@ -239,9 +241,13 @@ export class VoiceTopology {
|
|||||||
}
|
}
|
||||||
|
|
||||||
shutdown(): void {
|
shutdown(): void {
|
||||||
|
if (this.kvSubscription && this.messageHandler) {
|
||||||
|
this.kvSubscription.removeAllListeners('message');
|
||||||
|
}
|
||||||
if (this.kvSubscription) {
|
if (this.kvSubscription) {
|
||||||
this.kvSubscription.disconnect();
|
this.kvSubscription.disconnect();
|
||||||
this.kvSubscription = null;
|
this.kvSubscription = null;
|
||||||
}
|
}
|
||||||
|
this.messageHandler = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user