impress
Version:
Enterprise application server for Node.js
218 lines (203 loc) • 5.99 kB
JavaScript
;
const { node, metarhia, wt } = require('./deps.js');
const { Semaphore } = metarhia.metautil;
class Planner {
constructor(path, config, { applications, console }) {
this.path = path;
this.config = config;
this.applications = applications;
this.console = console;
this.tasks = new Map();
this.nextId = 0;
this.now = metarhia.metautil.nowDate();
this.topics = new Map();
return this.init();
}
async init() {
await metarhia.metautil.ensureDirectory(this.path);
const now = metarhia.metautil.nowDate();
try {
const files = await node.fsp.readdir(this.path, { withFileTypes: true });
for (const file of files) {
if (file.isDirectory()) continue;
const { name } = file;
if (!name.endsWith('.json')) continue;
const base = node.path.basename(name, '.json');
const [date, id] = metarhia.metautil.split(base, '-id-');
if (date === now) {
const nextId = parseInt(id, 10) + 1;
if (nextId > this.nextId) this.nextId = nextId;
}
const filePath = node.path.join(this.path, name);
const data = await node.fsp.readFile(filePath, 'utf8');
this.restore(JSON.parse(data));
}
} catch (error) {
this.console.error(error.stack);
}
return this;
}
restore(data) {
const { id, app, name, every, args, run } = data;
const startedTask = this.tasks.get(id);
if (startedTask) return true;
const task = {
id,
app,
name,
every: metarhia.metautil.parseEvery(every),
success: undefined,
result: null,
error: null,
lastStart: 0,
lastEnd: 0,
executing: false,
runCount: 0,
timer: null,
args,
run,
};
this.tasks.set(id, task);
const started = this.start(id);
return started;
}
async add(task) {
const now = metarhia.metautil.nowDate();
if (this.now !== now) {
this.nextId = 0;
this.now = now;
}
const id = now + '-id-' + this.nextId.toString();
if (this.tasks.has(id)) {
this.console.error(new Error(`Task ${id} already exists`));
return '';
}
task.id = id;
const every = metarhia.metautil.parseEvery(task.every);
const next = metarhia.metautil.nextEvent(every);
if (next === -1) {
this.console.error(new Error(`Can't schedule a task ${id} in the past`));
return '';
}
this.restore({ id, ...task });
this.nextId++;
const filePath = node.path.join(this.path, id + '.json');
try {
const data = JSON.stringify(task);
await node.fsp.writeFile(filePath, data);
} catch (error) {
this.console.error(error.stack);
}
return id;
}
async remove(id) {
const task = this.tasks.get(id);
if (task) {
this.tasks.delete(id);
if (task.timer) {
clearTimeout(task.timer);
task.timer = null;
}
}
const filePath = node.path.join(this.path, id + '.json');
try {
await node.fsp.unlink(filePath);
} catch (error) {
if (error.code !== 'ENOENT') {
this.console.error(error.stack);
}
}
}
stop(name = '') {
for (const task of this.tasks.values()) {
if (name !== '' && name !== task.name) continue;
this.remove(task.id);
}
}
start(id) {
const task = this.tasks.get(id);
if (!task || task.timer) return false;
const next = metarhia.metautil.nextEvent(task.every);
if (next === -1) {
this.remove(id);
return false;
}
if (next === 0) return false;
task.timer = setTimeout(() => {
const once = task.every.ms === 0;
this.execute(task, once);
}, next);
return true;
}
async enter(name) {
let semaphore = this.topics.get(name);
if (!semaphore) {
const { concurrency, size, timeout } = this.config;
semaphore = new Semaphore({ concurrency, size, timeout });
this.topics.set(name, semaphore);
}
return semaphore.enter();
}
leave(name) {
const semaphore = this.topics.get(name);
if (!semaphore) return;
if (semaphore.empty) {
this.topics.delete(name);
}
semaphore.leave();
}
async execute(task, once = false) {
if (task.executing) return void this.fail(task, 'Already started task');
task.lastStart = Date.now();
task.executing = true;
try {
await this.enter(task.name);
task.result = await this.invoke(task);
} catch (error) {
task.error = error;
if (error.message === 'Semaphore timeout') {
this.fail(task, 'Scheduler queue is full');
} else {
this.console.error(error.stack);
}
} finally {
this.leave(task.name);
}
task.success = !task.error;
task.lastEnd = Date.now();
task.executing = false;
task.runCount++;
task.timer = null;
if (once) this.remove(task.id);
else this.start(task.id);
}
async invoke(task) {
const { port1: port, port2 } = new wt.MessageChannel();
const data = { method: task.run, args: task.args };
const msg = { name: 'invoke', data, port };
return new Promise((resolve, reject) => {
port2.on('message', ({ error, data }) => {
port2.close();
if (error) reject(error);
else resolve(data);
});
const app = this.applications.get(task.app);
if (!app) {
const data = JSON.stringify(task);
const error = new Error('No application for task: ' + data);
return void this.console.error(error);
}
app.pool.next().then(
(next) => next.postMessage(msg, [port]),
() => this.console.error(new Error('No thread available')),
);
});
}
fail(task, reason) {
const { id, name, run, args } = task;
const target = `${name} (${id}) ${run}(${JSON.stringify(args)})`;
const msg = `${reason}, can't execute: ${target}`;
this.console.error(msg);
}
}
module.exports = { Planner };