From 5ae4caad20ddb61e0bde9ae644b0f88c754b3116 Mon Sep 17 00:00:00 2001 From: Flegma Date: Wed, 8 Apr 2026 13:50:21 +0200 Subject: [PATCH 1/2] fix: add lifecycle cleanup hooks and error handling for async operations Add OnApplicationShutdown to RedisManagerService to clear health check intervals and disconnect Redis on shutdown. Cap recursive setTimeout retries in assignServer to 10. Replace void fire-and-forget patterns with .catch() error handlers in AppModule, SocketsService, and MatchesModule. --- src/app.module.ts | 4 +- .../match-assistant.service.ts | 7 + src/matches/matches.module.ts | 124 +++++++++++------- .../redis-manager/redis-manager.service.ts | 13 +- src/sockets/sockets.service.ts | 16 ++- 5 files changed, 108 insertions(+), 56 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index 45f43a4a..d3d10462 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -142,7 +142,9 @@ export class AppModule implements OnApplicationBootstrap { public async onApplicationBootstrap() { try { - void this.discordBot.setup(); + this.discordBot.setup().catch((error) => { + this.logger.error("Discord bot setup failed", error); + }); await this.typesense.setup(); await this.system.detectFeatures(); } catch (error) { diff --git a/src/matches/match-assistant/match-assistant.service.ts b/src/matches/match-assistant/match-assistant.service.ts index 83df1baa..0043b3e4 100644 --- a/src/matches/match-assistant/match-assistant.service.ts +++ b/src/matches/match-assistant/match-assistant.service.ts @@ -255,6 +255,13 @@ export class MatchAssistantService { error, ); if (error instanceof FailedToCreateOnDemandServer) { + if (tries >= 10) { + this.logger.error( + `[${matchId}] max retries reached for server assignment`, + ); + await this.updateMatchStatus(matchId, "WaitingForServer"); + return; + } setTimeout(async () => { this.logger.log(`[${matchId}] try retry assign server....`); await this.assignServer(matchId, ++tries); diff --git a/src/matches/matches.module.ts b/src/matches/matches.module.ts index 3a6f31fa..fae03af5 100644 --- a/src/matches/matches.module.ts +++ b/src/matches/matches.module.ts @@ -125,67 +125,93 @@ export class MatchesModule implements NestModule { return; } - void scheduleMatchQueue.add( - CheckForScheduledMatches.name, - {}, - { - repeat: { - pattern: "* * * * *", + scheduleMatchQueue + .add( + CheckForScheduledMatches.name, + {}, + { + repeat: { + pattern: "* * * * *", + }, }, - }, - ); + ) + .catch((err) => { + this.logger.error("Failed to add CheckForScheduledMatches job", err); + }); - void scheduleMatchQueue.add( - CancelExpiredMatches.name, - {}, - { - repeat: { - pattern: "* * * * *", + scheduleMatchQueue + .add( + CancelExpiredMatches.name, + {}, + { + repeat: { + pattern: "* * * * *", + }, }, - }, - ); + ) + .catch((err) => { + this.logger.error("Failed to add CancelExpiredMatches job", err); + }); - void scheduleMatchQueue.add( - RemoveCancelledMatches.name, - {}, - { - repeat: { - pattern: "* * * * *", + scheduleMatchQueue + .add( + RemoveCancelledMatches.name, + {}, + { + repeat: { + pattern: "* * * * *", + }, }, - }, - ); + ) + .catch((err) => { + this.logger.error("Failed to add RemoveCancelledMatches job", err); + }); - void matchServersQueue.add( - CheckForTournamentStart.name, - {}, - { - repeat: { - pattern: "* * * * *", + matchServersQueue + .add( + CheckForTournamentStart.name, + {}, + { + repeat: { + pattern: "* * * * *", + }, }, - }, - ); + ) + .catch((err) => { + this.logger.error("Failed to add CheckForTournamentStart job", err); + }); - void matchServersQueue.add( - CleanAbandonedMatches.name, - {}, - { - repeat: { - pattern: "0 0 * * *", + matchServersQueue + .add( + CleanAbandonedMatches.name, + {}, + { + repeat: { + pattern: "0 0 * * *", + }, }, - }, - ); + ) + .catch((err) => { + this.logger.error("Failed to add CleanAbandonedMatches job", err); + }); - void matchServersQueue.add( - CancelInvalidTournaments.name, - {}, - { - repeat: { - pattern: "* * * * *", + matchServersQueue + .add( + CancelInvalidTournaments.name, + {}, + { + repeat: { + pattern: "* * * * *", + }, }, - }, - ); + ) + .catch((err) => { + this.logger.error("Failed to add CancelInvalidTournaments job", err); + }); - void this.generatePlayerRatings(); + this.generatePlayerRatings().catch((err) => { + this.logger.error("Failed to generate player ratings", err); + }); } /** diff --git a/src/redis/redis-manager/redis-manager.service.ts b/src/redis/redis-manager/redis-manager.service.ts index fd08b542..16b0d882 100644 --- a/src/redis/redis-manager/redis-manager.service.ts +++ b/src/redis/redis-manager/redis-manager.service.ts @@ -1,10 +1,10 @@ -import { Injectable, Logger } from "@nestjs/common"; +import { Injectable, Logger, OnApplicationShutdown } from "@nestjs/common"; import IORedis, { Redis, RedisOptions } from "ioredis"; import { ConfigService } from "@nestjs/config"; import { RedisConfig } from "../../configs/types/RedisConfig"; @Injectable() -export class RedisManagerService { +export class RedisManagerService implements OnApplicationShutdown { private config: RedisConfig; protected connections: { @@ -22,6 +22,15 @@ export class RedisManagerService { this.config = this.configService.get("redis"); } + onApplicationShutdown() { + for (const [, interval] of Object.entries(this.healthCheckIntervals)) { + clearInterval(interval); + } + for (const [, conn] of Object.entries(this.connections)) { + conn.disconnect(); + } + } + public getConnection(connection = "default"): Redis { if (!this.connections[connection]) { const currentConnection: Redis = (this.connections[connection] = diff --git a/src/sockets/sockets.service.ts b/src/sockets/sockets.service.ts index 47298248..503797ab 100644 --- a/src/sockets/sockets.service.ts +++ b/src/sockets/sockets.service.ts @@ -33,8 +33,12 @@ export class SocketsService { const sub = this.redisManager.getConnection("sub"); - void sub.subscribe("broadcast-message"); - void sub.subscribe("send-message-to-steam-id"); + sub.subscribe("broadcast-message").catch((err) => { + this.logger.error("Failed to subscribe to broadcast-message", err); + }); + sub.subscribe("send-message-to-steam-id").catch((err) => { + this.logger.error("Failed to subscribe to send-message-to-steam-id", err); + }); sub.on("message", (channel, message) => { const { steamId, event, data } = JSON.parse(message) as { steamId: string; @@ -44,10 +48,14 @@ export class SocketsService { switch (channel) { case "broadcast-message": - void this.broadcastMessage(event, data); + this.broadcastMessage(event, data).catch((err) => { + this.logger.error("broadcast-message error", err); + }); break; case "send-message-to-steam-id": - void this.sendMessageToSteamId(steamId, event, data); + this.sendMessageToSteamId(steamId, event, data).catch((err) => { + this.logger.error("send-message-to-steam-id error", err); + }); break; } }); From ed17a62e87cbbc8dfcc25c55d203ce5fdca1a89e Mon Sep 17 00:00:00 2001 From: Flegma Date: Fri, 10 Apr 2026 12:58:20 +0200 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20revert=20.catch()=20swallowing=20?= =?UTF-8?q?=E2=80=94=20let=20queue=20failures=20crash=20process?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.module.ts | 4 +- src/matches/matches.module.ts | 124 +++++++++++++-------------------- src/sockets/sockets.service.ts | 16 ++--- 3 files changed, 54 insertions(+), 90 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index d3d10462..45f43a4a 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -142,9 +142,7 @@ export class AppModule implements OnApplicationBootstrap { public async onApplicationBootstrap() { try { - this.discordBot.setup().catch((error) => { - this.logger.error("Discord bot setup failed", error); - }); + void this.discordBot.setup(); await this.typesense.setup(); await this.system.detectFeatures(); } catch (error) { diff --git a/src/matches/matches.module.ts b/src/matches/matches.module.ts index fae03af5..3a6f31fa 100644 --- a/src/matches/matches.module.ts +++ b/src/matches/matches.module.ts @@ -125,93 +125,67 @@ export class MatchesModule implements NestModule { return; } - scheduleMatchQueue - .add( - CheckForScheduledMatches.name, - {}, - { - repeat: { - pattern: "* * * * *", - }, + void scheduleMatchQueue.add( + CheckForScheduledMatches.name, + {}, + { + repeat: { + pattern: "* * * * *", }, - ) - .catch((err) => { - this.logger.error("Failed to add CheckForScheduledMatches job", err); - }); + }, + ); - scheduleMatchQueue - .add( - CancelExpiredMatches.name, - {}, - { - repeat: { - pattern: "* * * * *", - }, + void scheduleMatchQueue.add( + CancelExpiredMatches.name, + {}, + { + repeat: { + pattern: "* * * * *", }, - ) - .catch((err) => { - this.logger.error("Failed to add CancelExpiredMatches job", err); - }); + }, + ); - scheduleMatchQueue - .add( - RemoveCancelledMatches.name, - {}, - { - repeat: { - pattern: "* * * * *", - }, + void scheduleMatchQueue.add( + RemoveCancelledMatches.name, + {}, + { + repeat: { + pattern: "* * * * *", }, - ) - .catch((err) => { - this.logger.error("Failed to add RemoveCancelledMatches job", err); - }); + }, + ); - matchServersQueue - .add( - CheckForTournamentStart.name, - {}, - { - repeat: { - pattern: "* * * * *", - }, + void matchServersQueue.add( + CheckForTournamentStart.name, + {}, + { + repeat: { + pattern: "* * * * *", }, - ) - .catch((err) => { - this.logger.error("Failed to add CheckForTournamentStart job", err); - }); + }, + ); - matchServersQueue - .add( - CleanAbandonedMatches.name, - {}, - { - repeat: { - pattern: "0 0 * * *", - }, + void matchServersQueue.add( + CleanAbandonedMatches.name, + {}, + { + repeat: { + pattern: "0 0 * * *", }, - ) - .catch((err) => { - this.logger.error("Failed to add CleanAbandonedMatches job", err); - }); + }, + ); - matchServersQueue - .add( - CancelInvalidTournaments.name, - {}, - { - repeat: { - pattern: "* * * * *", - }, + void matchServersQueue.add( + CancelInvalidTournaments.name, + {}, + { + repeat: { + pattern: "* * * * *", }, - ) - .catch((err) => { - this.logger.error("Failed to add CancelInvalidTournaments job", err); - }); + }, + ); - this.generatePlayerRatings().catch((err) => { - this.logger.error("Failed to generate player ratings", err); - }); + void this.generatePlayerRatings(); } /** diff --git a/src/sockets/sockets.service.ts b/src/sockets/sockets.service.ts index 503797ab..47298248 100644 --- a/src/sockets/sockets.service.ts +++ b/src/sockets/sockets.service.ts @@ -33,12 +33,8 @@ export class SocketsService { const sub = this.redisManager.getConnection("sub"); - sub.subscribe("broadcast-message").catch((err) => { - this.logger.error("Failed to subscribe to broadcast-message", err); - }); - sub.subscribe("send-message-to-steam-id").catch((err) => { - this.logger.error("Failed to subscribe to send-message-to-steam-id", err); - }); + void sub.subscribe("broadcast-message"); + void sub.subscribe("send-message-to-steam-id"); sub.on("message", (channel, message) => { const { steamId, event, data } = JSON.parse(message) as { steamId: string; @@ -48,14 +44,10 @@ export class SocketsService { switch (channel) { case "broadcast-message": - this.broadcastMessage(event, data).catch((err) => { - this.logger.error("broadcast-message error", err); - }); + void this.broadcastMessage(event, data); break; case "send-message-to-steam-id": - this.sendMessageToSteamId(steamId, event, data).catch((err) => { - this.logger.error("send-message-to-steam-id error", err); - }); + void this.sendMessageToSteamId(steamId, event, data); break; } });