mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-05-07 13:15:52 +02:00
feat: show cronjobs in acp (#14068)
* feat: show cronjobs in acp add a wrapper for cronjobs and display in acp * test: add running to spec * test: fix running, simplify calls * test: prevent crash on stopJob
This commit is contained in:
@@ -62,6 +62,7 @@
|
|||||||
"connect-redis": "9.0.0",
|
"connect-redis": "9.0.0",
|
||||||
"cookie-parser": "1.4.7",
|
"cookie-parser": "1.4.7",
|
||||||
"cron": "4.4.0",
|
"cron": "4.4.0",
|
||||||
|
"cronstrue": "3.13.0",
|
||||||
"cropperjs": "1.6.2",
|
"cropperjs": "1.6.2",
|
||||||
"csrf-sync": "4.2.1",
|
"csrf-sync": "4.2.1",
|
||||||
"daemon": "1.1.0",
|
"daemon": "1.1.0",
|
||||||
|
|||||||
8
public/language/en-GB/admin/advanced/jobs.json
Normal file
8
public/language/en-GB/admin/advanced/jobs.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"jobs": "Jobs",
|
||||||
|
"job-name": "Job Name",
|
||||||
|
"schedule": "Schedule",
|
||||||
|
"next-run": "Next Run",
|
||||||
|
"last-duration": "Last Duration",
|
||||||
|
"running": "Running"
|
||||||
|
}
|
||||||
@@ -78,6 +78,7 @@
|
|||||||
"advanced/logs": "Logs",
|
"advanced/logs": "Logs",
|
||||||
"advanced/errors": "Errors",
|
"advanced/errors": "Errors",
|
||||||
"advanced/cache": "Cache",
|
"advanced/cache": "Cache",
|
||||||
|
"advanced/jobs": "Jobs",
|
||||||
"development/logger": "Logger",
|
"development/logger": "Logger",
|
||||||
"development/info": "Info",
|
"development/info": "Info",
|
||||||
|
|
||||||
|
|||||||
@@ -186,6 +186,8 @@ paths:
|
|||||||
$ref: 'read/admin/advanced/cache.yaml'
|
$ref: 'read/admin/advanced/cache.yaml'
|
||||||
/api/admin/advanced/cache/dump:
|
/api/admin/advanced/cache/dump:
|
||||||
$ref: 'read/admin/advanced/cache/dump.yaml'
|
$ref: 'read/admin/advanced/cache/dump.yaml'
|
||||||
|
/api/admin/advanced/jobs:
|
||||||
|
$ref: 'read/admin/advanced/jobs.yaml'
|
||||||
/api/admin/development/logger:
|
/api/admin/development/logger:
|
||||||
$ref: 'read/admin/development/logger.yaml'
|
$ref: 'read/admin/development/logger.yaml'
|
||||||
/api/admin/development/info:
|
/api/admin/development/info:
|
||||||
|
|||||||
35
public/openapi/read/admin/advanced/jobs.yaml
Normal file
35
public/openapi/read/admin/advanced/jobs.yaml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- admin
|
||||||
|
summary: Get cron job info
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: ""
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
jobs:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
cronTime:
|
||||||
|
type: string
|
||||||
|
cronTimeHuman:
|
||||||
|
type: string
|
||||||
|
nextRun:
|
||||||
|
type: integer
|
||||||
|
nextRunISO:
|
||||||
|
type: integer
|
||||||
|
duration:
|
||||||
|
type: integer
|
||||||
|
durationReadable:
|
||||||
|
type: string
|
||||||
|
running:
|
||||||
|
type: boolean
|
||||||
|
- $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
|
||||||
@@ -1,46 +1,55 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const winston = require('winston');
|
|
||||||
const { CronJob } = require('cron');
|
|
||||||
|
|
||||||
const db = require('../database');
|
const db = require('../database');
|
||||||
const meta = require('../meta');
|
const meta = require('../meta');
|
||||||
const topics = require('../topics');
|
const topics = require('../topics');
|
||||||
const utils = require('../utils');
|
const utils = require('../utils');
|
||||||
|
const cron = require('../cron');
|
||||||
|
|
||||||
const activitypub = module.parent.exports;
|
const activitypub = module.parent.exports;
|
||||||
|
|
||||||
const Jobs = module.exports;
|
const Jobs = module.exports;
|
||||||
|
|
||||||
Jobs.start = () => {
|
Jobs.start = async () => {
|
||||||
activitypub.helpers.log('[activitypub/jobs] Registering jobs.');
|
activitypub.helpers.log('[activitypub/jobs] Registering jobs.');
|
||||||
async function tryCronJob(method) {
|
async function tryCronJob(method) {
|
||||||
if (!meta.config.activitypubEnabled) {
|
if (meta.config.activityPubEnabled) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await method();
|
await method();
|
||||||
} catch (err) {
|
|
||||||
winston.error(err.stack);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
new CronJob('0 0 * * *', async () => {
|
|
||||||
await tryCronJob(async () => {
|
|
||||||
await activitypub.notes.prune();
|
|
||||||
await db.sortedSetsRemoveRangeByScore(['activities:datetime'], '-inf', Date.now() - 604800000);
|
|
||||||
});
|
|
||||||
}, null, true, null, null, false); // change last argument to true for debugging
|
|
||||||
|
|
||||||
new CronJob('*/30 * * * *', async () => {
|
await cron.addJob({
|
||||||
await tryCronJob(activitypub.actors.prune);
|
name: 'ap:notes:prune',
|
||||||
}, null, true, null, null, false); // change last argument to true for debugging
|
cronTime: '0 0 * * *',
|
||||||
|
runOnInit: false,
|
||||||
|
onTick: async () => {
|
||||||
|
await tryCronJob(async () => {
|
||||||
|
await activitypub.notes.prune();
|
||||||
|
await db.sortedSetsRemoveRangeByScore(['activities:datetime'], '-inf', Date.now() - 604800000);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
new CronJob('0 * * * * *', async () => {
|
await cron.addJob({
|
||||||
await tryCronJob(retryFailedMessages);
|
name: 'ap:actors:prune',
|
||||||
}, null, true, null, null, false); // change last argument to true for debugging
|
cronTime: '*/30 * * * *',
|
||||||
|
runOnInit: false,
|
||||||
|
onTick: async () => await tryCronJob(activitypub.actors.prune),
|
||||||
|
});
|
||||||
|
|
||||||
new CronJob('15 * * * *', async () => {
|
await cron.addJob({
|
||||||
await tryCronJob(backfill);
|
name: 'ap:retry:send',
|
||||||
}, null, true, null, null, false); // change last argument to true for debugging
|
cronTime: '0 * * * * *',
|
||||||
|
runOnInit: false,
|
||||||
|
onTick: async () => await tryCronJob(retryFailedMessages),
|
||||||
|
});
|
||||||
|
|
||||||
|
await cron.addJob({
|
||||||
|
name: 'ap:backfill',
|
||||||
|
cronTime: '15 * * * *',
|
||||||
|
runOnInit: false,
|
||||||
|
onTick: async () => await tryCronJob(backfill),
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
async function retryFailedMessages() {
|
async function retryFailedMessages() {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const cronJob = require('cron').CronJob;
|
|
||||||
const winston = require('winston');
|
const winston = require('winston');
|
||||||
const nconf = require('nconf');
|
const nconf = require('nconf');
|
||||||
const util = require('util');
|
const util = require('util');
|
||||||
@@ -12,6 +11,7 @@ const db = require('./database');
|
|||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
const plugins = require('./plugins');
|
const plugins = require('./plugins');
|
||||||
const pubsub = require('./pubsub');
|
const pubsub = require('./pubsub');
|
||||||
|
const cron = require('./cron');
|
||||||
|
|
||||||
const Analytics = module.exports;
|
const Analytics = module.exports;
|
||||||
|
|
||||||
@@ -32,19 +32,28 @@ const runJobs = nconf.get('runJobs');
|
|||||||
Analytics.pause = false;
|
Analytics.pause = false;
|
||||||
|
|
||||||
Analytics.init = async function () {
|
Analytics.init = async function () {
|
||||||
new cronJob('*/10 * * * * *', (async () => {
|
await cron.addJob({
|
||||||
if (Analytics.pause) return;
|
name: 'analytics:publish',
|
||||||
publishLocalAnalytics();
|
cronTime: '*/10 * * * * *',
|
||||||
if (runJobs) {
|
runOnAllNodes: true,
|
||||||
await sleep(2000);
|
onTick: async () => {
|
||||||
await Analytics.writeData();
|
if (Analytics.pause) return;
|
||||||
}
|
publishLocalAnalytics();
|
||||||
}), null, true);
|
if (runJobs) {
|
||||||
|
await sleep(2000);
|
||||||
|
await Analytics.writeData();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (runJobs) {
|
if (runJobs) {
|
||||||
new cronJob('*/30 * * * *', (async () => {
|
await cron.addJob({
|
||||||
await db.sortedSetsRemoveRangeByScore(['ip:recent'], '-inf', Date.now() - 172800000);
|
name: 'prune:ip:recent',
|
||||||
}), null, true);
|
cronTime: '*/30 * * * *',
|
||||||
|
onTick: async () => {
|
||||||
|
await db.sortedSetsRemoveRangeByScore(['ip:recent'], '-inf', Date.now() - 172800000);
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (runJobs) {
|
if (runJobs) {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const adminController = {
|
|||||||
errors: require('./admin/errors'),
|
errors: require('./admin/errors'),
|
||||||
database: require('./admin/database'),
|
database: require('./admin/database'),
|
||||||
cache: require('./admin/cache'),
|
cache: require('./admin/cache'),
|
||||||
|
jobs: require('./admin/jobs'),
|
||||||
plugins: require('./admin/plugins'),
|
plugins: require('./admin/plugins'),
|
||||||
settings: require('./admin/settings'),
|
settings: require('./admin/settings'),
|
||||||
logger: require('./admin/logger'),
|
logger: require('./admin/logger'),
|
||||||
|
|||||||
12
src/controllers/admin/jobs.js
Normal file
12
src/controllers/admin/jobs.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const jobsController = module.exports;
|
||||||
|
|
||||||
|
const cron = require('../../cron');
|
||||||
|
|
||||||
|
jobsController.get = async function (req, res) {
|
||||||
|
const jobs = await cron.getJobs();
|
||||||
|
|
||||||
|
res.render('admin/advanced/jobs', { jobs });
|
||||||
|
};
|
||||||
|
|
||||||
98
src/cron.js
Normal file
98
src/cron.js
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const nconf = require('nconf');
|
||||||
|
const { CronJob } = require('cron');
|
||||||
|
const cronstrue = require('cronstrue');
|
||||||
|
const winston = require('winston');
|
||||||
|
|
||||||
|
const db = require('./database');
|
||||||
|
const utils = require('./utils');
|
||||||
|
|
||||||
|
const jobs = Object.create(null);
|
||||||
|
|
||||||
|
exports.deleteJobs = async function () {
|
||||||
|
const jobs = await db.getSortedSetRange('cronJobs', 0, -1);
|
||||||
|
await db.deleteAll(jobs.map(name => `cronJob:${name}`));
|
||||||
|
await db.delete('cronJobs');
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.addJob = async function (options) {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
cronTime,
|
||||||
|
onTick,
|
||||||
|
onComplete = null,
|
||||||
|
start = true,
|
||||||
|
runOnInit = false,
|
||||||
|
runOnAllNodes = false,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const isJobEnabled = nconf.get('runJobs');
|
||||||
|
|
||||||
|
if (!isJobEnabled && !runOnAllNodes) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!name || !cronTime || typeof onTick !== 'function') {
|
||||||
|
throw new Error('[cron] Invalid options');
|
||||||
|
}
|
||||||
|
if (Object.hasOwn(jobs, name)) {
|
||||||
|
throw new Error('[cron] Job with that name already exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
const job = new CronJob(cronTime, async function () {
|
||||||
|
const start = Date.now();
|
||||||
|
try {
|
||||||
|
await db.setObjectField(`cronJob:${name}`, 'running', 1);
|
||||||
|
await onTick();
|
||||||
|
await db.deleteObjectField(`cronJob:${name}`, 'lastError');
|
||||||
|
} catch (err) {
|
||||||
|
winston.error(`[cron] ${err.stack}`);
|
||||||
|
await db.setObjectField(`cronJob:${name}`, 'lastError', err.stack);
|
||||||
|
} finally {
|
||||||
|
await db.setObject(`cronJob:${name}`, {
|
||||||
|
running: 0,
|
||||||
|
duration: Date.now() - start,
|
||||||
|
nextRun: job.nextDate().toMillis(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, onComplete, start, null, null, runOnInit);
|
||||||
|
|
||||||
|
jobs[name] = job;
|
||||||
|
|
||||||
|
await db.sortedSetAdd('cronJobs', Date.now(), name);
|
||||||
|
await db.setObject(`cronJob:${name}`, {
|
||||||
|
name,
|
||||||
|
cronTime,
|
||||||
|
cronTimeHuman: cronstrue.toString(cronTime),
|
||||||
|
nextRun: job.nextDate().toMillis(),
|
||||||
|
running: runOnInit ? 1 : 0,
|
||||||
|
});
|
||||||
|
winston.verbose(`[cron/jobs] Registered job: ${name} (${cronTime})`);
|
||||||
|
return job;
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.getJobs = async function () {
|
||||||
|
const jobNames = await db.getSortedSetRange('cronJobs', 0, -1);
|
||||||
|
const jobs = await db.getObjects(jobNames.map(name => `cronJob:${name}`));
|
||||||
|
jobs.forEach((job) => {
|
||||||
|
if (job) {
|
||||||
|
job.running = parseInt(job.running, 10) === 1;
|
||||||
|
job.duration = job.duration || 0;
|
||||||
|
job.durationReadable = formatDuration(job.duration);
|
||||||
|
job.nextRunISO = utils.toISOString(job.nextRun);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
jobs.sort((a, b) => b.cronTimeHuman.localeCompare(a.cronTimeHuman));
|
||||||
|
return jobs;
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatDuration(ms) {
|
||||||
|
const totalSeconds = Math.floor(ms / 1000);
|
||||||
|
const minutes = Math.floor(totalSeconds / 60);
|
||||||
|
const seconds = totalSeconds % 60;
|
||||||
|
if (minutes > 0) {
|
||||||
|
return `${minutes}m${String(seconds).padStart(2, '0')}s`;
|
||||||
|
}
|
||||||
|
return `${seconds}s`;
|
||||||
|
}
|
||||||
@@ -3,13 +3,13 @@
|
|||||||
const nconf = require('nconf');
|
const nconf = require('nconf');
|
||||||
const winston = require('winston');
|
const winston = require('winston');
|
||||||
const validator = require('validator');
|
const validator = require('validator');
|
||||||
const cronJob = require('cron').CronJob;
|
|
||||||
const { setTimeout } = require('timers/promises');
|
const { setTimeout } = require('timers/promises');
|
||||||
|
|
||||||
const db = require('../database');
|
const db = require('../database');
|
||||||
const analytics = require('../analytics');
|
const analytics = require('../analytics');
|
||||||
const pubsub = require('../pubsub');
|
const pubsub = require('../pubsub');
|
||||||
const utils = require('../utils');
|
const utils = require('../utils');
|
||||||
|
const cron = require('../cron');
|
||||||
|
|
||||||
const Errors = module.exports;
|
const Errors = module.exports;
|
||||||
|
|
||||||
@@ -19,13 +19,18 @@ let counters = {};
|
|||||||
let total = {};
|
let total = {};
|
||||||
|
|
||||||
Errors.init = async function () {
|
Errors.init = async function () {
|
||||||
new cronJob('0 * * * * *', async () => {
|
await cron.addJob({
|
||||||
publishLocalErrors();
|
name: 'errors:publish',
|
||||||
if (runJobs) {
|
cronTime: '0 * * * * *',
|
||||||
await setTimeout(2000);
|
runOnAllNodes: true,
|
||||||
await Errors.writeData();
|
onTick: async () => {
|
||||||
}
|
publishLocalErrors();
|
||||||
}, null, true);
|
if (runJobs) {
|
||||||
|
await setTimeout(2000);
|
||||||
|
await Errors.writeData();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (runJobs) {
|
if (runJobs) {
|
||||||
pubsub.on('errors:publish', (data) => {
|
pubsub.on('errors:publish', (data) => {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const winston = require('winston');
|
const winston = require('winston');
|
||||||
const cron = require('cron').CronJob;
|
|
||||||
const nconf = require('nconf');
|
const nconf = require('nconf');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
@@ -18,6 +17,7 @@ const plugins = require('./plugins');
|
|||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
const emailer = require('./emailer');
|
const emailer = require('./emailer');
|
||||||
const ttlCache = require('./cache/ttl');
|
const ttlCache = require('./cache/ttl');
|
||||||
|
const cron = require('./cron');
|
||||||
|
|
||||||
const Notifications = module.exports;
|
const Notifications = module.exports;
|
||||||
|
|
||||||
@@ -73,9 +73,13 @@ Notifications.getAllNotificationTypes = async function () {
|
|||||||
return results.types.concat(results.privilegedTypes);
|
return results.types.concat(results.privilegedTypes);
|
||||||
};
|
};
|
||||||
|
|
||||||
Notifications.startJobs = function () {
|
Notifications.startJobs = async function () {
|
||||||
winston.verbose('[notifications.init] Registering jobs.');
|
winston.verbose('[notifications.init] Registering jobs.');
|
||||||
new cron('*/30 * * * *', Notifications.prune, null, true);
|
await cron.addJob({
|
||||||
|
name: 'notifications:prune',
|
||||||
|
cronTime: '*/30 * * * *',
|
||||||
|
onTick: Notifications.prune,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
Notifications.get = async function (nid) {
|
Notifications.get = async function (nid) {
|
||||||
|
|||||||
@@ -3,18 +3,20 @@
|
|||||||
const nconf = require('nconf');
|
const nconf = require('nconf');
|
||||||
const winston = require('winston');
|
const winston = require('winston');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const cronJob = require('cron').CronJob;
|
|
||||||
|
|
||||||
const request = require('../request');
|
const request = require('../request');
|
||||||
|
const cron = require('../cron');
|
||||||
const pkg = require('../../package.json');
|
const pkg = require('../../package.json');
|
||||||
|
|
||||||
const meta = require('../meta');
|
const meta = require('../meta');
|
||||||
|
|
||||||
module.exports = function (Plugins) {
|
module.exports = function (Plugins) {
|
||||||
Plugins.startJobs = function () {
|
Plugins.startJobs = async function () {
|
||||||
new cronJob('0 0 0 * * *', (async () => {
|
await cron.addJob({
|
||||||
await Plugins.submitUsageData();
|
name: 'plugins:submitUsageData',
|
||||||
}), null, true);
|
cronTime: '0 0 0 * * *',
|
||||||
|
onTick: Plugins.submitUsageData,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
Plugins.submitUsageData = async function () {
|
Plugins.submitUsageData = async function () {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ const path = require('path');
|
|||||||
const winston = require('winston');
|
const winston = require('winston');
|
||||||
const mime = require('mime');
|
const mime = require('mime');
|
||||||
const validator = require('validator');
|
const validator = require('validator');
|
||||||
const cronJob = require('cron').CronJob;
|
|
||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
|
|
||||||
const db = require('../database');
|
const db = require('../database');
|
||||||
@@ -16,6 +15,7 @@ const user = require('../user');
|
|||||||
const topics = require('../topics');
|
const topics = require('../topics');
|
||||||
const file = require('../file');
|
const file = require('../file');
|
||||||
const meta = require('../meta');
|
const meta = require('../meta');
|
||||||
|
const cron = require('../cron');
|
||||||
|
|
||||||
module.exports = function (Posts) {
|
module.exports = function (Posts) {
|
||||||
Posts.uploads = {};
|
Posts.uploads = {};
|
||||||
@@ -30,18 +30,26 @@ module.exports = function (Posts) {
|
|||||||
return fullPath.startsWith(pathPrefix) && await file.exists(fullPath) ? filePath : false;
|
return fullPath.startsWith(pathPrefix) && await file.exists(fullPath) ? filePath : false;
|
||||||
}))).filter(Boolean);
|
}))).filter(Boolean);
|
||||||
|
|
||||||
const runJobs = nconf.get('runJobs');
|
Posts.uploads.startJobs = async function () {
|
||||||
if (runJobs) {
|
const runJobs = nconf.get('runJobs');
|
||||||
new cronJob('0 2 * * 0', async () => {
|
if (!runJobs) {
|
||||||
const orphans = await Posts.uploads.cleanOrphans();
|
return;
|
||||||
if (orphans.length) {
|
}
|
||||||
winston.info(`[posts/uploads] Deleting ${orphans.length} orphaned uploads...`);
|
|
||||||
orphans.forEach((relPath) => {
|
await cron.addJob({
|
||||||
process.stdout.write(`${chalk.red(' - ')} ${relPath}`);
|
name: 'posts:uploads:cleanupOrphans',
|
||||||
});
|
cronTime: '0 2 * * 0',
|
||||||
}
|
onTick: async () => {
|
||||||
}, null, true);
|
const orphans = await Posts.uploads.cleanOrphans();
|
||||||
}
|
if (orphans.length) {
|
||||||
|
winston.info(`[posts/uploads] Deleting ${orphans.length} orphaned uploads...`);
|
||||||
|
orphans.forEach((relPath) => {
|
||||||
|
process.stdout.write(`${chalk.red(' - ')} ${relPath}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
Posts.uploads.sync = async function (pid) {
|
Posts.uploads.sync = async function (pid) {
|
||||||
// Scans a post's content and updates sorted set of uploads
|
// Scans a post's content and updates sorted set of uploads
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ module.exports = function (app, name, middleware, controllers) {
|
|||||||
helpers.setupAdminPageRoute(app, `/${name}/advanced/errors`, middlewares, controllers.admin.errors.get);
|
helpers.setupAdminPageRoute(app, `/${name}/advanced/errors`, middlewares, controllers.admin.errors.get);
|
||||||
helpers.setupAdminPageRoute(app, `/${name}/advanced/errors/export`, middlewares, controllers.admin.errors.export);
|
helpers.setupAdminPageRoute(app, `/${name}/advanced/errors/export`, middlewares, controllers.admin.errors.export);
|
||||||
helpers.setupAdminPageRoute(app, `/${name}/advanced/cache`, middlewares, controllers.admin.cache.get);
|
helpers.setupAdminPageRoute(app, `/${name}/advanced/cache`, middlewares, controllers.admin.cache.get);
|
||||||
|
helpers.setupAdminPageRoute(app, `/${name}/advanced/jobs`, middlewares, controllers.admin.jobs.get);
|
||||||
|
|
||||||
helpers.setupAdminPageRoute(app, `/${name}/development/logger`, middlewares, controllers.admin.logger.get);
|
helpers.setupAdminPageRoute(app, `/${name}/development/logger`, middlewares, controllers.admin.logger.get);
|
||||||
helpers.setupAdminPageRoute(app, `/${name}/development/info`, middlewares, controllers.admin.info.get);
|
helpers.setupAdminPageRoute(app, `/${name}/development/info`, middlewares, controllers.admin.info.get);
|
||||||
|
|||||||
12
src/start.js
12
src/start.js
@@ -35,11 +35,13 @@ start.start = async function () {
|
|||||||
await sockets.init(webserver.server);
|
await sockets.init(webserver.server);
|
||||||
|
|
||||||
if (nconf.get('runJobs')) {
|
if (nconf.get('runJobs')) {
|
||||||
require('./notifications').startJobs();
|
await require('./cron').deleteJobs();
|
||||||
require('./user').startJobs();
|
await require('./notifications').startJobs();
|
||||||
require('./plugins').startJobs();
|
await require('./user').startJobs();
|
||||||
require('./topics').scheduled.startJobs();
|
await require('./plugins').startJobs();
|
||||||
require('./activitypub').jobs.start();
|
await require('./topics').scheduled.startJobs();
|
||||||
|
await require('./posts').uploads.startJobs();
|
||||||
|
await require('./activitypub').jobs.start();
|
||||||
await db.delete('locks');
|
await db.delete('locks');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const winston = require('winston');
|
const winston = require('winston');
|
||||||
const { CronJob } = require('cron');
|
|
||||||
|
|
||||||
const db = require('../database');
|
const db = require('../database');
|
||||||
const posts = require('../posts');
|
const posts = require('../posts');
|
||||||
@@ -13,18 +12,17 @@ const groups = require('../groups');
|
|||||||
const user = require('../user');
|
const user = require('../user');
|
||||||
const activitypub = require('../activitypub');
|
const activitypub = require('../activitypub');
|
||||||
const plugins = require('../plugins');
|
const plugins = require('../plugins');
|
||||||
|
const cron = require('../cron');
|
||||||
|
|
||||||
const Scheduled = module.exports;
|
const Scheduled = module.exports;
|
||||||
|
|
||||||
Scheduled.startJobs = function () {
|
Scheduled.startJobs = async function () {
|
||||||
winston.verbose('[scheduled topics] Starting jobs.');
|
winston.verbose('[scheduled topics] Starting jobs.');
|
||||||
new CronJob('*/1 * * * *', async () => {
|
await cron.addJob({
|
||||||
try {
|
name: 'topics:publish:scheduled',
|
||||||
await Scheduled.handleExpired();
|
cronTime: '*/1 * * * *',
|
||||||
} catch (err) {
|
onTick: Scheduled.handleExpired,
|
||||||
winston.error(err.stack);
|
});
|
||||||
}
|
|
||||||
}, null, true);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Scheduled.handleExpired = async function () {
|
Scheduled.handleExpired = async function () {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
const validator = require('validator');
|
const validator = require('validator');
|
||||||
const winston = require('winston');
|
const winston = require('winston');
|
||||||
const cronJob = require('cron').CronJob;
|
|
||||||
|
|
||||||
const db = require('../database');
|
const db = require('../database');
|
||||||
const meta = require('../meta');
|
const meta = require('../meta');
|
||||||
@@ -14,14 +13,6 @@ const slugify = require('../slugify');
|
|||||||
const plugins = require('../plugins');
|
const plugins = require('../plugins');
|
||||||
|
|
||||||
module.exports = function (User) {
|
module.exports = function (User) {
|
||||||
new cronJob('0 * * * *', (async () => {
|
|
||||||
try {
|
|
||||||
await User.autoApprove();
|
|
||||||
} catch (err) {
|
|
||||||
winston.error(err.stack);
|
|
||||||
}
|
|
||||||
}), null, true);
|
|
||||||
|
|
||||||
User.createOrQueue = async function (req, userData, opts = {}) {
|
User.createOrQueue = async function (req, userData, opts = {}) {
|
||||||
User.checkUsernameLength(userData.username);
|
User.checkUsernameLength(userData.username);
|
||||||
const queue = await User.shouldQueueUser(req.ip);
|
const queue = await User.shouldQueueUser(req.ip);
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const winston = require('winston');
|
const winston = require('winston');
|
||||||
const cronJob = require('cron').CronJob;
|
|
||||||
const db = require('../database');
|
const db = require('../database');
|
||||||
const meta = require('../meta');
|
const meta = require('../meta');
|
||||||
|
const cron = require('../cron');
|
||||||
|
|
||||||
const jobs = {};
|
const jobs = {};
|
||||||
|
|
||||||
module.exports = function (User) {
|
module.exports = function (User) {
|
||||||
User.startJobs = function () {
|
User.startJobs = async function () {
|
||||||
winston.verbose('[user/jobs] (Re-)starting jobs...');
|
winston.verbose('[user/jobs] (Re-)starting jobs...');
|
||||||
|
|
||||||
let { digestHour } = meta.config;
|
let { digestHour } = meta.config;
|
||||||
@@ -22,20 +22,31 @@ module.exports = function (User) {
|
|||||||
|
|
||||||
User.stopJobs();
|
User.stopJobs();
|
||||||
|
|
||||||
startDigestJob('digest.daily', `0 ${digestHour} * * *`, 'day');
|
await startDigestJob('digest.daily', `0 ${digestHour} * * *`, 'day');
|
||||||
startDigestJob('digest.weekly', `0 ${digestHour} * * 0`, 'week');
|
await startDigestJob('digest.weekly', `0 ${digestHour} * * 0`, 'week');
|
||||||
startDigestJob('digest.monthly', `0 ${digestHour} 1 * *`, 'month');
|
await startDigestJob('digest.monthly', `0 ${digestHour} 1 * *`, 'month');
|
||||||
|
|
||||||
jobs['reset.clean'] = new cronJob('0 0 * * *', User.reset.clean, null, true);
|
jobs['reset.clean'] = await cron.addJob({
|
||||||
winston.verbose('[user/jobs] Starting job (reset.clean)');
|
name: 'user:reset:clean',
|
||||||
|
cronTime: '0 0 * * *',
|
||||||
|
onTick: User.reset.clean,
|
||||||
|
});
|
||||||
|
|
||||||
|
await cron.addJob({
|
||||||
|
name: 'user:autoApprove',
|
||||||
|
cronTime: '0 * * * *',
|
||||||
|
onTick: User.autoApprove,
|
||||||
|
});
|
||||||
|
|
||||||
winston.verbose(`[user/jobs] jobs started`);
|
winston.verbose(`[user/jobs] jobs started`);
|
||||||
};
|
};
|
||||||
|
|
||||||
function startDigestJob(name, cronString, term) {
|
async function startDigestJob(name, cronString, term) {
|
||||||
jobs[name] = new cronJob(cronString, (async () => {
|
const newJob = await cron.addJob({
|
||||||
winston.verbose(`[user/jobs] Digest job (${name}) started.`);
|
name,
|
||||||
try {
|
cronTime: cronString,
|
||||||
|
onTick: async () => {
|
||||||
|
winston.verbose(`[user/jobs] Digest job (${name}) started.`);
|
||||||
if (name === 'digest.weekly') {
|
if (name === 'digest.weekly') {
|
||||||
const counter = await db.increment('biweeklydigestcounter');
|
const counter = await db.increment('biweeklydigestcounter');
|
||||||
if (counter % 2) {
|
if (counter % 2) {
|
||||||
@@ -43,24 +54,27 @@ module.exports = function (User) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
await User.digest.execute({ interval: term });
|
await User.digest.execute({ interval: term });
|
||||||
} catch (err) {
|
},
|
||||||
winston.error(err.stack);
|
});
|
||||||
}
|
if (newJob) {
|
||||||
}), null, true);
|
jobs[name] = newJob;
|
||||||
winston.verbose(`[user/jobs] Starting job (${name})`);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
User.stopJobs = function () {
|
User.stopJobs = function () {
|
||||||
let terminated = 0;
|
let terminated = 0;
|
||||||
// Terminate any active cron jobs
|
// Terminate any active cron jobs
|
||||||
for (const jobId of Object.keys(jobs)) {
|
for (const [name, job] of Object.entries(jobs)) {
|
||||||
winston.verbose(`[user/jobs] Terminating job (${jobId})`);
|
winston.info(`[user/jobs] Terminating job (${name})`);
|
||||||
jobs[jobId].stop();
|
if (job) {
|
||||||
delete jobs[jobId];
|
job.stop();
|
||||||
|
delete jobs[name];
|
||||||
|
}
|
||||||
|
|
||||||
terminated += 1;
|
terminated += 1;
|
||||||
}
|
}
|
||||||
if (terminated > 0) {
|
if (terminated > 0) {
|
||||||
winston.verbose(`[user/jobs] ${terminated} jobs terminated`);
|
winston.info(`[user/jobs] ${terminated} jobs terminated`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<th class="text-end"><i class="fa-solid invisible fa-sort-down"></i> <a href="#" class="text-reset">hit ratio</a></th>
|
<th class="text-end"><i class="fa-solid invisible fa-sort-down"></i> <a href="#" class="text-reset">hit ratio</a></th>
|
||||||
<th class="text-end"><i class="fa-solid invisible fa-sort-down"></i> <a href="#" class="text-reset">hits/sec</a></th>
|
<th class="text-end"><i class="fa-solid invisible fa-sort-down"></i> <a href="#" class="text-reset">hits/sec</a></th>
|
||||||
<th class="text-end"><i class="fa-solid invisible fa-sort-down"></i> <a href="#" class="text-reset">ttl</a></th>
|
<th class="text-end"><i class="fa-solid invisible fa-sort-down"></i> <a href="#" class="text-reset">ttl</a></th>
|
||||||
<th></td>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="text-xs text-tabular">
|
<tbody class="text-xs text-tabular">
|
||||||
|
|||||||
36
src/views/admin/advanced/jobs.tpl
Normal file
36
src/views/admin/advanced/jobs.tpl
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
|
||||||
|
<div class="d-flex flex-column gap-2 px-lg-4">
|
||||||
|
<div class="d-flex border-bottom py-2 m-0 sticky-top acp-page-main-header align-items-center justify-content-between flex-wrap gap-2">
|
||||||
|
<div class="">
|
||||||
|
<h4 class="fw-bold tracking-tight mb-0">[[admin/advanced/jobs:jobs]]</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table id="jobs-table" class="table">
|
||||||
|
<thead>
|
||||||
|
<tr class="text-sm">
|
||||||
|
<th>[[admin/advanced/jobs:job-name]]</th>
|
||||||
|
<th>[[admin/advanced/jobs:schedule]]</th>
|
||||||
|
<th>[[admin/advanced/jobs:next-run]]</th>
|
||||||
|
<th class="text-end">[[admin/advanced/jobs:last-duration]]</th>
|
||||||
|
<th class="text-center">[[admin/advanced/jobs:running]]</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="text-xs text-tabular">
|
||||||
|
{{{ each jobs }}}
|
||||||
|
<tr class="align-middle">
|
||||||
|
<td>{./name}</td>
|
||||||
|
<td>{./cronTimeHuman} <span class="text-secondary">({./cronTime})</span></td>
|
||||||
|
<td><span class="timeago" title="{./nextRunISO}"></span></td>
|
||||||
|
<td class="text-end">{./durationReadable}</td>
|
||||||
|
<td class="text-center">{{{ if ./running }}}Yes{{{ else }}}No{{{ end }}}</td>
|
||||||
|
</tr>
|
||||||
|
{{{ end }}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
@@ -190,6 +190,7 @@
|
|||||||
<a class="btn btn-ghost btn-sm text-start" href="{relative_path}/admin/advanced/events">[[admin/menu:advanced/events]]</a>
|
<a class="btn btn-ghost btn-sm text-start" href="{relative_path}/admin/advanced/events">[[admin/menu:advanced/events]]</a>
|
||||||
<a class="btn btn-ghost btn-sm text-start" href="{relative_path}/admin/advanced/hooks">[[admin/menu:advanced/hooks]]</a>
|
<a class="btn btn-ghost btn-sm text-start" href="{relative_path}/admin/advanced/hooks">[[admin/menu:advanced/hooks]]</a>
|
||||||
<a class="btn btn-ghost btn-sm text-start" href="{relative_path}/admin/advanced/cache">[[admin/menu:advanced/cache]]</a>
|
<a class="btn btn-ghost btn-sm text-start" href="{relative_path}/admin/advanced/cache">[[admin/menu:advanced/cache]]</a>
|
||||||
|
<a class="btn btn-ghost btn-sm text-start" href="{relative_path}/admin/advanced/jobs">[[admin/menu:advanced/jobs]]</a>
|
||||||
<a class="btn btn-ghost btn-sm text-start" href="{relative_path}/admin/advanced/errors">[[admin/menu:advanced/errors]]</a>
|
<a class="btn btn-ghost btn-sm text-start" href="{relative_path}/admin/advanced/errors">[[admin/menu:advanced/errors]]</a>
|
||||||
<a class="btn btn-ghost btn-sm text-start" href="{relative_path}/admin/advanced/logs">[[admin/menu:advanced/logs]]</a>
|
<a class="btn btn-ghost btn-sm text-start" href="{relative_path}/admin/advanced/logs">[[admin/menu:advanced/logs]]</a>
|
||||||
{{{ if env }}}
|
{{{ if env }}}
|
||||||
|
|||||||
@@ -2297,14 +2297,12 @@ describe('User', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('user jobs', () => {
|
describe('user jobs', () => {
|
||||||
it('should start user jobs', (done) => {
|
it('should start user jobs', async () => {
|
||||||
User.startJobs();
|
await User.startJobs();
|
||||||
done();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should stop user jobs', (done) => {
|
it('should stop user jobs', async () => {
|
||||||
User.stopJobs();
|
User.stopJobs();
|
||||||
done();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should send digest', (done) => {
|
it('should send digest', (done) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user