fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis. This is needed for better sonar
- name: Set node version to ${{ matrix.node-version }}
- uses: actions/setup-node@v1
+ uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
steps:
- name: Checkout
- uses: actions/checkout@v1
+ uses: actions/checkout@v2
- name: Setup Node
- uses: actions/setup-node@v1
+ uses: actions/setup-node@v2
with:
node-version: 12
- run: npm ci
- run: npm run lint
- - run: npm run test
+ - run: npm run test:prod
- run: npm run coverage
publish-npm:
steps:
- name: Checkout
- uses: actions/checkout@v1
+ uses: actions/checkout@v2
- name: Setup Node
- uses: actions/setup-node@v1
+ uses: actions/setup-node@v2
with:
node-version: 12
registry-url: https://registry.npmjs.org/
.history/
.scannerwork
lib
-lib.min
.nyc_output/
coverage/
lib/
-lib.min/
"request": "launch",
"name": "Launch Test Debug",
"cwd": "${workspaceFolder}",
- "preLaunchTask": "Development build",
+ // "preLaunchTask": "Development build",
"runtimeExecutable": "npm",
- "runtimeArgs": ["run-script", "test:debug:vscode"],
+ "runtimeArgs": ["run-script", "test:debug"],
"skipFiles": ["<node_internals>/**"],
"stopOnEntry": true
},
"request": "launch",
"name": "Launch Benchmark Debug",
"cwd": "${workspaceFolder}",
- "preLaunchTask": "Development build",
+ // "preLaunchTask": "Development build",
"runtimeExecutable": "npm",
- "runtimeArgs": ["run-script", "benchmark:debug:vscode"],
+ "runtimeArgs": ["run-script", "benchmark:debug"],
"skipFiles": ["<node_internals>/**"],
"stopOnEntry": true
}
{
"version": "2.0.0",
"tasks": [
+ {
+ "label": "Build",
+ "type": "npm",
+ "script": "build:prod"
+ },
{
"label": "Development build",
"type": "npm",
- "script": "build:dev"
+ "script": "build"
}
]
}
## [2.0.0] - not released yet
+### Bug fixes
+
+- Now a thread/process by default is not deleted when the task submitted take more time than maxInactiveTime configured (issue #70).
+
### Breaking Changes
-We changed some internal structures, but you shouldn't be too affected by them as these are internal changes.
+- `maxInactiveTime` default behavior is now changed, if you want to keep the old behavior set `killBehavior` to `KillBehaviors.HARD`.
+ _Find more details on our JSDoc._
+
+- We changed some internal structures, but you shouldn't be too affected by them as these are internal changes.
#### New `export` strategy
-# Node Thread Pool :arrow_double_up: :on:
-
-[![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com)
-[![Dependabot](https://badgen.net/dependabot/dependabot/dependabot-core/?icon=dependabot)](https://badgen.net/dependabot/dependabot/dependabot-core/?icon=dependabot)
-[![npm w](https://img.shields.io/npm/dw/poolifier)](https://www.npmjs.com/package/poolifier)
-[![Actions Status](https://github.com/pioardi/node-pool/workflows/NodeCI/badge.svg)](https://github.com/pioardi/node-pool/actions)
-[![Coverage Status](https://coveralls.io/repos/github/pioardi/poolifier/badge.svg?branch=master)](https://coveralls.io/github/pioardi/poolifier?branch=master)
-[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com)
-[![NODEP](https://img.shields.io/static/v1?label=dependencies&message=no%20dependencies&color=brightgreen)](https://img.shields.io/static/v1?label=dependencies&message=no%20dependencies&color=brightgreen)
-[![Gitter](https://badges.gitter.im/poolifier/community.svg)](https://gitter.im/poolifier/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
+<div align="center">
+<img src="./docs/logo.png" width="475" height="400"/>
+</div>
+
+<h2 align="center">Node Thread Pool :arrow_double_up: :on:</h2>
+
+<p align="center">
+ <a href="https://www.npmjs.com/package/poolifier">
+ <img alt="Weekly Downloads" src="https://img.shields.io/npm/dw/poolifier"></a>
+ <a href="https://github.com/pioardi/node-pool/actions">
+ <img alt="Actions Status" src="https://github.com/pioardi/node-pool/workflows/NodeCI/badge.svg"></a>
+ <a href="https://sonarcloud.io/dashboard?id=pioardi_poolifier">
+ <img alt="Quality Gate Status" src="https://sonarcloud.io/api/project_badges/measure?project=pioardi_poolifier&metric=alert_status"></a>
+ <a href="https://sonarcloud.io/component_measures/metric/coverage/list?id=pioardi_poolifier">
+ <img alt="Code coverage" src="https://sonarcloud.io/api/project_badges/measure?project=pioardi_poolifier&metric=coverage"></a>
+ <a href="https://standardjs.com">
+ <img alt="Javascript Standard Style Guide" src="https://img.shields.io/badge/code_style-standard-brightgreen.svg"></a>
+ <a href="https://gitter.im/poolifier/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge">
+ <img alt="Gitter chat" src="https://badges.gitter.im/poolifier/community.svg"></a>
+ <a href="https://badgen.net/dependabot/dependabot/dependabot-core/?icon=dependabot">
+ <img alt="Dependabot" src="https://badgen.net/dependabot/dependabot/dependabot-core/?icon=dependabot"></a>
+ <a href="http://makeapullrequest.com">
+ <img alt="PR Welcome" src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square"></a>
+ <a href="https://img.shields.io/static/v1?label=dependencies&message=no%20dependencies&color=brightgreen">
+ <img alt="No dependencies" src="https://img.shields.io/static/v1?label=dependencies&message=no%20dependencies&color=brightgreen"></a>
+</p>
## Why Poolifier?
With poolifier you can improve your **performance** and resolve problems related to the event loop.
Moreover you can execute your CPU tasks using an API designed to improve the **developer experience**.
+- Performance :racehorse:
+- Security :bank: :cop: [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=pioardi_poolifier&metric=security_rating)](https://sonarcloud.io/dashboard?id=pioardi_poolifier) [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=pioardi_poolifier&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=pioardi_poolifier)
+- Easy to use :couple:
+- Easy switch from a pool to another, easy to tune :heavy_check_mark:
+- Dynamic pool size :heavy_check_mark:
+- No runtime dependencies :heavy_check_mark:
+- Proper async integration with node async hooks :heavy_check_mark:
+- Support for worker threads and cluster node modules :heavy_check_mark:
+- Support sync and async tasks :heavy_check_mark:
+- General guidance on pools to use :heavy_check_mark:
+- Widely tested :heavy_check_mark:
+- Error handling out of the box :heavy_check_mark:
+- Active community :heavy_check_mark:
+- Code quality :octocat: [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=pioardi_poolifier&metric=bugs)](https://sonarcloud.io/dashboard?id=pioardi_poolifier)
+ [![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=pioardi_poolifier&metric=code_smells)](https://sonarcloud.io/dashboard?id=pioardi_poolifier)
+ [![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=pioardi_poolifier&metric=duplicated_lines_density)](https://sonarcloud.io/dashboard?id=pioardi_poolifier)
+ [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=pioardi_poolifier&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=pioardi_poolifier)
+ [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=pioardi_poolifier&metric=reliability_rating)](https://sonarcloud.io/dashboard?id=pioardi_poolifier)
+ [![Technical Debt](https://sonarcloud.io/api/project_badges/measure?project=pioardi_poolifier&metric=sqale_index)](https://sonarcloud.io/dashboard?id=pioardi_poolifier)
+
## Contents
<h3 align="center">
// eslint-disable-next-line no-process-exit
process.exit()
})
- // run async
- .run({ async: true })
+ .run()
}
"dev": true
},
"@types/node": {
- "version": "14.14.27",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.27.tgz",
- "integrity": "sha512-Ecfmo4YDQPwuqTCl1yBxLV5ihKfRlkBmzUEDcfIRvDxOTGQEeikr317Ln7Gcv0tjA8dVgKI3rniqW2G1OyKDng==",
+ "version": "14.14.28",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.28.tgz",
+ "integrity": "sha512-lg55ArB+ZiHHbBBttLpzD07akz0QPrZgUODNakeC09i62dnrywr9mFErHuaPlB6I7z+sEbK+IYmplahvplCj2g==",
"dev": true
},
"@types/parse-json": {
"dev": true
},
"@typescript-eslint/eslint-plugin": {
- "version": "4.15.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.15.0.tgz",
- "integrity": "sha512-DJgdGZW+8CFUTz5C/dnn4ONcUm2h2T0itWD85Ob5/V27Ndie8hUoX5HKyGssvR8sUMkAIlUc/AMK67Lqa3kBIQ==",
+ "version": "4.15.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.15.1.tgz",
+ "integrity": "sha512-yW2epMYZSpNJXZy22Biu+fLdTG8Mn6b22kR3TqblVk50HGNV8Zya15WAXuQCr8tKw4Qf1BL4QtI6kv6PCkLoJw==",
"dev": true,
"requires": {
- "@typescript-eslint/experimental-utils": "4.15.0",
- "@typescript-eslint/scope-manager": "4.15.0",
+ "@typescript-eslint/experimental-utils": "4.15.1",
+ "@typescript-eslint/scope-manager": "4.15.1",
"debug": "^4.1.1",
"functional-red-black-tree": "^1.0.1",
"lodash": "^4.17.15",
}
},
"@typescript-eslint/experimental-utils": {
- "version": "4.15.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.15.0.tgz",
- "integrity": "sha512-V4vaDWvxA2zgesg4KPgEGiomWEBpJXvY4ZX34Y3qxK8LUm5I87L+qGIOTd9tHZOARXNRt9pLbblSKiYBlGMawg==",
+ "version": "4.15.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.15.1.tgz",
+ "integrity": "sha512-9LQRmOzBRI1iOdJorr4jEnQhadxK4c9R2aEAsm7WE/7dq8wkKD1suaV0S/JucTL8QlYUPU1y2yjqg+aGC0IQBQ==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.3",
- "@typescript-eslint/scope-manager": "4.15.0",
- "@typescript-eslint/types": "4.15.0",
- "@typescript-eslint/typescript-estree": "4.15.0",
+ "@typescript-eslint/scope-manager": "4.15.1",
+ "@typescript-eslint/types": "4.15.1",
+ "@typescript-eslint/typescript-estree": "4.15.1",
"eslint-scope": "^5.0.0",
"eslint-utils": "^2.0.0"
}
},
"@typescript-eslint/parser": {
- "version": "4.15.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.15.0.tgz",
- "integrity": "sha512-L6Dtbq8Bc7g2aZwnIBETpmUa9XDKCMzKVwAArnGp5Mn7PRNFjf3mUzq8UeBjL3K8t311hvevnyqXAMSmxO8Gpg==",
+ "version": "4.15.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.15.1.tgz",
+ "integrity": "sha512-V8eXYxNJ9QmXi5ETDguB7O9diAXlIyS+e3xzLoP/oVE4WCAjssxLIa0mqCLsCGXulYJUfT+GV70Jv1vHsdKwtA==",
"dev": true,
"requires": {
- "@typescript-eslint/scope-manager": "4.15.0",
- "@typescript-eslint/types": "4.15.0",
- "@typescript-eslint/typescript-estree": "4.15.0",
+ "@typescript-eslint/scope-manager": "4.15.1",
+ "@typescript-eslint/types": "4.15.1",
+ "@typescript-eslint/typescript-estree": "4.15.1",
"debug": "^4.1.1"
}
},
"@typescript-eslint/scope-manager": {
- "version": "4.15.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.15.0.tgz",
- "integrity": "sha512-CSNBZnCC2jEA/a+pR9Ljh8Y+5TY5qgbPz7ICEk9WCpSEgT6Pi7H2RIjxfrrbUXvotd6ta+i27sssKEH8Azm75g==",
+ "version": "4.15.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.15.1.tgz",
+ "integrity": "sha512-ibQrTFcAm7yG4C1iwpIYK7vDnFg+fKaZVfvyOm3sNsGAerKfwPVFtYft5EbjzByDJ4dj1WD8/34REJfw/9wdVA==",
"dev": true,
"requires": {
- "@typescript-eslint/types": "4.15.0",
- "@typescript-eslint/visitor-keys": "4.15.0"
+ "@typescript-eslint/types": "4.15.1",
+ "@typescript-eslint/visitor-keys": "4.15.1"
}
},
"@typescript-eslint/types": {
- "version": "4.15.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.15.0.tgz",
- "integrity": "sha512-su4RHkJhS+iFwyqyXHcS8EGPlUVoC+XREfy5daivjLur9JP8GhvTmDipuRpcujtGC4M+GYhUOJCPDE3rC5NJrg==",
+ "version": "4.15.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.15.1.tgz",
+ "integrity": "sha512-iGsaUyWFyLz0mHfXhX4zO6P7O3sExQpBJ2dgXB0G5g/8PRVfBBsmQIc3r83ranEQTALLR3Vko/fnCIVqmH+mPw==",
"dev": true
},
"@typescript-eslint/typescript-estree": {
- "version": "4.15.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.15.0.tgz",
- "integrity": "sha512-jG6xTmcNbi6xzZq0SdWh7wQ9cMb2pqXaUp6bUZOMsIlu5aOlxGxgE/t6L/gPybybQGvdguajXGkZKSndZJpksA==",
+ "version": "4.15.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.15.1.tgz",
+ "integrity": "sha512-z8MN3CicTEumrWAEB2e2CcoZa3KP9+SMYLIA2aM49XW3cWIaiVSOAGq30ffR5XHxRirqE90fgLw3e6WmNx5uNw==",
"dev": true,
"requires": {
- "@typescript-eslint/types": "4.15.0",
- "@typescript-eslint/visitor-keys": "4.15.0",
+ "@typescript-eslint/types": "4.15.1",
+ "@typescript-eslint/visitor-keys": "4.15.1",
"debug": "^4.1.1",
"globby": "^11.0.1",
"is-glob": "^4.0.1",
}
},
"@typescript-eslint/visitor-keys": {
- "version": "4.15.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.15.0.tgz",
- "integrity": "sha512-RnDtJwOwFucWFAMjG3ghCG/ikImFJFEg20DI7mn4pHEx3vC48lIAoyjhffvfHmErRDboUPC7p9Z2il4CLb7qxA==",
+ "version": "4.15.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.15.1.tgz",
+ "integrity": "sha512-tYzaTP9plooRJY8eNlpAewTOqtWW/4ff/5wBjNVaJ0S0wC4Gpq/zDVRTJa5bq2v1pCNQ08xxMCndcvR+h7lMww==",
"dev": true,
"requires": {
- "@typescript-eslint/types": "4.15.0",
+ "@typescript-eslint/types": "4.15.1",
"eslint-visitor-keys": "^2.0.0"
}
},
}
},
"eslint-plugin-jsdoc": {
- "version": "31.6.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-31.6.1.tgz",
- "integrity": "sha512-5hCV3u+1VSEUMyfdTl+dpWsioD7tqQr2ILQw+KbXrF42AVxCLO8gnNLR6zDCDjqGGpt79V1sgY0RRchCWuCigg==",
+ "version": "32.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-32.0.1.tgz",
+ "integrity": "sha512-7T6cKNGJsJ1SxhG4vbEBi2fRmUL3DHzRNsiQZri4vkgIQjCoBb40ZNxSNwBqiVz16C7tW3qLncPoNXsiIFdzcg==",
"dev": true,
"requires": {
"comment-parser": "1.1.2",
},
"dependencies": {
"globals": {
- "version": "13.5.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-13.5.0.tgz",
- "integrity": "sha512-TMJe2Iu/qCIEFnG7IQ62C9N/iKdgX5wSvmGOVuk75+UAGDW+Yv/hH5+Ky6d/8UMqo4WCzhFCy+pHsvv09zhBoQ==",
+ "version": "13.6.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.6.0.tgz",
+ "integrity": "sha512-YFKCX0SiPg7l5oKYCJ2zZGxcXprVXHcSnVuvzrT3oSENQonVLqM5pf9fN5dLGZGyCjhw8TN8Btwe/jKnZ0pjvQ==",
"dev": true,
"requires": {
"type-fest": "^0.20.2"
"dev": true
},
"handlebars": {
- "version": "4.7.6",
- "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz",
- "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==",
+ "version": "4.7.7",
+ "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz",
+ "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==",
"dev": true,
"requires": {
"minimist": "^1.2.5",
"description": "Library on top of node js worker threads that implement various worker pools type",
"main": "lib/index.js",
"scripts": {
- "build": "rollup --config",
- "build:dev": "rollup --config --environment BUILD:development",
- "benchmark": "npm run build && node benchmarks/bench.js",
- "benchmark:debug": "npm run build:dev && node -r source-map-support/register --inspect-brk benchmarks/bench.js",
- "benchmark:debug:vscode": "node -r source-map-support/register benchmarks/bench.js",
- "test": "npm run build && nyc mocha --exit --timeout 20000 'tests/**/*.test.js'",
- "test:debug": "npm run build:dev && mocha -r source-map-support/register --inspect-brk --exit 'tests/**/*.test.js'",
- "test:debug:vscode": "mocha -r source-map-support/register --exit 'tests/**/*.test.js'",
+ "build": "rollup --config --environment BUILD:development",
+ "build:prod": "rollup --config",
+ "benchmark": "npm run build && node -r source-map-support/register benchmarks/bench.js",
+ "benchmark:debug": "npm run build && node -r source-map-support/register --inspect benchmarks/bench.js",
+ "benchmark:prod": "npm run build:prod && node -r source-map-support/register benchmarks/bench.js",
+ "test": "npm run build && nyc mocha -r source-map-support/register --exit --timeout 20000 'tests/**/*.test.js'",
+ "test:debug": "npm run build && mocha -r source-map-support/register --inspect --exit --timeout 20000 'tests/**/*.test.js'",
+ "test:prod": "npm run build:prod && nyc mocha -r source-map-support/register --exit --timeout 20000 'tests/**/*.test.js'",
"sonar": "sonar-scanner",
"coverage": "nyc report --reporter=lcov --check-coverage --lines 80",
"coverage:html": "nyc report --reporter=html --check-coverage --lines 80",
"lib"
],
"devDependencies": {
- "@types/node": "^14.14.27",
- "@typescript-eslint/eslint-plugin": "^4.15.0",
- "@typescript-eslint/parser": "^4.15.0",
+ "@types/node": "^14.14.28",
+ "@typescript-eslint/eslint-plugin": "^4.15.1",
+ "@typescript-eslint/parser": "^4.15.1",
"benchmark": "^2.1.4",
"eslint": "^7.20.0",
"eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.22.1",
- "eslint-plugin-jsdoc": "^31.6.1",
+ "eslint-plugin-jsdoc": "^32.0.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettierx": "^0.17.1",
"eslint-plugin-promise": "^4.3.1",
import del from 'rollup-plugin-delete'
const isDevelopmentBuild = process.env.BUILD === 'development'
+const isAnalyze = process.env.ANALYZE
export default {
input: 'src/index.ts',
- output: [
- {
- dir: 'lib',
- format: 'cjs',
- sourcemap: !!isDevelopmentBuild,
- preserveModules: true,
- preserveModulesRoot: 'src'
- },
- isDevelopmentBuild && {
- file: 'lib.min/index.js',
- format: 'cjs',
- sourcemap: !!isDevelopmentBuild,
- plugins: [terser({ numWorkers: 2 })]
- }
- ],
+ output: {
+ ...(isDevelopmentBuild ? { dir: 'lib' } : { file: 'lib/index.js' }),
+ format: 'cjs',
+ sourcemap: !!isDevelopmentBuild,
+ ...(isDevelopmentBuild && { preserveModules: true }),
+ ...(isDevelopmentBuild && { preserveModulesRoot: 'src' }),
+ ...(!isDevelopmentBuild && { plugins: [terser({ numWorkers: 2 })] })
+ },
external: ['async_hooks', 'cluster', 'events', 'worker_threads'],
plugins: [
typescript({
: 'tsconfig.json'
}),
del({
- targets: ['lib/*', 'lib.min/*']
+ targets: ['lib/*']
}),
- isDevelopmentBuild && analyze()
+ isAnalyze && analyze()
]
}
sonar.projectName=poolifier
sonar.projectVersion=2.0.0
sonar.host.url=https://sonarcloud.io
-sonar.sources=lib
+sonar.sources=src
sonar.tests=tests
# sonar.login=TOKEN #To use for local troubleshooting
sonar.verbose=false
export { AbstractWorker } from './worker/abstract-worker'
export { ClusterWorker } from './worker/cluster-worker'
export { ThreadWorker } from './worker/thread-worker'
-export type { WorkerOptions } from './worker/worker-options'
+export { KillBehaviors } from './worker/worker-options'
+export type { KillBehavior, WorkerOptions } from './worker/worker-options'
/**
* - `key`: The `Worker`
- * - `value`: Number of tasks that has been assigned to that worker since it started
+ * - `value`: Number of tasks currently in progress on the worker.
*/
public readonly tasks: Map<Worker, number> = new Map<Worker, number>()
if (!this.filePath) {
throw new Error('Please specify a file with a worker implementation')
}
-
this.setupHook()
for (let i = 1; i <= this.numberOfWorkers; i++) {
* @param worker Workers whose tasks are increased.
*/
protected increaseWorkersTask (worker: Worker): void {
- const numberOfTasksTheWorkerHas = this.tasks.get(worker)
- if (numberOfTasksTheWorkerHas !== undefined) {
- this.tasks.set(worker, numberOfTasksTheWorkerHas + 1)
+ const numberOfTasksInProgress = this.tasks.get(worker)
+ if (numberOfTasksInProgress !== undefined) {
+ this.tasks.set(worker, numberOfTasksInProgress + 1)
+ } else {
+ throw Error('Worker could not be found in tasks map')
+ }
+ }
+
+ /**
+ * Decrease the number of tasks that the given workers has done.
+ *
+ * @param worker Workers whose tasks are decreased.
+ */
+ protected decreaseWorkersTasks (worker: Worker): void {
+ const numberOfTasksInProgress = this.tasks.get(worker)
+ if (numberOfTasksInProgress !== undefined) {
+ this.tasks.set(worker, numberOfTasksInProgress - 1)
} else {
throw Error('Worker could not be found in tasks map')
}
const listener: (message: MessageValue<Response>) => void = message => {
if (message.id === messageId) {
this.unregisterWorkerMessageListener(worker, listener)
- this.increaseWorkersTask(worker)
+ this.decreaseWorkersTasks(worker)
if (message.error) reject(message.error)
else resolve(message.data as Response)
}
import type { Worker } from 'cluster'
import type { JSONValue } from '../../utility-types'
+import { isKillBehavior, KillBehaviors } from '../../worker/worker-options'
import type { ClusterPoolOptions } from './fixed'
import { FixedClusterPool } from './fixed'
}
// All workers are busy, create a new worker
- const worker = this.createAndSetupWorker()
- this.registerWorkerMessageListener<Data>(worker, message => {
- if (message.kill) {
- this.sendToWorker(worker, { kill: 1 })
- void this.destroyWorker(worker)
+ const workerCreated = this.createAndSetupWorker()
+ this.registerWorkerMessageListener<Data>(workerCreated, message => {
+ const tasksInProgress = this.tasks.get(workerCreated)
+ if (
+ isKillBehavior(KillBehaviors.HARD, message.kill) ||
+ tasksInProgress === 0
+ ) {
+ // Kill received from the worker, means that no new tasks are submitted to that worker for a while ( > maxInactiveTime)
+ this.sendToWorker(workerCreated, { kill: 1 })
+ void this.destroyWorker(workerCreated)
}
})
- return worker
+ return workerCreated
}
}
import type { JSONValue } from '../../utility-types'
+import { isKillBehavior, KillBehaviors } from '../../worker/worker-options'
import type { PoolOptions } from '../abstract-pool'
import type { ThreadWorkerWithMessageChannel } from './fixed'
import { FixedThreadPool } from './fixed'
}
// All workers are busy, create a new worker
- const worker = this.createAndSetupWorker()
- this.registerWorkerMessageListener<Data>(worker, message => {
- if (message.kill) {
- this.sendToWorker(worker, { kill: 1 })
- void this.destroyWorker(worker)
+ const workerCreated = this.createAndSetupWorker()
+ this.registerWorkerMessageListener<Data>(workerCreated, message => {
+ const tasksInProgress = this.tasks.get(workerCreated)
+ if (
+ isKillBehavior(KillBehaviors.HARD, message.kill) ||
+ tasksInProgress === 0
+ ) {
+ // Kill received from the worker, means that no new tasks are submitted to that worker for a while ( > maxInactiveTime)
+ this.sendToWorker(workerCreated, { kill: 1 })
+ void this.destroyWorker(workerCreated)
}
})
- return worker
+ return workerCreated
}
}
import type { Worker } from 'cluster'
import type { MessagePort } from 'worker_threads'
+import type { KillBehavior } from './worker/worker-options'
/**
* Make all properties in T non-readonly
/**
* Kill code.
*/
- readonly kill?: number
+ readonly kill?: KillBehavior | 1
/**
* Error.
*/
import type { Worker } from 'cluster'
import type { MessagePort } from 'worker_threads'
import type { MessageValue } from '../utility-types'
-import type { WorkerOptions } from './worker-options'
+import type { KillBehavior, WorkerOptions } from './worker-options'
+import { KillBehaviors } from './worker-options'
+
+const DEFAULT_MAX_INACTIVE_TIME = 1000 * 60
+const DEFAULT_KILL_BEHAVIOR: KillBehavior = KillBehaviors.SOFT
/**
* Base class containing some shared logic for all poolifier workers.
* The maximum time to keep this worker alive while idle. The pool automatically checks and terminates this worker when the time expires.
*/
protected readonly maxInactiveTime: number
+ /**
+ * The kill behavior set as option on the Worker constructor or a default value.
+ */
+ protected readonly killBehavior: KillBehavior
/**
* Whether the worker is working asynchronously or not.
*/
isMain: boolean,
fn: (data: Data) => Response,
protected mainWorker?: MainWorker | null,
- public readonly opts: WorkerOptions = {}
+ public readonly opts: WorkerOptions = {
+ killBehavior: DEFAULT_KILL_BEHAVIOR,
+ maxInactiveTime: DEFAULT_MAX_INACTIVE_TIME
+ }
) {
super(type)
-
- this.maxInactiveTime = this.opts.maxInactiveTime ?? 1000 * 60
+ this.killBehavior = this.opts.killBehavior ?? DEFAULT_KILL_BEHAVIOR
+ this.maxInactiveTime =
+ this.opts.maxInactiveTime ?? DEFAULT_MAX_INACTIVE_TIME
this.async = !!this.opts.async
this.lastTask = Date.now()
if (!fn) throw new Error('fn parameter is mandatory')
*/
protected checkAlive (): void {
if (Date.now() - this.lastTask > this.maxInactiveTime) {
- this.sendToMainWorker({ kill: 1 })
+ this.sendToMainWorker({ kill: this.killBehavior })
}
}
+/**
+ * Enumeration of kill behaviors.
+ */
+export const KillBehaviors = Object.freeze({
+ /**
+ * If `currentTime - lastActiveTime` is greater than `maxInactiveTime` but a task is still running, then the worker **wont** be deleted.
+ */
+ SOFT: 'SOFT',
+ /**
+ * If `lastActiveTime` is greater than `maxInactiveTime` but a task is still running, then the worker will be deleted.
+ */
+ HARD: 'HARD'
+} as const)
+
+/**
+ * Kill behavior.
+ */
+export type KillBehavior = keyof typeof KillBehaviors
+
+/**
+ * Detects whether the given value is a kill behavior or not.
+ *
+ * @template KB Which specific KillBehavior to test against.
+ * @param killBehavior Which kind of kill behavior to detect. Default: `KillBehaviors.HARD`.
+ * @param value Any value.
+ * @returns `true` if `value` was strictly equals to `killBehavior`, otherwise `false`.
+ */
+export function isKillBehavior<KB extends KillBehavior> (
+ killBehavior: KB,
+ value: unknown
+): value is KB {
+ return value === killBehavior
+}
+
/**
* Options for workers.
*/
* Maximum waiting time in milliseconds for tasks.
*
* After this time, newly created workers will be terminated.
+ * The last active time of your worker unit will be updated when a task is submitted to a worker or when a worker terminate a task.
+ *
+ * - If `killBehavior` is set to `KillBehaviors.HARD` this value represents also the timeout for the tasks that you submit to the pool,
+ * when this timeout expires your tasks is interrupted and the worker is killed if is not part of the minimum size of the pool.
+ * - If `killBehavior` is set to `KillBehaviors.SOFT` your tasks have no timeout and your workers will not be terminated until your task is.
*
* @default 60.000 ms
*/
* @default false
*/
async?: boolean
+ /**
+ * `killBehavior` dictates if your async unit (worker/process) will be deleted in case that a task is active on it.
+ *
+ * - SOFT: If `currentTime - lastActiveTime` is greater than `maxInactiveTime` but a task is still running, then the worker **wont** be deleted.
+ * - HARD: If `lastActiveTime` is greater than `maxInactiveTime` but a task is still running, then the worker will be deleted.
+ *
+ * This option only apply to the newly created workers.
+ *
+ * @default KillBehaviors.SOFT
+ */
+ killBehavior?: KillBehavior
}
--- /dev/null
+const expect = require('expect')
+const { FixedThreadPool } = require('../../../lib/index')
+const expectedError = new Error('Worker could not be found in tasks map')
+
+class StubPoolWithTasksMapClear extends FixedThreadPool {
+ removeAllWorker () {
+ this.tasks.clear()
+ }
+}
+
+class StubPoolWithIsMainMethod extends FixedThreadPool {
+ isMain () {
+ return false
+ }
+}
+
+describe('Abstract pool test suite ', () => {
+ it('Simulate worker not found during increaseWorkersTask', () => {
+ const pool = new StubPoolWithTasksMapClear(
+ 1,
+ './tests/worker-files/cluster/testWorker.js',
+ {
+ errorHandler: e => console.error(e)
+ }
+ )
+ // simulate worker not found.
+ pool.removeAllWorker()
+ expect(() => pool.increaseWorkersTask()).toThrowError(expectedError)
+ })
+
+ it('Simulate worker not found during decreaseWorkersTasks', () => {
+ const pool = new StubPoolWithTasksMapClear(
+ 1,
+ './tests/worker-files/cluster/testWorker.js',
+ {
+ errorHandler: e => console.error(e)
+ }
+ )
+ // simulate worker not found.
+ pool.removeAllWorker()
+ expect(() => pool.decreaseWorkersTasks()).toThrowError(expectedError)
+ })
+
+ it('Simulate pool creation from a non main thread/process', () => {
+ expect(() => {
+ const pool = new StubPoolWithIsMainMethod(
+ 1,
+ './tests/worker-files/cluster/testWorker.js',
+ {
+ errorHandler: e => console.error(e)
+ }
+ )
+ }).toThrowError()
+ })
+})
max,
'./tests/worker-files/cluster/testWorker.js',
{
- errorHandler: e => console.error(e),
- onlineHandler: () => console.log('worker is online')
+ errorHandler: e => console.error(e)
}
)
pool.execute({ test: 'test' })
}
expect(pool.workers.length).toBeGreaterThan(min)
- await new Promise(resolve => setTimeout(resolve, 2000))
+ await new Promise(resolve => setTimeout(resolve, 3000))
expect(pool.workers.length).toBe(min)
})
it('Shutdown test', async () => {
})
})
pool.destroy()
- await new Promise(resolve => setTimeout(resolve, 1000))
+ await new Promise(resolve => setTimeout(resolve, 2000))
expect(closedWorkers).toBe(min)
})
const res = await pool1.execute({ test: 'test' })
expect(res).toBeFalsy()
})
+
+ it('Verify scale processes up and down is working when long running task is used:hard', async () => {
+ const longRunningPool = new DynamicClusterPool(
+ min,
+ max,
+ './tests/worker/cluster/longRunningWorkerHardBehavior.js'
+ )
+ expect(longRunningPool.workers.length).toBe(min)
+ for (let i = 0; i < max * 10; i++) {
+ longRunningPool.execute({ test: 'test' })
+ }
+ expect(longRunningPool.workers.length).toBe(max)
+ await new Promise(resolve => setTimeout(resolve, 3000))
+ // Here we expect the workers to be at the max size since that the task is still running
+ expect(longRunningPool.workers.length).toBe(min)
+ })
+
+ it('Verify scale processes up and down is working when long running task is used:soft', async () => {
+ const longRunningPool = new DynamicClusterPool(
+ min,
+ max,
+ './tests/worker/cluster/longRunningWorkerSoftBehavior.js'
+ )
+ expect(longRunningPool.workers.length).toBe(min)
+ for (let i = 0; i < max * 10; i++) {
+ longRunningPool.execute({ test: 'test' })
+ }
+ expect(longRunningPool.workers.length).toBe(max)
+ await new Promise(resolve => setTimeout(resolve, 3000))
+ // Here we expect the workers to be at the max size since that the task is still running
+ expect(longRunningPool.workers.length).toBe(max)
+ })
})
numberOfWorkers,
'./tests/worker-files/cluster/testWorker.js',
{
- errorHandler: e => console.error(e),
- onlineHandler: () => console.log('worker is online')
+ errorHandler: e => console.error(e)
}
)
const emptyPool = new FixedClusterPool(
1,
'./tests/worker-files/cluster/errorWorker.js',
{
- errorHandler: e => console.error(e),
- onlineHandler: () => console.log('worker is online')
+ errorHandler: e => console.error(e)
}
)
})
})
await pool.destroy()
- await new Promise(resolve => setTimeout(resolve, 200))
+ await new Promise(resolve => setTimeout(resolve, 500))
expect(closedWorkers).toBe(numberOfWorkers)
})
max,
'./tests/worker-files/thread/testWorker.js',
{
- errorHandler: e => console.error(e),
- onlineHandler: () => console.log('worker is online')
+ errorHandler: e => console.error(e)
}
)
pool.execute({ test: 'test' })
}
expect(pool.workers.length).toBe(max)
- await new Promise(resolve => setTimeout(resolve, 1000))
+ await new Promise(resolve => setTimeout(resolve, 1500))
expect(pool.workers.length).toBe(min)
})
+
it('Shutdown test', async () => {
let closedThreads = 0
pool.workers.forEach(w => {
const res = await pool1.execute({ test: 'test' })
expect(res).toBeFalsy()
})
+
+ it('Verify scale thread up and down is working when long running task is used:hard', async () => {
+ const longRunningPool = new DynamicThreadPool(
+ min,
+ max,
+ './tests/worker/thread/longRunningWorkerHardBehavior.js',
+ {
+ errorHandler: e => console.error(e),
+ onlineHandler: () => console.log('worker is online')
+ }
+ )
+ expect(longRunningPool.workers.length).toBe(min)
+ for (let i = 0; i < max * 10; i++) {
+ longRunningPool.execute({ test: 'test' })
+ }
+ expect(longRunningPool.workers.length).toBe(max)
+ await new Promise(resolve => setTimeout(resolve, 1500))
+ expect(longRunningPool.workers.length).toBe(min)
+ })
+
+ it('Verify scale thread up and down is working when long running task is used:soft', async () => {
+ const longRunningPool = new DynamicThreadPool(
+ min,
+ max,
+ './tests/worker/thread/longRunningWorkerSoftBehavior.js',
+ {
+ errorHandler: e => console.error(e),
+ onlineHandler: () => console.log('worker is online')
+ }
+ )
+ expect(longRunningPool.workers.length).toBe(min)
+ for (let i = 0; i < max * 10; i++) {
+ longRunningPool.execute({ test: 'test' })
+ }
+ expect(longRunningPool.workers.length).toBe(max)
+ await new Promise(resolve => setTimeout(resolve, 1500))
+ // Here we expect the workers to be at the max size since that the task is still running
+ expect(longRunningPool.workers.length).toBe(max)
+ })
})
numberOfThreads,
'./tests/worker-files/thread/testWorker.js',
{
- errorHandler: e => console.error(e),
- onlineHandler: () => console.log('worker is online')
+ errorHandler: e => console.error(e)
}
)
const emptyPool = new FixedThreadPool(
'use strict'
-const { ClusterWorker } = require('../../../lib/index')
+const { ClusterWorker, KillBehaviors } = require('../../../lib/index')
async function error (data) {
return new Promise((resolve, reject) => {
module.exports = new ClusterWorker(error, {
maxInactiveTime: 500,
- async: true
+ async: true,
+ killBehavior: KillBehaviors.HARD
})
'use strict'
-const { ClusterWorker } = require('../../../lib/index')
+const { ClusterWorker, KillBehaviors } = require('../../../lib/index')
async function sleep (data) {
return new Promise((resolve, reject) => {
})
}
-module.exports = new ClusterWorker(sleep, { maxInactiveTime: 500, async: true })
+module.exports = new ClusterWorker(sleep, {
+ maxInactiveTime: 500,
+ async: true,
+ killBehavior: KillBehaviors.HARD
+})
'use strict'
-const { ClusterWorker } = require('../../../lib/index')
+const { ClusterWorker, KillBehaviors } = require('../../../lib/index')
function echo (data) {
return data
}
-module.exports = new ClusterWorker(echo, { maxInactiveTime: 500 })
+module.exports = new ClusterWorker(echo, {
+ maxInactiveTime: 500,
+ killBehavior: KillBehaviors.HARD
+})
'use strict'
-const { ClusterWorker } = require('../../../lib/index')
+const { ClusterWorker, KillBehaviors } = require('../../../lib/index')
function test (data) {}
-module.exports = new ClusterWorker(test, { maxInactiveTime: 500 })
+module.exports = new ClusterWorker(test, {
+ maxInactiveTime: 500,
+ killBehavior: KillBehaviors.HARD
+})
'use strict'
-const { ClusterWorker } = require('../../../lib/index')
+const { ClusterWorker, KillBehaviors } = require('../../../lib/index')
function error (data) {
throw new Error('Error Message from ClusterWorker')
module.exports = new ClusterWorker(error, {
maxInactiveTime: 500,
- async: false
+ async: false,
+ killBehavior: KillBehaviors.HARD
})
'use strict'
-const { ClusterWorker } = require('../../../lib/index')
+const { ClusterWorker, KillBehaviors } = require('../../../lib/index')
const { isMaster } = require('cluster')
function test (data) {
return isMaster
}
-module.exports = new ClusterWorker(test, { maxInactiveTime: 500 })
+module.exports = new ClusterWorker(test, {
+ maxInactiveTime: 500,
+ killBehavior: KillBehaviors.HARD
+})
'use strict'
-const { ThreadWorker } = require('../../../lib/index')
+const { ThreadWorker, KillBehaviors } = require('../../../lib/index')
async function sleep (data) {
return new Promise((resolve, reject) => {
})
}
-module.exports = new ThreadWorker(sleep, { maxInactiveTime: 500, async: true })
+module.exports = new ThreadWorker(sleep, {
+ maxInactiveTime: 500,
+ async: true,
+ killBehavior: KillBehaviors.HARD
+})
'use strict'
-const { ThreadWorker } = require('../../../lib/index')
+const { ThreadWorker, KillBehaviors } = require('../../../lib/index')
function echo (data) {
return data
}
-module.exports = new ThreadWorker(echo, { maxInactiveTime: 500 })
+module.exports = new ThreadWorker(echo, {
+ maxInactiveTime: 500,
+ killBehavior: KillBehaviors.HARD
+})
'use strict'
-const { ThreadWorker } = require('../../../lib/index')
+const { ThreadWorker, KillBehaviors } = require('../../../lib/index')
function test (data) {}
-module.exports = new ThreadWorker(test, { maxInactiveTime: 500 })
+module.exports = new ThreadWorker(test, {
+ maxInactiveTime: 500,
+ killBehavior: KillBehaviors.HARD
+})
'use strict'
-const { ThreadWorker } = require('../../../lib/index')
+const { ThreadWorker, KillBehaviors } = require('../../../lib/index')
function error (data) {
throw new Error(data)
}
-module.exports = new ThreadWorker(error, { maxInactiveTime: 500 })
+module.exports = new ThreadWorker(error, {
+ maxInactiveTime: 500,
+ killBehavior: KillBehaviors.HARD
+})
'use strict'
-const { ThreadWorker } = require('../../../lib/index')
+const { ThreadWorker, KillBehaviors } = require('../../../lib/index')
const { isMainThread } = require('worker_threads')
function test (data) {
return isMainThread
}
-module.exports = new ThreadWorker(test, { maxInactiveTime: 500 })
+module.exports = new ThreadWorker(test, {
+ maxInactiveTime: 500,
+ killBehavior: KillBehaviors.HARD
+})
--- /dev/null
+'use strict'
+const { ClusterWorker, KillBehaviors } = require('../../../lib/index')
+
+async function sleep (data) {
+ return new Promise((resolve, reject) => {
+ setTimeout(() => resolve(data), 50000)
+ })
+}
+
+module.exports = new ClusterWorker(sleep, {
+ maxInactiveTime: 500,
+ async: true,
+ killBehavior: KillBehaviors.HARD
+})
--- /dev/null
+'use strict'
+const { ClusterWorker } = require('../../../lib/index')
+
+async function sleep (data) {
+ return new Promise((resolve, reject) => {
+ setTimeout(() => resolve(data), 50000)
+ })
+}
+
+module.exports = new ClusterWorker(sleep, {
+ maxInactiveTime: 500,
+ async: true
+})
--- /dev/null
+'use strict'
+const { ThreadWorker, KillBehaviors } = require('../../../lib/index')
+
+async function sleep (data) {
+ return new Promise((resolve, reject) => {
+ setTimeout(() => resolve(data), 50000)
+ })
+}
+
+module.exports = new ThreadWorker(sleep, {
+ maxInactiveTime: 500,
+ async: true,
+ killBehavior: KillBehaviors.HARD
+})
--- /dev/null
+'use strict'
+const { ThreadWorker } = require('../../../lib/index')
+
+async function sleep (data) {
+ return new Promise((resolve, reject) => {
+ setTimeout(() => resolve(data), 50000)
+ })
+}
+
+module.exports = new ThreadWorker(sleep, {
+ maxInactiveTime: 500,
+ async: true
+})