├── .editorconfig ├── .eslintrc ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── app.js ├── app └── extend │ └── context.js ├── appveyor.yml ├── examples ├── async.js └── basic.js ├── lib ├── core │ ├── config │ │ └── index.js │ ├── constants.js │ ├── context │ │ ├── hooks.js │ │ └── index.js │ ├── devtool.js │ ├── entry.js │ ├── exception │ │ └── index.js │ ├── flow │ │ ├── constants.js │ │ ├── controller │ │ │ └── index.js │ │ ├── index.js │ │ ├── rule.js │ │ └── rule_manager.js │ ├── index.js │ ├── log │ │ ├── index.js │ │ └── util.js │ ├── node │ │ ├── entrance.js │ │ ├── resource.js │ │ ├── statistic.js │ │ └── tree.js │ ├── qps │ │ └── index.js │ ├── slots │ │ ├── base.js │ │ ├── chain.js │ │ ├── index.js │ │ ├── logger.js │ │ ├── node_selector.js │ │ ├── res_node_builder.js │ │ └── statistic.js │ └── sph.js └── index.js ├── package.json └── test ├── async.test.js ├── core ├── log │ └── log.test.js └── node │ └── index.test.js ├── help └── index.js └── index.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [makefile] 16 | indent_style = tab 17 | indent_size = 4 18 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-egg", 3 | "parserOptions": { 4 | "ecmaVersion": 8 5 | }, 6 | "rules": { 7 | "no-trailing-spaces": "off", 8 | "indent": "error", 9 | "strict": "off", 10 | "comma-dangle": "off", 11 | "prefer-const": "off", 12 | "no-unused-vars": "off", 13 | "no-bitwise": "off" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules/ 3 | .idea 4 | coverage 5 | yarn.lock 6 | 7 | ### macOS template 8 | .DS_Store 9 | .AppleDouble 10 | .LSOverride 11 | 12 | # Icon must end with two \r 13 | Icon 14 | 15 | # Thumbnails 16 | ._* 17 | 18 | # Files that might appear in the root of a volume 19 | .DocumentRevisions-V100 20 | .fseventsd 21 | .Spotlight-V100 22 | .TemporaryItems 23 | .Trashes 24 | .VolumeIcon.icns 25 | .com.apple.timemachine.donotpresent 26 | 27 | # Directories potentially created on remote AFP share 28 | .AppleDB 29 | .AppleDesktop 30 | Network Trash Folder 31 | Temporary Items 32 | .apdisk 33 | 34 | # VSCode 35 | .vscode -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "printWidth": 120, 6 | "arrowParens": "avoid", 7 | "overrides": [ 8 | { "files": ".editorconfig", "options": { "parser": "yaml" } }, 9 | { "files": "LICENSE", "options": { "parser": "markdown" } } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sentinel Node.js -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const Sentinel = require('./lib/index.js'); 2 | 3 | module.exports = app => { 4 | const options = app.config.sentinel; 5 | 6 | app.sentinel = new Sentinel(Object.assign({}, options, { 7 | appName: app.appName, 8 | logger: app.logger, 9 | })); 10 | }; 11 | -------------------------------------------------------------------------------- /app/extend/context.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | 4 | }; 5 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: '8, 10, 12' 4 | 5 | install: 6 | - ps: Install-Product node $env:nodejs_version 7 | - npm i npminstall && node_modules\.bin\npminstall 8 | 9 | test_script: 10 | - node --version 11 | - npm --version 12 | - npm run test 13 | 14 | build: off 15 | -------------------------------------------------------------------------------- /examples/async.js: -------------------------------------------------------------------------------- 1 | const async_hooks = require('async_hooks'); 2 | const fs = require('fs'); 3 | const devTool = require('../lib/core/devtool'); 4 | 5 | 6 | const Sentinel = require('../lib'); 7 | 8 | const logger = console; 9 | 10 | logger.write = console.log; 11 | 12 | const client = new Sentinel({ 13 | appName: 'sentinel-test', 14 | async: true, 15 | logger: console, 16 | blockLogger: console, 17 | }); 18 | 19 | const Constants = Sentinel.Constants; 20 | 21 | 22 | const log = str => fs.writeSync(1, `${str}\n`); 23 | 24 | async function main() { 25 | let entry; 26 | try { 27 | entry = client.entry('resourceName1'); 28 | 29 | await entry.runInAsync('asyncResourceName1', async () => { 30 | await sleep(1); 31 | log('run in asyncResourceName1', async_hooks.executionAsyncId()); 32 | }); 33 | 34 | await entry.runInAsync('asyncResourceName2', async () => { 35 | log('run in asyncResourceName2 ', async_hooks.executionAsyncId()); 36 | const nestedEntry = client.entry('nestedInResourceName2'); 37 | 38 | nestedEntry.runInAsync('nestedAsync', async () => { 39 | log('run in nestedAsync'); 40 | }).then(() => { 41 | nestedEntry.exit(); 42 | }); 43 | await sleep(1); 44 | }); 45 | 46 | const entry2 = client.entry('hello world'); 47 | log('run in asyncResourceName2 hello world'); 48 | entry2.exit(); 49 | 50 | } catch (e) { 51 | console.error(e); 52 | } finally { 53 | if (entry) { 54 | entry.exit(); 55 | } 56 | 57 | console.log(Constants.ROOT.toString()); 58 | 59 | // contextNodesMap size should be 1 60 | console.log('final contextNodes size', devTool.Context.size); 61 | 62 | client.close(); 63 | } 64 | } 65 | 66 | function sleep(n) { 67 | return new Promise(resolve => { 68 | setTimeout(resolve, n); 69 | }); 70 | } 71 | 72 | main(); 73 | 74 | -------------------------------------------------------------------------------- /examples/basic.js: -------------------------------------------------------------------------------- 1 | 2 | const FlowRuleManager = require('../lib/core/flow/rule_manager'); 3 | const Sentinel = require('../lib'); 4 | 5 | const logger = console; 6 | 7 | logger.write = console.log; 8 | 9 | const client = new Sentinel({ 10 | appName: 'sentinel-test', 11 | async: true, 12 | logger: console, 13 | blockLogger: console, 14 | }); 15 | 16 | const Constants = Sentinel.Constants; 17 | 18 | function loadFlowRules() { 19 | FlowRuleManager.loadRules([ 20 | { resource: 'otherStuff', count: '1', metricType: 1 } 21 | ]); 22 | } 23 | 24 | function main() { 25 | loadFlowRules(); 26 | 27 | let entry; 28 | try { 29 | entry = client.entry('main'); 30 | otherStuff(); 31 | otherStuff(); 32 | } catch (e) { 33 | console.error(e); 34 | } finally { 35 | if (entry !== null) { 36 | entry.exit(); 37 | } 38 | console.log(Constants.ROOT.toString()); 39 | 40 | client.close(); 41 | } 42 | } 43 | 44 | function otherStuff() { 45 | let entry; 46 | try { 47 | entry = client.entry('otherStuff'); 48 | } catch (e) { 49 | console.error(e); 50 | } finally { 51 | if (entry) { 52 | entry.exit(); 53 | } 54 | } 55 | } 56 | 57 | main(); 58 | 59 | -------------------------------------------------------------------------------- /lib/core/config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('sentinel:config'); 4 | 5 | const SentinelConfig = new Map(); 6 | 7 | module.exports = SentinelConfig; 8 | -------------------------------------------------------------------------------- /lib/core/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 1999-2018 Alibaba Group Holding Ltd. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const EntranceNode = require('./node/entrance'); 18 | const ResourceNode = require('./node/resource'); 19 | 20 | /** 21 | * @author mark.ck 22 | */ 23 | 24 | const Constants = { 25 | 26 | SENTINEL_VERSION: require('../../package').version || '0.1.0', 27 | 28 | MAX_CONTEXT_NAME_SIZE: 2000, 29 | MAX_SLOT_CHAIN_SIZE: 6000, 30 | 31 | ROOT_ID: 'machine-root', 32 | 33 | // 多进程模式下,node 的不同进程,默认处于不同context 34 | CONTEXT_DEFAULT_NAME: 'sentinel_default_context_' + process.pid, 35 | 36 | /** 37 | * A virtual resource identifier for total inbound statistics. 38 | */ 39 | TOTAL_IN_RESOURCE_NAME: '__total_inbound_traffic__', 40 | 41 | /** 42 | * Global ROOT statistic node that represents the universal parent node. 43 | */ 44 | ROOT: new EntranceNode('machine-root'), 45 | 46 | /** 47 | * Global statistic node for inbound traffic. 48 | */ 49 | INBOUND_STAT_NODE: new ResourceNode(), 50 | 51 | /** 52 | * The global switch for Sentinel. 53 | */ 54 | ON: true, 55 | 56 | /** 57 | * The flag for sentinel async_hooks type 58 | */ 59 | AsyncTypeFlag: 'Sentinel', 60 | }; 61 | 62 | module.exports = Constants; 63 | -------------------------------------------------------------------------------- /lib/core/context/hooks.js: -------------------------------------------------------------------------------- 1 | const async_hooks = require('async_hooks'); 2 | const debug = require('debug')('sentinel:context:hooks'); 3 | const { AsyncTypeFlag } = require('../constants'); 4 | 5 | /** 6 | * 性能风险,默认不开启,后续优化提供 7 | */ 8 | module.exports = Context => { 9 | const hook = async_hooks.createHook({ 10 | init(asyncId, type, triggerAsyncId) { 11 | if (type === AsyncTypeFlag) { 12 | debug(`current ${asyncId} trigged by ${triggerAsyncId}`); 13 | } 14 | }, 15 | before(asyncId) { 16 | // find parent context before async callback called if exist 17 | if (Context.exist(asyncId)) { 18 | debug(`before: ${asyncId}`); 19 | const entry = Context.get(asyncId); 20 | entry.entry(); 21 | } 22 | }, 23 | after(asyncId) { 24 | if (Context.exist(asyncId)) { 25 | debug(`after: ${asyncId}`); 26 | const entry = Context.get(asyncId); 27 | // async callback completed, entry exit. 28 | entry.exit(); 29 | Context.remove(asyncId); 30 | } 31 | }, 32 | destroy(asyncId) { 33 | if (Context.exist(asyncId)) { 34 | debug(`destroy: ${asyncId}`); 35 | } 36 | }, 37 | }); 38 | return hook; 39 | }; 40 | -------------------------------------------------------------------------------- /lib/core/context/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('sentinel:context'); 4 | const { executionAsyncId, triggerAsyncId } = require('async_hooks'); 5 | const Constants = require('../constants'); 6 | const hooks = require('./hooks'); 7 | const EntranceNode = require('../node/entrance'); 8 | 9 | // async hooks 10 | let hook = null; 11 | 12 | // store context in different async scope 13 | const contextNameNodeMap = new Map(); 14 | 15 | const CONTEXT_NAME = Constants.CONTEXT_DEFAULT_NAME; 16 | 17 | const KEY = Symbol.for(CONTEXT_NAME); 18 | 19 | 20 | const defalutContextNode = new EntranceNode(CONTEXT_NAME); 21 | Constants.ROOT.addChild(defalutContextNode); 22 | 23 | // root context 24 | contextNameNodeMap.set(CONTEXT_NAME, defalutContextNode); 25 | 26 | class Context { 27 | 28 | static get size() { 29 | return contextNameNodeMap.size; 30 | } 31 | 32 | static getContext() { 33 | if (!Context[KEY]) { 34 | Context[KEY] = new Context(CONTEXT_NAME, defalutContextNode); 35 | } 36 | return Context[KEY]; 37 | } 38 | 39 | static get(key) { 40 | debug('get entry, key: %s', key); 41 | return contextNameNodeMap.get(key); 42 | } 43 | 44 | static set(key = CONTEXT_NAME, node) { 45 | debug('set entry, key: %s, current size: %s', key, contextNameNodeMap.size); 46 | if (contextNameNodeMap.size <= Constants.MAX_CONTEXT_NAME_SIZE) { 47 | contextNameNodeMap.set(key, node); 48 | } 49 | } 50 | 51 | static exist(key) { 52 | return !!contextNameNodeMap.get(key); 53 | } 54 | 55 | static remove(key) { 56 | contextNameNodeMap.delete(key); 57 | } 58 | 59 | static enter(entry) { 60 | const eid = executionAsyncId(); 61 | debug('enter entry: %s, eid: %s', entry.name, eid); 62 | entry.eid = eid; 63 | Context.set(eid, entry); 64 | } 65 | 66 | static exit(entry) { 67 | debug('exit entry: %s, eid: %s', entry.name, entry.eid); 68 | let parent; 69 | if (entry && entry.parent) { 70 | parent = entry.parent; 71 | entry.parent = null; 72 | parent.child = null; 73 | } 74 | if (entry && entry.eid) { 75 | Context.remove(entry.eid); 76 | } 77 | 78 | // pop parent entry into current scope 79 | if (parent && parent.eid === entry.eid) { 80 | Context.set(parent.eid, parent); 81 | } 82 | } 83 | 84 | static getParent() { 85 | // find parent in current async scope 86 | const eid = executionAsyncId(); 87 | const parent = Context.get(eid); 88 | if (parent) { 89 | return parent; 90 | } 91 | 92 | // try to find parent in parent async scope 93 | const pid = triggerAsyncId(); 94 | return Context.get(pid); 95 | 96 | } 97 | 98 | constructor(name = '', entranceNode) { 99 | this.origin = ''; 100 | this.name = name; 101 | this.entranceNode = entranceNode; 102 | this.curEntry = null; 103 | } 104 | 105 | getLastNode() { 106 | if (this.curEntry && this.curEntry.curNode) { 107 | return this.curEntry.curNode; 108 | } 109 | return this.entranceNode; 110 | } 111 | 112 | static enableAsyncHook() { 113 | if (!hook) { 114 | hook = hooks(Context); 115 | } 116 | hook.enable(); 117 | 118 | } 119 | 120 | static disableAsyncHook() { 121 | if (hook) { 122 | hook.disable(); 123 | } 124 | } 125 | } 126 | 127 | 128 | Context.debug = () => { 129 | debug(contextNameNodeMap); 130 | }; 131 | 132 | 133 | module.exports = Context; 134 | -------------------------------------------------------------------------------- /lib/core/devtool.js: -------------------------------------------------------------------------------- 1 | const Context = require('./context'); 2 | const Slots = require('./slots'); 3 | 4 | exports.Context = Context; 5 | exports.cleanChainMap = Slots.cleanChainMap; 6 | -------------------------------------------------------------------------------- /lib/core/entry.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { AsyncResource, executionAsyncId, triggerAsyncId } = require('async_hooks'); 4 | const debug = require('debug')('sentinel:entry'); 5 | const Context = require('./context'); 6 | const { AsyncTypeFlag } = require('./constants'); 7 | const SlotAPI = require('./slots'); 8 | 9 | class Entry { 10 | /** 11 | * 12 | * @param {string} name resource name 13 | * @param {string} entryType 'IN': input bound; 'OUT': output bound 14 | * @param {SlotChain} chain chain of slot 15 | * @param {Context} context context 16 | * @param {Entry} parent parent entry 17 | */ 18 | constructor(name, entryType = Entry.EntryType.OUT, chain, context, parent) { 19 | this.name = name; 20 | this.entryType = entryType; 21 | this.chain = chain; 22 | this.createTime = Date.now(); 23 | this.context = context || Context.get(); 24 | 25 | // try to find parent entry 26 | parent = parent || Context.getParent(); 27 | 28 | // push this to linked entry chain 29 | if (parent) { 30 | parent.child = this; 31 | this.parent = parent; 32 | } 33 | 34 | // payload 35 | this.payload = { 36 | entry: this, 37 | context: this.context, 38 | resourceName: name, 39 | entryType, 40 | node: null, 41 | count: undefined, 42 | }; 43 | 44 | Context.enter(this); 45 | } 46 | 47 | entry(count) { 48 | 49 | this.payload.count = count; 50 | if (this.chain) { 51 | this.chain.entry(this.payload); 52 | } 53 | } 54 | 55 | runInAsync(name, fn, ...args) { 56 | 57 | const asyncResource = new AsyncResource( 58 | AsyncTypeFlag, { requireManualDestroy: false } 59 | ); 60 | const asyncId = asyncResource.asyncId(); 61 | 62 | const resourceName = name || this.name; 63 | const chain = SlotAPI.lookProcessChain(resourceName); 64 | const entry = new Entry(resourceName, this.entryType, chain, this.context, this); 65 | 66 | debug('create async scope with asyncId: %s, resourceName: %s', asyncId, resourceName); 67 | 68 | // save current context in AsyncContextMap 69 | Context.set(asyncId, entry); 70 | return asyncResource.runInAsyncScope(fn, ...args); 71 | } 72 | 73 | exit(count = 1) { 74 | Context.exit(this); 75 | if (!isNaN(count)) { 76 | this.payload.count = count; 77 | } 78 | 79 | if (this.chain) { 80 | this.chain.exit(this.payload); 81 | } 82 | 83 | if (this.parent) { 84 | this.parent.child = null; 85 | } 86 | 87 | // If child entry exist (this may caused by uncaughtd exception), exit recursively. 88 | let child = this.child; 89 | while (child) { 90 | child.exit(); 91 | child = child.child; 92 | } 93 | return this; 94 | } 95 | } 96 | 97 | Entry.EntryType = { 98 | IN: 'IN', 99 | OUT: 'OUT', 100 | }; 101 | 102 | module.exports = Entry; 103 | -------------------------------------------------------------------------------- /lib/core/exception/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class SentinelBlockError extends Error { 4 | constructor(message, rule) { 5 | super(message); 6 | this.name = 'SentinelBlockError'; 7 | this.rule = rule; 8 | } 9 | } 10 | 11 | class FlowException extends SentinelBlockError { 12 | constructor(message, rule) { 13 | super(message, rule); 14 | this.name = 'FlowException'; 15 | } 16 | } 17 | 18 | class CircuitBreakerException extends SentinelBlockError { 19 | constructor(message, rule) { 20 | super(message, rule); 21 | this.name = 'CircuitBreakerException'; 22 | } 23 | } 24 | 25 | class SystemBlockException extends SentinelBlockError { 26 | constructor(message, rule) { 27 | super(message, rule); 28 | this.name = 'SystemBlockException'; 29 | } 30 | } 31 | 32 | exports.SentinelBlockError = SentinelBlockError; 33 | exports.FlowException = FlowException; 34 | exports.CircuitBreakerException = CircuitBreakerException; 35 | exports.SystemBlockException = SystemBlockException; 36 | -------------------------------------------------------------------------------- /lib/core/flow/constants.js: -------------------------------------------------------------------------------- 1 | const FlowMetricType = { 2 | CONCURRENCY: 0, 3 | QPS: 1, 4 | }; 5 | 6 | const FlowRelationStrategy = { 7 | DIRECT: 0, 8 | ASSOCIATED_RESOURCE: 1, 9 | CHAIN: 2, 10 | }; 11 | 12 | const FlowControlBehavior = { 13 | REJECT: 0, 14 | WARM_UP: 1, 15 | THROTTLING: 2, 16 | WARM_UP_THROTTLING: 3, 17 | }; 18 | 19 | Object.freeze(FlowMetricType); 20 | Object.freeze(FlowRelationStrategy); 21 | Object.freeze(FlowControlBehavior); 22 | 23 | exports.FlowMetricType = FlowMetricType; 24 | exports.FlowRelationStrategy = FlowRelationStrategy; 25 | exports.FlowControlBehavior = FlowControlBehavior; 26 | exports.FlowConstants = Object.freeze({ 27 | LIMIT_ORIGIN_DEFAULT: 'default', 28 | LIMIT_ORIGIN_OTHER: 'other', 29 | DEFAULT_SAMPLE_COUNT: 2, 30 | DEFAULT_WINDOW_INTERVAL_MS: 1000, 31 | }); 32 | -------------------------------------------------------------------------------- /lib/core/flow/controller/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class TrafficShapingController { 4 | 5 | } 6 | 7 | class DefaultQpsController extends TrafficShapingController { 8 | constructor(count) { 9 | super(); 10 | 11 | this.count = count; 12 | } 13 | 14 | canPass(node, acquireCount) { 15 | if (!node) { 16 | return false; 17 | } 18 | 19 | return ((node.passQps + acquireCount) <= this.count); 20 | } 21 | } 22 | 23 | class WarmUpController extends TrafficShapingController { 24 | constructor(count, tokenRate, coldFactor) { 25 | super(); 26 | this.count = count; 27 | this.tokenRate = tokenRate; 28 | this.coldFactor = coldFactor; 29 | } 30 | 31 | canPass(node, acquireCount) { 32 | if (!node) { 33 | return -1; 34 | } 35 | if (!node.lastRecordTime) { 36 | node.lastRecordTime = Date.now(); 37 | } 38 | const newToken = (Date.now() - node.lastRecordTime) * this.tokenRate; 39 | const _newToken = newToken > this.fullToken ? this.fullToken : newToken; 40 | 41 | return _newToken - acquireCount >= 0; 42 | } 43 | } 44 | 45 | class RateLimiterController extends TrafficShapingController { 46 | 47 | constructor(count, timeout) { 48 | super(); 49 | this.count = count; 50 | this.maxQueueingTimeMs = timeout; 51 | this.latestPassedTime = -1; 52 | } 53 | 54 | async canPass(node, acquireCount) { 55 | if (acquireCount <= 0) { 56 | return true; 57 | } 58 | if (this.count <= 0) { 59 | return false; 60 | } 61 | if (!node) { 62 | return false; 63 | } 64 | 65 | const currentTime = Date.now(); 66 | 67 | // 本次请求预计消耗时间 68 | const costTime = Math.round(1.0 * (acquireCount) / this.count * 1000); 69 | 70 | // 漏桶内所有水滴流完所需时刻 71 | const expectedTime = costTime + this.latestPassedTime; 72 | 73 | // 如果本次请求的时刻比漏桶内所有水滴流完的时刻大,则更新漏桶内水滴流完的时刻, 74 | // 注意这里使用的是 currentTime 而非 currentTime + costTime,会有些偏差,但影响不大; 75 | if (expectedTime <= currentTime) { 76 | this.latestPassedTime = currentTime; 77 | return true; 78 | } 79 | 80 | let waitTime = expectedTime - currentTime; 81 | // wait too long, block it 82 | if (waitTime > this.maxQueueingTimeMs) { 83 | return false; 84 | } 85 | this.latestPassedTime += costTime; 86 | const oldTime = this.latestPassedTime; 87 | try { 88 | waitTime = oldTime - currentTime; 89 | if (waitTime > this.maxQueueingTimeMs) { 90 | this.latestPassedTime -= costTime; 91 | return false; 92 | } 93 | // in race condition waitTime may <= 0 94 | if (waitTime > 0) { 95 | await sleep(waitTime); 96 | } 97 | return true; 98 | } catch (e) { 99 | console.error('wait error: ', e); 100 | } 101 | 102 | return false; 103 | } 104 | } 105 | 106 | async function sleep(timeout) { 107 | return new Promise(resolve => { 108 | setTimeout(resolve, timeout); 109 | }); 110 | } 111 | 112 | exports.TrafficShapingController = TrafficShapingController; 113 | exports.WarmUpController = WarmUpController; 114 | exports.DefaultQpsController = DefaultQpsController; 115 | exports.RateLimiterController = RateLimiterController; 116 | -------------------------------------------------------------------------------- /lib/core/flow/index.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('sentinel:flow:flow_slot'); 2 | const { FlowException } = require('../exception'); 3 | const LinkedProcessorSlot = require('../slots/base'); 4 | const ruleManager = require('./rule_manager'); 5 | 6 | class FlowSlot extends LinkedProcessorSlot { 7 | 8 | checkFlowRules(resourceName, node, acquireCount) { 9 | const ruleList = ruleManager.getRulesFor(resourceName); 10 | debug('Checking flow rule for: %s, existing rules: %j', resourceName, ruleList); 11 | for (let i = 0, len = ruleList.length; i < len; i += 1) { 12 | const rule = ruleList[i]; 13 | if (!rule.passCheck(node, acquireCount)) { 14 | throw new FlowException(resourceName, rule); 15 | } 16 | } 17 | } 18 | 19 | entry(payload) { 20 | const { resourceName, node, count } = payload; 21 | this.checkFlowRules(resourceName, node, count); 22 | 23 | super.fireEntry(payload); 24 | } 25 | 26 | exit(payload) { 27 | super.fireExit(payload); 28 | } 29 | 30 | } 31 | 32 | module.exports = FlowSlot; 33 | -------------------------------------------------------------------------------- /lib/core/flow/rule.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { FlowMetricType, FlowRelationStrategy, FlowControlBehavior, FlowConstants } = require('./constants'); 4 | const SentinelConfig = require('../config'); 5 | const { DefaultQpsController, RateLimiterController, WarmUpController } = require('./controller'); 6 | 7 | class FlowRule { 8 | constructor(options) { 9 | this.resource = options.resource; 10 | this.limitOrigin = options.limitOrigin || FlowConstants.LIMIT_ORIGIN_DEFAULT; 11 | this.count = options.count; 12 | this.metricType = options.metricType || FlowMetricType.QPS; 13 | this.strategy = options.strategy || FlowRelationStrategy.DIRECT; 14 | this.warmUpPeriodSec = options.warmUpPeriodSec; 15 | this.maxQueueingTimeMs = options.maxQueueingTimeMs; 16 | this.controlBehavior = options.controlBehavior || FlowControlBehavior.REJECT; 17 | this.controller = this.controller || new DefaultQpsController(this.count); 18 | switch (this.controlBehavior) { 19 | case FlowControlBehavior.THROTTLING: 20 | this.controller = new RateLimiterController(this.count, this.maxQueueingTimeMs); 21 | break; 22 | case FlowControlBehavior.WARM_UP: 23 | this.controller = new WarmUpController(this.count, this.warmUpPeriodSec, SentinelConfig.get('coldFactor')); 24 | break; 25 | case FlowControlBehavior.REJECT: 26 | default: 27 | this.controller = new DefaultQpsController(this.count); 28 | } 29 | } 30 | 31 | passCheck(node, count) { 32 | return this.controller.canPass(node, count); 33 | } 34 | } 35 | 36 | module.exports = FlowRule; 37 | -------------------------------------------------------------------------------- /lib/core/flow/rule_manager.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('sentinel:flow:rule_manager'); 4 | const FlowRule = require('./rule'); 5 | 6 | let flowRules = new Map(); // list keyed by resourceName 7 | 8 | exports.getRulesFor = resourceName => { 9 | return flowRules.get(resourceName) || []; 10 | }; 11 | 12 | exports.loadRules = rules => { 13 | const m = new Map(); 14 | if (rules && Array.isArray(rules)) { 15 | for (const conf of rules) { 16 | const rule = new FlowRule(conf); 17 | const list = m.get(rule.resource) || []; 18 | list.push(rule); 19 | m.set(rule.resource, list); 20 | } 21 | } 22 | 23 | debug('Flow rules loaded: %o', m); 24 | flowRules = m; 25 | }; 26 | 27 | exports.getRules = () => flowRules; 28 | -------------------------------------------------------------------------------- /lib/core/index.js: -------------------------------------------------------------------------------- 1 | const SPH = require('./sph'); 2 | 3 | /** 4 | * 抛出异常的方式定义资源,示例代码如下: 5 | * 6 | * let entry = null; 7 | * // 务必保证finally会被执行 8 | * try { 9 | * // 资源名可使用任意有业务语义的字符串 10 | * entry = sphU.entry('resourceName'); 11 | * // 被保护的业务逻辑 12 | * // do something... 13 | * } catch (err) { 14 | * // 资源访问阻止,被限流或被降级 15 | * // 进行相应的处理操作 16 | * } finally { 17 | * if (entry !== undefined) { 18 | * entry.exit(); 19 | * } 20 | * } 21 | */ 22 | 23 | module.exports = SPH; 24 | -------------------------------------------------------------------------------- /lib/core/log/index.js: -------------------------------------------------------------------------------- 1 | 2 | const LOGGERS = {}; 3 | 4 | exports.setLoggers = loggers => { 5 | Object.assign(LOGGERS, loggers); 6 | }; 7 | 8 | 9 | exports.getLoggers = () => LOGGERS; 10 | 11 | 12 | exports.getLogger = name => { 13 | return LOGGERS[name] || LOGGERS.defaultLogger || console; 14 | }; 15 | -------------------------------------------------------------------------------- /lib/core/log/util.js: -------------------------------------------------------------------------------- 1 | const utility = require('utility'); 2 | 3 | let lastBlockedSecond; 4 | let lastIndex = 1; 5 | 6 | /** 7 | * 该秒被拦截的资源序号 8 | * 默认记录上一秒时间戳,若相同,序号递增 9 | * @param {*} second log time 10 | */ 11 | function getIndex(second) { 12 | const last = lastBlockedSecond; 13 | lastBlockedSecond = second; 14 | if (second !== last) { 15 | lastIndex = 1; 16 | return lastIndex; 17 | } 18 | lastIndex += 1; 19 | return lastIndex; 20 | } 21 | 22 | // 2014-06-20 16:35:10|1|sayHello(java.lang.String,long),FlowException,default,origin|61,0 23 | // 日志格式: 时间戳|序号(当前秒)|资源名称|拦截的原因|生效规则的调用来源|被拦截资源的调用者|61 被拦截的数量,0则代表可以忽略 24 | exports.blockLogFormat = function blockLogFormat( 25 | resourceName, 26 | blockType, 27 | limitOrigin, 28 | consumerApp = 'origin', 29 | blockedQps, 30 | ) { 31 | const second = utility.YYYYMMDDHHmmss(); 32 | const index = getIndex(second); 33 | 34 | return `${second}|${index}|${resourceName},${blockType},${limitOrigin},${consumerApp}|${blockedQps},0`; 35 | }; 36 | -------------------------------------------------------------------------------- /lib/core/node/entrance.js: -------------------------------------------------------------------------------- 1 | const TreeStatNode = require('./tree'); 2 | 3 | class EntranceNode extends TreeStatNode { 4 | get avgRt() { 5 | let rt = 0; 6 | let totalQps = 0; 7 | for (const node of this.childNodes) { 8 | rt += node.avgRt * node.passQps; 9 | totalQps += node.passQps; 10 | } 11 | return rt / (totalQps === 0 ? 1 : totalQps); 12 | } 13 | 14 | get blockedQps() { 15 | let blockedQps = 0; 16 | for (const node of this.childNodes) { 17 | blockedQps += node.blockedQps; 18 | } 19 | return blockedQps; 20 | } 21 | 22 | get blockRequest() { 23 | let r = 0; 24 | for (const node of this.childNodes) { 25 | r += node.blockRequest; 26 | } 27 | return r; 28 | } 29 | 30 | get curThreadNum() { 31 | let r = 0; 32 | for (const node of this.childNodes) { 33 | r += node.curThreadNum; 34 | } 35 | return r; 36 | } 37 | 38 | get totalQps() { 39 | let r = 0; 40 | for (const node of this.childNodes) { 41 | r += node.totalQps; 42 | } 43 | return r; 44 | } 45 | 46 | get successQps() { 47 | let r = 0; 48 | for (const node of this.childNodes) { 49 | r += node.successQps; 50 | } 51 | return r; 52 | } 53 | 54 | get passQps() { 55 | let r = 0; 56 | for (const node of this.childNodes) { 57 | r += node.passQps; 58 | } 59 | return r; 60 | } 61 | 62 | get totalRequest() { 63 | let r = 0; 64 | for (const node of this.childNodes) { 65 | r += node.totalRequest; 66 | } 67 | return r; 68 | } 69 | } 70 | 71 | module.exports = EntranceNode; 72 | -------------------------------------------------------------------------------- /lib/core/node/resource.js: -------------------------------------------------------------------------------- 1 | 2 | const StatisticsNode = require('./statistic'); 3 | 4 | class ResourceNode extends StatisticsNode { 5 | 6 | } 7 | 8 | module.exports = ResourceNode; 9 | -------------------------------------------------------------------------------- /lib/core/node/statistic.js: -------------------------------------------------------------------------------- 1 | const QPSCounter = require('../qps'); 2 | const debug = require('debug')('sentinel:node:statistics'); 3 | 4 | function transformFloat(n) { 5 | const result = Math.round(n * 100); 6 | if (result === 0) return 0; 7 | return result / 100; 8 | } 9 | 10 | class StatisticsNode { 11 | constructor(id) { 12 | this.id = id; 13 | this.total = new QPSCounter(); 14 | this.pass = new QPSCounter(); 15 | this.block = new QPSCounter(); 16 | this.exception = new QPSCounter(); 17 | this.rt = new QPSCounter(); 18 | this.success = new QPSCounter(); 19 | this.minRt = new QPSCounter(); 20 | this.concurrency = 0; 21 | } 22 | 23 | get resourceName() { 24 | return this.id; 25 | } 26 | 27 | get className() { 28 | return this.constructor.name; 29 | } 30 | 31 | /** 32 | * @return {number} Incoming request + block request per second. 33 | */ 34 | get totalQps() { 35 | return this.total.get(); 36 | } 37 | 38 | get totalQpsAvg() { 39 | return this.total.getAvg(2); 40 | } 41 | 42 | 43 | /** 44 | * @return {number} Incoming request per second. 45 | */ 46 | get passQps() { 47 | return this.pass.get(); 48 | } 49 | 50 | get passQpsAvg() { 51 | return this.pass.getAvg(2); 52 | } 53 | 54 | 55 | get exceptionQps() { 56 | return this.exception.get(); 57 | } 58 | 59 | get exceptionQpsAvg() { 60 | return this.exception.getAvg(2); 61 | } 62 | 63 | 64 | get exceptionRequest() { 65 | return this.exception.getMin(); 66 | } 67 | 68 | get successQps() { 69 | return this.success.get(); 70 | } 71 | 72 | get successQpsAvg() { 73 | return this.success.getAvg(2); 74 | } 75 | 76 | /** 77 | * @return {number} blocked per second. 78 | */ 79 | get blockedQps() { 80 | return this.block.get(); 81 | } 82 | 83 | get blockedQpsAvg() { 84 | return this.block.getAvg(2); 85 | } 86 | 87 | /** 88 | * @return {number} Incoming request + block request per minute. 89 | */ 90 | get totalRequest() { 91 | return this.total.getMin(); 92 | } 93 | 94 | /** 95 | * @return {number} Incoming request request per minute. 96 | */ 97 | get passRequest() { 98 | return this.pass.getMin(); 99 | } 100 | 101 | 102 | /** 103 | * @return {number} blocked request per minute. 104 | */ 105 | get blockRequest() { 106 | return this.block.getMin(); 107 | } 108 | 109 | /** 110 | * @return {number} Average response per second. 111 | */ 112 | get avgRt() { 113 | const successCount = this.success.getAvg(2); 114 | if (successCount === 0) { 115 | return 0; 116 | } 117 | return this.rt.getAvg(2) / successCount; 118 | } 119 | 120 | /** 121 | * Get current active thread count. 122 | * 123 | * @return {int} current active thread count 124 | */ 125 | get curThreadNum() { 126 | return this.concurrency; 127 | } 128 | 129 | addTotalRequest(count) { 130 | this.total.plus(count); 131 | } 132 | 133 | /** 134 | * Increase the block count. 135 | * @param {int} count count to add 136 | * @return {void} 137 | */ 138 | addPassRequest(count) { 139 | this.pass.plus(count); 140 | } 141 | 142 | /** 143 | * block count 144 | * @param {int} count count to add 145 | * @return {void} 146 | */ 147 | increaseBlockedQps(count) { 148 | this.block.plus(count); 149 | } 150 | 151 | /** 152 | * exception count 153 | * @param {int} count count to add 154 | * @return {void} 155 | */ 156 | increaseExceptionQps(count) { 157 | this.exception.plus(count); 158 | } 159 | 160 | addRtAndSuccess(rt, successCount) { 161 | this.success.plus(successCount); 162 | this.rt.plus(rt); 163 | } 164 | 165 | 166 | /** 167 | * Increase current thread count. 168 | * @return {int} current active thread count 169 | */ 170 | increaseConcurrency() { 171 | return ++this.concurrency; 172 | } 173 | 174 | /** 175 | * Decrease current thread count. 176 | * @return {int} current active thread count 177 | */ 178 | decreaseConcurrency() { 179 | return --this.concurrency; 180 | } 181 | 182 | log() { 183 | return `pq:${transformFloat(this.passQps)} bq:${transformFloat(this.blockedQps)} tq:${transformFloat(this.totalQps)}` + 184 | ` rt:${transformFloat(this.avgRt)} 1mp:${transformFloat(this.passRequest)} 1mb:${transformFloat(this.blockRequest)} 1mt:${transformFloat(this.totalRequest)}`; 185 | } 186 | /** 187 | * print debug message 188 | * pq:passQps bq:blockedQps tq:totalQps rt:averageRt 1mp:1m-passed 1mb:1m-blocked 1mt:1m-total 189 | */ 190 | debug() { 191 | debug(this.log()); 192 | } 193 | 194 | toString() { 195 | return this.log(); 196 | } 197 | 198 | toJSON() { 199 | return { 200 | timeStamp: Date.now(), 201 | resourceName: this.id, 202 | avgRt: this.avgRt, 203 | curThreadNum: this.curThreadNum, 204 | exceptionQps: this.exceptionQpsAvg, 205 | exceptionRequest: this.exceptionRequest, 206 | blockedQps: this.blockedQpsAvg, 207 | blockRequest: this.blockRequest, 208 | successQps: this.successQpsAvg, 209 | passQps: this.passQpsAvg, // 每秒成功通过请求 210 | passRequest: this.passRequest, 211 | passReqQps: 0, // 每秒到来的请求, 暂无 212 | totalQps: this.totalQpsAvg, 213 | totalRequest: this.totalRequest, 214 | className: this.className, 215 | }; 216 | } 217 | } 218 | 219 | module.exports = StatisticsNode; 220 | -------------------------------------------------------------------------------- /lib/core/node/tree.js: -------------------------------------------------------------------------------- 1 | const StatisticsNode = require('./statistic'); 2 | 3 | class TreeStatNode extends StatisticsNode { 4 | constructor(id) { 5 | super(id); 6 | this.childNodes = []; 7 | } 8 | 9 | addChild(node) { 10 | if (!this.childNodes.includes(node)) { 11 | this.childNodes.push(node); 12 | } 13 | } 14 | 15 | toString() { 16 | return print(this); 17 | } 18 | 19 | toJSON() { 20 | const json = super.toJSON(); 21 | json.childNodes = this.childNodes.map(child => child.toJSON()); 22 | return json; 23 | } 24 | } 25 | 26 | function print(root, tab = 0) { 27 | let str = ''; 28 | const spaces = '-'.repeat(tab); 29 | const type = root.className; 30 | str = `${spaces}${type}:${root.resourceName}(${TreeStatNode.prototype.log.call(root)})\n`; 31 | const childNodes = root.childNodes || []; 32 | 33 | for (const childNode of childNodes) { 34 | str += print(childNode, tab + 1); 35 | } 36 | return str; 37 | } 38 | 39 | module.exports = TreeStatNode; 40 | -------------------------------------------------------------------------------- /lib/core/qps/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class QPSCounter { 4 | constructor() { 5 | this.ts = [[], []]; 6 | this.counts = [[], []]; 7 | this.tmpCounts = []; 8 | for (let i = 0; i < 60; i += 1) { 9 | this.counts[0][i] = 0; 10 | this.ts[0][i] = 0; 11 | this.counts[1][i] = 0; 12 | this.ts[1][i] = 0; 13 | this.tmpCounts[i] = 0; 14 | } 15 | } 16 | 17 | _refresh(timestamp) { 18 | const now = timestamp || Date.now(); 19 | const date = new Date(now); 20 | const index = date.getMinutes() % 2; 21 | const second = date.getSeconds(); 22 | 23 | if (now - this.ts[index][second] > 2000) { 24 | this.ts[index][second] = now; 25 | this.counts[index][second] = 0; 26 | } 27 | 28 | return { 29 | now, 30 | index, 31 | second, 32 | }; 33 | } 34 | 35 | plus(count = 1) { 36 | const { index, second } = this._refresh(); 37 | 38 | if (count > 0) { 39 | this.counts[index][second] += count; 40 | } 41 | 42 | return this.counts[index][second]; 43 | } 44 | 45 | get(n = 0) { 46 | const { index, second } = this._refresh(Date.now() - 1000 * n); 47 | return this.counts[index][second]; 48 | } 49 | 50 | getLastSecond(n = 1) { 51 | return this.get(1); 52 | } 53 | 54 | getAvg(n = 1) { 55 | if (n <= 0) { 56 | return this.get(); 57 | } 58 | 59 | let total = 0; 60 | for (let i = 0; i < n; i += 1) { 61 | total += this.get(1 + i); 62 | } 63 | 64 | return total / n; 65 | } 66 | 67 | getMin() { 68 | return this.getAvg(60) * 60; 69 | } 70 | } 71 | 72 | module.exports = QPSCounter; 73 | -------------------------------------------------------------------------------- /lib/core/slots/base.js: -------------------------------------------------------------------------------- 1 | class LinkedProcessorSlot { 2 | constructor() { 3 | this.next = null; 4 | } 5 | 6 | entry(payload) { 7 | throw new Error('entry method not implemented'); 8 | } 9 | 10 | exit(payload) { 11 | throw new Error('exit method not implemented'); 12 | } 13 | 14 | fireEntry(payload) { 15 | if (this.next !== null) { 16 | this.next.entry(payload); 17 | } 18 | } 19 | 20 | fireExit(context, entry, node, count) { 21 | if (this.next !== null) { 22 | this.next.exit(context, entry, node, count); 23 | } 24 | } 25 | } 26 | 27 | module.exports = LinkedProcessorSlot; 28 | -------------------------------------------------------------------------------- /lib/core/slots/chain.js: -------------------------------------------------------------------------------- 1 | const LinkedProcessorSlot = require('./base'); 2 | 3 | class FirstSlot extends LinkedProcessorSlot { 4 | entry(payload) { 5 | // do nothing but call next 6 | super.fireEntry(payload); 7 | } 8 | 9 | exit(payload) { 10 | // do nothing but call next 11 | super.fireExit(payload); 12 | } 13 | } 14 | 15 | class SlotChain { 16 | constructor() { 17 | this.first = new FirstSlot(); 18 | this.end = this.first; 19 | } 20 | 21 | addFirst(slot) { 22 | slot.next = this.first.next; 23 | this.first.next = slot; 24 | if (this.first === this.end) { 25 | this.end = slot; 26 | } 27 | } 28 | 29 | addLast(slot) { 30 | this.end.next = slot; 31 | this.end = slot; 32 | } 33 | 34 | entry(payload) { 35 | this.first.entry(payload); 36 | } 37 | 38 | exit(payload) { 39 | this.first.exit(payload); 40 | } 41 | } 42 | 43 | module.exports = SlotChain; 44 | -------------------------------------------------------------------------------- /lib/core/slots/index.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('sentinel:slots:index'); 2 | const SlotChain = require('./chain'); 3 | const FlowSlot = require('../flow'); 4 | const LoggerSlot = require('./logger'); 5 | const TreeNodeSelectorSlot = require('./node_selector'); 6 | const ResourceNodeBuilderSlot = require('./res_node_builder'); 7 | const StatisticSlot = require('./statistic'); 8 | 9 | const MAX_SLOT_CHAIN_SIZE = 6000; 10 | let chainMap = new Map(); 11 | 12 | 13 | function buildChain() { 14 | const chain = new SlotChain(); 15 | 16 | chain.addLast(new TreeNodeSelectorSlot()); 17 | chain.addLast(new ResourceNodeBuilderSlot()); 18 | chain.addLast(new LoggerSlot()); 19 | chain.addLast(new StatisticSlot()); 20 | chain.addLast(new FlowSlot()); 21 | 22 | return chain; 23 | } 24 | 25 | function lookProcessChain(resourceName) { 26 | let chain = chainMap.get(resourceName); 27 | if (chain === undefined) { 28 | if (chainMap.size > MAX_SLOT_CHAIN_SIZE) { 29 | debug('Resource size exceeds the threshold 6000'); 30 | return null; 31 | } 32 | 33 | debug('Building slot chain for resource: ', resourceName); 34 | chain = buildChain(); 35 | chainMap.set(resourceName, chain); 36 | } 37 | 38 | return chain; 39 | } 40 | 41 | function cleanChainMap() { 42 | chainMap.clear(); 43 | return chainMap; 44 | } 45 | 46 | exports.buildChain = buildChain; 47 | exports.lookProcessChain = lookProcessChain; 48 | exports.SlotChain = SlotChain; 49 | exports.cleanChainMap = cleanChainMap; 50 | -------------------------------------------------------------------------------- /lib/core/slots/logger.js: -------------------------------------------------------------------------------- 1 | const LinkedProcessorSlot = require('./base'); 2 | const { SentinelBlockError } = require('../exception'); 3 | const { getLogger } = require('../log'); 4 | const LoggerUtil = require('../log/util'); 5 | 6 | // TODO: eagleeye 7 | class LoggerSlot extends LinkedProcessorSlot { 8 | entry(payload) { 9 | try { 10 | super.fireEntry(payload); 11 | } catch (e) { 12 | if (e instanceof SentinelBlockError) { 13 | const { rule } = e; 14 | getLogger('blockLogger').write( 15 | LoggerUtil.blockLogFormat( 16 | rule.resourceName, 17 | 'FlowException', 18 | rule.limitOrigin, 19 | 'origin', 20 | payload.node.blockedQps, 21 | ), 22 | ); 23 | } 24 | throw e; 25 | } 26 | } 27 | 28 | exit(payload) { 29 | super.fireExit(payload); 30 | } 31 | } 32 | 33 | module.exports = LoggerSlot; 34 | -------------------------------------------------------------------------------- /lib/core/slots/node_selector.js: -------------------------------------------------------------------------------- 1 | const TreeStatNode = require('../node/tree'); 2 | const LinkedProcessorSlot = require('./base'); 3 | 4 | class TreeNodeSelectorSlot extends LinkedProcessorSlot { 5 | constructor() { 6 | super(); 7 | this.nodeMap = new Map(); 8 | } 9 | 10 | entry(payload) { 11 | const { context, resourceName, entry } = payload; 12 | let node = this.nodeMap.get(context.name); 13 | if (!node) { 14 | node = new TreeStatNode(resourceName); 15 | const parent = entry.parent; 16 | if (parent && parent.payload && parent.payload.node) { 17 | parent.payload.node.addChild(node); 18 | } else { 19 | context.getLastNode().addChild(node); 20 | } 21 | this.nodeMap.set(context.name, node); 22 | } 23 | // save node in payload 24 | payload.node = node; 25 | 26 | super.fireEntry(payload); 27 | } 28 | 29 | exit(payload) { 30 | // do nothing 31 | super.fireExit(payload); 32 | } 33 | 34 | } 35 | 36 | module.exports = TreeNodeSelectorSlot; 37 | -------------------------------------------------------------------------------- /lib/core/slots/res_node_builder.js: -------------------------------------------------------------------------------- 1 | const ResourceNode = require('../node/resource'); 2 | const LinkedProcessorSlot = require('./base'); 3 | 4 | class ResourceNodeBuilderSlot extends LinkedProcessorSlot { 5 | constructor() { 6 | super(); 7 | this.nodeMap = new Map(); 8 | this.resNode = null; 9 | } 10 | 11 | entry(payload) { 12 | const { node } = payload; 13 | if (this.resNode === null) { 14 | this.resNode = new ResourceNode(); 15 | this.nodeMap.set(node.id, this.resNode); 16 | } 17 | node.ResourceNode = this.resNode; 18 | 19 | // TODO: node for context origin 20 | super.fireEntry(payload); 21 | } 22 | 23 | exit(payload) { 24 | // do nothing 25 | super.fireExit(payload); 26 | } 27 | 28 | } 29 | 30 | module.exports = ResourceNodeBuilderSlot; 31 | -------------------------------------------------------------------------------- /lib/core/slots/statistic.js: -------------------------------------------------------------------------------- 1 | 2 | const debug = require('debug')('sentinel:slots:statistic'); 3 | const Entry = require('../entry'); 4 | const Constants = require('../constants'); 5 | const LinkedProcessorSlot = require('./base'); 6 | const { SentinelBlockError } = require('../exception'); 7 | 8 | class StatisticSlot extends LinkedProcessorSlot { 9 | 10 | entry(payload) { 11 | const { entryType, node, count } = payload; 12 | try { 13 | node.addTotalRequest(count); 14 | // disable about thread 15 | // node.increaseConcurrency(); 16 | super.fireEntry(payload); 17 | // pass the request 18 | node.addPassRequest(count); 19 | if (entryType === Entry.EntryType.IN) { 20 | // Add count for global inbound entry node for global statistics. 21 | // Constants.INBOUND_STAT_NODE.increaseConcurrency(); 22 | Constants.INBOUND_STAT_NODE.addPassRequest(count); 23 | } 24 | node.debug(); 25 | } catch (err) { 26 | if (err instanceof SentinelBlockError) { 27 | node.increaseBlockedQps(count); 28 | if (entryType === Entry.EntryType.IN) { 29 | // Add count for global inbound entry node for global statistics. 30 | Constants.INBOUND_STAT_NODE.increaseBlockedQps(count); 31 | } 32 | } else { 33 | node.increaseExceptionQps(); 34 | if (entryType === Entry.EntryType.IN) { 35 | // Add count for global inbound entry node for global statistics. 36 | Constants.INBOUND_STAT_NODE.increaseExceptionQps(count); 37 | } 38 | } 39 | throw err; 40 | } 41 | } 42 | 43 | exit(payload) { 44 | const { entry, node, count } = payload; 45 | let rt = Date.now() - entry.createTime; 46 | // Record response time and success count. 47 | node.addRtAndSuccess(rt, count); 48 | // disable about thread 49 | // node.decreaseConcurrency(); 50 | if (entry.entryType === Entry.EntryType.IN) { 51 | Constants.INBOUND_STAT_NODE.addRtAndSuccess(rt, count); 52 | // Constants.INBOUND_STAT_NODE.decreaseConcurrency(); 53 | } 54 | 55 | super.fireExit(payload); 56 | } 57 | 58 | } 59 | 60 | module.exports = StatisticSlot; 61 | -------------------------------------------------------------------------------- /lib/core/sph.js: -------------------------------------------------------------------------------- 1 | const devTool = require('./devtool'); 2 | const Context = require('./context'); 3 | const Entry = require('./entry'); 4 | const { lookProcessChain } = require('./slots'); 5 | 6 | class SPH { 7 | constructor(options) { 8 | this.options = options; 9 | if (options && options.async) { 10 | Context.enableAsyncHook(); 11 | } 12 | } 13 | 14 | entry(resourceName, entryType = Entry.EntryType.OUT, count = 1) { 15 | if (!Object.values(Entry.EntryType).includes(entryType)) { 16 | throw new Error(`unknown entry type: ${entryType}`); 17 | } 18 | 19 | const context = Context.getContext(); 20 | 21 | const chain = lookProcessChain(resourceName); 22 | 23 | const entryObj = new Entry(resourceName, entryType, chain, context); 24 | 25 | entryObj.entry(count); 26 | return entryObj; 27 | } 28 | 29 | asyncEntry(resourceName, entryType = Entry.EntryType.OUT, count = 1) { 30 | return this.entry(resourceName, entryType, count); 31 | } 32 | } 33 | 34 | SPH.devTool = devTool; 35 | module.exports = SPH; 36 | 37 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const Base = require('sdk-base'); 2 | 3 | const SPH = require('./core'); 4 | const Context = require('./core/context'); 5 | const { setLoggers } = require('./core/log'); 6 | const Constants = require('./core/constants'); 7 | const SentinelConfig = require('./core/config'); 8 | const { SentinelBlockError, FlowException, CircuitBreakerException, SystemBlockException } = require('./core/exception'); 9 | 10 | let singleton = null; 11 | 12 | class Sentinel extends Base { 13 | 14 | constructor(options) { 15 | super({ 16 | // initMethod: 'init', 17 | }); 18 | this.options = options; 19 | this.appName = options.appName; 20 | this.sph = new SPH(options); 21 | setLoggers({ 22 | defaultLogger: options.logger, 23 | blockLogger: options.blockLogger, 24 | }); 25 | try { 26 | if (options.sentinelConfig) { 27 | for (const key in options.sentinelConfig) { 28 | if (options.sentinelConfig.hasOwnProperty(key)) { 29 | SentinelConfig.set(key, options.sentinelConfig[key]); 30 | } 31 | } 32 | } 33 | } catch (e) { 34 | (options.logger || console).error('load sentinel config error: ', e); 35 | } 36 | 37 | if (singleton) { 38 | throw new Error('Sentinel client should keep in singleton, make sure you have invoke `close()` before destory another one'); 39 | } 40 | singleton = this; 41 | } 42 | 43 | close() { 44 | // TODO: 清理内存,退出 45 | Context.disableAsyncHook(); 46 | singleton = null; 47 | } 48 | 49 | entry(resourceName, entryType, count) { 50 | return this.sph.entry(resourceName, entryType, count); 51 | } 52 | 53 | get Constants() { 54 | return Constants; 55 | } 56 | } 57 | 58 | // Expose error 59 | Sentinel.SentinelBlockError = SentinelBlockError; 60 | Sentinel.FlowException = FlowException; 61 | Sentinel.CircuitBreakerException = CircuitBreakerException; 62 | Sentinel.SystemBlockException = SystemBlockException; 63 | 64 | // Expose constant 65 | Sentinel.Constants = Constants; 66 | 67 | // Expose Context 68 | Sentinel.Context = Context; 69 | 70 | module.exports = Sentinel; 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sentinel", 3 | "description": "A powerful framework for resilience (rate limiting and circuit breaking)", 4 | "version": "0.1.0", 5 | "license": "Apache-2.0", 6 | "scripts": { 7 | "lint": "eslint . --fix", 8 | "test": "npm run lint && egg-bin test", 9 | "test-local": "egg-bin test", 10 | "autod": "autod", 11 | "cov": "egg-bin cov", 12 | "pkgfiles": "egg-bin pkgfiles", 13 | "ci": "npm run lint && npm run cov" 14 | }, 15 | "main": "lib/index.js", 16 | "files": [ 17 | "lib", 18 | "app.js" 19 | ], 20 | "dependencies": { 21 | "debug": "^3.1.0", 22 | "qps": "^1.1.1", 23 | "sdk-base": "^3.4.0", 24 | "utility": "^1.13.1" 25 | }, 26 | "devDependencies": { 27 | "autod": "^3.0.1", 28 | "egg-bin": "^4.13.0", 29 | "eslint": "^4.19.1", 30 | "eslint-config-egg": "^7.0.0", 31 | "sinon": "^7.2.3", 32 | "urllib": "^2.34.1" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "git@github.com:alibaba/sentinel-nodejs.git" 37 | }, 38 | "bugs": { 39 | "url": "https://github.com/alibaba/sentinel-nodejs" 40 | }, 41 | "keywords": [ 42 | "sentinel" 43 | ], 44 | "ci": { 45 | "versions": "8, 10, 12" 46 | }, 47 | "engines": { 48 | "node": ">= 8.0.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/async.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const Context = require('../lib/core/context'); 5 | const { createClient, addRules } = require('./help'); 6 | 7 | describe('async.test.js', () => { 8 | let client = null; 9 | before(async () => { 10 | client = createClient({ async: true }); 11 | await client.ready(); 12 | addRules(client); 13 | }); 14 | 15 | after(() => { 16 | client.close(); 17 | client = null; 18 | }); 19 | 20 | it('should generate nested entries use `runInAsync`', async () => { 21 | let entry; 22 | try { 23 | entry = client.entry('resourceName1'); 24 | 25 | await entry.runInAsync('asyncResourceName1', async () => { 26 | // do something 27 | }); 28 | 29 | await entry.runInAsync('asyncResourceName2', async () => { 30 | const nestedEntry = client.entry('nestedInResourceName2'); 31 | nestedEntry 32 | .runInAsync('nestedAsync', async () => {}) 33 | .then(() => { 34 | nestedEntry.exit(); 35 | }); 36 | }); 37 | 38 | const entry2 = client.entry('hello world'); 39 | entry2.exit(); 40 | } catch (e) { 41 | console.error(e); 42 | } finally { 43 | if (entry) { 44 | entry.exit(); 45 | } 46 | assert.equal(Context.size, 1); 47 | const root = entry.payload.node; 48 | assert(entry.payload.node); 49 | const childs = entry.payload.node.childNodes; 50 | console.log(root.toString()); 51 | assert(childs.length === 2); 52 | } 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/core/log/log.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const Util = require('../../../lib/core/log/util'); 5 | 6 | describe('lib/core/log', () => { 7 | beforeEach(() => {}); 8 | 9 | afterEach(() => {}); 10 | 11 | describe('#blockLogFormat', () => { 12 | it('should format log with increased index', () => { 13 | const l1 = Util.blockLogFormat('test', 'type', 'app', 'origin', '1'); 14 | const l2 = Util.blockLogFormat('test', 'type', 'app', 'origin', '1'); 15 | const i1 = l1.split('|')[1]; 16 | const i2 = l2.split('|')[1]; 17 | assert.notEqual(l1, l2); 18 | assert(i1 === '1'); 19 | assert(i2 === '2'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/core/node/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const { wait } = require('../../help'); 5 | const TreeStatNode = require('../../../lib/core/node/tree'); 6 | const StatisticsNode = require('../../../lib/core/node/statistic'); 7 | const EntranceNode = require('../../../lib/core/node/entrance'); 8 | 9 | describe('lib/core/node', () => { 10 | beforeEach(() => {}); 11 | 12 | afterEach(() => {}); 13 | 14 | const logRegExp = /pq\:[0-9.]+\sbq\:[0-9.]+\stq\:[0-9.]+\srt\:[0-9.]+\s1mp\:[0-9.]+\s1mb\:[0-9.]+\s1mt\:[0-9.]+/; 15 | 16 | describe('statistics node', () => { 17 | const node = new StatisticsNode('id'); 18 | 19 | it('should increase total qps count when addTotalRequest() called', async () => { 20 | assert.equal(node.id, 'id'); 21 | assert.equal(node.resourceName, node.id); 22 | node.addTotalRequest(); 23 | assert.equal(node.totalQps, 1); 24 | 25 | node.addTotalRequest(2); 26 | assert.equal(node.totalQps, 3); 27 | 28 | await wait(1000); 29 | 30 | // avg qps of last 2 seconds should be 1.5 31 | assert.equal(node.totalQpsAvg, 1.5); 32 | 33 | // total qps of 1 min should be 3 34 | assert.equal(node.totalRequest, 3); 35 | }); 36 | 37 | it('should increase pass qps count when addPassRequest() called', async () => { 38 | assert.equal(node.id, 'id'); 39 | 40 | node.addPassRequest(); 41 | assert.equal(node.passQps, 1); 42 | 43 | node.addPassRequest(2); 44 | assert.equal(node.passQps, 3); 45 | await wait(1000); 46 | 47 | // avg qps of last 2 seconds should be 1.5 48 | assert.equal(node.passQpsAvg, 1.5); 49 | 50 | // total qps of 1 min should be 3 51 | assert.equal(node.passRequest, 3); 52 | }); 53 | 54 | it('should increase rt and success qps count when addRtAndSuccess() called', async () => { 55 | assert.equal(node.id, 'id'); 56 | 57 | node.addRtAndSuccess(20, 1); 58 | assert.equal(node.successQps, 1); 59 | 60 | node.addRtAndSuccess(80, 3); 61 | assert.equal(node.successQps, 4); 62 | 63 | await wait(1000); 64 | 65 | // avg qps of last 2 seconds should be 2 66 | assert.equal(node.successQpsAvg, 2); 67 | 68 | // arg rt in 2 seconds 69 | assert.equal(node.avgRt, 25); 70 | }); 71 | 72 | it('should increase blocked qps when increaseBlockedQps() called', async () => { 73 | node.increaseBlockedQps(); 74 | assert.equal(node.blockedQps, 1); 75 | 76 | node.increaseBlockedQps(2); 77 | assert.equal(node.blockedQps, 3); 78 | 79 | await wait(1000); 80 | 81 | // avg qps of last 2 seconds should be 1.5 82 | assert.equal(node.blockedQpsAvg, 1.5); 83 | 84 | // total qps of 1 min should be 3 85 | assert.equal(node.blockRequest, 3); 86 | }); 87 | 88 | it('should increase exception qps when increaseExceptionQps() called', async () => { 89 | node.increaseExceptionQps(); 90 | assert.equal(node.exceptionQps, 1); 91 | 92 | node.increaseExceptionQps(2); 93 | assert.equal(node.exceptionQps, 3); 94 | 95 | await wait(1000); 96 | 97 | // avg qps of last 2 seconds should be 1.5 98 | assert.equal(node.exceptionQpsAvg, 1.5); 99 | 100 | // total qps of 1 min should be 3 101 | assert.equal(node.exceptionRequest, 3); 102 | }); 103 | 104 | 105 | it('should update threadNum when decreaseConcurrency() and increaseConcurrency() called', async () => { 106 | assert.equal(node.concurrency, 0); 107 | 108 | node.increaseConcurrency(); 109 | assert.equal(node.concurrency, 1); 110 | 111 | 112 | node.decreaseConcurrency(); 113 | assert.equal(node.concurrency, 0); 114 | }); 115 | 116 | it('should return pure json object when toJSON() called', async () => { 117 | const json = node.toJSON(); 118 | assert.equal(json.resourceName, 'id'); 119 | assert.equal(json.className, 'StatisticsNode'); 120 | }); 121 | 122 | it('should return log stringwhen log() called', async () => { 123 | const log = node.log(); 124 | const str = node.toString(); 125 | assert(log !== '', 'log string should not be empty'); 126 | assert.equal(str, log); 127 | // should math the format: 128 | // pq:passQps bq:blockedQps tq:totalQps rt:averageRt 1mp:1m-passed 1mb:1m-blocked 1mt:1m-total 129 | assert(logRegExp.test(log), 'in right format'); 130 | }); 131 | }); 132 | 133 | describe('statistics node', () => { 134 | 135 | const node = new TreeStatNode('parent'); 136 | const c1 = new StatisticsNode('c1'); 137 | const c2 = new StatisticsNode('c2'); 138 | 139 | it('should add child nodes into parant node', async () => { 140 | 141 | node.addChild(c1); 142 | node.addChild(c2); 143 | 144 | assert.equal(node.childNodes.length, 2); 145 | assert.equal(node.childNodes[0], c1); 146 | assert.equal(node.childNodes[1], c2); 147 | }); 148 | 149 | it('should return nested string when toString() called', async () => { 150 | const str = node.toString().trim(); 151 | const strs = str.split('\n'); 152 | assert.equal(strs.length, 3); 153 | assert(logRegExp.test(strs[0])); 154 | assert(strs[1].startsWith('-')); 155 | assert(strs[2].startsWith('-')); 156 | assert(logRegExp.test(strs[1])); 157 | assert(logRegExp.test(strs[2])); 158 | }); 159 | 160 | it('should contains child nodes object when toJSON() called', async () => { 161 | const json = node.toJSON(); 162 | assert.equal(json.childNodes.length, 2); 163 | }); 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /test/help/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Sentinel = require('../../lib/index'); 4 | const FlowRuleManager = require('../../lib/core/flow/rule_manager'); 5 | 6 | const logger = console; 7 | 8 | logger.write = console.log; 9 | 10 | exports.createClient = function(options = {}) { 11 | return new Sentinel(Object.assign({ 12 | appName: 'sentinel-test', 13 | logger: console, 14 | blockLogger: console, 15 | }, options)); 16 | }; 17 | 18 | exports.addRules = function() { 19 | FlowRuleManager.loadRules([ 20 | { resource: 'qps1', count: '1', metricType: 1 } 21 | ]); 22 | }; 23 | 24 | exports.wait = function(millseconds) { 25 | return new Promise(resolve => { 26 | setTimeout(resolve, millseconds); 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const sinon = require('sinon'); 5 | const Entry = require('../lib/core/entry'); 6 | const Sentinel = require('../lib/index'); 7 | const { getLogger } = require('../lib/core/log'); 8 | 9 | const { createClient, addRules } = require('./help'); 10 | 11 | describe('lib/index', () => { 12 | let client = null; 13 | before(async () => { 14 | client = createClient(); 15 | await client.ready(); 16 | addRules(client); 17 | }); 18 | 19 | after(() => { 20 | client.close(); 21 | client = null; 22 | Sentinel.Constants.ROOT.debug(); 23 | }); 24 | 25 | afterEach(() => { 26 | sinon.restore(); 27 | }); 28 | 29 | it('should sentinel client created', () => { 30 | assert(client); 31 | }); 32 | 33 | 34 | it('should get right statistics if no traffic', () => { 35 | const node = Sentinel.Constants.ROOT; 36 | const json = node.toJSON(); 37 | assert(json); 38 | assert.equal(json.avgRt, 0); 39 | }); 40 | 41 | it('should entry created', () => { 42 | const name = 'test'; 43 | const e = client.entry(name); 44 | assert(e); 45 | assert.equal(e.name, name); 46 | assert.equal(e.entryType, Entry.EntryType.OUT); 47 | assert.equal(e.entryType, Entry.EntryType.OUT); 48 | e.exit(); 49 | }); 50 | 51 | 52 | it('should pass if qps <= limit', () => { 53 | const name = 'qps1'; 54 | let entry; 55 | try { 56 | entry = client.entry(name, 'IN', 1); 57 | assert(true, 'should pass'); 58 | } catch (e) { 59 | assert(false, 'should not throw FlowException'); 60 | } finally { 61 | entry && entry.exit(); 62 | } 63 | }); 64 | 65 | 66 | it('should throw exception if qps > limit', () => { 67 | const name = 'qps1'; 68 | let entry; 69 | try { 70 | entry = client.entry(name, 'IN', 2); 71 | assert(false, 'should throw exception'); 72 | } catch (e) { 73 | assert.equal(e.name, 'FlowException'); 74 | } finally { 75 | entry && entry.exit(); 76 | } 77 | }); 78 | 79 | it('should throw exception if qps > limit', () => { 80 | const name = 'qps1'; 81 | let entry; 82 | try { 83 | entry = client.entry(name, 'IN', 2); 84 | assert(false, 'should throw exception'); 85 | } catch (e) { 86 | assert.equal(e.name, 'FlowException'); 87 | } finally { 88 | entry && entry.exit(); 89 | } 90 | }); 91 | 92 | it('should write block logger when throw block exception', () => { 93 | const logger = getLogger('blockLogger'); 94 | const log = sinon.stub(logger, 'write'); 95 | const name = 'qps1'; 96 | let entry; 97 | try { 98 | entry = client.entry(name, 'IN', 2); 99 | assert(false, 'should throw exception'); 100 | } catch (e) { 101 | assert.equal(e.name, 'FlowException'); 102 | } finally { 103 | entry && entry.exit(); 104 | sinon.assert.calledOnce(log); 105 | } 106 | }); 107 | 108 | it('should print entry node tree', () => { 109 | const node = Sentinel.Constants.ROOT; 110 | assert.equal(node.constructor.name, 'EntranceNode'); 111 | const json = node.toJSON(); 112 | assert(json); 113 | assert(node.toString()); 114 | }); 115 | 116 | }); 117 | --------------------------------------------------------------------------------