Skip to content

Commit fda070a

Browse files
fix(game-server): resolve memory leaks and improve shutdown cleanup
* Clean up pending disconnect timers to prevent memory leaks during server shutdown * Remove stale `playerRoomIndex` entries when deleting rooms * Delete Redis canvas data during room cleanup and add Pino logging for cleanup operations * Add `SIGINT` (Ctrl+C) handling to the graceful shutdown sequence * Include Redis connection cleanup as part of graceful shutdown
1 parent 1dfd9db commit fda070a

2 files changed

Lines changed: 35 additions & 7 deletions

File tree

apps/game-server/src/rooms/RoomManager.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
import { Player, RoomConfig, ErrorCode } from '@scribblitz/types';
88
import { Room, ServerRoomState } from './Room';
9+
import { redis } from '../lib/redis';
10+
import logger from '../utils/logger';
911

1012
export class RoomManager {
1113
// Using a Map for O(1) lookups and efficient addition/removal of rooms
@@ -55,7 +57,21 @@ export class RoomManager {
5557

5658
if (!room) return;
5759

60+
//Timer cleanup
5861
room.cleanup();
62+
63+
// Clean up the player index for ALL players who were in this room
64+
// so they don't get stuck as "ghosts" in the RoomManager
65+
for (const playerId of room.getState().players.keys()) {
66+
this.playerRoomIndex.delete(playerId);
67+
}
68+
69+
// Delete the Redis canvas stream to free up database RAM
70+
// (Using fire-and-forget catch so we don't block room deletion if Redis hiccups)
71+
redis.del(`room:${roomCode}:canvas`).catch((err: Error) => {
72+
logger.error({ err, roomCode }, 'Failed to cleanup Redis canvas for deleted room');
73+
});
74+
5975
this.rooms.delete(roomCode);
6076
}
6177

apps/game-server/src/server.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,8 @@ io.on('connection', (socket: Socket) => {
331331

332332
//60 second cleanup timeout
333333
const timer = setTimeout(() => {
334+
disconnectTimers.delete(userId); //Clean up the timer reference from the map after execution
335+
334336
const roomCheck = roomManager.getRoom(roomCode);
335337
if (!roomCheck) return;
336338

@@ -371,8 +373,6 @@ io.on('connection', (socket: Socket) => {
371373
void abortGame(io, roomCode);
372374
}
373375
}
374-
375-
disconnectTimers.delete(userId); //Clean up the timer reference from the map after execution
376376
}, 60_000);
377377

378378
// Store the disconnect timer so it can be cleared if the user reconnects within the grace period
@@ -404,15 +404,27 @@ redis
404404
})
405405
.catch((err) => {
406406
logger.fatal({ err }, 'Redis fatal connection error');
407+
process.exit(1);
407408
});
408409

409-
// Graceful Shutdown to prevent Memory Leaks from orphan disconnect timers
410-
process.on('SIGTERM', () => {
411-
logger.info('SIGTERM received — cleaning up disconnect timers');
410+
const gracefulShutdown = async (signal: string) => {
411+
logger.info(`${signal} received — cleaning up disconnect timers and closing connections`);
412412
disconnectTimers.forEach((timer) => clearTimeout(timer));
413413
disconnectTimers.clear();
414-
httpServer.close(() => {
414+
415+
httpServer.close(async () => {
416+
try {
417+
// FIX: Cleanly shut down the Redis connection to prevent socket leaks in the DB
418+
await redis.quit();
419+
logger.info('Redis connection closed.');
420+
} catch (err) {
421+
logger.error({ err }, 'Error closing Redis connection');
422+
}
415423
logger.info('Server closed gracefully');
416424
process.exit(0);
417425
});
418-
});
426+
};
427+
428+
// Graceful Shutdown to prevent Memory Leaks from orphan disconnect timers
429+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
430+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));

0 commit comments

Comments
 (0)