Refactor timer system to run on main thread

Co-authored-by: AlecM33 <24642328+AlecM33@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-01-23 23:40:10 +00:00
parent 01660751e3
commit dfdea9a695
3 changed files with 215 additions and 117 deletions

View File

@@ -297,12 +297,11 @@ const Events = [
id: EVENT_IDS.RESTART_GAME,
stateChange: async (game, socketArgs, vars) => {
if (vars.instanceId !== vars.senderInstanceId
&& vars.timerManager.timerThreads[game.accessCode]
&& vars.timerManager.timers[game.accessCode]
) {
if (!vars.timerManager.timerThreads[game.accessCode].killed) {
vars.timerManager.timerThreads[game.accessCode].kill();
}
delete vars.timerManager.timerThreads[game.accessCode];
const timer = vars.timerManager.timers[game.accessCode];
timer.stopTimer();
delete vars.timerManager.timers[game.accessCode];
}
},
communicate: async (game, socketArgs, vars) => {
@@ -316,16 +315,62 @@ const Events = [
id: EVENT_IDS.TIMER_EVENT,
stateChange: async (game, socketArgs, vars) => {},
communicate: async (game, socketArgs, vars) => {
const thread = vars.timerManager.timerThreads[game.accessCode];
if (thread) {
if (!thread.killed && thread.exitCode === null) {
thread.send({
command: vars.timerEventSubtype,
accessCode: game.accessCode,
socketId: vars.requestingSocketId,
logLevel: vars.logger.logLevel
});
} else {
const timer = vars.timerManager.timers[game.accessCode];
if (timer) {
// Handle timer commands directly
switch (vars.timerEventSubtype) {
case GAME_PROCESS_COMMANDS.PAUSE_TIMER:
timer.stopTimer();
game.timerParams.paused = true;
game.timerParams.timeRemaining = timer.currentTimeInMillis;
await vars.gameManager.refreshGame(game);
vars.gameManager.namespace.in(game.accessCode).emit(
GAME_PROCESS_COMMANDS.PAUSE_TIMER,
timer.currentTimeInMillis
);
await vars.eventManager.publisher.publish(
REDIS_CHANNELS.ACTIVE_GAME_STREAM,
vars.eventManager.createMessageToPublish(
game.accessCode,
GAME_PROCESS_COMMANDS.PAUSE_TIMER,
vars.instanceId,
JSON.stringify({ timeRemaining: timer.currentTimeInMillis })
)
);
break;
case GAME_PROCESS_COMMANDS.RESUME_TIMER:
timer.resumeTimer();
game.timerParams.paused = false;
game.timerParams.timeRemaining = timer.currentTimeInMillis;
await vars.gameManager.refreshGame(game);
vars.gameManager.namespace.in(game.accessCode).emit(
GAME_PROCESS_COMMANDS.RESUME_TIMER,
timer.currentTimeInMillis
);
await vars.eventManager.publisher.publish(
REDIS_CHANNELS.ACTIVE_GAME_STREAM,
vars.eventManager.createMessageToPublish(
game.accessCode,
GAME_PROCESS_COMMANDS.RESUME_TIMER,
vars.instanceId,
JSON.stringify({ timeRemaining: timer.currentTimeInMillis })
)
);
break;
case GAME_PROCESS_COMMANDS.GET_TIME_REMAINING:
const socket = vars.gameManager.namespace.sockets.get(vars.requestingSocketId);
if (socket) {
vars.gameManager.namespace.to(socket.id).emit(
GAME_PROCESS_COMMANDS.GET_TIME_REMAINING,
timer.currentTimeInMillis,
game.timerParams.paused
);
}
break;
}
} else {
// Timer not on this instance, consult another container
if (vars.timerEventSubtype === GAME_PROCESS_COMMANDS.GET_TIME_REMAINING) {
const socket = vars.gameManager.namespace.sockets.get(vars.requestingSocketId);
if (socket) {
vars.gameManager.namespace.to(socket.id).emit(
@@ -334,50 +379,90 @@ const Events = [
game.timerParams.paused
);
}
} else {
await vars.eventManager.publisher?.publish(
REDIS_CHANNELS.ACTIVE_GAME_STREAM,
vars.eventManager.createMessageToPublish(
game.accessCode,
EVENT_IDS.SOURCE_TIMER_EVENT,
vars.instanceId,
JSON.stringify({ socketId: vars.requestingSocketId, timerEventSubtype: vars.timerEventSubtype })
)
);
}
} else { // we need to consult another container for the timer data
await vars.eventManager.publisher?.publish(
REDIS_CHANNELS.ACTIVE_GAME_STREAM,
vars.eventManager.createMessageToPublish(
game.accessCode,
EVENT_IDS.SOURCE_TIMER_EVENT,
vars.instanceId,
JSON.stringify({ socketId: vars.requestingSocketId, timerEventSubtype: vars.timerEventSubtype })
)
);
}
}
},
{
/* This event is a request from another instance to consult its timer data. In response
* to this event, this instance will check if it is home to a particular timer thread. */
* to this event, this instance will check if it is home to a particular timer. */
id: EVENT_IDS.SOURCE_TIMER_EVENT,
stateChange: async (game, socketArgs, vars) => {},
communicate: async (game, socketArgs, vars) => {
const thread = vars.timerManager.timerThreads[game.accessCode];
if (thread) {
if (!thread.killed && thread.exitCode === null) {
thread.send({
command: socketArgs.timerEventSubtype,
accessCode: game.accessCode,
socketId: socketArgs.socketId,
logLevel: vars.logger.logLevel
});
} else {
await vars.eventManager.publisher.publish(
REDIS_CHANNELS.ACTIVE_GAME_STREAM,
vars.eventManager.createMessageToPublish(
game.accessCode,
socketArgs.timerEventSubtype,
vars.instanceId,
JSON.stringify({
socketId: socketArgs.socketId,
timeRemaining: game.timerParams.timeRemaining,
paused: game.timerParams.paused
})
)
);
const timer = vars.timerManager.timers[game.accessCode];
if (timer) {
// Handle the timer command on this instance
switch (socketArgs.timerEventSubtype) {
case GAME_PROCESS_COMMANDS.PAUSE_TIMER:
timer.stopTimer();
game.timerParams.paused = true;
game.timerParams.timeRemaining = timer.currentTimeInMillis;
await vars.gameManager.refreshGame(game);
await vars.eventManager.publisher.publish(
REDIS_CHANNELS.ACTIVE_GAME_STREAM,
vars.eventManager.createMessageToPublish(
game.accessCode,
GAME_PROCESS_COMMANDS.PAUSE_TIMER,
vars.instanceId,
JSON.stringify({ timeRemaining: timer.currentTimeInMillis })
)
);
break;
case GAME_PROCESS_COMMANDS.RESUME_TIMER:
timer.resumeTimer();
game.timerParams.paused = false;
game.timerParams.timeRemaining = timer.currentTimeInMillis;
await vars.gameManager.refreshGame(game);
await vars.eventManager.publisher.publish(
REDIS_CHANNELS.ACTIVE_GAME_STREAM,
vars.eventManager.createMessageToPublish(
game.accessCode,
GAME_PROCESS_COMMANDS.RESUME_TIMER,
vars.instanceId,
JSON.stringify({ timeRemaining: timer.currentTimeInMillis })
)
);
break;
case GAME_PROCESS_COMMANDS.GET_TIME_REMAINING:
await vars.eventManager.publisher.publish(
REDIS_CHANNELS.ACTIVE_GAME_STREAM,
vars.eventManager.createMessageToPublish(
game.accessCode,
GAME_PROCESS_COMMANDS.GET_TIME_REMAINING,
vars.instanceId,
JSON.stringify({
socketId: socketArgs.socketId,
timeRemaining: timer.currentTimeInMillis
})
)
);
break;
}
} else {
// Timer not on this instance either, send back stored value
await vars.eventManager.publisher.publish(
REDIS_CHANNELS.ACTIVE_GAME_STREAM,
vars.eventManager.createMessageToPublish(
game.accessCode,
socketArgs.timerEventSubtype,
vars.instanceId,
JSON.stringify({
socketId: socketArgs.socketId,
timeRemaining: game.timerParams.timeRemaining,
paused: game.timerParams.paused
})
)
);
}
}
},

View File

@@ -30,11 +30,11 @@ class GameManager {
getActiveGame = async (accessCode) => {
const r = await this.eventManager.publisher.get(accessCode);
if (r === null && this.timerManager.timerThreads[accessCode]) {
if (!this.timerManager.timerThreads[accessCode].killed) {
this.timerManager.timerThreads[accessCode].kill();
}
delete this.timerManager.timerThreads[accessCode];
if (r === null && this.timerManager.timers[accessCode]) {
// Clean up timer reference if game no longer exists
const timer = this.timerManager.timers[accessCode];
timer.stopTimer();
delete this.timerManager.timers[accessCode];
}
let activeGame;
if (r !== null) {
@@ -109,43 +109,36 @@ class GameManager {
};
pauseTimer = async (game, logger) => {
const thread = this.timerManager.timerThreads[game.accessCode];
if (thread && !thread.killed) {
this.logger.debug('Timer thread found for game ' + game.accessCode);
thread.send({
command: GAME_PROCESS_COMMANDS.PAUSE_TIMER,
accessCode: game.accessCode,
logLevel: this.logger.logLevel
});
const timer = this.timerManager.timers[game.accessCode];
if (timer) {
this.logger.debug('Timer found for game ' + game.accessCode);
timer.stopTimer();
}
};
resumeTimer = async (game, logger) => {
const thread = this.timerManager.timerThreads[game.accessCode];
if (thread && !thread.killed) {
this.logger.debug('Timer thread found for game ' + game.accessCode);
thread.send({
command: GAME_PROCESS_COMMANDS.RESUME_TIMER,
accessCode: game.accessCode,
logLevel: this.logger.logLevel
});
const timer = this.timerManager.timers[game.accessCode];
if (timer) {
this.logger.debug('Timer found for game ' + game.accessCode);
timer.resumeTimer();
}
};
getTimeRemaining = async (game, socketId) => {
if (socketId) {
const thread = this.timerManager.timerThreads[game.accessCode];
if (thread && (!thread.killed && thread.exitCode === null)) {
thread.send({
command: GAME_PROCESS_COMMANDS.GET_TIME_REMAINING,
accessCode: game.accessCode,
socketId: socketId,
logLevel: this.logger.logLevel
});
} else if (thread) {
if (game.timerParams && game.timerParams.timeRemaining === 0) {
this.namespace.to(socketId).emit(GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, game.timerParams.timeRemaining, game.timerParams.paused);
}
const timer = this.timerManager.timers[game.accessCode];
if (timer) {
this.namespace.to(socketId).emit(
GAME_PROCESS_COMMANDS.GET_TIME_REMAINING,
timer.currentTimeInMillis,
game.timerParams.paused
);
} else if (game.timerParams && game.timerParams.timeRemaining === 0) {
this.namespace.to(socketId).emit(
GAME_PROCESS_COMMANDS.GET_TIME_REMAINING,
game.timerParams.timeRemaining,
game.timerParams.paused
);
}
}
};
@@ -248,15 +241,13 @@ class GameManager {
}
restartGame = async (game, namespace) => {
// kill any outstanding timer threads
const subProcess = this.timerManager.timerThreads[game.accessCode];
if (subProcess) {
if (!subProcess.killed) {
this.logger.info('Killing timer process ' + subProcess.pid + ' for: ' + game.accessCode);
this.timerManager.timerThreads[game.accessCode].kill();
}
this.logger.debug('Deleting reference to subprocess ' + subProcess.pid);
delete this.timerManager.timerThreads[game.accessCode];
// kill any outstanding timers
const timer = this.timerManager.timers[game.accessCode];
if (timer) {
this.logger.info('Stopping timer for: ' + game.accessCode);
timer.stopTimer();
this.logger.debug('Deleting reference to timer');
delete this.timerManager.timers[game.accessCode];
}
for (let i = 0; i < game.people.length; i ++) {

View File

@@ -1,6 +1,5 @@
const { fork } = require('child_process');
const path = require('path');
const { REDIS_CHANNELS, GAME_PROCESS_COMMANDS } = require('../../config/globals');
const ServerTimer = require('../ServerTimer');
const { REDIS_CHANNELS, GAME_PROCESS_COMMANDS, PRIMITIVES } = require('../../config/globals');
class TimerManager {
constructor (logger, instanceId) {
@@ -8,7 +7,7 @@ class TimerManager {
throw new Error('The server tried to instantiate more than one TimerManager');
}
logger.info('CREATING SINGLETON TIMER MANAGER');
this.timerThreads = {};
this.timers = {};
this.logger = logger;
this.subscriber = null;
this.instanceId = instanceId;
@@ -17,29 +16,52 @@ class TimerManager {
runTimer = async (game, namespace, eventManager, gameManager) => {
this.logger.debug('running timer for game ' + game.accessCode);
const gameProcess = fork(path.join(__dirname, '../GameProcess.js'));
this.timerThreads[game.accessCode] = gameProcess;
this.logger.debug('game ' + game.accessCode + ' now associated with subProcess ' + gameProcess.pid);
gameProcess.on('message', async (msg) => {
game = await gameManager.getActiveGame(game.accessCode);
await eventManager.handleEventById(msg.command, null, game, msg.socketId, game.accessCode, msg, null, false);
await gameManager.refreshGame(game);
await eventManager.publisher.publish(
REDIS_CHANNELS.ACTIVE_GAME_STREAM,
eventManager.createMessageToPublish(game.accessCode, msg.command, this.instanceId, JSON.stringify(msg))
);
});
gameProcess.on('exit', (code, signal) => {
this.logger.debug('Game timer thread ' + gameProcess.pid + ' exiting with code ' + code + ' - game ' + game.accessCode);
});
gameProcess.send({
command: GAME_PROCESS_COMMANDS.START_TIMER,
accessCode: game.accessCode,
logLevel: this.logger.logLevel,
hours: game.timerParams.hours,
minutes: game.timerParams.minutes
// Create timer instance directly on main thread
const timer = new ServerTimer(
game.timerParams.hours,
game.timerParams.minutes,
PRIMITIVES.CLOCK_TICK_INTERVAL_MILLIS,
this.logger
);
this.timers[game.accessCode] = timer;
this.logger.debug('game ' + game.accessCode + ' now has timer instance');
// Start the timer (paused initially as per original logic)
timer.runTimer(true).then(async () => {
this.logger.debug('Timer finished for ' + game.accessCode);
// Get fresh game state
const currentGame = await gameManager.getActiveGame(game.accessCode);
if (currentGame) {
// Handle END_TIMER event
await eventManager.handleEventById(
GAME_PROCESS_COMMANDS.END_TIMER,
null,
currentGame,
null,
currentGame.accessCode,
{},
null,
false
);
await gameManager.refreshGame(currentGame);
await eventManager.publisher.publish(
REDIS_CHANNELS.ACTIVE_GAME_STREAM,
eventManager.createMessageToPublish(
currentGame.accessCode,
GAME_PROCESS_COMMANDS.END_TIMER,
this.instanceId,
'{}'
)
);
}
// Clean up timer reference
delete this.timers[game.accessCode];
});
game.startTime = new Date().toJSON();
}
}