Merge pull request #2559 from bluewave-labs/feat/super-simple-queue

feat: super simple queue
This commit is contained in:
Alexander Holliday authored and GitHub committed 2025-07-23 10:38:13 -07:00
commit 812cff0fd9
8 files changed
+1463 -915

No files matched your search

+22 -4
View File
@@ -53,6 +53,9 @@ import { Queue, Worker } from "bullmq";
import PulseQueue from "./service/PulseQueue/PulseQueue.js";
import PulseQueueHelper from "./service/PulseQueue/PulseQueueHelper.js";
import SuperSimpleQueue from "./service/SuperSimpleQueue/SuperSimpleQueue.js";
import SuperSimpleQueueHelper from "./service/SuperSimpleQueue/SuperSimpleQueueHelper.js";
//Network service and dependencies
import NetworkService from "./service/networkService.js";
import axios from "axios";
@@ -191,23 +194,38 @@ const startApp = async () => {
// stringService,
// });
const pulseQueueHelper = new PulseQueueHelper({
// const pulseQueueHelper = new PulseQueueHelper({
// db,
// logger,
// networkService,
// statusService,
// notificationService,
// });
// const pulseQueue = await PulseQueue.create({
// appSettings,
// db,
// pulseQueueHelper,
// logger,
// });
const superSimpleQueueHelper = new SuperSimpleQueueHelper({
db,
logger,
networkService,
statusService,
notificationService,
});
const pulseQueue = await PulseQueue.create({
const superSimpleQueue = await SuperSimpleQueue.create({
appSettings,
db,
pulseQueueHelper,
logger,
helper: superSimpleQueueHelper,
});
// Register services
// ServiceRegistry.register(JobQueue.SERVICE_NAME, jobQueue);
ServiceRegistry.register(JobQueue.SERVICE_NAME, pulseQueue);
// ServiceRegistry.register(JobQueue.SERVICE_NAME, pulseQueue);
ServiceRegistry.register(JobQueue.SERVICE_NAME, superSimpleQueue);
ServiceRegistry.register(MongoDB.SERVICE_NAME, db);
ServiceRegistry.register(SettingsService.SERVICE_NAME, settingsService);
ServiceRegistry.register(EmailService.SERVICE_NAME, emailService);
Generated Executable → Regular
+1061 -743
View File
File diff suppressed because it is too large. Load diff
+1
View File
@@ -40,6 +40,7 @@
"ping": "0.4.4",
"sharp": "0.33.5",
"ssl-checker": "2.0.10",
"super-simple-scheduler": "1.3.0",
"swagger-ui-express": "5.0.1",
"winston": "^3.13.0"
},
@@ -0,0 +1,166 @@
import Scheduler from "super-simple-scheduler";
const SERVICE_NAME = "SuperSimpleQueue";
class SuperSimpleQueue {
static SERVICE_NAME = SERVICE_NAME;
constructor({ appSettings, db, logger, helper }) {
this.appSettings = appSettings;
this.db = db;
this.logger = logger;
this.helper = helper;
}
static async create({ appSettings, db, logger, helper }) {
const instance = new SuperSimpleQueue({ appSettings, db, logger, helper });
await instance.init();
return instance;
}
init = async () => {
try {
this.scheduler = new Scheduler({
// storeType: "mongo",
// storeType: "redis",
logLevel: "debug",
debug: true,
dbUri: this.appSettings.dbConnectionString,
});
this.scheduler.start();
this.scheduler.addTemplate("monitor-job", this.helper.getMonitorJob());
const monitors = await this.db.getAllMonitors();
for (const monitor of monitors) {
await this.addJob(monitor._id, monitor);
}
return true;
} catch (error) {
this.logger.error({
message: "Failed to initialize SuperSimpleQueue",
service: SERVICE_NAME,
method: "init",
details: error,
});
return false;
}
};
addJob = async (monitorId, monitor) => {
this.scheduler.addJob({
id: monitorId.toString(),
template: "monitor-job",
repeat: monitor.interval,
data: monitor.toObject(),
});
};
deleteJob = async (monitor) => {
this.scheduler.removeJob(monitor._id.toString());
};
pauseJob = async (monitor) => {
const result = this.scheduler.pauseJob(monitor._id.toString());
if (result === false) {
throw new Error("Failed to resume monitor");
}
this.logger.debug({
message: `Paused monitor ${monitor._id}`,
service: SERVICE_NAME,
method: "pauseJob",
});
};
resumeJob = async (monitor) => {
const result = this.scheduler.resumeJob(monitor._id.toString());
if (result === false) {
throw new Error("Failed to resume monitor");
}
this.logger.debug({
message: `Resumed monitor ${monitor._id}`,
service: SERVICE_NAME,
method: "resumeJob",
});
};
updateJob = async (monitor) => {
this.scheduler.updateJob(monitor._id.toString(), monitor.interval);
};
shutdown = async () => {
this.scheduler.stop();
};
getMetrics = async () => {
const jobs = await this.scheduler.getJobs();
const metrics = jobs.reduce(
(acc, job) => {
acc.totalRuns += job.runCount || 0;
acc.totalFailures += job.failCount || 0;
acc.jobs++;
if (job.failCount > 0 && job.lastFailedAt >= job.lsatRunAt) {
acc.failingJobs++;
}
if (job.lockedAt) {
acc.activeJobs++;
}
if (job.failCount > 0) {
acc.jobsWithFailures.push({
monitorId: job.id,
monitorUrl: job?.data?.url || null,
monitorType: job?.data?.type || null,
failedAt: job.lastFailedAt,
failCount: job.failCount,
failReason: job.lastFailReason,
});
}
return acc;
},
{
jobs: 0,
activeJobs: 0,
failingJobs: 0,
jobsWithFailures: [],
totalRuns: 0,
totalFailures: 0,
}
);
return metrics;
};
getJobs = async () => {
const jobs = await this.scheduler.getJobs();
return jobs.map((job) => {
return {
monitorId: job.id,
monitorUrl: job?.data?.url || null,
monitorType: job?.data?.type || null,
active: job.active,
lockedAt: job.lockedAt,
runCount: job.runCount || 0,
failCount: job.failCount || 0,
failReason: job.lastFailReason,
lastRunAt: job.lastRunAt,
lastFinishedAt: job.lastFinishedAt,
lastRunTook: job.lockedAt ? null : job.lastFinishedAt - job.lastRunAt,
lastFailedAt: job.lastFailedAt,
};
});
};
flushQueues = async () => {
const stopRes = this.scheduler.stop();
const flushRes = this.scheduler.flushJobs();
const initRes = await this.init();
return {
success: stopRes && flushRes && initRes,
};
};
obliterate = async () => {
console.log("obliterate not implemented");
};
}
export default SuperSimpleQueue;
@@ -0,0 +1,99 @@
const SERVICE_NAME = "SuperSimpleQueueHelper";
class SuperSimpleQueueHelper {
constructor({ db, logger, networkService, statusService, notificationService }) {
this.db = db;
this.logger = logger;
this.networkService = networkService;
this.statusService = statusService;
this.notificationService = notificationService;
}
getMonitorJob = () => {
return async (monitor) => {
try {
const monitorId = monitor._id;
if (!monitorId) {
throw new Error("No monitor id");
}
const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId);
if (maintenanceWindowActive) {
this.logger.info({
message: `Monitor ${monitorId} is in maintenance window`,
service: SERVICE_NAME,
method: "getMonitorJob",
});
return;
}
const networkResponse = await this.networkService.getStatus(monitor);
if (!networkResponse) {
throw new Error("No network response");
}
const {
monitor: updatedMonitor,
statusChanged,
prevStatus,
} = await this.statusService.updateStatus(networkResponse);
this.notificationService
.handleNotifications({
...networkResponse,
monitor: updatedMonitor,
prevStatus,
statusChanged,
})
.catch((error) => {
this.logger.error({
message: error.message,
service: SERVICE_NAME,
method: "getMonitorJob",
details: `Error sending notifications for job ${monitor._id}: ${error.message}`,
stack: error.stack,
});
});
} catch (error) {
this.logger.warn({
message: error.message,
service: error.service || SERVICE_NAME,
method: error.method || "getMonitorJob",
stack: error.stack,
});
throw error;
}
};
};
async isInMaintenanceWindow(monitorId) {
const maintenanceWindows = await this.db.getMaintenanceWindowsByMonitorId(monitorId);
// Check for active maintenance window:
const maintenanceWindowIsActive = maintenanceWindows.reduce((acc, window) => {
if (window.active) {
const start = new Date(window.start);
const end = new Date(window.end);
const now = new Date();
const repeatInterval = window.repeat || 0;
// If start is < now and end > now, we're in maintenance
if (start <= now && end >= now) return true;
// If maintenance window was set in the past with a repeat,
// we need to advance start and end to see if we are in range
while (start < now && repeatInterval !== 0) {
start.setTime(start.getTime() + repeatInterval);
end.setTime(end.getTime() + repeatInterval);
if (start <= now && end >= now) {
return true;
}
}
return false;
}
return acc;
}, false);
return maintenanceWindowIsActive;
}
}
export default SuperSimpleQueueHelper;
+38 -52
View File
@@ -1,56 +1,42 @@
<!-- name, email, url -->
<mjml>
<mj-head>
<mj-font
name="Roboto"
href="https://fonts.googleapis.com/css?family=Roboto:300,500"
></mj-font>
<mj-attributes>
<mj-all font-family="Roboto, Helvetica, sans-serif"></mj-all>
<mj-text
font-weight="300"
font-size="16px"
color="#616161"
line-height="24px"
></mj-text>
<mj-section padding="0px"></mj-section>
</mj-attributes>
</mj-head>
<mj-body>
<mj-section padding="20px 0">
<mj-column width="100%">
<mj-text
align="left"
font-size="10px"
>
Message from Checkmate Service
</mj-text>
</mj-column>
</mj-section>
<mj-section>
<mj-column width="100%">
<mj-text>
<p>Hello {{ name }}!</p>
<p>
You are receiving this email because a password reset request has been made for {{ email }}. Please use the link below on the site to
reset your password.
</p>
<a href="{{url}}">Reset Password</a>
<p>If you didn't request this, please ignore this email.</p>
<mj-head>
<mj-font name="Roboto" href="https://fonts.googleapis.com/css?family=Roboto:300,500"></mj-font>
<mj-attributes>
<mj-all font-family="Roboto, Helvetica, sans-serif"></mj-all>
<mj-text font-weight="300" font-size="16px" color="#616161" line-height="24px"></mj-text>
<mj-section padding="0px"></mj-section>
</mj-attributes>
</mj-head>
<mj-body>
<mj-section padding="20px 0">
<mj-column width="100%">
<mj-text align="left" font-size="10px">
Message from Checkmate Service
</mj-text>
</mj-column>
</mj-section>
<mj-section>
<mj-column width="100%">
<mj-text>
<p>Hello {{ name }}!</p>
<p>
You are receiving this email because a password reset request has been made for {{ email }}. Please use the link below on the site to
reset your password.
</p>
<a href="{{url}}">Reset Password</a>
<p>If you didn't request this, please ignore this email.</p>
<p>Thank you.</p>
</mj-text>
</mj-column>
<mj-column width="100%">
<mj-divider
border-width="1px"
border-color="#E0E0E0"
></mj-divider>
<mj-text font-size="12px">
<p>This email was sent by Checkmate.</p>
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
<p>Thank you.</p>
</mj-text>
</mj-column>
<mj-column width="100%">
<mj-divider border-width="1px" border-color="#E0E0E0"></mj-divider>
<mj-text font-size="12px">
<p>This email was sent by Checkmate.</p>
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
+41 -67
View File
@@ -1,68 +1,42 @@
<mjml>
<mj-head>
<mj-font
name="Roboto"
href="https://fonts.googleapis.com/css?family=Roboto:300,500"
></mj-font>
<mj-attributes>
<mj-all font-family="Roboto, Helvetica, sans-serif"></mj-all>
<mj-text
font-weight="300"
font-size="16px"
color="#616161"
line-height="24px"
></mj-text>
<mj-section padding="0px"></mj-section>
</mj-attributes>
</mj-head>
<mj-body>
<mj-section padding="20px 0">
<mj-column width="100%">
<mj-text
align="left"
font-size="10px"
>
Message from Checkmate Service
</mj-text>
</mj-column>
<mj-column
width="45%"
padding-top="20px"
>
<mj-text
align="center"
font-weight="500"
padding="0px"
font-size="18px"
color="red"
>
{{ monitor }} is down
</mj-text>
<mj-divider
border-width="2px"
border-color="#616161"
></mj-divider>
</mj-column>
</mj-section>
<mj-section>
<mj-column width="100%">
<mj-text>
<p>Hello {{ name }}!</p>
<p>We detected an incident on one of your monitors. Your service is currently down. We'll send a message to you once it is up again.</p>
<p><b>Monitor name:</b> {{ monitor }}</p>
<p><b>URL:</b> {{ url }}</p>
</mj-text>
</mj-column>
<mj-column width="100%">
<mj-divider
border-width="1px"
border-color="#E0E0E0"
></mj-divider>
<mj-button background-color="#1570EF"> View incident details </mj-button>
<mj-text font-size="12px">
<p>This email was sent by Checkmate.</p>
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
<mj-head>
<mj-font name="Roboto" href="https://fonts.googleapis.com/css?family=Roboto:300,500"></mj-font>
<mj-attributes>
<mj-all font-family="Roboto, Helvetica, sans-serif"></mj-all>
<mj-text font-weight="300" font-size="16px" color="#616161" line-height="24px"></mj-text>
<mj-section padding="0px"></mj-section>
</mj-attributes>
</mj-head>
<mj-body>
<mj-section padding="20px 0">
<mj-column width="100%">
<mj-text align="left" font-size="10px">
Message from Checkmate Service
</mj-text>
</mj-column>
<mj-column width="45%" padding-top="20px">
<mj-text align="center" font-weight="500" padding="0px" font-size="18px" color="red">
{{ monitor }} is down
</mj-text>
<mj-divider border-width="2px" border-color="#616161"></mj-divider>
</mj-column>
</mj-section>
<mj-section>
<mj-column width="100%">
<mj-text>
<p>Hello {{ name }}!</p>
<p>We detected an incident on one of your monitors. Your service is currently down. We'll send a message to you once it is up again.</p>
<p><b>Monitor name:</b> {{ monitor }}</p>
<p><b>URL:</b> {{ url }}</p>
</mj-text>
</mj-column>
<mj-column width="100%">
<mj-divider border-width="1px" border-color="#E0E0E0"></mj-divider>
<mj-button background-color="#1570EF"> View incident details </mj-button>
<mj-text font-size="12px">
<p>This email was sent by Checkmate.</p>
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
+35 -49
View File
@@ -1,51 +1,37 @@
<!-- name -->
<mjml>
<mj-head>
<mj-font
name="Roboto"
href="https://fonts.googleapis.com/css?family=Roboto:300,500"
></mj-font>
<mj-attributes>
<mj-all font-family="Roboto, Helvetica, sans-serif"></mj-all>
<mj-text
font-weight="300"
font-size="16px"
color="#616161"
line-height="24px"
></mj-text>
<mj-section padding="0px"></mj-section>
</mj-attributes>
</mj-head>
<mj-body>
<mj-section padding="20px 0">
<mj-column width="100%">
<mj-text
align="left"
font-size="10px"
>
Message from Checkmate Service
</mj-text>
</mj-column>
</mj-section>
<mj-section>
<mj-column width="100%">
<mj-text>
<p>Hello {{ name }}!</p>
<p>Thank you for trying out Checkmate! We developed it with great care to meet our own needs, and we're excited to share it with you.</p>
<p>Checkmate is an automated way of checking whether a service such as a website or an application is available or not.</p>
<p>We hope you find our service as valuable as we do.</p>
<p>Thank you.</p>
</mj-text>
</mj-column>
<mj-column width="100%">
<mj-divider
border-width="1px"
border-color="#E0E0E0"
></mj-divider>
<mj-text font-size="12px">
<p>This email was sent by Checkmate.</p>
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
<mj-head>
<mj-font name="Roboto" href="https://fonts.googleapis.com/css?family=Roboto:300,500"></mj-font>
<mj-attributes>
<mj-all font-family="Roboto, Helvetica, sans-serif"></mj-all>
<mj-text font-weight="300" font-size="16px" color="#616161" line-height="24px"></mj-text>
<mj-section padding="0px"></mj-section>
</mj-attributes>
</mj-head>
<mj-body>
<mj-section padding="20px 0">
<mj-column width="100%">
<mj-text align="left" font-size="10px">
Message from Checkmate Service
</mj-text>
</mj-column>
</mj-section>
<mj-section>
<mj-column width="100%">
<mj-text>
<p>Hello {{ name }}!</p>
<p>Thank you for trying out Checkmate! We developed it with great care to meet our own needs, and we're excited to share it with you.</p>
<p>Checkmate is an automated way of checking whether a service such as a website or an application is available or not.</p>
<p>We hope you find our service as valuable as we do.</p>
<p>Thank you.</p>
</mj-text>
</mj-column>
<mj-column width="100%">
<mj-divider border-width="1px" border-color="#E0E0E0"></mj-divider>
<mj-text font-size="12px">
<p>This email was sent by Checkmate.</p>
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>