diff --git a/package-lock.json b/package-lock.json index 57576cc..1678efe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -161,6 +161,15 @@ "integrity": "sha512-uM4mnmsIIPK/yeO+42F2RQhGUIs39K2RFmugcJANppXe6J1nvH87PvzPZYpza7Xhhs8Yn9yIAVdLZ84z61+0xQ==", "dev": true }, + "@types/node-cron": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-2.0.3.tgz", + "integrity": "sha512-gwBBGeY2XeYBLE0R01K9Sm2hvNcPGmoloL6aqthA3QmBB1GYXTHIJ42AGZL7bdXBRiwbRV8b6NB5iKpl20R3gw==", + "dev": true, + "requires": { + "@types/tz-offset": "*" + } + }, "@types/nodemailer": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.0.tgz", @@ -207,6 +216,12 @@ "@types/node": "*" } }, + "@types/tz-offset": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/@types/tz-offset/-/tz-offset-0.0.0.tgz", + "integrity": "sha512-XLD/llTSB6EBe3thkN+/I0L+yCTB6sjrcVovQdx2Cnl6N6bTzHmwe/J8mWnsXFgxLrj/emzdv8IR4evKYG2qxQ==", + "dev": true + }, "Base64": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/Base64/-/Base64-0.2.1.tgz", diff --git a/package.json b/package.json index e760ac1..0558eba 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "@types/graceful-fs": "^4.1.3", "@types/lodash": "^4.14.149", "@types/node": "^13.11.0", + "@types/node-cron": "^2.0.3", "@types/nodemailer": "^6.4.0", "@types/sqlite3": "^3.1.6", "bower": "^1.8.8", diff --git a/server/jobs/FixNodeFilenamesJob.js b/server/jobs/FixNodeFilenamesJob.js deleted file mode 100644 index b47bf43..0000000 --- a/server/jobs/FixNodeFilenamesJob.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -const Logger = require('../logger') -const NodeService = require('../services/nodeService') - -module.exports = { - description: 'Makes sure node files (holding fastd key, name, etc.) are correctly named.', - - run: function (callback) { - NodeService.fixNodeFilenames(function (err) { - if (err) { - Logger.tag('nodes', 'fix-filenames').error('Error fixing filenames:', err); - } - - callback(); - }); - } -} diff --git a/server/jobs/FixNodeFilenamesJob.ts b/server/jobs/FixNodeFilenamesJob.ts new file mode 100644 index 0000000..0c4a03e --- /dev/null +++ b/server/jobs/FixNodeFilenamesJob.ts @@ -0,0 +1,22 @@ +import Logger from "../logger"; +import NodeService from "../services/nodeService"; + +export default { + name: 'FixNodeFilenamesJob', + description: 'Makes sure node files (holding fastd key, name, etc.) are correctly named.', + + run: (): Promise => { + return new Promise( + (resolve, reject) => { + NodeService.fixNodeFilenames((err: any): void => { + if (err) { + Logger.tag('nodes', 'fix-filenames').error('Error fixing filenames:', err); + return reject(err); + } + + resolve(); + }); + } + ); + } +} diff --git a/server/jobs/MailQueueJob.js b/server/jobs/MailQueueJob.js deleted file mode 100644 index 5f69763..0000000 --- a/server/jobs/MailQueueJob.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -const Logger = require('../logger') -const MailService = require('../services/mailService') - -module.exports = { - description: 'Send pending emails (up to 5 attempts in case of failures).', - - run: function (callback) { - MailService.sendPendingMails(function (err) { - if (err) { - Logger.tag('mail', 'queue').error('Error sending pending mails:', err); - } - - callback(); - }); - } -} diff --git a/server/jobs/MailQueueJob.ts b/server/jobs/MailQueueJob.ts new file mode 100644 index 0000000..41a8e25 --- /dev/null +++ b/server/jobs/MailQueueJob.ts @@ -0,0 +1,22 @@ +import Logger from "../logger" +import MailService from "../services/mailService" + +export default { + name: 'MailQueueJob', + description: 'Send pending emails (up to 5 attempts in case of failures).', + + run: (): Promise => { + return new Promise( + (resolve, reject) => { + MailService.sendPendingMails((err: any): void => { + if (err) { + Logger.tag('mail', 'queue').error('Error sending pending mails:', err); + return reject(err); + } + + resolve(); + }); + } + ) + } +} diff --git a/server/jobs/MonitoringMailsSendingJob.js b/server/jobs/MonitoringMailsSendingJob.js deleted file mode 100644 index 973ce74..0000000 --- a/server/jobs/MonitoringMailsSendingJob.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -const Logger = require('../logger') -const MonitoringService = require('../services/monitoringService') - -module.exports = { - description: 'Sends monitoring emails depending on the monitoring state of nodes retrieved by the NodeInformationRetrievalJob.', - - run: function (callback) { - MonitoringService.sendMonitoringMails(function (err) { - if (err) { - Logger.tag('monitoring', 'mail-sending').error('Error sending monitoring mails:', err); - } - - callback(); - }); - } -} diff --git a/server/jobs/MonitoringMailsSendingJob.ts b/server/jobs/MonitoringMailsSendingJob.ts new file mode 100644 index 0000000..d85b594 --- /dev/null +++ b/server/jobs/MonitoringMailsSendingJob.ts @@ -0,0 +1,20 @@ +import Logger from "../logger"; +import MonitoringService from "../services/monitoringService"; + +export default { + name: 'MonitoringMailsSendingJob', + description: 'Sends monitoring emails depending on the monitoring state of nodes retrieved by the NodeInformationRetrievalJob.', + + run: (): Promise => { + return new Promise((resolve, reject) => { + MonitoringService.sendMonitoringMails((err: any): void => { + if (err) { + Logger.tag('monitoring', 'mail-sending').error('Error sending monitoring mails:', err); + return reject(err); + } + + resolve(); + }); + }); + } +}; diff --git a/server/jobs/NodeInformationRetrievalJob.js b/server/jobs/NodeInformationRetrievalJob.js deleted file mode 100644 index f0914e9..0000000 --- a/server/jobs/NodeInformationRetrievalJob.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -const Logger = require('../logger') -const MonitoringService = require('../services/monitoringService') - -module.exports = { - description: 'Fetches the nodes.json and calculates and stores the monitoring / online status for registered nodes.', - - run: function (callback) { - MonitoringService.retrieveNodeInformation(function (err) { - if (err) { - Logger.tag('monitoring', 'information-retrieval').error('Error retrieving node data:', err); - } - - callback(); - }); - } -} diff --git a/server/jobs/NodeInformationRetrievalJob.ts b/server/jobs/NodeInformationRetrievalJob.ts new file mode 100644 index 0000000..cea59c5 --- /dev/null +++ b/server/jobs/NodeInformationRetrievalJob.ts @@ -0,0 +1,22 @@ +import Logger from "../logger"; +import MonitoringService from "../services/monitoringService"; + +export default { + name: 'NodeInformationRetrievalJob', + description: 'Fetches the nodes.json and calculates and stores the monitoring / online status for registered nodes.', + + run: (): Promise => { + return new Promise( + (resolve, reject) => { + MonitoringService.retrieveNodeInformation((err: any): void => { + if (err) { + Logger.tag('monitoring', 'information-retrieval').error('Error retrieving node data:', err); + return reject(err); + } + + resolve(); + }); + } + ); + } +}; diff --git a/server/jobs/OfflineNodesDeletionJob.js b/server/jobs/OfflineNodesDeletionJob.js deleted file mode 100644 index 8b0ca73..0000000 --- a/server/jobs/OfflineNodesDeletionJob.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -const Logger = require('../logger') -const MonitoringService = require('../services/monitoringService') - -module.exports = { - description: 'Delete nodes that are offline for more than 100 days.', - - run: function (callback) { - MonitoringService.deleteOfflineNodes(function (err) { - if (err) { - Logger.tag('nodes', 'delete-offline').error('Error deleting offline nodes:', err); - } - - callback(); - }); - } -} diff --git a/server/jobs/OfflineNodesDeletionJob.ts b/server/jobs/OfflineNodesDeletionJob.ts new file mode 100644 index 0000000..db8476d --- /dev/null +++ b/server/jobs/OfflineNodesDeletionJob.ts @@ -0,0 +1,20 @@ +import MonitoringService from "../services/monitoringService"; +import Logger from "../logger"; + +export default { + name: 'OfflineNodesDeletionJob', + description: 'Delete nodes that are offline for more than 100 days.', + + run: (): Promise => { + return new Promise((resolve, reject) => { + MonitoringService.deleteOfflineNodes((err: any): void => { + if (err) { + Logger.tag('nodes', 'delete-offline').error('Error deleting offline nodes:', err); + return reject(err); + } + + resolve(); + }); + }); + } +}; diff --git a/server/jobs/scheduler.js b/server/jobs/scheduler.js deleted file mode 100644 index 5706155..0000000 --- a/server/jobs/scheduler.js +++ /dev/null @@ -1,99 +0,0 @@ -'use strict'; - -const _ = require('lodash'); -const cron = require('node-cron'); -const glob = require('glob'); -const moment = require('moment'); - -const config = require('../config').config -const Logger = require('../logger') - -const jobFiles = glob.sync(__dirname + '/*Job.js'); -_.each(jobFiles, function (jobFile) { - require(jobFile); -}); - -const tasks = {}; - -let taskId = 1; -function nextTaskId() { - const id = taskId; - taskId += 1; - return id; -} - -function schedule(expr, jobName) { - Logger.tag('jobs').info('Scheduling job: %s %s', expr, jobName); - - var job = require(`../jobs/${jobName}`); - - if (!_.isFunction(job.run)) { - throw new Error('The job ' + jobName + ' does not provide a "run" function.'); - } - - var id = nextTaskId(); - var task = { - id: id, - name: jobName, - description: job.description, - schedule: expr, - job: job, - runningSince: false, - lastRunStarted: false, - lastRunDuration: null, - state: 'idle', - enabled: true - }; - - task.run = function () { - if (task.runningSince || !task.enabled) { - // job is still running, skip execution - return; - } - - task.runningSince = moment(); - task.lastRunStarted = task.runningSince; - task.state = 'running'; - - job.run(function () { - var now = moment(); - var duration = now.diff(task.runningSince); - Logger.tag('jobs').profile('[%sms]\t%s', duration, task.name); - - task.runningSince = false; - task.lastRunDuration = duration; - task.state = 'idle'; - }); - }; - - cron.schedule(expr, task.run); - - tasks['' + id] = task; -} - -module.exports = { - init: function () { - Logger.tag('jobs').info('Scheduling background jobs...'); - - try { - schedule('0 */1 * * * *', 'MailQueueJob'); - schedule('15 */1 * * * *', 'FixNodeFilenamesJob'); - - if (config.client.monitoring.enabled) { - schedule('30 */15 * * * *', 'NodeInformationRetrievalJob'); - schedule('45 */5 * * * *', 'MonitoringMailsSendingJob'); - schedule('0 0 3 * * *', 'OfflineNodesDeletionJob'); // every night at 3:00 - } - } - catch (error) { - Logger.tag('jobs').error('Error during scheduling of background jobs:', error); - throw error; - } - - Logger.tag('jobs').info('Scheduling of background jobs done.'); - }, - - getTasks: function () { - return tasks; - } -} diff --git a/server/jobs/scheduler.ts b/server/jobs/scheduler.ts new file mode 100644 index 0000000..40899e4 --- /dev/null +++ b/server/jobs/scheduler.ts @@ -0,0 +1,126 @@ +import cron from "node-cron"; +import moment from "moment"; + +import {config} from "../config"; +import Logger from "../logger"; + +import MailQueueJob from "./MailQueueJob"; +import FixNodeFilenamesJob from "./FixNodeFilenamesJob"; +import NodeInformationRetrievalJob from "./NodeInformationRetrievalJob"; +import MonitoringMailsSendingJob from "./MonitoringMailsSendingJob"; +import OfflineNodesDeletionJob from "./OfflineNodesDeletionJob"; + +export interface Job { + name: string, + description: string, + + run(): Promise, +} + +export enum TaskState { + IDLE = "idle", + RUNNING = "running", + FAILED = "failed", +} + +export class Task { + constructor( + public id: number, + public name: string, + public description: string, + public schedule: string, + public job: Job, + public runningSince: moment.Moment | null, + public lastRunStarted: moment.Moment | null, + public lastRunDuration: number | null, + public state: TaskState, + public enabled: true, + ) {} + + run(): void { + if (this.runningSince || !this.enabled) { + // job is still running, skip execution + return; + } + + this.runningSince = moment(); + this.lastRunStarted = this.runningSince; + this.state = TaskState.RUNNING; + + const done = (state: TaskState):void => { + const now = moment(); + const duration = now.diff(this.runningSince || now); + Logger.tag('jobs').profile('[%sms]\t%s', duration, this.name); + + this.runningSince = null; + this.lastRunDuration = duration; + this.state = state; + }; + + this.job.run().then(() => { + done(TaskState.IDLE); + }).catch((err: any) => { + Logger.tag('jobs').error("Job %s failed: %s", this.name, err); + done(TaskState.FAILED); + }); + } +} + +type Tasks = {[key: string]: Task}; + +const tasks: Tasks = {}; + +let taskId = 1; +function nextTaskId(): number { + const id = taskId; + taskId += 1; + return id; +} + +function schedule(expr: string, job: Job): void { + Logger.tag('jobs').info('Scheduling job: %s %s', expr, job.name); + + const id = nextTaskId(); + + const task = new Task( + id, + job.name, + job.description, + expr, + job, + null, + null, + null, + TaskState.IDLE, + true + ); + + cron.schedule(expr, task.run); + + tasks['' + id] = task; +} + +export function init() { + Logger.tag('jobs').info('Scheduling background jobs...'); + + try { + schedule('0 */1 * * * *', MailQueueJob); + schedule('15 */1 * * * *', FixNodeFilenamesJob); + + if (config.client.monitoring.enabled) { + schedule('30 */15 * * * *', NodeInformationRetrievalJob); + schedule('45 */5 * * * *', MonitoringMailsSendingJob); + schedule('0 0 3 * * *', OfflineNodesDeletionJob); // every night at 3:00 + } + } + catch (error) { + Logger.tag('jobs').error('Error during scheduling of background jobs:', error); + throw error; + } + + Logger.tag('jobs').info('Scheduling of background jobs done.'); +} + +export function getTasks(): Tasks { + return tasks; +} diff --git a/server/main.ts b/server/main.ts index 978bb91..ca9f9bb 100755 --- a/server/main.ts +++ b/server/main.ts @@ -2,8 +2,8 @@ import "./init" import { config } from "./config" import Logger from "./logger" import * as db from "./db/database" -import scheduler from "./jobs/scheduler" -import { init as initRouter } from "./router" +import * as scheduler from "./jobs/scheduler" +import * as router from "./router" import app from "./app" Logger.tag('main', 'startup').info('Server starting up...'); @@ -13,7 +13,7 @@ db.init() Logger.tag('main').info('Initializing...'); scheduler.init(); - initRouter(); + router.init(); app.listen(config.server.port, '::'); })