feature/IO-3096-GlobalNotifications - Checkpoint - Header finalized, scenarioParser now uses ENV var for FILTER_SELF from watchers.

This commit is contained in:
Dave Richer
2025-02-28 17:33:46 -05:00
parent f51fa08961
commit f4a3b75a86
4 changed files with 70 additions and 49 deletions

View File

@@ -31,11 +31,11 @@ const { redisSocketEvents } = require("./server/web-sockets/redisSocketEvents");
const { ElastiCacheClient, DescribeCacheClustersCommand } = require("@aws-sdk/client-elasticache"); const { ElastiCacheClient, DescribeCacheClustersCommand } = require("@aws-sdk/client-elasticache");
const { InstanceRegion } = require("./server/utils/instanceMgr"); const { InstanceRegion } = require("./server/utils/instanceMgr");
const StartStatusReporter = require("./server/utils/statusReporter"); const StartStatusReporter = require("./server/utils/statusReporter");
const { registerCleanupTask, initializeCleanupManager } = require("./server/utils/cleanupManager");
const { loadEmailQueue } = require("./server/notifications/queues/emailQueue"); const { loadEmailQueue } = require("./server/notifications/queues/emailQueue");
const { loadAppQueue } = require("./server/notifications/queues/appQueue"); const { loadAppQueue } = require("./server/notifications/queues/appQueue");
const cleanupTasks = [];
let isShuttingDown = false;
const CLUSTER_RETRY_BASE_DELAY = 100; const CLUSTER_RETRY_BASE_DELAY = 100;
const CLUSTER_RETRY_MAX_DELAY = 5000; const CLUSTER_RETRY_MAX_DELAY = 5000;
const CLUSTER_RETRY_JITTER = 100; const CLUSTER_RETRY_JITTER = 100;
@@ -332,6 +332,9 @@ const main = async () => {
const server = http.createServer(app); const server = http.createServer(app);
// Initialize cleanup manager with signal handlers
initializeCleanupManager();
const { pubClient, ioRedis } = await applySocketIO({ server, app }); const { pubClient, ioRedis } = await applySocketIO({ server, app });
const redisHelpers = applyRedisHelpers({ pubClient, app, logger }); const redisHelpers = applyRedisHelpers({ pubClient, app, logger });
const ioHelpers = applyIOHelpers({ app, redisHelpers, ioRedis, logger }); const ioHelpers = applyIOHelpers({ app, redisHelpers, ioRedis, logger });
@@ -351,10 +354,6 @@ const main = async () => {
StatusReporter.end(); StatusReporter.end();
}); });
// Add SIGTERM signal handler
process.on("SIGTERM", handleSigterm);
process.on("SIGINT", handleSigterm); // Optional: Handle Ctrl+C
try { try {
await server.listen(port); await server.listen(port);
logger.log(`Server started on port ${port}`, "INFO", "api"); logger.log(`Server started on port ${port}`, "INFO", "api");
@@ -373,33 +372,3 @@ main().catch((error) => {
// Note: If we want the app to crash on all uncaught async operations, we would // Note: If we want the app to crash on all uncaught async operations, we would
// need to put a `process.exit(1);` here // need to put a `process.exit(1);` here
}); });
// Register a cleanup task
function registerCleanupTask(task) {
cleanupTasks.push(task);
}
// SIGTERM handler
async function handleSigterm() {
if (isShuttingDown) {
logger.log("sigterm-api", "WARN", null, null, { message: "Shutdown already in progress, ignoring signal." });
return;
}
isShuttingDown = true;
logger.log("sigterm-api", "WARN", null, null, { message: "SIGTERM Received. Starting graceful shutdown." });
try {
for (const task of cleanupTasks) {
logger.log("sigterm-api", "WARN", null, null, { message: `Running cleanup task: ${task.name}` });
await task();
}
logger.log("sigterm-api", "WARN", null, null, { message: `All cleanup tasks completed.` });
} catch (error) {
logger.log("sigterm-api-error", "ERROR", null, null, { message: error.message, stack: error.stack });
}
process.exit(0);
}

View File

