Mentions légales du service

Skip to content
Snippets Groups Projects
Commit f14232ee authored by Christophe Guillon's avatar Christophe Guillon
Browse files

Initial revision with simple example

parents
Branches
Tags
No related merge requests found
LICENSE 0 → 100644
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>
NodeJS Tasks
============
A simple example implementation of background and cancellable (with timeout) task.
Refer to example usage in the [test file](test-tasks.js) file.
Refer to implementation for the documentation in:
- [Task](task_thread.js)
- [WorkerTimeout](worker_timeout.js)
Install
-------
No installation necessary, though requirement on NodeJS >= 12.x.
Test
----
Test with:
node ./test-tasks.js
TEST: TEST6: ERROR (4): unknown error
TEST: TEST7: SUCCESS: result: undefined
TEST: TEST5: ERROR (1): ReferenceError: test_bogus_function_result is not defined
TEST: TEST3: SUCCESS: result: 746450240
TEST: TEST4: SUCCESS: result: 2336000
TEST: TEST2: TIMEOUT (2): timeout: after 1000 msecs
TEST: TEST1: TIMEOUT (2): timeout: after 1000 msecs
Licence
-------
This is distributed under The Unlicence please refer to <https://unlicense.org>
"use strict";
/* 4 DBG, 3: LOG, 2: WARN, 1: ERR, 0: NONE */
global.native_console = global.console;
global.log_level = 3;
const console = {
...global.console,
error: (...args) => { global.log_level >= 1 && global.native_console.error(...args); },
warn: (...args) => { global.log_level >= 2 && global.native_console.warn(...args); },
log: (...args) => { global.log_level >= 3 && global.native_console.log(...args); },
debug: (...args) => { global.log_level >= 4 && global.native_console.debug(...args); },
setLevel: (level) => { global.log_level = level; },
setDebug: (cond) => { if (cond) { global.log_level = 4; } },
};
module.exports = { console };
/**
* Implements a Task and runTask function on top of WorkerTimeout.
*
* @module task_thread
*/
"use strict";
const assert = require("assert");
const {
isMainThread,
parentPort,
workerData
} = require("worker_threads");
const { WorkerTimeout } = require("./worker_timeout.js");
const { console } = require("./log_console.js");
/**
Task is an asynchroneous and terminable task.
It calls back the given nodeback(err, res) with:
- if err is not null, an error/timeout occured and
err contains { taskname: taskname, exit_code: non-0-int, error: str, timeout: bool }
- otherwise, res contains { taskname: taskname, result: the result from postTaskResult() }
Use it for instance with:
const nodeback = (err, res) => { if (!err) { console.log(`Result: ${res.result}`); };
var task = Task("TASK1", "myfunction", [1, 2], "myfunction.js", { timeout: 1000 }, nodeback)
task.run()
Where for intance in myfunction.js we have:
...
assert(isTask);
const data = getTaskData();
if (data.funcname == "myfunction") {
const res = myfunction(...data.funcargs);
postTaskResult(res);
}
*/
class Task {
/**
* Task construction, no execution happens at this point.
*
* @param {str} taskname name of the task for identification in the returned response if needed
* @param {str} funcname name of the function to call (actually it is the task JS filename code to interpret this)
* @param {array} funcargs arguments to pass to the funciton (or [] or undefined for no arguments)
* @param {str} filename JS file to execute for the task (this file must get the funcname/funcargs from the
* getTaskData() object, execute the request and return the result with postTaskResult
* @param {dict} options passed to the underlying WorkerTimeout object (ref worker_timeout.js),
* for instance pass { timeout: 1000 } for a timeout of 1 sec
* @param {function} nodeback the user callback in nodeback style, i.e. err first, see the class
* usage above for details
*/
constructor(taskname, funcname, funcargs, filename, options, nodeback) {
this.taskname = taskname;
this.filename = filename;
this.funcname = funcname;
this.funcargs = funcargs;
this.options = options;
this.nodeback = nodeback;
}
/**
* Actual task run
*
* After calling this functionm the task should be considered running in a backgrounbd thread.
* The user callback may be called as soon as the event loop if available.
*/
run() {
assert.ok(this.worker === undefined, "Task.run() called twice");
this.worker = new WorkerTimeout(this.filename, {
...this.options,
name: this.taskname,
workerData: {
taskname: this.taskname,
funcname: this.funcname,
funcargs: this.funcargs
}});
this.taskId = `${this.taskname}-${this.worker.threadId}`;
console.debug(`START WORKER: ${this.taskId}: func_name: ${this.funcname}, funcargs: ${this.funcargs}`);
this.res = { taskname: this.taskname, result: undefined };
this.err = { taskname: this.taskname, exit_code: undefined, error: "", timeout: false };
this.worker.on("message", resp => {
console.debug(`RESULT: WORKER: ${this.taskId}: result received: ${resp}`);
this.res.result = resp;
});
this.worker.on("error", err => {
console.debug(`ERROR: WORKER: ${this.taskId}: message received: ${err}`);
this.err.error = err;
});
this.worker.on("exit", code => {
if (code == 1 && !this.err.error) {
this.err.error = `terminated`;
} else if (code == 2 && !this.err.error) {
this.err.timeout = true;
this.err.error = `timeout: after ${this.options.timeout} msecs`;
}
if (code != 0 && !this.err.error) {
this.err.error = `unknown error`;
}
this.err.exit_code = code;
if (this.err.timeout) {
console.debug(`TIMEOUT: WORKER: ${this.taskId}: worker terminated after ${this.options.timeout} msecs.`);
} else {
console.debug(`EXIT: WORKER: ${this.taskId} exited with code ${code}.`);
}
if (this.nodeback) {
if (code == 0) {
this.nodeback(null, this.res);
} else {
this.nodeback(this.err, null);
}
}
});
}
}
/**
* Directly create a task and call run().
* Shortcut for: var task = Task(...); task.run();
*/
function runTask(taskname, funcname, funcargs, filename, options, nodeback) {
var task = new Task(taskname, funcname, funcargs, filename, options, nodeback);
task.run()
}
/**
* isMain is true only in the main thread (i.e. not in a Task thread).
*/
const isMain = isMainThread;
/**
* isTask is true only in a task thread (i.e. not in the main thread).
*/
const isTask = !isMainThread;
/**
* getTaskData() gets the task input data which contains at least:
* {
* taskname: the taskname as passed to Task(....),
* funcname: the function name to call or a name for the actaula task to take,
* funcargs: the arguments to the task, generally arguments to pass to a function
* }
*/
function getTaskData() {
assert.ok(isTask, "getTaskData() must be called from a Task");
return workerData;
}
/**
* postTaskResult(res) return to the parent the result, can be anything including undefined
* if no result is generated.
* The result is then embedded into: res = {taskname: taskname, result: the result }
* and passed to the Task(...) nodeback(err, res) callback if no error/timeout occured.
*/
function postTaskResult(res) {
assert.ok(isTask, "postTaskResult(res) must be called from a Task");
return parentPort.postMessage(res);
}
module.exports = { Task, runTask, isMain, isTask, getTaskData, postTaskResult };
/*
* Simple example of using task_thread to run tasks with timeout
*
* Execute with:
* $ node test-workers.js
*
* Tested on nodejs v12.22.12 and v20.4.0, does not work on v10.x
*/
"use strict";
const assert = require("assert");
const util = require("util")
// Import from task_thread
const { runTask, postTaskResult, getTaskData, isMain, isTask } = require("./task_thread.js");
// Just import configurable console for debugging
const { console } = require("./log_console.js");
console.setDebug(false) // for no output with console.debug (the default)
//console.setDebug(true) // for debugging with console.debug
// Use the same file and dispatch to worker if in a Task
// Refer to runTask() below where __filename is passed
if (!isTask) {
main();
} else {
task_dispatch();
}
// Code for handling task
function task_dispatch() {
assert.ok(isTask);
const functions = {
"test_long_function": test_long_function,
"test_bogus_function": test_bogus_function,
"test_exit_function": test_exit_function,
};
const data = getTaskData()
const funcname = data.funcname;
const funcargs = data.funcargs;
const res = functions[funcname](...funcargs);
postTaskResult(res);
}
// Main program code: do some tests, expected output is (unordered):
//
// TEST: TEST6: ERROR (4): unknown error
// TEST: TEST7: SUCCESS: result: undefined
// TEST: TEST5: ERROR (1): ReferenceError: test_bogus_function_result is not defined
// TEST: TEST3: SUCCESS: result: 746450240
// TEST: TEST4: SUCCESS: result: 2336000
// TEST: TEST1: TIMEOUT (2): timeout: after 1000 msecs
// TEST: TEST2: TIMEOUT (2): timeout: after 1000 msecs
//
function main() {
assert.ok(isMain);
const nodeback = (err, res) => {
if (err) {
if (err.timeout) {
console.error(`TEST: ${err.taskname}: TIMEOUT (${err.exit_code}): ${err.error}`);
} else {
console.error(`TEST: ${err.taskname}: ERROR (${err.exit_code}): ${err.error}`);
}
} else {
console.log(`TEST: ${res.taskname}: SUCCESS: result: ${JSON.stringify(res.result)}`);
}
};
console.debug("START: main")
// Run with default timeout to 1 sec and callback above as a nodeback(err, res)
// Note that we give __filename (i.e. the current file) as the filename to execute
// though the code for the tasks may be in another file
const defaults = [__filename, { timeout: 1000 }, nodeback];
runTask("TEST1", "test_long_function", [10_000_000_000], ...defaults)
runTask("TEST2", "test_long_function", [10_000_000_000], ...defaults)
runTask("TEST3", "test_long_function", [1_000_000], ...defaults)
runTask("TEST4", "test_long_function", [2_000_000], ...defaults)
runTask("TEST5", "test_bogus_function", [], ...defaults)
runTask("TEST6", "test_exit_function", [4], ...defaults)
runTask("TEST7", "test_exit_function", [0], ...defaults)
console.debug("END: main")
}
// Just exists early
function test_exit_function(code) {
process.exit(code); // just exit early
}
// Generated an undefined reference error
function test_bogus_function() {
return test_bogus_function_result;
}
// Long function, above iters > 10_000_000_000
// will take more than 1 sec for sure
function test_long_function(iters) {
console.debug(`START long function: ${iters}`);
var acc = 0;
for (let i = 0; i < iters; i++) {
acc = (1103515245 * acc + 12345) & 0x7fffffff;
}
console.debug(`END long function: ${iters} => ${acc}`);
return acc;
}
/**
* Implements additional functionalities to base Worker class.
*
* Ref to Worker doc at https://nodejs.org/api/worker_threads.html#worker-threads
*
* @module worker_timeout
*/
"use strict";
const { Worker } = require("worker_threads");
/**
WorkerTimeout is a Worker with an optional timeout option.
If timeout is specified and > 0, the worker will be terminated
and the on "exit" callback received a code of 2.
*/
class WorkerTimeout extends Worker {
/**
* Same constructor as Worker
* @param {str} filename the file to load
* @param {dict} options the Worker options
* if timeout is passed, use it
*/
constructor(filename, options) {
super(filename, options);
this.timeout = false;
this.timerId = null;
if (options && options.timeout > 0) {
this.timerId = setTimeout(() => {
this.timeout = true;
this.terminate()
}, options.timeout);
}
}
/**
* Override exit callback in order to return
* code 2 for timeout, instead of 1 for other
* calls to terminate.
* @param {str} type event type
* @param {function} callback user callback
*/
on(type, callback) {
if (type == "exit" && this.timerId) {
return super.on(type, code => {
code = this.timeout ? 2: code;
callback(code);
});
}
return super.on(type, callback);
}
}
module.exports = { WorkerTimeout };
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment