Skip to content

Commit 8545b4a

Browse files
committed
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
1 parent 7546065 commit 8545b4a

4 files changed

Lines changed: 74 additions & 17 deletions

File tree

src/rcon/rcon.service.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,20 @@ export class RconService {
3434
password: matchData.id,
3535
});
3636

37+
const SEND_TIMEOUT = 5000;
3738
rcon.send = async (command) => {
38-
const payload = (
39-
await rcon.sendRaw(Buffer.from(command, "utf-8"))
40-
).toString();
39+
const sendPromise = rcon
40+
.sendRaw(Buffer.from(command, "utf-8"))
41+
.then((buf) => buf.toString());
4142

42-
return payload;
43+
const timeoutPromise = new Promise<never>((_, reject) => {
44+
setTimeout(
45+
() => reject(new Error(`RCON send timeout after ${SEND_TIMEOUT}ms`)),
46+
SEND_TIMEOUT,
47+
);
48+
});
49+
50+
return Promise.race([sendPromise, timeoutPromise]);
4351
};
4452

4553
rcon
@@ -54,8 +62,9 @@ export class RconService {
5462
});
5563

5664
try {
65+
let connectTimer: ReturnType<typeof setTimeout>;
5766
const timeoutPromise = new Promise<never>((_, reject) => {
58-
setTimeout(() => {
67+
connectTimer = setTimeout(() => {
5968
reject(
6069
new Error(
6170
`RCON connection timeout after ${this.CONNECTION_TIMEOUT}ms`,
@@ -65,6 +74,7 @@ export class RconService {
6574
});
6675

6776
await Promise.race([rcon.connect(), timeoutPromise]);
77+
clearTimeout(connectTimer!);
6878
} catch (error) {
6979
this.logger.warn("RCON connect error:", error);
7080
try {
@@ -74,11 +84,13 @@ export class RconService {
7484
} catch (cleanupError) {
7585
this.logger.warn("Error during RCON cleanup:", cleanupError);
7686
}
87+
return null;
7788
}
7889

90+
this.connections[matchId] = rcon;
7991
this.setupConnectionTimeout(matchId);
8092

81-
return (this.connections[matchId] = rcon);
93+
return rcon;
8294
}
8395

8496
private setupConnectionTimeout(matchId: string) {

src/redis/redis-manager/redis-manager.service.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,41 @@
1-
import { Injectable, Logger } from "@nestjs/common";
1+
import { Injectable, Logger, OnApplicationShutdown } from "@nestjs/common";
22
import IORedis, { Redis, RedisOptions } from "ioredis";
33
import { ConfigService } from "@nestjs/config";
44
import { RedisConfig } from "../../configs/types/RedisConfig";
55

66
@Injectable()
7-
export class RedisManagerService {
7+
export class RedisManagerService implements OnApplicationShutdown {
88
private config: RedisConfig;
99

1010
protected connections: {
1111
[key: string]: Redis;
1212
} = {};
1313

14+
private healthCheckIntervals: ReturnType<typeof setInterval>[] = [];
15+
1416
constructor(
1517
private readonly logger: Logger,
1618
private readonly configService: ConfigService,
1719
) {
1820
this.config = this.configService.get<RedisConfig>("redis")!;
1921
}
2022

23+
public async onApplicationShutdown() {
24+
for (const interval of this.healthCheckIntervals) {
25+
clearInterval(interval);
26+
}
27+
this.healthCheckIntervals = [];
28+
29+
for (const [name, connection] of Object.entries(this.connections)) {
30+
try {
31+
connection.disconnect();
32+
} catch (error) {
33+
this.logger.warn(`Error disconnecting Redis "${name}":`, error);
34+
}
35+
}
36+
this.connections = {};
37+
}
38+
2139
public getConnection(connection = "default"): Redis {
2240
if (!this.connections[connection]) {
2341
const currentConnection: Redis = (this.connections[connection] =
@@ -45,7 +63,7 @@ export class RedisManagerService {
4563

4664
const pingTimeoutError = `did not receive ping in time (5 seconds)`;
4765

48-
setInterval(async () => {
66+
const healthCheckInterval = setInterval(async () => {
4967
if (currentConnection.status === "ready") {
5068
await new Promise(async (resolve, reject) => {
5169
const timer = setTimeout(() => {
@@ -65,6 +83,8 @@ export class RedisManagerService {
6583
});
6684
}
6785
}, 5000);
86+
87+
this.healthCheckIntervals.push(healthCheckInterval);
6888
});
6989
}
7090
return this.connections[connection];

src/system/network.service.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
import os from "os";
2-
import { spawn } from "child_process";
3-
import { Injectable, Logger, OnApplicationBootstrap } from "@nestjs/common";
2+
import { ChildProcess, spawn } from "child_process";
3+
import {
4+
Injectable,
5+
Logger,
6+
OnApplicationBootstrap,
7+
OnApplicationShutdown,
8+
} from "@nestjs/common";
49

510
@Injectable()
6-
export class NetworkService implements OnApplicationBootstrap {
11+
export class NetworkService
12+
implements OnApplicationBootstrap, OnApplicationShutdown
13+
{
714
public publicIP: string;
815
public networkLimit?: number;
916

17+
private ipCheckInterval: ReturnType<typeof setInterval>;
18+
private spawnedProcesses = new Set<ChildProcess>();
19+
1020
constructor(private readonly logger: Logger) {}
1121

1222
public async getNetworkLimit() {
@@ -24,14 +34,23 @@ export class NetworkService implements OnApplicationBootstrap {
2434
public async onApplicationBootstrap() {
2535
await this.getPublicIP();
2636

27-
setInterval(
37+
this.ipCheckInterval = setInterval(
2838
async () => {
2939
await this.getPublicIP();
3040
},
3141
5 * 60 * 1000,
3242
);
3343
}
3444

45+
public async onApplicationShutdown() {
46+
clearInterval(this.ipCheckInterval);
47+
48+
for (const proc of this.spawnedProcesses) {
49+
proc.kill();
50+
}
51+
this.spawnedProcesses.clear();
52+
}
53+
3554
public getLanIP() {
3655
return this.getLanInterface().ipv4?.address;
3756
}
@@ -305,16 +324,16 @@ export class NetworkService implements OnApplicationBootstrap {
305324
done`,
306325
]);
307326

308-
process.on(process.env.DEV ? "SIGUSR2" : "SIGTERM", () => {
309-
monitor.kill();
310-
});
327+
this.spawnedProcesses.add(monitor);
311328

312329
monitor.stdin.on("error", async (error) => {
313330
this.logger.error("Error running processs", error);
331+
monitor.kill();
314332
reject(error);
315333
});
316334

317335
monitor.stderr.on("data", (error) => {
336+
monitor.kill();
318337
reject(error.toString());
319338
});
320339

@@ -327,6 +346,7 @@ export class NetworkService implements OnApplicationBootstrap {
327346
});
328347

329348
monitor.on("close", () => {
349+
this.spawnedProcesses.delete(monitor);
330350
resolve(
331351
returnData || "No data written to memory due to onData() handler",
332352
);

src/utilities/throttle.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,20 +48,25 @@ class Throttle {
4848
private totalBytes = 0;
4949
private isProcessing = false;
5050
private bytesPerSecond: number;
51+
private intervalId: ReturnType<typeof setInterval>;
5152
private queue: Array<{
5253
chunk: Buffer;
5354
send: () => void;
5455
}> = [];
5556

5657
constructor() {
57-
setInterval(() => {
58+
this.intervalId = setInterval(() => {
5859
this.totalBytes = 0;
5960
if (!this.isProcessing) {
6061
this.process();
6162
}
6263
}, 1000);
6364
}
6465

66+
public destroy() {
67+
clearInterval(this.intervalId);
68+
}
69+
6570
public setBytesPerSecond(bytesPerSecond: number) {
6671
if (this.bytesPerSecond !== bytesPerSecond) {
6772
this.bytesPerSecond = bytesPerSecond;

0 commit comments

Comments
 (0)