@@ -1,5 +1,6 @@
const { Queue, Worker } = require("bullmq"); const { Queue, Worker } = require("bullmq");
const { INSERT_NOTIFICATIONS_MUTATION } = require("../../graphql-client/queries"); const { INSERT_NOTIFICATIONS_MUTATION } = require("../../graphql-client/queries");
const { registerCleanupTask } = require("../../utils/cleanupManager");
const graphQLClient = require("../../graphql-client/graphql-client").client; const graphQLClient = require("../../graphql-client/graphql-client").client;
// Base time-related constant in minutes, sourced from environment variable or defaulting to 1 // Base time-related constant in minutes, sourced from environment variable or defaulting to 1
@@ -239,14 +240,14 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
logger.logger.error(`Consolidate job ${job.id} failed: ${err.message}`, { error: err }) logger.logger.error(`Consolidate job ${job.id} failed: ${err.message}`, { error: err })
); );
// Register cleanup task instead of direct process listeners
const shutdown = async () => { const shutdown = async () => {
logger.logger.info("Closing app queue workers..."); logger.logger.info("Closing app queue workers...");
await Promise.all([addWorker.close(), consolidateWorker.close()]); await Promise.all([addWorker.close(), consolidateWorker.close()]);
logger.logger.info("App queue workers closed"); logger.logger.info("App queue workers closed");
}; };
process.on("SIGTERM", shutdown); registerCleanupTask(shutdown);
process.on("SIGINT", shutdown);
} }
return addQueue; return addQueue;

View File

