From 8545b4afa76f5cd7583f5d8cb0ccb6a379b736af Mon Sep 17 00:00:00 2001 From: Flegma Date: Wed, 8 Apr 2026 14:02:18 +0200 Subject: [PATCH 1/2] fix: track and clean up spawned processes, timers, and connections - NetworkService: track IP check interval and spawned bash processes, add OnApplicationShutdown to clear interval and kill processes, kill monitor on error/stderr, remove process.on(SIGTERM) handler - Throttle: store setInterval ID, add destroy() method to clear it - RconService: only store connection after successful auth, add timeout to rcon.send() calls, clear connection timer after successful connect - RedisManagerService: track health check intervals, add OnApplicationShutdown to clear intervals and disconnect --- src/rcon/rcon.service.ts | 24 +++++++++---- .../redis-manager/redis-manager.service.ts | 26 ++++++++++++-- src/system/network.service.ts | 34 +++++++++++++++---- src/utilities/throttle.ts | 7 +++- 4 files changed, 74 insertions(+), 17 deletions(-) diff --git a/src/rcon/rcon.service.ts b/src/rcon/rcon.service.ts index d9fba3e..b4130c0 100644 --- a/src/rcon/rcon.service.ts +++ b/src/rcon/rcon.service.ts @@ -34,12 +34,20 @@ export class RconService { password: matchData.id, }); + const SEND_TIMEOUT = 5000; rcon.send = async (command) => { - const payload = ( - await rcon.sendRaw(Buffer.from(command, "utf-8")) - ).toString(); + const sendPromise = rcon + .sendRaw(Buffer.from(command, "utf-8")) + .then((buf) => buf.toString()); - return payload; + const timeoutPromise = new Promise((_, reject) => { + setTimeout( + () => reject(new Error(`RCON send timeout after ${SEND_TIMEOUT}ms`)), + SEND_TIMEOUT, + ); + }); + + return Promise.race([sendPromise, timeoutPromise]); }; rcon @@ -54,8 +62,9 @@ export class RconService { }); try { + let connectTimer: ReturnType; const timeoutPromise = new Promise((_, reject) => { - setTimeout(() => { + connectTimer = setTimeout(() => { reject( new Error( `RCON connection timeout after ${this.CONNECTION_TIMEOUT}ms`, @@ -65,6 +74,7 @@ export class RconService { }); await Promise.race([rcon.connect(), timeoutPromise]); + clearTimeout(connectTimer!); } catch (error) { this.logger.warn("RCON connect error:", error); try { @@ -74,11 +84,13 @@ export class RconService { } catch (cleanupError) { this.logger.warn("Error during RCON cleanup:", cleanupError); } + return null; } + this.connections[matchId] = rcon; this.setupConnectionTimeout(matchId); - return (this.connections[matchId] = rcon); + return rcon; } private setupConnectionTimeout(matchId: string) { diff --git a/src/redis/redis-manager/redis-manager.service.ts b/src/redis/redis-manager/redis-manager.service.ts index b3587f8..3bdc860 100644 --- a/src/redis/redis-manager/redis-manager.service.ts +++ b/src/redis/redis-manager/redis-manager.service.ts @@ -1,16 +1,18 @@ -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: { [key: string]: Redis; } = {}; + private healthCheckIntervals: ReturnType[] = []; + constructor( private readonly logger: Logger, private readonly configService: ConfigService, @@ -18,6 +20,22 @@ export class RedisManagerService { this.config = this.configService.get("redis")!; } + public async onApplicationShutdown() { + for (const interval of this.healthCheckIntervals) { + clearInterval(interval); + } + this.healthCheckIntervals = []; + + for (const [name, connection] of Object.entries(this.connections)) { + try { + connection.disconnect(); + } catch (error) { + this.logger.warn(`Error disconnecting Redis "${name}":`, error); + } + } + this.connections = {}; + } + public getConnection(connection = "default"): Redis { if (!this.connections[connection]) { const currentConnection: Redis = (this.connections[connection] = @@ -45,7 +63,7 @@ export class RedisManagerService { const pingTimeoutError = `did not receive ping in time (5 seconds)`; - setInterval(async () => { + const healthCheckInterval = setInterval(async () => { if (currentConnection.status === "ready") { await new Promise(async (resolve, reject) => { const timer = setTimeout(() => { @@ -65,6 +83,8 @@ export class RedisManagerService { }); } }, 5000); + + this.healthCheckIntervals.push(healthCheckInterval); }); } return this.connections[connection]; diff --git a/src/system/network.service.ts b/src/system/network.service.ts index 3631d01..3c0c212 100644 --- a/src/system/network.service.ts +++ b/src/system/network.service.ts @@ -1,12 +1,22 @@ import os from "os"; -import { spawn } from "child_process"; -import { Injectable, Logger, OnApplicationBootstrap } from "@nestjs/common"; +import { ChildProcess, spawn } from "child_process"; +import { + Injectable, + Logger, + OnApplicationBootstrap, + OnApplicationShutdown, +} from "@nestjs/common"; @Injectable() -export class NetworkService implements OnApplicationBootstrap { +export class NetworkService + implements OnApplicationBootstrap, OnApplicationShutdown +{ public publicIP: string; public networkLimit?: number; + private ipCheckInterval: ReturnType; + private spawnedProcesses = new Set(); + constructor(private readonly logger: Logger) {} public async getNetworkLimit() { @@ -24,7 +34,7 @@ export class NetworkService implements OnApplicationBootstrap { public async onApplicationBootstrap() { await this.getPublicIP(); - setInterval( + this.ipCheckInterval = setInterval( async () => { await this.getPublicIP(); }, @@ -32,6 +42,15 @@ export class NetworkService implements OnApplicationBootstrap { ); } + public async onApplicationShutdown() { + clearInterval(this.ipCheckInterval); + + for (const proc of this.spawnedProcesses) { + proc.kill(); + } + this.spawnedProcesses.clear(); + } + public getLanIP() { return this.getLanInterface().ipv4?.address; } @@ -305,16 +324,16 @@ export class NetworkService implements OnApplicationBootstrap { done`, ]); - process.on(process.env.DEV ? "SIGUSR2" : "SIGTERM", () => { - monitor.kill(); - }); + this.spawnedProcesses.add(monitor); monitor.stdin.on("error", async (error) => { this.logger.error("Error running processs", error); + monitor.kill(); reject(error); }); monitor.stderr.on("data", (error) => { + monitor.kill(); reject(error.toString()); }); @@ -327,6 +346,7 @@ export class NetworkService implements OnApplicationBootstrap { }); monitor.on("close", () => { + this.spawnedProcesses.delete(monitor); resolve( returnData || "No data written to memory due to onData() handler", ); diff --git a/src/utilities/throttle.ts b/src/utilities/throttle.ts index 3d1fc87..9e5b9a1 100644 --- a/src/utilities/throttle.ts +++ b/src/utilities/throttle.ts @@ -48,13 +48,14 @@ class Throttle { private totalBytes = 0; private isProcessing = false; private bytesPerSecond: number; + private intervalId: ReturnType; private queue: Array<{ chunk: Buffer; send: () => void; }> = []; constructor() { - setInterval(() => { + this.intervalId = setInterval(() => { this.totalBytes = 0; if (!this.isProcessing) { this.process(); @@ -62,6 +63,10 @@ class Throttle { }, 1000); } + public destroy() { + clearInterval(this.intervalId); + } + public setBytesPerSecond(bytesPerSecond: number) { if (this.bytesPerSecond !== bytesPerSecond) { this.bytesPerSecond = bytesPerSecond; From 8788f24401cdfe9853439a93916b7b89932b3b33 Mon Sep 17 00:00:00 2001 From: Flegma Date: Fri, 10 Apr 2026 12:24:01 +0200 Subject: [PATCH 2/2] fix: remove duplicated RCON changes (addressed in separate PR) --- src/rcon/rcon.service.ts | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/src/rcon/rcon.service.ts b/src/rcon/rcon.service.ts index b4130c0..d9fba3e 100644 --- a/src/rcon/rcon.service.ts +++ b/src/rcon/rcon.service.ts @@ -34,20 +34,12 @@ export class RconService { password: matchData.id, }); - const SEND_TIMEOUT = 5000; rcon.send = async (command) => { - const sendPromise = rcon - .sendRaw(Buffer.from(command, "utf-8")) - .then((buf) => buf.toString()); + const payload = ( + await rcon.sendRaw(Buffer.from(command, "utf-8")) + ).toString(); - const timeoutPromise = new Promise((_, reject) => { - setTimeout( - () => reject(new Error(`RCON send timeout after ${SEND_TIMEOUT}ms`)), - SEND_TIMEOUT, - ); - }); - - return Promise.race([sendPromise, timeoutPromise]); + return payload; }; rcon @@ -62,9 +54,8 @@ export class RconService { }); try { - let connectTimer: ReturnType; const timeoutPromise = new Promise((_, reject) => { - connectTimer = setTimeout(() => { + setTimeout(() => { reject( new Error( `RCON connection timeout after ${this.CONNECTION_TIMEOUT}ms`, @@ -74,7 +65,6 @@ export class RconService { }); await Promise.race([rcon.connect(), timeoutPromise]); - clearTimeout(connectTimer!); } catch (error) { this.logger.warn("RCON connect error:", error); try { @@ -84,13 +74,11 @@ export class RconService { } catch (cleanupError) { this.logger.warn("Error during RCON cleanup:", cleanupError); } - return null; } - this.connections[matchId] = rcon; this.setupConnectionTimeout(matchId); - return rcon; + return (this.connections[matchId] = rcon); } private setupConnectionTimeout(matchId: string) {