├── .eslintrc ├── .gitignore ├── CHANGES.md ├── CONTRIBUTING.md ├── LICENSE.md ├── Makefile ├── README.md ├── api.md ├── docs.json ├── index.js ├── lib ├── master.js ├── msg.js └── util.js ├── package.json └── test ├── helpers.js ├── restart-failed.js ├── test-exposed-properties.js ├── test-options-env.js ├── test-require.js ├── test-resize-disconnected.js ├── test-resize-event.js ├── test-resize-from-1-to-0.js ├── test-resize-from-1-to-2.js ├── test-resize-from-3-to-1.js ├── test-resize-from-fork.js ├── test-resize-slowly.js ├── test-resize-to-last-concurrent.js ├── test-resize-while-forking.js ├── test-resize-while-over-forking.js ├── test-restart-all.js ├── test-restart-and-recover.js ├── test-restart-failed-1.js ├── test-restart-failed-2.js ├── test-restart-failed-5.js ├── test-restart-ok.js ├── test-set-size.js ├── test-start-and-stop-events.js ├── test-start-stop-own.js ├── test-status-array.js ├── test-worker-augmentation.js ├── test-worker-id-sorting.js ├── test-worker-kill.js ├── test-worker-shutdown-cmd.js ├── test-worker-shutdown.js └── workers └── null.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "extends": "eslint:recommended", 6 | "rules": { 7 | "comma-dangle": 0, 8 | "no-extra-semi": 2, 9 | "valid-jsdoc": 2, 10 | "no-unexpected-multiline": 2, 11 | "no-console": 0 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.tgz 3 | coverage.html 4 | mocha 5 | node_modules 6 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | 2018-10-09, Version 2.2.4 2 | ========================= 3 | 4 | * fix: package.json to reduce vulnerabilities (snyk-bot) 5 | 6 | * package: correct doc link (Sam Roberts) 7 | 8 | * package: replace jscs with eslint 2.x (Sam Roberts) 9 | 10 | * Update URLs in CONTRIBUTING.md (#60) (Ryan Graham) 11 | 12 | 13 | 2016-05-03, Version 2.2.3 14 | ========================= 15 | 16 | * update/insert copyright notices (Ryan Graham) 17 | 18 | * relicense as Artistic-2.0 only (Ryan Graham) 19 | 20 | 21 | 2016-04-11, Version 2.2.2 22 | ========================= 23 | 24 | * Refer to licenses with a link (Sam Roberts) 25 | 26 | 27 | 2015-10-01, Version 2.2.1 28 | ========================= 29 | 30 | * Sort workers by numeric value of worker ID (Sam Roberts) 31 | 32 | * status: preserve the cluster size during restart (Sam Roberts) 33 | 34 | * Use strongloop conventions for licensing (Sam Roberts) 35 | 36 | 37 | 2015-09-15, Version 2.2.0 38 | ========================= 39 | 40 | * package: upgrade to tap 1.4.1 (Sam Roberts) 41 | 42 | * Refactor restart tests from mocha to tap (Sam Roberts) 43 | 44 | * Update eslint to 1.x (Sam Roberts) 45 | 46 | * Restart should start new then stop old (Sam Roberts) 47 | 48 | * Use formatted debug statements (Sam Roberts) 49 | 50 | 51 | 2015-07-24, Version 2.1.2 52 | ========================= 53 | 54 | * test: update to pass eslint (Sam Roberts) 55 | 56 | 57 | 2015-05-21, Version 2.1.1 58 | ========================= 59 | 60 | * shutdown: don't wait for workers that have exited (Sam Roberts) 61 | 62 | * master: sort require statements (Sam Roberts) 63 | 64 | 65 | 2015-05-06, Version 2.1.0 66 | ========================= 67 | 68 | * Avoid nextTick recursion by using setImmediate (Sam Roberts) 69 | 70 | * Fix busy-loop on resize after disconnect (Sam Roberts) 71 | 72 | * emit custom 'fork' events with s-c-c metadata (Ryan Graham) 73 | 74 | * test: clean up lint warnings (Ryan Graham) 75 | 76 | * deps: upgrade lodash to v3.x (Ryan Graham) 77 | 78 | * Record and report startTime of every process (Ryan Graham) 79 | 80 | 81 | 2015-03-30, Version 2.0.1 82 | ========================= 83 | 84 | * debug: switch from debuglog to visonmedia/debug (Sam Roberts) 85 | 86 | * ui: remove unused lib/ui.js and test/ui.js (Sam Roberts) 87 | 88 | * package: update jscs ignore rule (Sam Roberts) 89 | 90 | * Update README for strong-pm.io (Sam Roberts) 91 | 92 | * package: update eslint to 0.17 (Sam Roberts) 93 | 94 | * Update README (Sam Roberts) 95 | 96 | * lint: add eslint and jscs support (Sam Roberts) 97 | 98 | * Fix broken link (Rand McKinney) 99 | 100 | 101 | 2015-01-12, Version 2.0.0 102 | ========================= 103 | 104 | * Remove loadOptions(), its no longer used (Sam Roberts) 105 | 106 | * Update dev dependencies to work on Windows (Ryan Graham) 107 | 108 | * Add utility to humanize duration (Ryan Graham) 109 | 110 | * Add uptime to worker status (Ryan Graham) 111 | 112 | * Fix bad CLA URL in CONTRIBUTING.md (Ryan Graham) 113 | 114 | 115 | 2014-12-12, Version 1.0.2 116 | ========================= 117 | 118 | * package: update lodash to ^2.2 (Sam Roberts) 119 | 120 | 121 | 2014-11-03, Version 1.0.1 122 | ========================= 123 | 124 | * status: include current set-size (Sam Roberts) 125 | 126 | 127 | 2014-10-02, Version 1.0.0 128 | ========================= 129 | 130 | * package: remove module names from keywords (Sam Roberts) 131 | 132 | * Update contribution guidelines (Ryan Graham) 133 | 134 | 135 | 2014-08-21, Version 0.5.1 136 | ========================= 137 | 138 | * Update package license to match LICENSE.md (Sam Roberts) 139 | 140 | 141 | 2014-07-21, Version 0.5.0 142 | ========================= 143 | 144 | * Update package keywords, sort order, and bins (Sam Roberts) 145 | 146 | * Remove control channel and CLI (Sam Roberts) 147 | 148 | * Fix link to docs (again) (Rand McKinney) 149 | 150 | * Update link to doc (Rand McKinney) 151 | 152 | * doc: add CONTRIBUTING.md and LICENSE.md (Ben Noordhuis) 153 | 154 | * fix broken links in README (Sam Roberts) 155 | 156 | 157 | 2014-02-19, Version 0.4.0 158 | ========================= 159 | 160 | * Use 'slc clusterctl' in usage when run from slc (Sam Roberts) 161 | 162 | * Note existence of strong-supervisor in README (Sam Roberts) 163 | 164 | * Prefer cluster to size in command line and config (Sam Roberts) 165 | 166 | * example-master, log information about environment (Sam Roberts) 167 | 168 | * Install CLI as sl-clusterctl (Sam Roberts) 169 | 170 | * Apply Dual MIT/StrongLoop license (Sam Roberts) 171 | 172 | * Update jshint (Sam Roberts) 173 | 174 | 175 | 2014-01-25, Version 0.3.0 176 | ========================= 177 | 178 | * Add keywords to npm package (Sam Roberts) 179 | 180 | * Include master process ID in status (Sam Roberts) 181 | 182 | * Emit 'startRestart' when restart begins (Sam Roberts) 183 | 184 | * Emit 'setSize' event after size is set (Sam Roberts) 185 | 186 | * Size for loadOptions() allows 0, and mixed case (Sam Roberts) 187 | 188 | * Fix example-master, too complicated to be useful (Sam Roberts) 189 | 190 | * Clarify debug message about server status (Sam Roberts) 191 | 192 | * Clarify debug message about exit (Sam Roberts) 193 | 194 | * support windows variations of worker death (Sam Roberts) 195 | 196 | * support both windows and unix local sockets (Sam Roberts) 197 | 198 | * Test death by signal, as well as worker.destroy() (Sam Roberts) 199 | 200 | * Refactor resize test so size can be easily changed (Sam Roberts) 201 | 202 | * Add debug messages around server disconnection (Sam Roberts) 203 | 204 | * Remove env dump by every worker in debug mode (Sam Roberts) 205 | 206 | 207 | 2013-11-28, Version 0.2.2 208 | ========================= 209 | 210 | * clusterctl reports worker id correctly (Sam Roberts) 211 | 212 | * Update README.md (Rand McKinney) 213 | 214 | * Remove test code causing test worker to exit (Sam Roberts) 215 | 216 | * Worker debug output cleaned up and extended (Sam Roberts) 217 | 218 | * Update docs.json (Rand McKinney) 219 | 220 | * Extracted API docs into separate file (Rand McKinney) 221 | 222 | * Remove blanket from package.json, we use istanbul (Sam Roberts) 223 | 224 | 225 | 2013-11-06, Version 0.2.1 226 | ========================= 227 | 228 | * start/stop stubs for worker return an EventEmitter (Sam Roberts) 229 | 230 | 231 | 2013-10-29, Version 0.2.0 232 | ========================= 233 | 234 | * Revert premature update of package version (Sam Roberts) 235 | 236 | * Doc using `npm install`, not `slnode install` (Sam Roberts) 237 | 238 | * Detect and fail on invalid multiple instantiation (Sam Roberts) 239 | 240 | * Add restart command, to restart all workers (Sam Roberts) 241 | 242 | * Test reorganized into categories, and size reset (Sam Roberts) 243 | 244 | * Throttle worker restarts after an abormal exit (Sam Roberts) 245 | 246 | * Test worker, add support for commands in env (Sam Roberts) 247 | 248 | * Test worker, add support for exit by erroring (Sam Roberts) 249 | 250 | * Setup all workers with _control and birth time (Sam Roberts) 251 | 252 | * Use DEBUG or NODE_DEBUG to enable logging (Sam Roberts) 253 | 254 | * Keep control.options.size in sync with setSize() (Sam Roberts) 255 | 256 | * Support shutting down a cluster (Sam Roberts) 257 | 258 | * Fix test failure involving client/server race (Sam Roberts) 259 | 260 | * Add master.status(), a public method for cluster state. (Michael Schoonmaker) 261 | 262 | * Use blanket ~1.1.5 instead of 'latest' (Sam Roberts) 263 | 264 | * Use mocha tap reporter (Sam Roberts) 265 | 266 | * Cli factored into a callable function (Sam Roberts) 267 | 268 | * Minor edits per style guide. Rm'd dangling API header. (Edmond Meinfelder) 269 | 270 | 271 | 2013-08-29, Version 0.1.0 272 | ========================= 273 | 274 | * Document the loadOptions function (Sam Roberts) 275 | 276 | * Optional loading of options from configuration (Sam Roberts) 277 | 278 | * Adding docs.json for doc effort. (Edmond Meinfelder) 279 | 280 | * Rename control.msg to control.cmd (Sam Roberts) 281 | 282 | * update coverage filename (slnode) 283 | 284 | * Send SHUTDOWN message before disconnecting (Sam Roberts) 285 | 286 | 287 | 2013-07-15, Version 0.0.2 288 | ========================= 289 | 290 | * First release! 291 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### Contributing ### 2 | 3 | Thank you for your interest in `strong-cluster-control`, an open source project 4 | administered by StrongLoop. 5 | 6 | Contributing to `strong-cluster-control` is easy. In a few simple steps: 7 | 8 | * Ensure that your effort is aligned with the project's roadmap by 9 | talking to the maintainers, especially if you are going to spend a 10 | lot of time on it. 11 | 12 | * Make something better or fix a bug. 13 | 14 | * Adhere to code style outlined in the [Google C++ Style Guide][] and 15 | [Google Javascript Style Guide][]. 16 | 17 | * Sign the [Contributor License Agreement](https://cla.strongloop.com/agreements/strongloop/strong-cluster-control) 18 | 19 | * Submit a pull request through Github. 20 | 21 | 22 | ### Contributor License Agreement ### 23 | 24 | ``` 25 | Individual Contributor License Agreement 26 | 27 | By signing this Individual Contributor License Agreement 28 | ("Agreement"), and making a Contribution (as defined below) to 29 | StrongLoop, Inc. ("StrongLoop"), You (as defined below) accept and 30 | agree to the following terms and conditions for Your present and 31 | future Contributions submitted to StrongLoop. Except for the license 32 | granted in this Agreement to StrongLoop and recipients of software 33 | distributed by StrongLoop, You reserve all right, title, and interest 34 | in and to Your Contributions. 35 | 36 | 1. Definitions 37 | 38 | "You" or "Your" shall mean the copyright owner or the individual 39 | authorized by the copyright owner that is entering into this 40 | Agreement with StrongLoop. 41 | 42 | "Contribution" shall mean any original work of authorship, 43 | including any modifications or additions to an existing work, that 44 | is intentionally submitted by You to StrongLoop for inclusion in, 45 | or documentation of, any of the products owned or managed by 46 | StrongLoop ("Work"). For purposes of this definition, "submitted" 47 | means any form of electronic, verbal, or written communication 48 | sent to StrongLoop or its representatives, including but not 49 | limited to communication or electronic mailing lists, source code 50 | control systems, and issue tracking systems that are managed by, 51 | or on behalf of, StrongLoop for the purpose of discussing and 52 | improving the Work, but excluding communication that is 53 | conspicuously marked or otherwise designated in writing by You as 54 | "Not a Contribution." 55 | 56 | 2. You Grant a Copyright License to StrongLoop 57 | 58 | Subject to the terms and conditions of this Agreement, You hereby 59 | grant to StrongLoop and recipients of software distributed by 60 | StrongLoop, a perpetual, worldwide, non-exclusive, no-charge, 61 | royalty-free, irrevocable copyright license to reproduce, prepare 62 | derivative works of, publicly display, publicly perform, 63 | sublicense, and distribute Your Contributions and such derivative 64 | works under any license and without any restrictions. 65 | 66 | 3. You Grant a Patent License to StrongLoop 67 | 68 | Subject to the terms and conditions of this Agreement, You hereby 69 | grant to StrongLoop and to recipients of software distributed by 70 | StrongLoop a perpetual, worldwide, non-exclusive, no-charge, 71 | royalty-free, irrevocable (except as stated in this Section) 72 | patent license to make, have made, use, offer to sell, sell, 73 | import, and otherwise transfer the Work under any license and 74 | without any restrictions. The patent license You grant to 75 | StrongLoop under this Section applies only to those patent claims 76 | licensable by You that are necessarily infringed by Your 77 | Contributions(s) alone or by combination of Your Contributions(s) 78 | with the Work to which such Contribution(s) was submitted. If any 79 | entity institutes a patent litigation against You or any other 80 | entity (including a cross-claim or counterclaim in a lawsuit) 81 | alleging that Your Contribution, or the Work to which You have 82 | contributed, constitutes direct or contributory patent 83 | infringement, any patent licenses granted to that entity under 84 | this Agreement for that Contribution or Work shall terminate as 85 | of the date such litigation is filed. 86 | 87 | 4. You Have the Right to Grant Licenses to StrongLoop 88 | 89 | You represent that You are legally entitled to grant the licenses 90 | in this Agreement. 91 | 92 | If Your employer(s) has rights to intellectual property that You 93 | create, You represent that You have received permission to make 94 | the Contributions on behalf of that employer, that Your employer 95 | has waived such rights for Your Contributions, or that Your 96 | employer has executed a separate Corporate Contributor License 97 | Agreement with StrongLoop. 98 | 99 | 5. The Contributions Are Your Original Work 100 | 101 | You represent that each of Your Contributions are Your original 102 | works of authorship (see Section 8 (Submissions on Behalf of 103 | Others) for submission on behalf of others). You represent that to 104 | Your knowledge, no other person claims, or has the right to claim, 105 | any right in any intellectual property right related to Your 106 | Contributions. 107 | 108 | You also represent that You are not legally obligated, whether by 109 | entering into an agreement or otherwise, in any way that conflicts 110 | with the terms of this Agreement. 111 | 112 | You represent that Your Contribution submissions include complete 113 | details of any third-party license or other restriction (including, 114 | but not limited to, related patents and trademarks) of which You 115 | are personally aware and which are associated with any part of 116 | Your Contributions. 117 | 118 | 6. You Don't Have an Obligation to Provide Support for Your Contributions 119 | 120 | You are not expected to provide support for Your Contributions, 121 | except to the extent You desire to provide support. You may provide 122 | support for free, for a fee, or not at all. 123 | 124 | 6. No Warranties or Conditions 125 | 126 | StrongLoop acknowledges that unless required by applicable law or 127 | agreed to in writing, You provide Your Contributions on an "AS IS" 128 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 129 | EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES 130 | OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR 131 | FITNESS FOR A PARTICULAR PURPOSE. 132 | 133 | 7. Submission on Behalf of Others 134 | 135 | If You wish to submit work that is not Your original creation, You 136 | may submit it to StrongLoop separately from any Contribution, 137 | identifying the complete details of its source and of any license 138 | or other restriction (including, but not limited to, related 139 | patents, trademarks, and license agreements) of which You are 140 | personally aware, and conspicuously marking the work as 141 | "Submitted on Behalf of a Third-Party: [named here]". 142 | 143 | 8. Agree to Notify of Change of Circumstances 144 | 145 | You agree to notify StrongLoop of any facts or circumstances of 146 | which You become aware that would make these representations 147 | inaccurate in any respect. Email us at callback@strongloop.com. 148 | ``` 149 | 150 | [Google C++ Style Guide]: https://google.github.io/styleguide/cppguide.html 151 | [Google Javascript Style Guide]: https://google.github.io/styleguide/javascriptguide.xml 152 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) IBM Corp. 2013,2016. All Rights Reserved. 2 | Node module: strong-cluster-control 3 | This project is licensed under the Artistic License 2.0, full text below. 4 | 5 | -------- 6 | 7 | The Artistic License 2.0 8 | 9 | Copyright (c) 2000-2006, The Perl Foundation. 10 | 11 | Everyone is permitted to copy and distribute verbatim copies 12 | of this license document, but changing it is not allowed. 13 | 14 | Preamble 15 | 16 | This license establishes the terms under which a given free software 17 | Package may be copied, modified, distributed, and/or redistributed. 18 | The intent is that the Copyright Holder maintains some artistic 19 | control over the development of that Package while still keeping the 20 | Package available as open source and free software. 21 | 22 | You are always permitted to make arrangements wholly outside of this 23 | license directly with the Copyright Holder of a given Package. If the 24 | terms of this license do not permit the full use that you propose to 25 | make of the Package, you should contact the Copyright Holder and seek 26 | a different licensing arrangement. 27 | 28 | Definitions 29 | 30 | "Copyright Holder" means the individual(s) or organization(s) 31 | named in the copyright notice for the entire Package. 32 | 33 | "Contributor" means any party that has contributed code or other 34 | material to the Package, in accordance with the Copyright Holder's 35 | procedures. 36 | 37 | "You" and "your" means any person who would like to copy, 38 | distribute, or modify the Package. 39 | 40 | "Package" means the collection of files distributed by the 41 | Copyright Holder, and derivatives of that collection and/or of 42 | those files. A given Package may consist of either the Standard 43 | Version, or a Modified Version. 44 | 45 | "Distribute" means providing a copy of the Package or making it 46 | accessible to anyone else, or in the case of a company or 47 | organization, to others outside of your company or organization. 48 | 49 | "Distributor Fee" means any fee that you charge for Distributing 50 | this Package or providing support for this Package to another 51 | party. It does not mean licensing fees. 52 | 53 | "Standard Version" refers to the Package if it has not been 54 | modified, or has been modified only in ways explicitly requested 55 | by the Copyright Holder. 56 | 57 | "Modified Version" means the Package, if it has been changed, and 58 | such changes were not explicitly requested by the Copyright 59 | Holder. 60 | 61 | "Original License" means this Artistic License as Distributed with 62 | the Standard Version of the Package, in its current version or as 63 | it may be modified by The Perl Foundation in the future. 64 | 65 | "Source" form means the source code, documentation source, and 66 | configuration files for the Package. 67 | 68 | "Compiled" form means the compiled bytecode, object code, binary, 69 | or any other form resulting from mechanical transformation or 70 | translation of the Source form. 71 | 72 | 73 | Permission for Use and Modification Without Distribution 74 | 75 | (1) You are permitted to use the Standard Version and create and use 76 | Modified Versions for any purpose without restriction, provided that 77 | you do not Distribute the Modified Version. 78 | 79 | 80 | Permissions for Redistribution of the Standard Version 81 | 82 | (2) You may Distribute verbatim copies of the Source form of the 83 | Standard Version of this Package in any medium without restriction, 84 | either gratis or for a Distributor Fee, provided that you duplicate 85 | all of the original copyright notices and associated disclaimers. At 86 | your discretion, such verbatim copies may or may not include a 87 | Compiled form of the Package. 88 | 89 | (3) You may apply any bug fixes, portability changes, and other 90 | modifications made available from the Copyright Holder. The resulting 91 | Package will still be considered the Standard Version, and as such 92 | will be subject to the Original License. 93 | 94 | 95 | Distribution of Modified Versions of the Package as Source 96 | 97 | (4) You may Distribute your Modified Version as Source (either gratis 98 | or for a Distributor Fee, and with or without a Compiled form of the 99 | Modified Version) provided that you clearly document how it differs 100 | from the Standard Version, including, but not limited to, documenting 101 | any non-standard features, executables, or modules, and provided that 102 | you do at least ONE of the following: 103 | 104 | (a) make the Modified Version available to the Copyright Holder 105 | of the Standard Version, under the Original License, so that the 106 | Copyright Holder may include your modifications in the Standard 107 | Version. 108 | 109 | (b) ensure that installation of your Modified Version does not 110 | prevent the user installing or running the Standard Version. In 111 | addition, the Modified Version must bear a name that is different 112 | from the name of the Standard Version. 113 | 114 | (c) allow anyone who receives a copy of the Modified Version to 115 | make the Source form of the Modified Version available to others 116 | under 117 | 118 | (i) the Original License or 119 | 120 | (ii) a license that permits the licensee to freely copy, 121 | modify and redistribute the Modified Version using the same 122 | licensing terms that apply to the copy that the licensee 123 | received, and requires that the Source form of the Modified 124 | Version, and of any works derived from it, be made freely 125 | available in that license fees are prohibited but Distributor 126 | Fees are allowed. 127 | 128 | 129 | Distribution of Compiled Forms of the Standard Version 130 | or Modified Versions without the Source 131 | 132 | (5) You may Distribute Compiled forms of the Standard Version without 133 | the Source, provided that you include complete instructions on how to 134 | get the Source of the Standard Version. Such instructions must be 135 | valid at the time of your distribution. If these instructions, at any 136 | time while you are carrying out such distribution, become invalid, you 137 | must provide new instructions on demand or cease further distribution. 138 | If you provide valid instructions or cease distribution within thirty 139 | days after you become aware that the instructions are invalid, then 140 | you do not forfeit any of your rights under this license. 141 | 142 | (6) You may Distribute a Modified Version in Compiled form without 143 | the Source, provided that you comply with Section 4 with respect to 144 | the Source of the Modified Version. 145 | 146 | 147 | Aggregating or Linking the Package 148 | 149 | (7) You may aggregate the Package (either the Standard Version or 150 | Modified Version) with other packages and Distribute the resulting 151 | aggregation provided that you do not charge a licensing fee for the 152 | Package. Distributor Fees are permitted, and licensing fees for other 153 | components in the aggregation are permitted. The terms of this license 154 | apply to the use and Distribution of the Standard or Modified Versions 155 | as included in the aggregation. 156 | 157 | (8) You are permitted to link Modified and Standard Versions with 158 | other works, to embed the Package in a larger work of your own, or to 159 | build stand-alone binary or bytecode versions of applications that 160 | include the Package, and Distribute the result without restriction, 161 | provided the result does not expose a direct interface to the Package. 162 | 163 | 164 | Items That are Not Considered Part of a Modified Version 165 | 166 | (9) Works (including, but not limited to, modules and scripts) that 167 | merely extend or make use of the Package, do not, by themselves, cause 168 | the Package to be a Modified Version. In addition, such works are not 169 | considered parts of the Package itself, and are not subject to the 170 | terms of this license. 171 | 172 | 173 | General Provisions 174 | 175 | (10) Any use, modification, and distribution of the Standard or 176 | Modified Versions is governed by this Artistic License. By using, 177 | modifying or distributing the Package, you accept this license. Do not 178 | use, modify, or distribute the Package, if you do not accept this 179 | license. 180 | 181 | (11) If your Modified Version has been derived from a Modified 182 | Version made by someone other than you, you are nevertheless required 183 | to ensure that your Modified Version complies with the requirements of 184 | this license. 185 | 186 | (12) This license does not grant you the right to use any trademark, 187 | service mark, tradename, or logo of the Copyright Holder. 188 | 189 | (13) This license includes the non-exclusive, worldwide, 190 | free-of-charge patent license to make, have made, use, offer to sell, 191 | sell, import and otherwise transfer the Package with respect to any 192 | patent claims licensable by the Copyright Holder that are necessarily 193 | infringed by the Package. If you institute patent litigation 194 | (including a cross-claim or counterclaim) against any party alleging 195 | that the Package constitutes direct or contributory patent 196 | infringement, then this Artistic License to you shall terminate on the 197 | date that such litigation is filed. 198 | 199 | (14) Disclaimer of Warranty: 200 | THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS 201 | IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED 202 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR 203 | NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL 204 | LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL 205 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 206 | DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF 207 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 208 | 209 | 210 | -------- 211 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile 2 | 3 | -include local.mk 4 | 5 | default: test 6 | 7 | .PHONY: test 8 | 9 | test: 10 | ./node_modules/.bin/mocha -R tap 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # strong-cluster-control 2 | 3 | node cluster API wrapper and extensions 4 | 5 | It is an extension of the node cluster module, not a replacement. 6 | 7 | - runs `size` workers (optionally), and monitors them for unexpected death 8 | - soft shutdown as well as hard termination of workers 9 | - throttles worker restart rate if they are exiting abnormally 10 | 11 | It can be added to an existing application using the node cluster module without 12 | modifying how that application is currently starting up or using cluster, and 13 | still make use of additional features. 14 | 15 | This is a component of the StrongLoop process manager, see http://strong-pm.io. 16 | 17 | 18 | ## Install 19 | 20 | npm install --save strong-cluster-control 21 | 22 | 23 | ## Example 24 | 25 | To instantiate cluster-control: 26 | 27 | ```javascript 28 | var cluster = require('cluster'); 29 | var control = require('strong-cluster-control'); 30 | 31 | // global setup here... 32 | 33 | control.start({ 34 | size: control.CPUS 35 | }).on('error', function(er) { 36 | console.error(er); 37 | }); 38 | 39 | if(cluster.isWorker) { 40 | // do work here... 41 | } 42 | ``` 43 | 44 | 45 | ## API 46 | 47 | See [api](./api.md). 48 | 49 | 50 | ## License 51 | 52 | strong-cluster-control uses a dual license model. 53 | 54 | You may use this library under the terms of the [Artistic 2.0 license][], 55 | or under the terms of the [StrongLoop Subscription Agreement][]. 56 | 57 | [Artistic 2.0 license]: http://opensource.org/licenses/Artistic-2.0 58 | [StrongLoop Subscription Agreement]: http://strongloop.com/license 59 | -------------------------------------------------------------------------------- /api.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | The controller is exported by the `strong-cluster-control` module. 4 | 5 | control = require('strong-cluster-control') 6 | 7 | ## Methods 8 | 9 | ### control.start([options],[callback]) 10 | 11 | Start the controller. 12 | 13 | * `options`: {Object} An options object, see below for supported properties, 14 | no default, and options object is not required. 15 | * `callback`: {Function} A callback function, it is set as a listener for 16 | the `'start'` event. 17 | 18 | The options are: 19 | 20 | * `size`: {Integer} Number of workers that should be running, the default 21 | is to *not* control the number of workers, see `setSize()` 22 | 23 | * `env`: {Object} Environment properties object passed to `cluster.fork()` if 24 | the controller has to start a worker to resize the cluster, default is null. 25 | 26 | * `shutdownTimeout`: {Milliseconds} Number of milliseconds to wait after 27 | shutdown before terminating a worker, the default is 5 seconds, see 28 | `.shutdown()` 29 | * `terminateTimeout`: {Milliseconds} Number of milliseconds to wait after 30 | terminate before killing a worker, the default is 5 seconds, see 31 | `.terminate()` 32 | * `throttleDelay`: {Milliseconds} Number of milliseconds to delay restarting 33 | workers after they are exiting abnormally. Abnormal is defined as 34 | as *not* suicide, see `worker.suicide` in 35 | [cluster docs](http://nodejs.org/docs/latest/api/cluster.html) 36 | 37 | For convenience during setup, it is not necessary to wrap `.start()` in a protective 38 | conditional `if(cluster.isMaster) {control.start()}`, when called in workers it quietly 39 | does nothing but call its callback. 40 | 41 | The 'start' event is emitted after the controller is started. 42 | 43 | ### control.stop([callback]) 44 | 45 | Stop the controller, after stopping workers (if the size is being controlled, 46 | see `setSize()`). 47 | 48 | Remove event listeners that were set on the `cluster` module. 49 | 50 | * `callback`: {Function} A callback function, it is set as a listener for 51 | the `'stop'` event. 52 | 53 | The 'stop' event is emitted after the controller is stopped. 54 | 55 | When there are no workers or listeners, node will exit, unless the application 56 | has non-cluster handles open. Open handles can be closed in the 'stop' event 57 | callback to allow node to shutdown gracefully, or `process.exit()` can be 58 | called, as appropriate for the application. 59 | 60 | ### control.restart() 61 | 62 | Restart workers one by one, until all current workers have been restarted. 63 | 64 | This can be used to do a rolling upgrade, if the underlying code has changed. 65 | 66 | Old workers will be restarted only if the last worker to be restarted stays 67 | alive for more than `throttleDelay` milliseconds, ensuring that the current 68 | workers will not all be killed while the new workers are failing to start. 69 | 70 | ### control.status() 71 | 72 | Returns the current cluster status. Its properties include: 73 | 74 | - `master`: {Object} 75 | - `pid`: The pid of the Master process. 76 | - `workers`: {Array} An Array of Objects containing the following properties: 77 | - `id`: The id of the Worker within the Master. 78 | - `pid`: The pid of the Worker process. 79 | - `uptime`: The age of the Worker process in milliseconds. 80 | - `startTime`: The time the Worker was started in milliseconds since epoh. 81 | 82 | ### control.setSize(N) 83 | 84 | Set the size of the cluster. 85 | 86 | * `N`: {Integer or null} The size of the cluster is the number of workers 87 | that should be maintained online. A size of `null` clears the size, and 88 | disables the size control feature of the controller. 89 | 90 | The cluster size can be set explicitly with `setSize()`, or implicitly through 91 | the `options` provided to `.start()`, or through the control channel. 92 | 93 | The size cannot be set until the controller has been started, and will not be 94 | maintained after the cluster has stopped. 95 | 96 | Once set, the controller will listen on cluster `fork` and `exit` events, 97 | and resize the cluster back to the set size if necessary. After the cluster has 98 | been resized, the 'resize' event will be emitted. 99 | 100 | When a resize is necessary, workers will be started or stopped one-by-one until 101 | the cluster is the set size. 102 | 103 | Cluster workers are started with `cluster.fork(control.options.env)`, so the 104 | environment can be set, but must be the same for each worker. After a worker has 105 | been started, the 'startWorker' event will be emitted. 106 | 107 | Cluster workers are stopped with `.shutdown()`. After a worker has been stopped, 108 | the 'stopWorker' event will be emitted. 109 | 110 | ### control.shutdown(id) 111 | 112 | Disconnect worker `id` and take increasingly agressive action until it exits. 113 | 114 | * `id` {Number} Cluster worker ID, see `cluster.workers` in 115 | [cluster docs](http://nodejs.org/docs/latest/api/cluster.html) 116 | 117 | The effect of disconnect on a worker is to close all the servers in the worker, 118 | wait for them to close, and then exit. This process may not occur in a timely 119 | fashion if, for example, the server connections do not close. In order to 120 | gracefully close any open connections, a worker may listen to the `SHUTDOWN` 121 | message, see `control.cmd.SHUTDOWN`. 122 | 123 | Sends a `SHUTDOWN` message to the identified worker, calls 124 | `worker.disconnect()`, and sets a timer for `control.options.shutdownTimeout`. 125 | If the worker has not exited by that time, calls `.terminate()` on the worker. 126 | 127 | ### control.terminate(id) 128 | 129 | Terminate worker `id`, taking increasingly aggressive action until it exits. 130 | 131 | * `id` {Number} Cluster worker ID, see `cluster.workers` in 132 | [cluster docs](http://nodejs.org/docs/latest/api/cluster.html) 133 | 134 | The effect of sending SIGTERM to a node process should be to cause it to exit. 135 | This may not occur in a timely fashion if, for example, the process is ignoring 136 | SIGTERM, or busy looping. 137 | 138 | Calls `worker.kill("SIGTERM")` on the identified worker, and sets a timer for 139 | `control.options.terminateTimeout`. If the worker has not exited by that time, 140 | calls `worker.("SIGKILL")` on the worker. 141 | 142 | ## Properties 143 | 144 | ### control.options 145 | 146 | A copy of the options set by calling `.start()`. 147 | 148 | It will have any default values set in it, and will be kept synchronized with 149 | changes made via explicit calls, such as to `.setSize()`. 150 | 151 | Visible for diagnostic and logging purposes. Do *not* modify the options 152 | directly. 153 | 154 | ### control.cmd.SHUTDOWN 155 | 156 | * {String} `'CLUSTER_CONTROL_shutdown'` 157 | 158 | The `SHUTDOWN` message is sent by `.shutdown()` before disconnecting the worker, 159 | and can be used to gracefully close any open connections before the 160 | `control.options.shutdownTimeout` expires. 161 | 162 | All connections will be closed at the TCP level when the worker exits or is 163 | terminated, but this message gives the opportunity to close at a more 164 | application-appropriate time, for example, after any outstanding requests have 165 | been completed. 166 | 167 | The message format is: 168 | 169 | { cmd: control.cmd.SHUTDOWN } 170 | 171 | It can be received in a worker by listening for a `'message'` event with a 172 | matching `cmd` property: 173 | 174 | process.on('message', function(msg) { 175 | if(msg.cmd === control.cmd.SHUTDOWN) { 176 | // Close any open connections as soon as they are idle... 177 | } 178 | }); 179 | 180 | ### control.CPUS 181 | 182 | The number of CPUs reported by node's `os.cpus().length`, this is a good default 183 | for the cluster size, in the absence of application specific analysis of what 184 | would be an optimal number of workers. 185 | 186 | ## Events 187 | 188 | ### 'start' 189 | 190 | Event emitted after control has started. Control is considered started 191 | after the 'listening' event has been emitted. 192 | 193 | Starting of workers happens in the background, if you are specifically 194 | interested in knowing when all the workers have started, see the 'resize' 195 | event. 196 | 197 | ### 'stop' 198 | 199 | Event emitted after control has stopped, see `.stop()`. 200 | 201 | ### 'error' 202 | 203 | * {Error Object} 204 | 205 | Event emitted when an error occurs. The only current source of errors is the control 206 | protocol, which may require logging of the errors, but should not effect the 207 | operation of the controller. 208 | 209 | ### 'setSize' 210 | 211 | * {Integer} size, the number of workers requested (will always 212 | be the same as `cluster.options.size`) 213 | 214 | Event emitted after `setSize()` is called. 215 | 216 | ### 'resize' 217 | 218 | * {Integer} size, the number of workers now that resize is complete (will always 219 | be the same as `cluster.options.size`) 220 | 221 | Event emitted after a resize of the cluster is complete. At this point, no more 222 | workers will be forked or shutdown by the controller until either the size is 223 | changed or workers fork or exit, see `setSize()`. 224 | 225 | ### 'startWorker' 226 | 227 | * `worker` {Worker object} 228 | 229 | Event emitted after a worker which was started during a resize comes online, see the 230 | node API documentation for description of `worker` and "online". 231 | 232 | ### 'startRestart' 233 | 234 | * `workers` {Array of worker IDs} Workers that are going to be restarted. 235 | 236 | Event emitted after `restart()` is called with array of worker IDs that will be 237 | restarted. 238 | 239 | ### 'restart' 240 | 241 | Event emitted after after all the workers have been restarted. 242 | 243 | ### 'stopWorker' 244 | 245 | * `worker` {Worker object} 246 | * `code` {Number} the exit code, if it exited normally. 247 | * `signal` {String} the name of the signal if it exited due to signal 248 | 249 | Event emitted after a worker which was shutdown during a resize exits, see the node 250 | API documentation for a description of `worker`. 251 | 252 | The values of `code` and `signal`, as well as of `worker.suicide`, can be used 253 | to determine how gracefully the worker was stopped. See `.terminate()`. 254 | -------------------------------------------------------------------------------- /docs.json: -------------------------------------------------------------------------------- 1 | { 2 | "content": [ 3 | "api.md" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013,2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | // cluster-control: 7 | 8 | var assert = require('assert'); 9 | var cluster = require('cluster'); 10 | var VERSION = require('./package.json').version; 11 | 12 | if (cluster._strongControlMaster) { 13 | assert( 14 | cluster._strongControlMaster.VERSION === VERSION, 15 | 'Multiple versions of strong-cluster-control are being initialized.\n' + 16 | 'This version ' + VERSION + ' is incompatible with already initialized\n' + 17 | 'version ' + cluster._strongControlMaster.VERSION + '.\n' 18 | ); 19 | module.exports = cluster._strongControlMaster; 20 | return; 21 | } 22 | 23 | if (cluster.isMaster) { 24 | module.exports = require('./lib/master'); 25 | module.exports.VERSION = VERSION; 26 | cluster._strongControlMaster = module.exports; 27 | } else { 28 | exports = module.exports = new (require('events').EventEmitter); 29 | // Calling .start() in a worker is a nul op 30 | exports.start = function(options, callback) { 31 | // both options and callback are optional, adjust position based on type 32 | // XXX cut-n-paste from lib/master, is it possible to factor out, maybe 33 | // into a function that modifies arguments? 34 | if (typeof callback === 'undefined') { 35 | if (typeof options === 'function') { 36 | callback = options; 37 | options = undefined; 38 | } 39 | } 40 | 41 | if (callback) { 42 | process.nextTick(callback); 43 | } 44 | return this; 45 | }; 46 | exports.stop = function(callback) { 47 | if (callback) { 48 | process.nextTick(callback); 49 | } 50 | return this; 51 | }; 52 | exports.cmd = require('./lib/msg'); 53 | } 54 | -------------------------------------------------------------------------------- /lib/master.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013,2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | // master: cluster control occurs in the master process 7 | 8 | /* eslint consistent-this: 0 */ 9 | 10 | var EventEmitter = require('events').EventEmitter; 11 | var _ = require('lodash'); 12 | var assert = require('assert'); 13 | var cluster = require('cluster'); 14 | var clusterSize = require('./util').clusterSize; 15 | var debug = require('debug')('strong-cluster-control:master'); 16 | var liveWorkerIds = require('./util').liveWorkerIds; 17 | var msg = require('./msg'); 18 | var nextTick = setImmediate; 19 | var os = require('os'); 20 | var util = require('util'); 21 | 22 | // Master is a singleton, as is the cluster master 23 | var master = new EventEmitter(); 24 | 25 | var OPTION_DEFAULTS = { 26 | shutdownTimeout: 5000, 27 | terminateTimeout: 5000, 28 | throttleDelay: 2000, 29 | }; 30 | 31 | master.status = status; 32 | master.setSize = setSize; 33 | master._resize = resize; 34 | master._startOne = startOne; 35 | master._stopOne = stopOne; 36 | master.shutdown = shutdown; 37 | master.terminate = terminate; 38 | master.options = util._extend({}, OPTION_DEFAULTS); 39 | master.restart = restart; 40 | master.getRestarting = getRestarting; 41 | master.start = start; 42 | master.stop = stop; 43 | master.CPUS = os.cpus().length; 44 | master.cmd = msg; 45 | master.startTime = Date.now(); 46 | 47 | function status() { 48 | var retval = {}; 49 | 50 | retval.master = { 51 | pid: process.pid, 52 | setSize: this.options.size, 53 | startTime: master.startTime, 54 | }; 55 | 56 | retval.workers = []; 57 | 58 | var now = Date.now(); 59 | 60 | for (var id in cluster.workers) { 61 | var w = toWorker(id); 62 | retval.workers.push({ 63 | id: id, 64 | pid: w.process.pid, 65 | uptime: now - w.startTime, 66 | startTime: w.startTime, 67 | }); 68 | } 69 | 70 | debug('status: %j', retval); 71 | 72 | return retval; 73 | } 74 | 75 | // cluster-control will set the size up and down in the process of restarting 76 | // the cluster. During this time, we want the 'advertised target size' of the 77 | // cluster to remain stable, since that is the size the cluster will be once the 78 | // restart is complete. If invisible is set, cluster will go to `size`, but that 79 | // size will not be stored in self.options.size, or returned by status(). 80 | function setSize(size, invisible) { 81 | var self = this; 82 | 83 | debug('set size to %d from %d', size, self.size); 84 | 85 | self.size = size; 86 | 87 | if (!invisible) 88 | self.options.size = size; 89 | 90 | nextTick(self.emit.bind(self, 'setSize', self.options.size)); 91 | nextTick(self._resize.bind(self)); 92 | 93 | return self; 94 | } 95 | 96 | // Remember, suicide is the normal exit path, if a worker dies without suicide 97 | // being set then it is abnormal, and we record the time. 98 | function _recordSuicide(self, worker) { 99 | if (worker && !worker.suicide) { 100 | self._lastAbnormalExit = Date.now(); 101 | debug( 102 | 'abormal exit by worker %s at time %s', 103 | worker.id, self._lastAbnormalExit); 104 | } 105 | } 106 | 107 | // With a few seconds of an abnormal exit, throttle the worker creation rate to 108 | // one per second. We wait for longer than the throttleDelay to see if the new 109 | // worker is going to stay up, or just exit right away. 110 | function _startDelay(self) { 111 | var throttlePeriod = self.options.throttleDelay * 2; 112 | if (Date.now() - self._lastAbnormalExit < throttlePeriod) { 113 | return self.options.throttleDelay; 114 | } 115 | return 0; 116 | } 117 | 118 | function resize(worker) { 119 | var self = this; 120 | 121 | if (worker) 122 | debug('_resize: worker %s', worker.id); 123 | 124 | _recordSuicide(self, worker); 125 | 126 | if (self.size === null || self.size === undefined) { 127 | return self; 128 | } 129 | 130 | if (self._resizing) { 131 | // don't start doing multiple resizes in parallel, the events get listened 132 | // on by both sets, and get multi-counted 133 | return self; 134 | } 135 | 136 | var currentSize = clusterSize(); 137 | 138 | debug('resize to %d from %d (apparent size %d)', 139 | self.size, currentSize, self.options.size); 140 | 141 | self._resizing = true; 142 | 143 | if (currentSize < self.size) { 144 | _startOneAfterDelay(self, _startDelay(self), resized); 145 | } else if (currentSize > self.size) { 146 | self._stopOne(resized); 147 | } else { 148 | debug('worker count resized to %j', self.size); 149 | self._resizing = false; 150 | self.emit('resize', self.size); 151 | } 152 | 153 | function resized() { 154 | self._resizing = false; 155 | self._resize(); 156 | } 157 | 158 | return self; 159 | } 160 | 161 | function _startOneAfterDelay(self, delay, callback) { 162 | if (delay) { 163 | debug('delay worker start by %s', delay); 164 | setTimeout(function() { 165 | self._startOne(callback); 166 | }, delay); 167 | } else { 168 | self._startOne(callback); 169 | } 170 | } 171 | 172 | function startOne(callback) { 173 | var self = this; 174 | var worker = cluster.fork(self.options.env); 175 | 176 | worker.once('online', online); 177 | worker.once('exit', exit); 178 | 179 | function online() { 180 | debug('worker %s online', this.id); 181 | self.emit('startWorker', worker); 182 | worker.removeListener('exit', exit); 183 | callback(worker); 184 | } 185 | 186 | function exit() { 187 | // XXX TODO handle failure to start, this will currently busy loop 188 | debug('one worker %s started exit suicide? %j', this.id, this.suicide); 189 | worker.removeListener('online', online); 190 | callback(); 191 | } 192 | } 193 | 194 | // shutdown the first worker that has not had .disconnect() called on it already 195 | function stopOne(callback) { 196 | var self = this; 197 | // XXX(sam) picks by key order, semi random? should it sort by id, and 198 | // disconnect the lowest? or sort by age, when I have it, and do the oldest? 199 | var workerIds = liveWorkerIds(); 200 | for (var i = 0; i < workerIds.length; i++) { 201 | var id = workerIds[i]; 202 | var worker = cluster.workers[id]; 203 | var connected = !worker.suicide; // suicide is set after .disconnect() 204 | 205 | debug('considering worker %s for stop connected?', id, connected); 206 | 207 | if (connected) { 208 | self.shutdown(worker.id); 209 | } 210 | worker.once('exit', function(code, sig) { 211 | debug('one worker stopped: %d reason %j', this.id, sig || code); 212 | self.emit('stopWorker', worker, code, sig); 213 | callback(); 214 | }); 215 | return; 216 | } 217 | debug('found no workers to stop'); 218 | nextTick(callback); 219 | } 220 | 221 | // Return worker by id, ensuring it is annotated with an _control property. 222 | function toWorker(id) { 223 | var worker = cluster.workers[id]; 224 | assert(worker, 'worker id ' + id + ' invalid'); 225 | worker._control = worker._control || {}; 226 | worker.startTime = worker.startTime || Date.now(); 227 | return worker; 228 | } 229 | 230 | function setupControl(id) { 231 | return toWorker(id); 232 | } 233 | 234 | function shutdown(id) { 235 | var worker = toWorker(id); 236 | 237 | debug('shutdown %s already?', id, !!worker._control.exitTimer); 238 | 239 | if (worker._control.exitTimer) { 240 | return master; 241 | } 242 | worker.send({cmd: msg.SHUTDOWN}); 243 | worker.disconnect(); 244 | 245 | worker._control.exitTimer = setTimeout(function() { 246 | worker._control.exitTimer = null; 247 | master.terminate(null, worker); 248 | }, master.options.shutdownTimeout); 249 | 250 | worker.once('exit', function(code, signal) { 251 | debug('shutdown exit for worker %j', worker.id); 252 | clearTimeout(worker._control.exitTimer); 253 | master.emit('shutdown', this, code, signal); 254 | }); 255 | 256 | return master; 257 | } 258 | 259 | // The worker arg is because as soon as a worker's comm channel closes, its 260 | // removed from cluster.workers (the timing of this is not documented in node 261 | // API), but it can be some time until exit occurs, or never. since we want to 262 | // catch this, and TERM or KILL the worker, we need to keep a reference to the 263 | // worker object, and pass it to terminate ourself, because we can no longer 264 | // look it up by id. 265 | function terminate(id, shutdownWorker) { 266 | var worker = shutdownWorker || toWorker(id); 267 | 268 | debug('terminate %s already?', id, !!worker._control.exitTimer); 269 | 270 | if (worker._control.exitTimer) { 271 | return master; 272 | } 273 | worker.kill(); 274 | 275 | worker._control.exitTimer = setTimeout(function() { 276 | worker.kill('SIGKILL'); 277 | }, master.options.terminateTimeout); 278 | 279 | worker.once('exit', function(code, signal) { 280 | clearTimeout(worker._control.exitTimer); 281 | if (!shutdownWorker) { 282 | master.emit('terminate', this, code, signal); 283 | } 284 | }); 285 | 286 | return master; 287 | } 288 | 289 | // Increase size (invisibly) to one past the current configured size. Once the 290 | // new worker has started, set the size back to the current configured size, the 291 | // oldest worker will be killed and the new one will remain. Repeat until all 292 | // old workers have been restarted. 293 | function restart() { 294 | var self = this; 295 | var wasRestarting = self._restartIds; 296 | 297 | self._restartIds = _.union(self._restartIds, liveWorkerIds()); 298 | 299 | debug('restart from size %d of %j', self.options.size, self._restartIds); 300 | 301 | nextTick(function() { 302 | self.emit('startRestart', self._restartIds); 303 | }); 304 | 305 | if (wasRestarting) 306 | return self; 307 | 308 | nextTick(_restart); 309 | 310 | function _restart() { 311 | self._restartIds = _.intersection(self._restartIds, liveWorkerIds()); 312 | 313 | if (self._restartIds.length === 0 || self.options.size === 0) { 314 | self._restartIds = null; 315 | return self.emit('restart'); 316 | } 317 | 318 | startOne(); 319 | } 320 | 321 | function startOne() { 322 | self.setSize(self.options.size + 1, true); 323 | 324 | self.once('resize', stopOne); 325 | } 326 | 327 | function stopOne() { 328 | waitForStability(function() { 329 | self.setSize(self.options.size); 330 | self.once('resize', _restart); 331 | }); 332 | } 333 | 334 | function waitForStability(callback) { 335 | // Stable when the last abnormal exit was sufficiently long ago. 336 | setTimeout(check, self.options.throttleDelay).unref(); 337 | 338 | function check() { 339 | if (unstable()) 340 | return setTimeout(check, 1000).unref(); 341 | return callback(); 342 | } 343 | 344 | function unstable() { 345 | var uptime = Date.now() - self._lastAbnormalExit; 346 | var undersized = clusterSize() < self.size; 347 | var unstable = undersized || uptime < self.options.throttleDelay; 348 | debug('unstable? %j undersized? %j uptime %d ms', 349 | unstable, undersized, uptime); 350 | return unstable; 351 | } 352 | } 353 | 354 | return self; 355 | } 356 | 357 | function getRestarting() { 358 | return _.clone(this._restartIds); 359 | } 360 | 361 | // Functions need to be shared between start and stop, so they can be removed 362 | // from the events on stop. 363 | function onExit(worker, code, signal) { 364 | var reason = signal || code; 365 | debug('on worker %s exit by %s suicide? %j', 366 | worker.id, reason, worker.suicide); 367 | master._resize(worker); 368 | } 369 | 370 | function onFork(worker) { 371 | debug('on worker %s fork', worker.id); 372 | setupControl(worker.id); 373 | master._resize(); 374 | master.emit('fork', worker); 375 | // emits on worker itself, _after_ it has been setup for control 376 | worker.emit('fork'); 377 | } 378 | 379 | function start(options, callback) { 380 | var self = master; 381 | 382 | // both options and callback are optional, adjust position based on type 383 | if (typeof callback === 'undefined') { 384 | if (typeof options === 'function') { 385 | callback = options; 386 | options = undefined; 387 | } 388 | } 389 | 390 | options = options || {}; 391 | 392 | self.options = {}; 393 | self.options = util._extend(self.options, OPTION_DEFAULTS); 394 | self.options = util._extend(self.options, options); 395 | 396 | options = self.options; 397 | 398 | debug('start %j cb? %s', options, callback); 399 | 400 | self.setSize(options.size); 401 | 402 | // When doing a stop/start, forget any previous abnormal exits 403 | self._lastAbnormalExit = undefined; 404 | 405 | cluster.on('exit', onExit); 406 | cluster.on('fork', onFork); 407 | 408 | for (var id in cluster.workers) { 409 | setupControl(id); 410 | } 411 | 412 | if (callback) { 413 | self.once('start', callback); 414 | } 415 | 416 | nextTick(function() { 417 | self.emit('start'); 418 | }); 419 | 420 | self._running = true; 421 | 422 | return self; 423 | } 424 | 425 | function stop(callback) { 426 | var self = master; 427 | if (callback) { 428 | self.once('stop', callback); 429 | } 430 | 431 | if (self._running && self.size != null) { 432 | // We forked workers, stop should shut them down 433 | self.setSize(0); 434 | self.once('resize', function(size) { 435 | if (size === 0) { 436 | stopListening(); 437 | } 438 | }); 439 | } else { 440 | stopListening(); 441 | } 442 | return self; 443 | 444 | function stopListening() { 445 | self.setSize(undefined); 446 | cluster.removeListener('exit', onExit); 447 | cluster.removeListener('fork', onFork); 448 | nextTick(self.emit.bind(self, 'stop')); 449 | } 450 | } 451 | 452 | module.exports = master; 453 | -------------------------------------------------------------------------------- /lib/msg.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | // msg: definition of cmd names used for messages 7 | 8 | var prefix = 'CLUSTER_CONTROL_'; 9 | 10 | exports.SHUTDOWN = prefix + 'shutdown'; 11 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | 'use strict'; 7 | 8 | var _ = require('lodash'); 9 | var cluster = require('cluster'); 10 | 11 | exports.liveWorkerIds = liveWorkerIds; 12 | exports.clusterSize = clusterSize; 13 | 14 | // Workers remain in cluster.workers after they have have exited. 15 | function liveWorkerIds() { 16 | var workerIds = Object.keys(cluster.workers).filter(function(id) { 17 | var worker = cluster.workers[id]; 18 | var isAlive = worker.process.signalCode == null && 19 | worker.process.exitCode == null; 20 | return isAlive; 21 | }); 22 | 23 | // Object keys for scalars are of type String, sort by their numeric value. 24 | return _.sortBy(workerIds, Number); 25 | } 26 | 27 | function clusterSize() { 28 | return liveWorkerIds().length; 29 | } 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "strong-cluster-control", 3 | "version": "2.2.4", 4 | "description": "node cluster API wrapper and extensions", 5 | "license": "Artistic-2.0", 6 | "keywords": [ 7 | "cluster", 8 | "forever", 9 | "master", 10 | "pm", 11 | "runner", 12 | "strongloop", 13 | "strongops", 14 | "supervisor" 15 | ], 16 | "main": "index.js", 17 | "scripts": { 18 | "pretest": "eslint --ignore-path .gitignore .", 19 | "test": "tap test/test-*.js" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/strongloop/strong-cluster-control.git" 24 | }, 25 | "author": { 26 | "name": "Sam Roberts", 27 | "email": "sam@strongloop.com" 28 | }, 29 | "dependencies": { 30 | "debug": "^2.1.3", 31 | "lodash": "^4.17.5" 32 | }, 33 | "devDependencies": { 34 | "eslint": "^2.13.1", 35 | "eslint-config-strongloop": "^2.1.0", 36 | "tap": "^1.4.1" 37 | }, 38 | "engines": { 39 | "node": "*" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | 'use strict'; 7 | 8 | var _ = require('lodash'); 9 | var cluster = require('cluster'); 10 | var clusterSize = require('../lib/util').clusterSize; 11 | 12 | exports.workerCount = clusterSize; 13 | exports.pickWorker = pickWorker; 14 | exports.assertWorker = assertWorker; 15 | 16 | function randomInteger(I) { 17 | return Math.floor(Math.random() * I); 18 | } 19 | 20 | function pickWorker() { 21 | var workerIds = Object.keys(cluster.workers); 22 | var pickId = workerIds[randomInteger(workerIds.length)]; 23 | return cluster.workers[pickId]; 24 | } 25 | 26 | function assertWorker(t, worker) { 27 | var isN = _.isFinite; 28 | t.assert(isN(_.parseInt(worker.id, 10)), 'id invalid: ' + worker.id); 29 | t.assert(isN(worker.uptime), 'uptime invalid: ' + worker.uptime); 30 | t.assert(isN(worker.startTime), 'startTime invalid: ' + worker.startTime); 31 | t.assert(isN(worker.pid) && worker.pid >= 1, 'pid invalid: ' + worker.pid); 32 | } 33 | -------------------------------------------------------------------------------- /test/restart-failed.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | 'use strict'; 7 | 8 | var _ = require('lodash'); 9 | var debug = require('debug')('strong-cluster-control:test'); 10 | var cluster = require('cluster'); 11 | var control = require('../'); 12 | 13 | var SIZE = global.SIZE; 14 | 15 | if (cluster.isWorker) { 16 | if (cluster.worker.id > SIZE) 17 | process.exit(3); 18 | return; 19 | } 20 | 21 | var tap = require('tap'); 22 | 23 | tap.test('good workers are not killed', function(t) { 24 | var ended; 25 | 26 | control.start({size: SIZE}); 27 | 28 | control.once('resize', function() { 29 | debug('reached size, restart'); 30 | control.restart(); 31 | }); 32 | 33 | cluster.on('exit', function(w) { 34 | if (ended) return; 35 | 36 | debug('w %d died: alive %d', w.id, _.size(cluster.workers)); 37 | 38 | if (_.size(cluster.workers) < SIZE) { 39 | t.fail('good workers died'); 40 | end(); 41 | } else if (w.id > (SIZE + 3)) { 42 | t.pass('good workers survived'); 43 | end(); 44 | } 45 | }); 46 | 47 | function end() { 48 | if (!ended) { 49 | control.stop(); 50 | t.end(); 51 | } 52 | ended = true; 53 | } 54 | }); 55 | -------------------------------------------------------------------------------- /test/test-exposed-properties.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | 'use strict'; 7 | 8 | var _ = require('lodash'); 9 | var master = require('../lib/master'); 10 | var tap = require('tap'); 11 | var os = require('os'); 12 | 13 | tap.test('master should expose', function(t) { 14 | t.test('cpu count, for easy use as a default size', function(t) { 15 | t.equal(master.CPUS, os.cpus().length); 16 | t.end(); 17 | }); 18 | 19 | t.test('master process start time', function(t) { 20 | t.assert(_.isFinite(master.startTime)); 21 | t.end(); 22 | }); 23 | 24 | t.test('message cmd names in master', function(t) { 25 | t.equal(master.cmd.SHUTDOWN, 'CLUSTER_CONTROL_shutdown'); 26 | t.end(); 27 | }); 28 | 29 | t.end(); 30 | }); 31 | -------------------------------------------------------------------------------- /test/test-options-env.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | 'use strict'; 7 | 8 | var cluster = require('cluster'); 9 | var master = require('../lib/master'); 10 | var tap = require('tap'); 11 | 12 | tap.test('should use options.env with fork', function(t) { 13 | cluster.setupMaster({ 14 | exec: 'test/workers/null.js' 15 | }); 16 | 17 | t.on('end', master.stop); 18 | 19 | master.start({ 20 | size: 1, 21 | env: {SOME_VAR: 'MY VALUE'} 22 | }); 23 | 24 | master.once('startWorker', function(worker) { 25 | worker.once('message', function(msg) { 26 | t.equal(msg.env.SOME_VAR, 'MY VALUE'); 27 | t.end(); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/test-require.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | var _ = require('lodash'); 7 | var control = require('../index'); 8 | var master = require('../lib/master'); 9 | var tap = require('tap'); 10 | 11 | tap.test('require', function(t) { 12 | t.test('should expose master', function(t) { 13 | t.equal(control.start, master.start); 14 | t.equal(control.stop, master.stop); 15 | t.equal(control.ADDR, master.ADDR); 16 | t.end(); 17 | }); 18 | 19 | t.test('should set the master process startTime', function(t) { 20 | t.assert(_.isFinite(master.startTime)); 21 | t.end(); 22 | }); 23 | 24 | t.end(); 25 | }); 26 | -------------------------------------------------------------------------------- /test/test-resize-disconnected.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | 'use strict'; 7 | 8 | var cluster = require('cluster'); 9 | var control = require('../'); 10 | var debug = require('debug')('strong-cluster-control:test'); 11 | var fmt = require('util').format; 12 | 13 | if (cluster.isWorker) { 14 | debug('worker starting'); 15 | return; 16 | } 17 | 18 | var tap = require('tap'); 19 | 20 | tap.test('resize disconnected', function(t) { 21 | control.start({size: 1}); 22 | 23 | control.once('resize', function() { 24 | debug('reached size: ', summary()); 25 | 26 | cluster.workers[1].disconnect(); 27 | debug('disconnected:', summary()); 28 | control.setSize(0); 29 | debug('resizing:', summary()); 30 | }); 31 | 32 | control.on('resize', function() { 33 | if (size() === 0) { 34 | return control.stop(t.end); 35 | } 36 | }); 37 | 38 | function summary() { 39 | return Object.keys(cluster.workers).map(function(id) { 40 | return fmt('%d:suicide=%j', id, cluster.workers[id].suicide); 41 | }).join(' '); 42 | } 43 | 44 | function size() { 45 | return Object.keys(cluster.workers).length; 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /test/test-resize-event.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | 'use strict'; 7 | 8 | var cluster = require('cluster'); 9 | var master = require('../lib/master'); 10 | var pickWorker = require('./helpers').pickWorker; 11 | var tap = require('tap'); 12 | 13 | cluster.setupMaster({ 14 | exec: 'test/workers/null.js' 15 | }); 16 | 17 | tap.test('should get resize event on return to configured size', function(t) { 18 | function assertClusterResizesToConfiguredSizeAfter(somethingHappens, t) { 19 | var SIZE = 5; 20 | master.start({size: SIZE}); 21 | master.once('resize', function(size) { 22 | t.equal(size, SIZE); 23 | somethingHappens(checkSizeInvariant); 24 | }); 25 | 26 | function checkSizeInvariant() { 27 | master.once('resize', function(size) { 28 | t.equal(size, master.options.size); 29 | master.stop(t.end); 30 | }); 31 | } 32 | } 33 | 34 | t.test('after worker disconnect', function(t) { 35 | assertClusterResizesToConfiguredSizeAfter(function(done) { 36 | pickWorker() 37 | .once('exit', done) 38 | .disconnect(); 39 | }, t); 40 | }); 41 | 42 | t.test('after worker destroy', function(t) { 43 | assertClusterResizesToConfiguredSizeAfter(function(done) { 44 | pickWorker() 45 | .once('exit', done) 46 | .destroy('SIGKILL'); // .destroy is other, better, name for .kill 47 | }, t); 48 | }); 49 | 50 | t.test('after worker signal', function(t) { 51 | assertClusterResizesToConfiguredSizeAfter(function(done) { 52 | pickWorker() 53 | .once('exit', done) 54 | .process.kill('SIGKILL'); 55 | // use process.kill(), because it just sends a signal, whereas 56 | // worker .kill() first disconnects, and then signals, so signal 57 | // is usually never received 58 | }, t); 59 | }); 60 | 61 | t.test('after worker exit', function(t) { 62 | assertClusterResizesToConfiguredSizeAfter(function(done) { 63 | pickWorker() 64 | .once('exit', done) 65 | .send({cmd: 'EXIT'}); 66 | }, t); 67 | }); 68 | 69 | t.test('after worker fork', function(t) { 70 | assertClusterResizesToConfiguredSizeAfter(function(done) { 71 | cluster.fork(); 72 | cluster.once('online', done); 73 | }, t); 74 | }); 75 | 76 | t.end(); 77 | }); 78 | -------------------------------------------------------------------------------- /test/test-resize-from-1-to-0.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | 'use strict'; 7 | 8 | var cluster = require('cluster'); 9 | var master = require('../lib/master'); 10 | var tap = require('tap'); 11 | var workerCount = require('./helpers').workerCount; 12 | 13 | cluster.setupMaster({ 14 | exec: 'test/workers/null.js' 15 | }); 16 | 17 | tap.test('from size 1 to 0', function(t) { 18 | master.start({size: 1}); 19 | master.once('startWorker', function() { 20 | master.setSize(0); 21 | }); 22 | 23 | master.once('stopWorker', function(worker) { 24 | t.assert(worker.process.pid, 'worker is valid'); 25 | master.once('resize', function() { 26 | t.equal(workerCount(), 0); 27 | master.stop(t.end); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/test-resize-from-1-to-2.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | 'use strict'; 7 | 8 | var cluster = require('cluster'); 9 | var master = require('../lib/master'); 10 | var tap = require('tap'); 11 | var workerCount = require('./helpers').workerCount; 12 | 13 | cluster.setupMaster({ 14 | exec: 'test/workers/null.js' 15 | }); 16 | 17 | tap.test('from size 1 to 2', function(t) { 18 | master.start({size: 1}); 19 | master.once('startWorker', function() { 20 | master.setSize(2); 21 | master.once('startWorker', function() { 22 | t.equal(workerCount(), 2); 23 | master.stop(t.end); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/test-resize-from-3-to-1.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | 'use strict'; 7 | 8 | var cluster = require('cluster'); 9 | var master = require('../lib/master'); 10 | var tap = require('tap'); 11 | var workerCount = require('./helpers').workerCount; 12 | 13 | cluster.setupMaster({ 14 | exec: 'test/workers/null.js' 15 | }); 16 | 17 | tap.test('from size 3 to 1', function(t) { 18 | master.start({size: 3}); 19 | master.on('startWorker', function() { 20 | if (workerCount() === 3) { 21 | master.setSize(1); 22 | } 23 | }); 24 | master.once('stopWorker', function() { 25 | t.equal(workerCount(), 2); 26 | master.once('stopWorker', function() { 27 | t.equal(workerCount(), 1); 28 | master.stop(t.end); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/test-resize-from-fork.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | 'use strict'; 7 | 8 | var _ = require('lodash'); 9 | var cluster = require('cluster'); 10 | var master = require('../lib/master'); 11 | var tap = require('tap'); 12 | var workerCount = require('./helpers').workerCount; 13 | 14 | cluster.setupMaster({ 15 | exec: 'test/workers/null.js' 16 | }); 17 | 18 | tap.test('should resize from a fork before start', function(t) { 19 | var sawNewWorker = 0; 20 | cluster.fork(); 21 | master.start({size: 3}); 22 | master.once('startWorker', function startWorker(worker) { 23 | // Make sure our argument is really a worker. 24 | t.assert(worker); 25 | t.assert(worker.id); 26 | t.assert(worker.process.pid); 27 | t.assert(_.isFinite(worker.startTime)); 28 | 29 | sawNewWorker += 1; 30 | 31 | if (sawNewWorker === 2) { 32 | t.equal(workerCount(), 3); 33 | t.end(); 34 | } 35 | master.once('startWorker', startWorker); 36 | }); 37 | t.on('end', master.stop); 38 | }); 39 | -------------------------------------------------------------------------------- /test/test-resize-slowly.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | 'use strict'; 7 | 8 | var cluster = require('cluster'); 9 | var master = require('../lib/master'); 10 | var tap = require('tap'); 11 | 12 | cluster.setupMaster({ 13 | exec: 'test/workers/null.js' 14 | }); 15 | 16 | tap.test('slowly when workers exit unexpectedly', function(t) { 17 | // Run for some time, with workers set to error on start. Without 18 | // throttling, workers get forked faster than dozens per second, with 19 | // throttling, it should be no more than a couple a second. 20 | var TIME = 5000; 21 | var FORKS = TIME / 1000 * 2; 22 | var forks = 0; 23 | 24 | master.start({ 25 | size: 3, 26 | env: {cmd: 'EXIT'} 27 | }); 28 | 29 | cluster.on('fork', function forkCounter() { 30 | forks++; 31 | }); 32 | 33 | setTimeout(function() { 34 | t.assert(forks < FORKS, 'forked ' + forks + ' times!'); 35 | master.stop(t.end); 36 | }, TIME); 37 | }); 38 | -------------------------------------------------------------------------------- /test/test-resize-to-last-concurrent.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | 'use strict'; 7 | 8 | var cluster = require('cluster'); 9 | var master = require('../lib/master'); 10 | var tap = require('tap'); 11 | var workerCount = require('./helpers').workerCount; 12 | 13 | cluster.setupMaster({ 14 | exec: 'test/workers/null.js' 15 | }); 16 | 17 | tap.test('to last of concurrent resizes', function(t) { 18 | master.start({size: 10}); 19 | master.on('startWorker', function() { 20 | if (workerCount() === 3) { 21 | master.setSize(5); 22 | } 23 | if (workerCount() === 5) { 24 | master.setSize(2); 25 | } 26 | }); 27 | 28 | master.on('stopWorker', function() { 29 | if (workerCount() === 3) { 30 | master.setSize(0); 31 | } 32 | if (workerCount() === 0) { 33 | master.stop(t.end); 34 | } 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/test-resize-while-forking.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | 'use strict'; 7 | 8 | var cluster = require('cluster'); 9 | var master = require('../lib/master'); 10 | var tap = require('tap'); 11 | var workerCount = require('./helpers').workerCount; 12 | 13 | cluster.setupMaster({ 14 | exec: 'test/workers/null.js' 15 | }); 16 | 17 | tap.test('while workers are forked', function(t) { 18 | master.start({size: 10}); 19 | master.on('startWorker', function() { 20 | if (workerCount() === 1) { 21 | cluster.fork(); 22 | } 23 | if (workerCount() === 3) { 24 | master.setSize(5); 25 | cluster.fork(); 26 | } 27 | if (workerCount() === 5) { 28 | master.setSize(2); 29 | } 30 | }); 31 | 32 | master.on('stopWorker', function() { 33 | if (workerCount() === 3) { 34 | master.setSize(0); 35 | } 36 | if (workerCount() === 0) { 37 | master.stop(t.end); 38 | } 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/test-resize-while-over-forking.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | 'use strict'; 7 | 8 | var cluster = require('cluster'); 9 | var master = require('../lib/master'); 10 | var tap = require('tap'); 11 | var workerCount = require('./helpers').workerCount; 12 | 13 | cluster.setupMaster({ 14 | exec: 'test/workers/null.js' 15 | }); 16 | 17 | tap.test('while too many workers are forked', function(t) { 18 | master.start({size: 5}); 19 | master.on('startWorker', function() { 20 | if (workerCount() === 3) { 21 | cluster.fork(); 22 | cluster.fork(); 23 | cluster.fork(); 24 | cluster.fork(); 25 | } 26 | }); 27 | master.once('resize', function(size) { 28 | t.equal(size, 5); 29 | master.stop(t.end); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/test-restart-all.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | 'use strict'; 7 | 8 | var _ = require('lodash'); 9 | var cluster = require('cluster'); 10 | var debug = require('debug')('strong-cluster-control:test'); 11 | var master = require('../lib/master'); 12 | var tap = require('tap'); 13 | 14 | debug('master', process.pid); 15 | 16 | cluster.setupMaster({ 17 | exec: 'test/workers/null.js' 18 | }); 19 | 20 | tap.test('restart all workers', function(t) { 21 | var SIZE = 5; 22 | var DELAY = 200; 23 | 24 | t.plan(4); 25 | 26 | master.start({size: SIZE, throttleDelay: DELAY}); 27 | master.once('resize', function() { 28 | var oldWorkers = Object.keys(cluster.workers); 29 | master.restart(); 30 | t.deepEqual(master.getRestarting(), oldWorkers); 31 | master.once('startRestart', function(workers) { 32 | t.deepEqual(workers, oldWorkers); 33 | }); 34 | master.once('restart', function() { 35 | var stillAlive = _.intersection( 36 | oldWorkers, Object.keys(cluster.workers)); 37 | t.equal(stillAlive.length, 0, 'no old workers are still alive'); 38 | t.deepEqual(master.getRestarting(), null); 39 | master.stop(); 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/test-restart-and-recover.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | 'use strict'; 7 | 8 | var _ = require('lodash'); 9 | var cluster = require('cluster'); 10 | var debug = require('debug')('strong-cluster-control:test'); 11 | var master = require('../lib/master'); 12 | var tap = require('tap'); 13 | 14 | debug('master', process.pid); 15 | 16 | cluster.setupMaster({ 17 | exec: 'test/workers/null.js' 18 | }); 19 | 20 | tap.test('restart all current after new stop dieing', function(t) { 21 | var SIZE = 5; 22 | 23 | master.start({ 24 | size: SIZE, 25 | throttleDelay: 200, // default is 2 sec, we'd like test to run faster 26 | }); 27 | 28 | master.once('resize', function() { 29 | debug('after resize, cmd workers to exit'); 30 | var oldWorkers = Object.keys(cluster.workers); 31 | process.env.cmd = 'EXIT'; 32 | master.restart(); 33 | master.once('restart', function() { 34 | t.assert(!process.env.cmd, 35 | 'restart should not finish until workers stop dieing'); 36 | var stillAlive = _.intersection(oldWorkers, Object.keys(cluster.workers)); 37 | debug('on restart, orig:', oldWorkers); 38 | debug('on restart, aliv:', stillAlive); 39 | t.equal(stillAlive.length, 0, 'no old workers are still alive'); 40 | t.end(); 41 | master.stop(); 42 | }); 43 | 44 | // Run until twice the current number of workers have been forked, 45 | // and check that some of the originals are still around. 46 | var forks = 0; 47 | cluster.once('fork', checkDone); 48 | function checkDone() { 49 | forks++; 50 | if (forks > 2 * SIZE ) { 51 | var stillAlive = _.intersection( 52 | oldWorkers, Object.keys(cluster.workers)); 53 | debug('after a while, orig:', oldWorkers); 54 | debug('after a while, aliv:', stillAlive); 55 | t.assert(stillAlive.length >= SIZE - 1, 'old workers mostly alive'); 56 | delete process.env.cmd; 57 | } else { 58 | cluster.once('fork', checkDone); 59 | } 60 | } 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/test-restart-failed-1.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | global.SIZE = 1; 7 | require('./restart-failed'); 8 | -------------------------------------------------------------------------------- /test/test-restart-failed-2.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | global.SIZE = 2; 7 | require('./restart-failed'); 8 | -------------------------------------------------------------------------------- /test/test-restart-failed-5.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | global.SIZE = 5; 7 | require('./restart-failed'); 8 | -------------------------------------------------------------------------------- /test/test-restart-ok.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | 'use strict'; 7 | 8 | var _ = require('lodash'); 9 | var debug = require('debug')('strong-cluster-control:test'); 10 | var cluster = require('cluster'); 11 | var control = require('../'); 12 | 13 | var SIZE = 3; 14 | 15 | if (cluster.isWorker) { 16 | return; 17 | } 18 | 19 | var tap = require('tap'); 20 | 21 | tap.test('good workers are not killed', function(t) { 22 | var old; 23 | control.start({size: SIZE}); 24 | 25 | control.once('resize', function() { 26 | debug('reached size, restart'); 27 | control.restart(); 28 | }); 29 | 30 | control.on('startRestart', function(_old) { 31 | old = _old; 32 | }); 33 | 34 | control.on('restart', function() { 35 | var fresh = Object.keys(cluster.workers); 36 | var remaining = _.intersection(old, fresh); 37 | 38 | debug('old %j fresh %j remaining: %j', old, fresh, remaining); 39 | 40 | t.equal(remaining.length, 0); 41 | t.end(); 42 | 43 | cluster.removeListener('fork', checkStatus); 44 | cluster.removeListener('exit', checkStatus); 45 | 46 | control.stop(); 47 | }); 48 | 49 | cluster.on('fork', checkStatus); 50 | cluster.on('exit', checkStatus); 51 | 52 | function checkStatus() { 53 | var status = control.status().master; 54 | debug('status: %j', status); 55 | t.equal(status.setSize, SIZE); 56 | } 57 | }); 58 | -------------------------------------------------------------------------------- /test/test-set-size.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | 'use strict'; 7 | 8 | var cluster = require('cluster'); 9 | var master = require('../lib/master'); 10 | var tap = require('tap'); 11 | 12 | cluster.setupMaster({ 13 | exec: 'test/workers/null.js' 14 | }); 15 | 16 | tap.test('set size', function(t) { 17 | t.test('should set in options when changed', function(t) { 18 | master.start({size: 0}); 19 | t.equal(master.options.size, 0); 20 | t.equal(master.size, 0); 21 | t.equal(master.status().master.setSize, 0); 22 | master.setSize(1); 23 | t.equal(master.options.size, 1); 24 | t.equal(master.size, 1); 25 | t.equal(master.status().master.setSize, 1); 26 | t.end(); 27 | }); 28 | 29 | t.test('should emit when set', function(t) { 30 | master.start({size: 0}); 31 | master.setSize(0); 32 | master.once('setSize', function(size) { 33 | t.equal(size, 0); 34 | t.end(); 35 | }); 36 | }); 37 | 38 | t.end(); 39 | }); 40 | -------------------------------------------------------------------------------- /test/test-start-and-stop-events.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | 'use strict'; 7 | 8 | var cluster = require('cluster'); 9 | var master = require('../lib/master'); 10 | var tap = require('tap'); 11 | 12 | cluster.setupMaster({ 13 | exec: 'test/workers/null.js' 14 | }); 15 | 16 | tap.test('should start and stop', function(t) { 17 | t.test('notifying with events', function(t) { 18 | master.start(); 19 | master.once('start', function() { 20 | master.stop(); 21 | master.once('stop', t.end); 22 | }); 23 | }); 24 | 25 | t.test('notifying with callbacks', function(t) { 26 | master.start(function() { 27 | master.stop(t.end); 28 | }); 29 | }); 30 | 31 | t.end(); 32 | }); 33 | -------------------------------------------------------------------------------- /test/test-start-stop-own.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | 'use strict'; 7 | 8 | var cluster = require('cluster'); 9 | var master = require('../lib/master'); 10 | var tap = require('tap'); 11 | var workerCount = require('./helpers').workerCount; 12 | 13 | cluster.setupMaster({ 14 | exec: 'test/workers/null.js' 15 | }); 16 | 17 | tap.test('should stop workers it started', function(t) { 18 | process.throwDeprecation = true; 19 | master.start({size: 1}, function() { 20 | master.once('resize', function() { 21 | master.stop(function() { 22 | t.equal(workerCount(), 0); 23 | t.end(); 24 | }); 25 | }); 26 | }); 27 | }); 28 | 29 | tap.test('should not stop workers it did not start', function(t) { 30 | master.start(function() { 31 | cluster.fork(); 32 | cluster.once('online', function() { 33 | t.equal(workerCount(), 1); 34 | master.stop(function() { 35 | t.equal(workerCount(), 1); 36 | cluster.disconnect(); 37 | t.end(); 38 | }); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/test-status-array.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | /*eslint-env mocha*/ 7 | 'use strict'; 8 | 9 | var _ = require('lodash'); 10 | var cluster = require('cluster'); 11 | var master = require('../lib/master'); 12 | var tap = require('tap'); 13 | var workerCount = require('./helpers').workerCount; 14 | var assertWorker = require('./helpers').assertWorker; 15 | 16 | tap.test('should report status array', function(t) { 17 | cluster.setupMaster({ 18 | exec: 'test/workers/null.js' 19 | }); 20 | 21 | t.on('end', function() { 22 | cluster.disconnect(); 23 | }); 24 | 25 | t.test('for 0 workers', function(t) { 26 | var rsp = master.status(); 27 | delete rsp.master.setSize; 28 | t.assert(_.isFinite(rsp.master.startTime)); 29 | delete rsp.master.startTime; 30 | t.equal(workerCount(), 0); 31 | t.deepEqual(rsp, {master: {pid: process.pid}, workers: []}); 32 | t.end(); 33 | }); 34 | 35 | t.test('for 1 workers', function(t) { 36 | cluster.fork(); 37 | cluster.once('fork', function() { 38 | t.equal(workerCount(), 1); 39 | var rsp = master.status(); 40 | t.equal(rsp.workers.length, 1); 41 | assertWorker(t, rsp.workers[0]); 42 | t.end(); 43 | }); 44 | }); 45 | 46 | t.test('for 2 workers', function(t) { 47 | cluster.fork(); 48 | cluster.once('fork', function() { 49 | var rsp = master.status(); 50 | t.equal(rsp.workers.length, 2); 51 | rsp.workers.forEach(assertWorker.bind(null, t)); 52 | t.end(); 53 | }); 54 | }); 55 | 56 | t.test('for 0 workers, after resize', function(t) { 57 | cluster.once('online', function() { 58 | cluster.disconnect(function() { 59 | var rsp = master.status(); 60 | delete rsp.master.setSize; 61 | t.assert(_.isFinite(rsp.master.startTime), 'start time'); 62 | delete rsp.master.startTime; 63 | t.deepEqual(rsp, {master: {pid: process.pid}, workers: []}, 'status'); 64 | t.end(); 65 | }); 66 | }); 67 | cluster.fork(); 68 | }); 69 | 70 | t.end(); 71 | }); 72 | -------------------------------------------------------------------------------- /test/test-worker-augmentation.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | 'use strict'; 7 | 8 | var _ = require('lodash'); 9 | var cluster = require('cluster'); 10 | var master = require('../lib/master'); 11 | var tap = require('tap'); 12 | 13 | cluster.setupMaster({ 14 | exec: 'workers/null.js' 15 | }); 16 | 17 | tap.test('should stop workers it started', function(t) { 18 | master.start(function() { 19 | var worker = cluster.fork(); 20 | worker.once('fork', function() { 21 | t.assert(_.isFinite(worker.startTime)); 22 | t.assert(_.isFinite(worker.process.pid)); 23 | worker.process.kill('SIGINT'); 24 | }).on('exit', function(code, signal) { 25 | var SIGINT = 2; 26 | if (signal) 27 | t.equal(signal, 'SIGINT'); 28 | else 29 | // 0.10 exits with 0x80 + signal number 30 | t.equal(code - 0x80, SIGINT); 31 | t.end(); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/test-worker-id-sorting.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | 'use strict'; 7 | 8 | var cluster = require('cluster'); 9 | 10 | if (cluster.isWorker) 11 | return; 12 | 13 | var _ = require('lodash'); 14 | var liveWorkerIds = require('../lib/util').liveWorkerIds; 15 | var tap = require('tap'); 16 | 17 | // Must be greater than 10 so we can see order is ..., '9', '10', '11', ..., not 18 | // '10', '11', '9', ... 19 | var N = 15; 20 | var workers = _.times(N, function() { 21 | return cluster.fork(); 22 | }); 23 | 24 | // These will be in creation order: always ascending. 25 | var workerIds = workers.map(function(w) { 26 | return String(w.id); 27 | }); 28 | 29 | tap.equal(workerIds.length, N, 'should be N workers'); 30 | // Sorting should also be in ascending order. 31 | tap.strictDeepEqual(liveWorkerIds(), workerIds, 'sort should preserve order'); 32 | 33 | cluster.disconnect(); 34 | -------------------------------------------------------------------------------- /test/test-worker-kill.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | 'use strict'; 7 | 8 | var cluster = require('cluster'); 9 | var debug = require('debug')('strong-cluster-control:test'); 10 | var master = require('../lib/master'); 11 | var tap = require('tap'); 12 | 13 | cluster.setupMaster({ 14 | exec: 'test/workers/null.js' 15 | }); 16 | 17 | tap.test('should kill workers that refuse to die', function(t) { 18 | function assertWorkerIsKilledAfter(action, t) { 19 | master.start({shutdownTimeout: 100, terminateTimeout: 100}); 20 | 21 | var worker = cluster.fork(); 22 | 23 | cluster.once('online', setBusy); 24 | 25 | function setBusy() { 26 | worker.send({cmd: 'LOOP'}); 27 | worker.on('message', function(msg) { 28 | if (msg.cmd === 'LOOP') { 29 | stopWorker(); 30 | } 31 | }); 32 | } 33 | 34 | function stopWorker() { 35 | master[action](worker.id); 36 | worker.once('exit', function(code, signal) { 37 | debug('exit with', code, signal); 38 | if (process.platform === 'win32') { 39 | // SIGTERM is emulated by libuv on Windows by calling 40 | // TerminateProcess(), which cannot be blocked or caught 41 | t.equal(signal, 'SIGTERM'); 42 | } else { 43 | // SIGTERM can be ignored on Unix, but SIGKILL cannot 44 | t.equal(signal, 'SIGKILL'); 45 | } 46 | t.equal(code, null); 47 | master.stop(t.end); 48 | }); 49 | } 50 | } 51 | 52 | t.test('with terminate', function(t) { 53 | assertWorkerIsKilledAfter('terminate', t); 54 | }); 55 | 56 | t.test('with shutdown', function(t) { 57 | assertWorkerIsKilledAfter('shutdown', t); 58 | }); 59 | 60 | t.end(); 61 | }); 62 | -------------------------------------------------------------------------------- /test/test-worker-shutdown-cmd.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | 'use strict'; 7 | 8 | var cluster = require('cluster'); 9 | var debug = require('debug')('strong-cluster-control:test'); 10 | var master = require('../lib/master'); 11 | var net = require('net'); 12 | var tap = require('tap'); 13 | 14 | cluster.setupMaster({ 15 | exec: 'test/workers/null.js' 16 | }); 17 | 18 | tap.test('should notify workers they are being shutdown', function(t) { 19 | var worker = cluster.fork(); 20 | 21 | worker.once('online', function() { 22 | debug('online, send graceful'); 23 | worker.send({cmd: 'GRACEFUL'}); 24 | }); 25 | 26 | worker.once('listening', function (address) { 27 | debug('master, listening on address', address); 28 | net.connect(address.port, onceConnected); 29 | }); 30 | 31 | function onceConnected() { 32 | debug('master, once connected', this.address()); 33 | // We have a tcp connection, but worker may not have accepted it yet, we 34 | // have to make sure not to send the shutdown notification until the 35 | // worker has accepted the connection. We ensure this by pumping data 36 | // through. 37 | this.write('X'); 38 | this.once('data', function() { 39 | // now we are sure the server knows about the connection, shutdown 40 | this.once('data', function(data) { 41 | t.equal(String(data), 'bye'); 42 | serverBye = true; 43 | maybeDone(); 44 | }); 45 | master.shutdown(worker.id); 46 | }); 47 | } 48 | 49 | worker.on('exit', function(code) { 50 | t.equal(code, 0); 51 | serverExit = true; 52 | maybeDone(); 53 | }); 54 | 55 | // We know we are done when the child sends us 'bye', evidence it has 56 | // received the notification to do a graceful close, and when the exit 57 | // status is 0, evidence that the closing of the server connections has 58 | // allowed the child to disconnect and exit normally. 59 | var serverBye; 60 | var serverExit; 61 | 62 | function maybeDone() { 63 | if (serverBye && serverExit) 64 | master.stop(t.end); 65 | } 66 | }); 67 | -------------------------------------------------------------------------------- /test/test-worker-shutdown.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | 'use strict'; 7 | 8 | var cluster = require('cluster'); 9 | var master = require('../lib/master'); 10 | var tap = require('tap'); 11 | 12 | cluster.setupMaster({ 13 | exec: 'test/workers/null.js' 14 | }); 15 | 16 | tap.test('should shutdown a worker with', function(t) { 17 | t.test('no connections', function(t) { 18 | var worker = cluster.fork(); 19 | cluster.once('online', shutdown); 20 | 21 | function shutdown() { 22 | master.setSize(0); 23 | worker.once('exit', function(code, signal) { 24 | t.equal(signal, null); 25 | t.equal(code, 0); 26 | master.stop(t.end); 27 | }); 28 | } 29 | }); 30 | 31 | t.test('connections', function(t) { 32 | master.start({shutdownTimeout: 100}); 33 | 34 | var worker = cluster.fork(); 35 | cluster.once('online', setBusy); 36 | 37 | function setBusy() { 38 | worker.send({cmd: 'BUSY'}); 39 | worker.on('message', function(msg) { 40 | if (msg.cmd === 'BUSY') { 41 | shutdown(); 42 | } 43 | }); 44 | } 45 | 46 | function shutdown() { 47 | master.setSize(0); 48 | worker.once('exit', function(code) { 49 | // On unix, node catches SIGTERM and exits with non-zero status. On 50 | // Windows, it dies with SIGTERM. Either way, its not a normal exit 51 | // (code === 0). 52 | t.notEqual(code, 0, 'signalled'); 53 | master.stop(t.end); 54 | }); 55 | } 56 | }); 57 | 58 | t.end(); 59 | }); 60 | -------------------------------------------------------------------------------- /test/workers/null.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013,2015. All Rights Reserved. 2 | // Node module: strong-cluster-control 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | // null worker... it should not exit until explicitly disconnected 7 | 8 | var assert = require('assert'); 9 | var cluster = require('cluster'); 10 | var net = require('net'); 11 | 12 | var control = require('../../index'); 13 | var debug = require('debug')('strong-cluster-control:workers:null'); 14 | 15 | debug('worker start id', cluster.worker.id, 'cmd:', process.env.cmd); 16 | 17 | assert(!cluster.isMaster); 18 | 19 | onCommand(process.env); 20 | 21 | //debug('worker env', process.env); 22 | debug('worker argv', process.argv); 23 | 24 | process.send({ 25 | env: process.env, 26 | argv: process.argv 27 | }); 28 | 29 | process.on('message', onCommand); 30 | 31 | function onCommand(msg) { 32 | if (msg.cmd === 'EXIT') { 33 | return process.exit(msg.code); 34 | } 35 | if (msg.cmd === 'BUSY') { 36 | makeBusy(function() { 37 | return process.send({cmd: 'BUSY'}); 38 | }); 39 | } 40 | if (msg.cmd === 'LOOP') { 41 | makeUnexitable(function() { 42 | return process.send({cmd: 'LOOP'}); 43 | }); 44 | } 45 | if (msg.cmd === 'GRACEFUL') { 46 | return shutdownGracefully(); 47 | } 48 | if (msg.cmd === 'ERROR') { 49 | throw Error('On command, I error!'); 50 | } 51 | if (msg.cmd === 'TEST-API-STUB') { 52 | testApiStub(); 53 | } 54 | } 55 | 56 | process.on('internalMessage', function(msg) { 57 | debug('worker internalMessage', msg); 58 | }); 59 | 60 | process.on('disconnect', function() { 61 | debug('worker disconnect'); 62 | }); 63 | 64 | process.on('exit', function() { 65 | debug('worker exit'); 66 | }); 67 | 68 | // Make ourselves busy, but when we get notified of shutdown, close the server 69 | // connections, allowing disconnect to continue. 70 | function shutdownGracefully() { 71 | var connections = []; 72 | var server = makeBusy(function() { 73 | }); 74 | process.on('message', function(msg) { 75 | debug('worker, message', msg, 76 | 'shutdown?', control.cmd.SHUTDOWN, 77 | 'connections=', connections.length); 78 | if (msg.cmd === control.cmd.SHUTDOWN) { 79 | connections.forEach(function(conn) { 80 | debug('worker says bye to peer', conn.remotePort); 81 | conn.end('bye'); 82 | }); 83 | } 84 | }); 85 | 86 | // echo data 87 | server.on('connection', function(connection) { 88 | debug('worker, on server/connection'); 89 | connection.on('data', function(data) { 90 | this.write(data); 91 | }); 92 | }); 93 | 94 | // remember connection, so we can close it on graceful shutdown 95 | server.on('connection', connections.push.bind(connections)); 96 | } 97 | 98 | function makeUnexitable(callback) { 99 | process.on('SIGTERM', function() { }); // Ignore SIGTERM 100 | process.on('exit', function() { 101 | /* eslint no-constant-condition:0 */ 102 | /* eslint no-empty:0 */ 103 | while (true){} 104 | }); 105 | process.nextTick(callback); 106 | } 107 | 108 | // disconnect does not take place until all servers are closed, which doesn't 109 | // happen until all connections to servers are closed, so create a connection to 110 | // self and don't close 111 | function makeBusy(callback) { 112 | var server; 113 | var port; 114 | 115 | server = net.createServer() 116 | .listen(0, function() { 117 | port = server.address().port; 118 | debug('worker: listen on port', port); 119 | createClient(); 120 | }) 121 | .on('connection', acceptClient) 122 | .on('close', function() { 123 | debug('worker: on server/close'); 124 | }); 125 | 126 | 127 | function acceptClient(accept) { 128 | var remotePort = accept.remotePort; 129 | debug('worker: accept', accept.address(), remotePort); 130 | 131 | accept.on('close', function() { 132 | debug('worker: on accept/close', remotePort); 133 | }); 134 | accept.on('end', function() { 135 | debug('worker: on accept/end, .end() our side'); 136 | accept.end(); 137 | }); 138 | } 139 | 140 | function createClient() { 141 | debug('worker: connect to port', port); 142 | net.connect(port) 143 | .on('connect', function() { 144 | debug('worker: on client/connect, send ONLINE to master'); 145 | callback(); 146 | }); 147 | } 148 | return server; 149 | } 150 | 151 | function testApiStub() { 152 | control.start({ 153 | size: control.CPUS 154 | }).on('error', function() { 155 | }).stop(function () { 156 | }).once('error', function() { 157 | }); 158 | 159 | process.exit(); 160 | } 161 | --------------------------------------------------------------------------------