@@ -2,6 +2,7 @@ const { Queue, Worker } = require("bullmq");
const { sendTaskEmail } = require("../../email/sendemail"); const { sendTaskEmail } = require("../../email/sendemail");
const generateEmailTemplate = require("../../email/generateTemplate"); const generateEmailTemplate = require("../../email/generateTemplate");
const { InstanceEndpoints } = require("../../utils/instanceMgr"); const { InstanceEndpoints } = require("../../utils/instanceMgr");
const { registerCleanupTask } = require("../../utils/cleanupManager");
const EMAIL_CONSOLIDATION_DELAY_IN_MINS = (() => { const EMAIL_CONSOLIDATION_DELAY_IN_MINS = (() => {
const envValue = process.env?.APP_CONSOLIDATION_DELAY_IN_MINS; const envValue = process.env?.APP_CONSOLIDATION_DELAY_IN_MINS;
@@ -61,11 +62,11 @@ const loadEmailQueue = async ({ pubClient, logger }) => {
const { user, firstName, lastName } = recipient; const { user, firstName, lastName } = recipient;
const userKey = `${redisKeyPrefix}:${user}`; const userKey = `${redisKeyPrefix}:${user}`;
await pubClient.rpush(userKey, body); await pubClient.rpush(userKey, body);
await pubClient.expire(userKey, NOTIFICATION_EXPIRATION / 1000); // Set expiration await pubClient.expire(userKey, NOTIFICATION_EXPIRATION / 1000);
const detailsKey = `email:recipientDetails:${jobId}:${user}`; const detailsKey = `email:recipientDetails:${jobId}:${user}`;
await pubClient.hsetnx(detailsKey, "firstName", firstName || ""); await pubClient.hsetnx(detailsKey, "firstName", firstName || "");
await pubClient.hsetnx(detailsKey, "lastName", lastName || ""); await pubClient.hsetnx(detailsKey, "lastName", lastName || "");
await pubClient.expire(detailsKey, NOTIFICATION_EXPIRATION / 1000); // Set expiration await pubClient.expire(detailsKey, NOTIFICATION_EXPIRATION / 1000);
await pubClient.sadd(`email:recipients:${jobId}`, user); await pubClient.sadd(`email:recipients:${jobId}`, user);
logger.logger.debug(`Stored message for ${user} under ${userKey}: ${body}`); logger.logger.debug(`Stored message for ${user} under ${userKey}: ${body}`);
} }
@@ -79,12 +80,12 @@ const loadEmailQueue = async ({ pubClient, logger }) => {
{ {
jobId: `consolidate:${jobId}`, jobId: `consolidate:${jobId}`,
delay: EMAIL_CONSOLIDATION_DELAY, delay: EMAIL_CONSOLIDATION_DELAY,
attempts: 3, // Retry up to 3 times attempts: 3,
backoff: LOCK_EXPIRATION // Retry delay matches lock expiration (15s) backoff: LOCK_EXPIRATION
} }
); );
logger.logger.info(`Scheduled email consolidation for jobId ${jobId}`); logger.logger.info(`Scheduled email consolidation for jobId ${jobId}`);
await pubClient.expire(consolidateKey, CONSOLIDATION_KEY_EXPIRATION / 1000); // Convert to seconds await pubClient.expire(consolidateKey, CONSOLIDATION_KEY_EXPIRATION / 1000);
} else { } else {
logger.logger.debug(`Email consolidation already scheduled for jobId ${jobId}`); logger.logger.debug(`Email consolidation already scheduled for jobId ${jobId}`);
} }
@@ -104,7 +105,7 @@ const loadEmailQueue = async ({ pubClient, logger }) => {
logger.logger.info(`Consolidating emails for jobId ${jobId}`); logger.logger.info(`Consolidating emails for jobId ${jobId}`);
const lockKey = `lock:emailConsolidate:${jobId}`; const lockKey = `lock:emailConsolidate:${jobId}`;
const lockAcquired = await pubClient.set(lockKey, "locked", "NX", "EX", LOCK_EXPIRATION / 1000); // Convert to seconds const lockAcquired = await pubClient.set(lockKey, "locked", "NX", "EX", LOCK_EXPIRATION / 1000);
if (lockAcquired) { if (lockAcquired) {
try { try {
const recipientsSet = `email:recipients:${jobId}`; const recipientsSet = `email:recipients:${jobId}`;
@@ -118,7 +119,6 @@ const loadEmailQueue = async ({ pubClient, logger }) => {
const firstName = details.firstName || "User"; const firstName = details.firstName || "User";
const multipleUpdateString = messages.length > 1 ? "Updates" : "Update"; const multipleUpdateString = messages.length > 1 ? "Updates" : "Update";
const subject = `${multipleUpdateString} for job ${jobRoNumber} at ${bodyShopName}`; const subject = `${multipleUpdateString} for job ${jobRoNumber} at ${bodyShopName}`;
// Use the template instead of inline HTML
const emailBody = generateEmailTemplate({ const emailBody = generateEmailTemplate({
header: `${multipleUpdateString} for Job ${jobRoNumber}`, header: `${multipleUpdateString} for Job ${jobRoNumber}`,
subHeader: `Dear ${firstName},`, subHeader: `Dear ${firstName},`,
@@ -147,7 +147,7 @@ const loadEmailQueue = async ({ pubClient, logger }) => {
await pubClient.del(`email:consolidate:${jobId}`); await pubClient.del(`email:consolidate:${jobId}`);
} catch (err) { } catch (err) {
logger.logger.error(`Email consolidation error for jobId ${jobId}: ${err.message}`, { error: err }); logger.logger.error(`Email consolidation error for jobId ${jobId}: ${err.message}`, { error: err });
throw err; // Trigger retry if attempts remain throw err;
} finally { } finally {
await pubClient.del(lockKey); await pubClient.del(lockKey);
} }
@@ -173,14 +173,13 @@ const loadEmailQueue = async ({ pubClient, logger }) => {
logger.logger.error(`Email consolidate job ${job.id} failed: ${err.message}`, { error: err }) logger.logger.error(`Email consolidate job ${job.id} failed: ${err.message}`, { error: err })
); );
// Graceful shutdown // Register cleanup task instead of direct process listeners
const shutdown = async () => { const shutdown = async () => {
logger.logger.info("Closing email queue workers..."); logger.logger.info("Closing email queue workers...");
await Promise.all([emailAddWorker.close(), emailConsolidateWorker.close()]); await Promise.all([emailAddWorker.close(), emailConsolidateWorker.close()]);
logger.logger.info("Email queue workers closed"); logger.logger.info("Email queue workers closed");
}; };
process.on("SIGTERM", shutdown); registerCleanupTask(shutdown);
process.on("SIGINT", shutdown);
} }
return emailAddQueue; return emailAddQueue;

View File

@@ -0,0 +1,52 @@
// server/utils/cleanupManager.js
const logger = require("./logger");
let cleanupTasks = [];
let isShuttingDown = false;
/**
* Register a cleanup task to be executed during shutdown
* @param {Function} task - The cleanup task to register
*/
function registerCleanupTask(task) {
cleanupTasks.push(task);
}
/**
* Handle SIGTERM signal for graceful shutdown
*/
async function handleSigterm() {
if (isShuttingDown) {
logger.log("sigterm-api", "WARN", null, null, { message: "Shutdown already in progress, ignoring signal." });
return;
}
isShuttingDown = true;
logger.log("sigterm-api", "WARN", null, null, { message: "SIGTERM Received. Starting graceful shutdown." });
try {
for (const task of cleanupTasks) {
logger.log("sigterm-api", "WARN", null, null, { message: `Running cleanup task: ${task.name}` });
await task();
}
logger.log("sigterm-api", "WARN", null, null, { message: `All cleanup tasks completed.` });
} catch (error) {
logger.log("sigterm-api-error", "ERROR", null, null, { message: error.message, stack: error.stack });
}
process.exit(0);
}
/**
* Initialize cleanup manager with process event listeners
*/
function initializeCleanupManager() {
process.on("SIGTERM", handleSigterm);
process.on("SIGINT", handleSigterm); // Handle Ctrl+C
}
module.exports = {
registerCleanupTask,
initializeCleanupManager
};