├── test ├── supports │ ├── v7.9.0.bin │ ├── data_client.js │ ├── async_data_client.js │ ├── api_client.js │ ├── async_api_client.js │ ├── close_client.js │ ├── get_server.js │ ├── sub.js │ ├── sub_timeout │ │ ├── data_client.js │ │ └── api_client.js │ ├── pub.js │ ├── notify_client.js │ ├── case_1 │ │ ├── data_client.js │ │ └── api_client.js │ ├── client.js │ ├── invoke.js │ ├── cluster_server.js │ ├── server.js │ └── registry_client.js ├── async.test.js ├── follower.test.js ├── event.test.js ├── register_error.test.js ├── server.test.js ├── ready.test.js ├── close.test.js ├── subscribe.test.js ├── cluster.test.js ├── utils.test.js ├── lazy.test.js ├── edge_case.test.js ├── connection.test.js ├── client.test.js └── index.test.js ├── .eslintrc ├── .eslintignore ├── lib ├── const.js ├── default_transcode.js ├── protocol │ ├── byte_buffer.js │ ├── response.js │ ├── request.js │ └── packet.js ├── default_logger.js ├── symbol.js ├── api_client.js ├── wrapper │ ├── cluster.js │ ├── single.js │ └── base.js ├── utils.js ├── connection.js ├── index.js ├── server.js ├── follower.js └── leader.js ├── .gitignore ├── .github ├── workflows │ ├── release.yml │ └── nodejs.yml └── PULL_REQUEST_TEMPLATE.md ├── LICENSE ├── package.json ├── index.js ├── CHANGELOG.md └── README.md /test/supports/v7.9.0.bin: -------------------------------------------------------------------------------- 1 | ok -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-egg" 3 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | test/fixtures 2 | coverage 3 | examples/**/app/public 4 | logs 5 | run -------------------------------------------------------------------------------- /lib/const.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.VERSION = 1; 4 | exports.REQUEST = 0; 5 | exports.RESPONSE = 1; 6 | -------------------------------------------------------------------------------- /lib/default_transcode.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const S_JSON = require('serialize-json'); 4 | 5 | exports.encode = S_JSON.encode; 6 | exports.decode = S_JSON.decode; 7 | -------------------------------------------------------------------------------- /lib/protocol/byte_buffer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ByteBuffer = require('byte'); 4 | 5 | // avoid create many buffer 6 | module.exports = new ByteBuffer({ 7 | size: 1024 * 1024, 8 | }); 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | *.log 4 | npm-debug.log* 5 | .logs 6 | logs 7 | *.swp 8 | run 9 | *-run 10 | .idea 11 | .DS_Store 12 | .tmp 13 | 14 | .* 15 | !.github 16 | !.eslintignore 17 | !.eslintrc 18 | !.gitignore 19 | !.travis.yml 20 | *.bin 21 | package-lock.json 22 | -------------------------------------------------------------------------------- /lib/protocol/response.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Packet = require('./packet'); 4 | const Constant = require('../const'); 5 | 6 | class Response extends Packet { 7 | constructor(options) { 8 | super(Object.assign({ 9 | type: Constant.RESPONSE, 10 | }, options)); 11 | } 12 | } 13 | 14 | module.exports = Response; 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | release: 9 | name: Node.js 10 | uses: node-modules/github-actions/.github/workflows/node-release.yml@master 11 | secrets: 12 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 13 | GIT_TOKEN: ${{ secrets.GIT_TOKEN }} 14 | -------------------------------------------------------------------------------- /lib/default_logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Logger, ConsoleTransport } = require('egg-logger'); 4 | const { consoleFormatter } = require('egg-logger/lib/utils'); 5 | const logger = new Logger(); 6 | logger.set('console', new ConsoleTransport({ 7 | level: 'INFO', 8 | formatter: consoleFormatter, 9 | })); 10 | 11 | module.exports = logger; 12 | -------------------------------------------------------------------------------- /test/supports/data_client.js: -------------------------------------------------------------------------------- 1 | const Base = require('sdk-base'); 2 | const { sleep } = require('../../lib/utils'); 3 | 4 | class DataClient extends Base { 5 | constructor() { 6 | super({ initMethod: '_init' }); 7 | } 8 | 9 | * _init() { 10 | yield sleep(5000); 11 | } 12 | 13 | * echo(str) { 14 | return str; 15 | } 16 | } 17 | 18 | module.exports = DataClient; 19 | -------------------------------------------------------------------------------- /lib/protocol/request.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const utils = require('../utils'); 4 | const Packet = require('./packet'); 5 | const Constant = require('../const'); 6 | 7 | class Request extends Packet { 8 | constructor(options) { 9 | const id = utils.nextId(); 10 | super(Object.assign({ 11 | id, 12 | type: Constant.REQUEST, 13 | }, options)); 14 | } 15 | } 16 | 17 | module.exports = Request; 18 | -------------------------------------------------------------------------------- /test/supports/async_data_client.js: -------------------------------------------------------------------------------- 1 | const Base = require('sdk-base'); 2 | const { sleep } = require('../../lib/utils'); 3 | 4 | class DataClient extends Base { 5 | constructor() { 6 | super({ initMethod: '_init' }); 7 | } 8 | 9 | async _init() { 10 | await sleep(5000); 11 | } 12 | 13 | async echo(str) { 14 | await sleep(10); 15 | return str; 16 | } 17 | } 18 | 19 | module.exports = DataClient; 20 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | Job: 11 | name: Node.js 12 | uses: node-modules/github-actions/.github/workflows/node-test.yml@master 13 | with: 14 | os: 'ubuntu-latest, macos-latest, windows-latest' 15 | version: '14.18.0, 14, 16, 18, 20, 22' 16 | secrets: 17 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 18 | -------------------------------------------------------------------------------- /test/supports/api_client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const APIClientBase = require('../../').APIClientBase; 4 | 5 | class APIClient extends APIClientBase { 6 | get DataClient() { 7 | return require('./data_client'); 8 | } 9 | 10 | get clusterOptions() { 11 | return { 12 | name: 'api_client_test', 13 | responseTimeout: 100, 14 | }; 15 | } 16 | 17 | * echo(str) { 18 | return yield this._client.echo(str); 19 | } 20 | } 21 | 22 | module.exports = APIClient; 23 | -------------------------------------------------------------------------------- /test/supports/async_api_client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const APIClientBase = require('../../').APIClientBase; 4 | 5 | class APIClient extends APIClientBase { 6 | get DataClient() { 7 | return require('./async_data_client'); 8 | } 9 | 10 | get clusterOptions() { 11 | return { 12 | name: 'api_client_test', 13 | responseTimeout: 100, 14 | }; 15 | } 16 | 17 | async echo(str) { 18 | return await this._client.echo(str); 19 | } 20 | } 21 | 22 | module.exports = APIClient; 23 | -------------------------------------------------------------------------------- /test/supports/close_client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const Base = require('sdk-base'); 6 | 7 | class CloseClient extends Base { 8 | constructor(options) { 9 | super(options); 10 | 11 | fs.writeFileSync(path.join(__dirname, `${process.version}.bin`), 'ok'); 12 | this.ready(true); 13 | } 14 | 15 | destroy() { 16 | fs.unlinkSync(path.join(__dirname, `${process.version}.bin`)); 17 | } 18 | } 19 | 20 | module.exports = CloseClient; 21 | -------------------------------------------------------------------------------- /test/supports/get_server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const co = require('co'); 4 | const assert = require('assert'); 5 | const server_copy = require('./server'); 6 | const server = require('../../lib/server'); 7 | 8 | co(function* () { 9 | const instance = yield server.create('xxx', 10000); 10 | const instance_2 = yield server_copy.create('yyy', 10000); 11 | 12 | assert(instance && instance === instance_2); 13 | 14 | instance.close(); 15 | console.log('success'); 16 | }).catch(err => { 17 | console.error(err); 18 | }); 19 | -------------------------------------------------------------------------------- /test/supports/sub.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const cluster = require('../../'); 4 | const NotifyClient = require('./notify_client'); 5 | 6 | const exit = process.argv[2] === 'true'; 7 | 8 | const client = cluster(NotifyClient, { port: 6789 }) 9 | .delegate('publish', 'invoke') 10 | .create(); 11 | 12 | client.subscribe({ 13 | dataId: 'test-id', 14 | }, val => { 15 | console.log('receive val', val); 16 | 17 | if (exit) { 18 | process.exit(0); 19 | } 20 | 21 | if (process.send) { 22 | process.send(process.pid); 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /test/supports/sub_timeout/data_client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Base = require('sdk-base'); 4 | 5 | class DataClient extends Base { 6 | constructor(options = {}) { 7 | super(options); 8 | } 9 | 10 | async _init() { 11 | // ... 12 | } 13 | 14 | subscribe(reg, listener) { 15 | const { key } = reg; 16 | const match = key.match(/timeout:(\d+)/); 17 | if (!match) { 18 | throw new Error('not a timeout key'); 19 | } 20 | setTimeout(() => { 21 | listener('hello:' + match[1]); 22 | }, +match[1]); 23 | } 24 | 25 | close() { 26 | } 27 | } 28 | 29 | module.exports = DataClient; 30 | -------------------------------------------------------------------------------- /test/supports/pub.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const co = require('co'); 4 | const cluster = require('../../'); 5 | const NotifyClient = require('./notify_client'); 6 | const sleep = timeout => cb => setTimeout(cb, timeout); 7 | 8 | const client = cluster(NotifyClient, { port: 6789 }) 9 | .delegate('publish', 'invoke') 10 | .create(); 11 | 12 | const running = true; 13 | 14 | co(function* () { 15 | 16 | while (running) { 17 | yield client.publish({ 18 | dataId: 'test-id', 19 | publishData: Date.now(), 20 | }); 21 | 22 | yield sleep(2000); 23 | } 24 | 25 | }).catch(err => { 26 | console.error(err); 27 | process.exit(1); 28 | }); 29 | -------------------------------------------------------------------------------- /test/async.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const APIClient = require('./supports/async_api_client'); 5 | 6 | describe('test/async.test.js', () => { 7 | it('should support auto delegate async function', async function() { 8 | const leader = new APIClient(); 9 | const follower = new APIClient(); 10 | 11 | await Promise.all([ 12 | leader.ready(), 13 | follower.ready(), 14 | ]); 15 | 16 | let ret = await follower.echo('hello'); 17 | assert(ret === 'hello'); 18 | 19 | ret = await leader.echo('hello'); 20 | assert(ret === 'hello'); 21 | 22 | await Promise.all([ 23 | follower.close(), 24 | leader.close(), 25 | ]); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 10 | 11 | ##### Checklist 12 | 13 | 14 | - [ ] `npm test` passes 15 | - [ ] tests and/or benchmarks are included 16 | - [ ] documentation is changed or added 17 | - [ ] commit message follows commit guidelines 18 | 19 | ##### Affected core subsystem(s) 20 | 21 | 22 | 23 | ##### Description of change 24 | -------------------------------------------------------------------------------- /test/supports/sub_timeout/api_client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const DataClient = require('./data_client'); 4 | const APIClientBase = require('../../../lib/api_client'); 5 | 6 | class ApiClient extends APIClientBase { 7 | constructor(options = {}) { 8 | super(Object.assign({}, options, { initMethod: '_init' })); 9 | } 10 | 11 | get DataClient() { 12 | return DataClient; 13 | } 14 | 15 | get clusterOptions() { 16 | return { 17 | port: parseInt(process.env.NODE_CLUSTER_CLIENT_PORT || 7777), 18 | singleMode: false, 19 | subscribeTimeout: 1000, 20 | }; 21 | } 22 | 23 | async _init() { 24 | await this._client.ready(); 25 | } 26 | 27 | subscribe(config, listener) { 28 | this._client.subscribe(config, listener); 29 | } 30 | 31 | publish(reg) { 32 | this._client.publish(reg); 33 | } 34 | 35 | close() { 36 | return this._client.close(); 37 | } 38 | } 39 | 40 | module.exports = ApiClient; 41 | -------------------------------------------------------------------------------- /test/supports/notify_client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Base = require('sdk-base'); 4 | 5 | class NotifyClient extends Base { 6 | constructor() { 7 | super(); 8 | this._registered = new Map(); 9 | this.ready(true); 10 | } 11 | 12 | /** 13 | * subscribe 14 | * 15 | * @param {Object} reg 16 | * - {String} dataId - the dataId 17 | * @param {Function} listener - the listener 18 | */ 19 | subscribe(reg, listener) { 20 | const key = reg.dataId; 21 | this.on(key, listener); 22 | } 23 | 24 | /** 25 | * publish 26 | * 27 | * @param {Object} reg 28 | * - {String} dataId - the dataId 29 | * - {String} publishData - the publish data 30 | * @return {Boolean} result 31 | */ 32 | * publish(reg) { 33 | const key = reg.dataId; 34 | this.emit(key, reg.publishData); 35 | return true; 36 | } 37 | 38 | * commit(id, data) { 39 | console.log(id, data); 40 | return data; 41 | } 42 | } 43 | 44 | module.exports = NotifyClient; 45 | -------------------------------------------------------------------------------- /test/supports/case_1/data_client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Base = require('sdk-base'); 4 | 5 | class DataClient extends Base { 6 | constructor(options = {}) { 7 | super(options); 8 | 9 | this._cache = new Map(); 10 | } 11 | 12 | async _init() { 13 | this._cache.set('foo', { 14 | bar: 'bar', 15 | }); 16 | } 17 | 18 | subscribe(reg, listener) { 19 | const key = reg.dataId; 20 | 21 | process.nextTick(() => { 22 | listener(this._cache.get(key)); 23 | }); 24 | 25 | this.on(key, listener); 26 | } 27 | 28 | publish(reg) { 29 | process.nextTick(() => { 30 | if (!reg) { 31 | const err = new Error('empty reg'); 32 | err.name = 'EmptyRegError'; 33 | this.emit('error', err); 34 | return; 35 | } 36 | this._cache.set(reg.dataId, reg.data); 37 | this.emit(reg.dataId, this._cache.get(reg.dataId)); 38 | }); 39 | } 40 | 41 | close() { 42 | this._cache.clear(); 43 | } 44 | } 45 | 46 | module.exports = DataClient; 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 node_modules 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/symbol.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.init = Symbol.for('ClusterClient#init'); 4 | exports.logger = Symbol.for('ClusterClient#logger'); 5 | exports.isReady = Symbol.for('ClusterClient#isReady'); 6 | exports.innerClient = Symbol.for('ClusterClient#innerClient'); 7 | exports.subscribe = Symbol.for('ClusterClient#subscribe'); 8 | exports.unSubscribe = Symbol.for('ClusterClient#unSubscribe'); 9 | exports.publish = Symbol.for('ClusterClient#publish'); 10 | exports.invoke = Symbol.for('ClusterClient#invoke'); 11 | exports.subInfo = Symbol.for('ClusterClient#subInfo'); 12 | exports.pubInfo = Symbol.for('ClusterClient#pubInfo'); 13 | exports.closeHandler = Symbol.for('ClusterClient#closeHandler'); 14 | exports.close = Symbol.for('ClusterClient#close'); 15 | exports.subscribeMethodName = Symbol.for('ClusterClient#subscribeMethodName'); 16 | exports.unSubscribeMethodName = Symbol.for('ClusterClient#unSubscribeMethodName'); 17 | exports.publishMethodName = Symbol.for('ClusterClient#publishMethodName'); 18 | exports.closeByUser = Symbol.for('ClusterClient#closeByUser'); 19 | exports.singleMode = Symbol.for('ClusterClient#singleMode'); 20 | exports.createClient = Symbol.for('ClusterClient#createClient'); 21 | -------------------------------------------------------------------------------- /test/follower.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const net = require('net'); 4 | const assert = require('assert'); 5 | const Follower = require('../lib/follower'); 6 | const transcode = require('../lib/default_transcode'); 7 | 8 | describe('test/follower.test.js', () => { 9 | let port; 10 | let server; 11 | before(done => { 12 | server = net.createServer(); 13 | server.on('connection', socket => { 14 | socket.once('data', () => { 15 | socket.destroy(); 16 | }); 17 | }); 18 | server.listen(0, () => { 19 | port = server.address().port; 20 | done(); 21 | }); 22 | }); 23 | 24 | after(done => { 25 | server.close(done); 26 | }); 27 | 28 | it('should ready failed if socket is closed', async function() { 29 | let count = 0; 30 | const follower = new Follower({ 31 | port, 32 | transcode, 33 | name: 'test', 34 | descriptors: new Map(), 35 | responseTimeout: 100, 36 | logger: { 37 | warn() { 38 | count++; 39 | }, 40 | }, 41 | }); 42 | try { 43 | await follower.ready(); 44 | assert(false); 45 | } catch (err) { 46 | assert(err && err.message.includes('The socket was closed')); 47 | } 48 | 49 | assert(count === 0); 50 | assert(!follower._socket); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/supports/client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Base = require('sdk-base'); 4 | 5 | class Client extends Base { 6 | constructor(options) { 7 | super(options); 8 | this.registerInfo = new Map(); 9 | this.ready(true); 10 | } 11 | 12 | subscribe(reg, listener) { 13 | if (!this.registerInfo.has(reg.key)) { 14 | this.registerInfo.set(reg.key, []); 15 | } 16 | process.nextTick(() => { 17 | listener(this.registerInfo.get(reg.key)); 18 | }); 19 | this.on(reg.key, listener); 20 | } 21 | 22 | unSubscribe(reg, listener) { 23 | if (listener) { 24 | this.removeListener(reg.key, listener); 25 | } else { 26 | this.removeAllListeners(reg.key); 27 | } 28 | } 29 | 30 | publish(reg) { 31 | const arr = this.registerInfo.get(reg.key) || []; 32 | arr.push(reg.value); 33 | this.registerInfo.set(reg.key, arr); 34 | process.nextTick(() => { this.emit(reg.key, arr); }); 35 | } 36 | 37 | unPublish(reg) { 38 | const arr = this.registerInfo.get(reg.key) || []; 39 | const index = arr.indexOf(reg.value); 40 | if (index >= 0) { 41 | arr.splice(index, 1); 42 | } 43 | this.registerInfo.set(reg.key, arr); 44 | process.nextTick(() => { this.emit(reg.key, arr); }); 45 | } 46 | 47 | close() { 48 | this.registerInfo.clear(); 49 | } 50 | } 51 | 52 | module.exports = Client; 53 | -------------------------------------------------------------------------------- /test/supports/case_1/api_client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const DataClient = require('./data_client'); 5 | const APIClientBase = require('../../../lib/api_client'); 6 | 7 | class ApiClient extends APIClientBase { 8 | constructor(options = {}) { 9 | super(Object.assign({}, options, { initMethod: '_init' })); 10 | } 11 | 12 | get DataClient() { 13 | return DataClient; 14 | } 15 | 16 | get clusterOptions() { 17 | return { 18 | port: parseInt(process.env.NODE_CLUSTER_CLIENT_PORT || 7777), 19 | singleMode: process.env.NODE_CLUSTER_CLIENT_SINGLE_MODE === '1', 20 | }; 21 | } 22 | 23 | async _init() { 24 | await this._client.ready(); 25 | this._client.subscribe({ 26 | dataId: 'foo', 27 | }, val => { 28 | try { 29 | this._setFoo(val); 30 | this.foo = val; 31 | this.emit('foo', val); 32 | } catch (err) { 33 | this.emit('error', err); 34 | this.emit('foo', val); 35 | } 36 | }); 37 | await this.await('foo'); 38 | // await this.awaitFirst([ 'foo', 'error' ]); 39 | } 40 | 41 | _setFoo(val) { 42 | assert(typeof val === 'object'); 43 | assert.deepEqual(Object.keys(val), [ 'bar' ]); 44 | 45 | val.xxx = 'yyy'; 46 | } 47 | 48 | publish(reg) { 49 | this._client.publish(reg); 50 | } 51 | 52 | close() { 53 | return this._client.close(); 54 | } 55 | } 56 | 57 | module.exports = ApiClient; 58 | -------------------------------------------------------------------------------- /test/supports/invoke.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const co = require('co'); 4 | const is = require('is-type-of'); 5 | const assert = require('assert'); 6 | const cluster = require('../../'); 7 | const NotifyClient = require('./notify_client'); 8 | 9 | const client = cluster(NotifyClient, { 10 | port: 6789, 11 | transcode: { 12 | encode(obj) { 13 | if (is.date(obj)) { 14 | return Buffer.from(JSON.stringify({ 15 | type: 'date', 16 | data: obj.getTime(), 17 | })); 18 | } else if (is.buffer(obj)) { 19 | return Buffer.from(JSON.stringify({ 20 | type: 'buffer', 21 | data: obj.toString('hex'), 22 | })); 23 | } 24 | return Buffer.from(JSON.stringify(obj)); 25 | }, 26 | decode(buf) { 27 | const obj = JSON.parse(buf); 28 | if (obj.type === 'date') { 29 | return new Date(obj.data); 30 | } else if (obj.type === 'buffer') { 31 | return Buffer.from(obj.data, 'hex'); 32 | } 33 | return obj; 34 | }, 35 | }, 36 | }) 37 | .delegate('publish', 'invoke') 38 | .create(); 39 | 40 | co(function* () { 41 | 42 | let ret = yield client.commit('123', new Date()); 43 | assert(is.date(ret)); 44 | ret = yield client.commit('123', Buffer.from('hello')); 45 | assert(is.buffer(ret) && ret.toString() === 'hello'); 46 | ret = yield client.commit('123', { name: 'peter' }); 47 | assert(is.object(ret) && ret.name === 'peter'); 48 | 49 | console.log('success'); 50 | process.exit(0); 51 | }).catch(err => { 52 | console.error(err); 53 | process.exit(1); 54 | }); 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cluster-client", 3 | "version": "3.7.0", 4 | "description": "Sharing Connection among Multi-Process Nodejs", 5 | "main": "./index.js", 6 | "files": [ 7 | "lib", 8 | "index.js" 9 | ], 10 | "scripts": { 11 | "lint": "eslint . --ext .js", 12 | "test": "npm run lint && npm run test-local", 13 | "test-local": "egg-bin test", 14 | "ci": "egg-bin cov" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/node-modules/cluster-client.git" 19 | }, 20 | "keywords": [ 21 | "cluster", 22 | "multi-process" 23 | ], 24 | "author": "gxcsoccer@126.com", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/node-modules/cluster-client/issues" 28 | }, 29 | "homepage": "https://github.com/node-modules/cluster-client#readme", 30 | "dependencies": { 31 | "byte": "^2.0.0", 32 | "co": "^4.6.0", 33 | "egg-logger": "^3.5.0", 34 | "is-type-of": "^1.4.0", 35 | "json-stringify-safe": "^5.0.1", 36 | "long": "^4.0.0", 37 | "sdk-base": "^4.2.1", 38 | "serialize-json": "^1.0.3", 39 | "tcp-base": "^3.2.0", 40 | "utility": "^2.1.0" 41 | }, 42 | "devDependencies": { 43 | "address": "2", 44 | "await-event": "^2.1.0", 45 | "coffee": "^5.2.1", 46 | "detect-port": "2", 47 | "egg-bin": "^6.4.1", 48 | "egg-mock": "^5.4.0", 49 | "eslint": "^8.30.0", 50 | "eslint-config-egg": "^12.1.0", 51 | "mm": "^2.4.1", 52 | "pedding": "^1.1.0", 53 | "spy": "^1.0.0", 54 | "urllib": "^3.27.1" 55 | }, 56 | "engines": { 57 | "node": ">=14.18.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/event.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mm = require('mm'); 4 | const net = require('net'); 5 | const { APIClientBase } = require('..'); 6 | 7 | describe('test/event.test.js', () => { 8 | let port; 9 | let client; 10 | class ClusterClient extends APIClientBase { 11 | get DataClient() { 12 | return require('./supports/client'); 13 | } 14 | 15 | get clusterOptions() { 16 | return { 17 | responseTimeout: 1000, 18 | port, 19 | }; 20 | } 21 | 22 | subscribe(...args) { 23 | return this._client.subscribe(...args); 24 | } 25 | 26 | publish(...args) { 27 | return this._client.publish(...args); 28 | } 29 | 30 | close() { 31 | return this._client.close(); 32 | } 33 | } 34 | 35 | before(async function() { 36 | const server = net.createServer(); 37 | port = await new Promise(resolve => { 38 | server.listen(0, () => { 39 | const address = server.address(); 40 | console.log('using port =>', address.port); 41 | server.close(); 42 | resolve(address.port); 43 | }); 44 | }); 45 | client = new ClusterClient(); 46 | }); 47 | after(async function() { 48 | await client.close(); 49 | }); 50 | 51 | it('should ok', async function() { 52 | mm(process, 'emitWarning', err => { 53 | client.emit('error', err); 54 | }); 55 | const subscribe = () => { 56 | let count = 10; 57 | while (count--) { 58 | client.subscribe({ key: 'foo' }, () => {}); 59 | } 60 | client.subscribe({ key: 'foo' }, () => { 61 | client.emit('foo'); 62 | }); 63 | }; 64 | 65 | await Promise.race([ 66 | client.await('error'), 67 | client.await('foo'), 68 | subscribe(), 69 | ]); 70 | }); 71 | 72 | }); 73 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const is = require('is-type-of'); 5 | const cluster = require('./lib'); 6 | const symbols = require('./lib/symbol'); 7 | const APIClientBase = require('./lib/api_client'); 8 | 9 | /** 10 | * Create an Wrapper 11 | * 12 | * @param {Function} clientClass - client class 13 | * @param {Object} options - wrapper options 14 | * @return {ClientWrapper} wrapper 15 | */ 16 | module.exports = cluster; 17 | 18 | /** 19 | * Close a ClusterClient 20 | * 21 | * @param {Object} client - ClusterClient instance to be closed 22 | * @return {Promise} returns a promise which will be resolved after fully closed 23 | */ 24 | module.exports.close = client => { 25 | assert(is.function(client[symbols.close]), '[cluster#close] client should be instanceof ClusterClient'); 26 | return client[symbols.close](); 27 | }; 28 | 29 | /** 30 | * API Client SuperClass 31 | * 32 | * @example 33 | * --------------------------------------------- 34 | * class ClusterClient extends APIClientBase { 35 | * get DataClient() { 36 | * return require('./supports/client'); 37 | * } 38 | * get delegates() { 39 | * return { 40 | * unPublish: 'invokeOneway', 41 | * }; 42 | * } 43 | * get clusterOptions() { 44 | * return { 45 | * responseTimeout: 1000, 46 | * port, 47 | * }; 48 | * } 49 | * subscribe(...args) { 50 | * return this._client.subscribe(...args); 51 | * } 52 | * unSubscribe(...args) { 53 | * return this._client.unSubscribe(...args); 54 | * } 55 | * publish(...args) { 56 | * return this._client.publish(...args); 57 | * } 58 | * unPublish(...args) { 59 | * return this._client.unPublish(...args); 60 | * } 61 | * close() { 62 | * return this._client.close(); 63 | * } 64 | * } 65 | */ 66 | module.exports.APIClientBase = APIClientBase; 67 | -------------------------------------------------------------------------------- /lib/api_client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const cluster = require('./'); 4 | const utils = require('./utils'); 5 | const Base = require('sdk-base'); 6 | const is = require('is-type-of'); 7 | 8 | class APIClientBase extends Base { 9 | constructor(options) { 10 | options = options || {}; 11 | super(options); 12 | 13 | const wrapper = (options.cluster || cluster)( 14 | this.DataClient, this.clusterOptions 15 | ); 16 | for (const from in this.delegates) { 17 | const to = this.delegates[from]; 18 | wrapper.delegate(from, to); 19 | } 20 | this._client = wrapper.create(options); 21 | utils.delegateEvents(this._client, this); 22 | 23 | if (!options.initMethod) { 24 | this._client.ready(err => { 25 | this.ready(err ? err : true); 26 | }); 27 | } 28 | } 29 | 30 | get isClusterClientLeader() { 31 | return this._client.isClusterClientLeader; 32 | } 33 | 34 | close() { 35 | if (is.function(this._client.close)) { 36 | return this._client.close().then(() => cluster.close(this._client)); 37 | } 38 | return cluster.close(this._client); 39 | } 40 | 41 | get DataClient() { 42 | /* istanbul ignore next */ 43 | throw new Error('[APIClient] DataClient is required'); 44 | } 45 | 46 | /** 47 | * the cluster options 48 | * 49 | * @property {Object} APIClientBase#clusterOptions 50 | */ 51 | get clusterOptions() { 52 | /* istanbul ignore next */ 53 | return {}; 54 | } 55 | 56 | /** 57 | * the delegates map 58 | * 59 | * @example 60 | * --------------------------- 61 | * delegates => { 62 | * subscribe: 'subscribe', 63 | * foo: 'invoke', 64 | * } 65 | * 66 | * @property {Object} APIClientBase#delegates 67 | */ 68 | get delegates() { 69 | /* istanbul ignore next */ 70 | return {}; 71 | } 72 | } 73 | 74 | module.exports = APIClientBase; 75 | -------------------------------------------------------------------------------- /test/register_error.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mm = require('mm'); 4 | const net = require('net'); 5 | const cluster = require('..'); 6 | const Client = require('./supports/client'); 7 | const Packet = require('../lib/protocol/packet'); 8 | const Response = require('../lib/protocol/response'); 9 | 10 | describe('test/register_error.test.js', () => { 11 | let port; 12 | before(done => { 13 | const server = net.createServer(); 14 | server.listen(0, () => { 15 | const address = server.address(); 16 | port = address.port; 17 | console.log('using port =>', port); 18 | server.close(); 19 | done(); 20 | }); 21 | }); 22 | afterEach(mm.restore); 23 | 24 | it('should register channel util success', async function() { 25 | const originDecode = Packet.decode; 26 | mm(Packet, 'decode', function(buf) { 27 | const ret = originDecode(buf); 28 | if (ret.connObj && ret.connObj.type === 'register_channel') { 29 | ret.connObj.type = 'xx'; 30 | mm.restore(); 31 | } 32 | return ret; 33 | }); 34 | 35 | const leader = cluster(Client, { port }).create(); 36 | const follower = cluster(Client, { port }).create(); 37 | 38 | await leader.ready(); 39 | await follower.ready(); 40 | 41 | await follower.close(); 42 | await leader.close(); 43 | }); 44 | 45 | it('should handle register_channel request in leader', async function() { 46 | mm(Response.prototype, 'encode', function() { 47 | mm.restore(); 48 | return Buffer.from('01010000000000000000000000000bb80000001f000000007b2274797065223a2272656769737465725f6368616e6e656c5f726573227d', 'hex'); 49 | }); 50 | 51 | const leader = cluster(Client, { port }).create(); 52 | const follower = cluster(Client, { port }).create(); 53 | 54 | await leader.ready(); 55 | await follower.ready(); 56 | 57 | await follower.close(); 58 | await leader.close(); 59 | 60 | // subscribe after close 61 | follower.subscribe({ foo: 'bar' }, val => { 62 | console.log(val); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /lib/wrapper/cluster.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('util').debuglog('cluster-client:lib:wrapper:cluster'); 4 | const Base = require('./base'); 5 | const Leader = require('../leader'); 6 | const Follower = require('../follower'); 7 | const ClusterServer = require('../server'); 8 | 9 | // Symbol 10 | const { 11 | init, 12 | logger, 13 | isReady, 14 | innerClient, 15 | createClient, 16 | closeHandler, 17 | } = require('../symbol'); 18 | 19 | class ClusterClient extends Base { 20 | constructor(options) { 21 | super(options); 22 | 23 | this[closeHandler] = () => { 24 | this[logger].warn('[ClusterClient:%s] %s closed, and try to init it again', this.options.name, this[innerClient].isLeader ? 'leader' : 'follower'); 25 | this[isReady] = false; 26 | this.ready(false); 27 | this[init]().catch(err => { this.ready(err); }); 28 | }; 29 | } 30 | 31 | async [createClient]() { 32 | const name = this.options.name; 33 | const port = this.options.port; 34 | let server; 35 | if (this.options.isLeader === true) { 36 | server = await ClusterServer.create(name, port, { maxListeners: this.options.maxListeners }); 37 | if (!server) { 38 | throw new Error(`create "${name}" leader failed, the port:${port} is occupied by other`); 39 | } 40 | } else if (this.options.isLeader === false) { 41 | // wait for leader active 42 | await ClusterServer.waitFor(port, this.options.maxWaitTime); 43 | } else { 44 | debug('[ClusterClient:%s] init cluster client, try to seize the leader on port:%d', name, port); 45 | server = await ClusterServer.create(name, port, { maxListeners: this.options.maxListeners }); 46 | } 47 | 48 | if (server) { 49 | debug('[ClusterClient:%s] has seized port %d, and serves as leader client.', name, port); 50 | return new Leader(Object.assign({ server }, this.options)); 51 | } 52 | debug('[ClusterClient:%s] gives up seizing port %d, and serves as follower client.', name, port); 53 | return new Follower(this.options); 54 | } 55 | } 56 | 57 | module.exports = ClusterClient; 58 | -------------------------------------------------------------------------------- /test/server.test.js: -------------------------------------------------------------------------------- 1 | const mm = require('mm'); 2 | const path = require('path'); 3 | const coffee = require('coffee'); 4 | const assert = require('assert'); 5 | const ClusterServer = require('../lib/server'); 6 | 7 | describe('test/server.test.js', () => { 8 | afterEach(mm.restore); 9 | 10 | it('should create different type of server in one process', done => { 11 | coffee.fork(path.join(__dirname, 'supports/get_server')) 12 | .expect('stdout', 'success\n') 13 | .end(done); 14 | }); 15 | 16 | it('should return null create with same name', async function() { 17 | const server1 = await ClusterServer.create('same-name', 10001); 18 | assert(server1); 19 | const server2 = await ClusterServer.create('same-name', 10001); 20 | assert(server2 === null); 21 | await server1.close(); 22 | }); 23 | 24 | it('should create success if previous closed by ClusterServer.close', async function() { 25 | const server1 = await ClusterServer.create('previous-closed', 10002); 26 | assert(server1); 27 | await ClusterServer.close('previous-closed', server1); 28 | const server2 = await ClusterServer.create('previous-closed', 10002); 29 | assert(server2); 30 | await ClusterServer.close('previous-closed', server1); 31 | }); 32 | 33 | it('should throw error when port is not a number', async function() { 34 | await assert.rejects(async () => { 35 | await ClusterServer.create('same-name', undefined); 36 | }, /port should be a number, but got undefined/); 37 | await assert.rejects(async () => { 38 | await ClusterServer.create('same-name', null); 39 | }, /port should be a number, but got null/); 40 | await assert.rejects(async () => { 41 | await ClusterServer.create('same-name'); 42 | }, /port should be a number, but got undefined/); 43 | await assert.rejects(async () => { 44 | await ClusterServer.create('same-name', 'foo'); 45 | }, /port should be a number, but got "foo"/); 46 | await assert.rejects(async () => { 47 | await ClusterServer.create('same-name', '0'); 48 | }, /port should be a number, but got "0"/); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/ready.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const net = require('net'); 4 | const assert = require('assert'); 5 | const Base = require('sdk-base'); 6 | const APIClientBase = require('..').APIClientBase; 7 | 8 | describe('test/ready.test.js', () => { 9 | let port; 10 | before(done => { 11 | const server = net.createServer(); 12 | server.listen(0, () => { 13 | const address = server.address(); 14 | port = address.port; 15 | console.log('using port =>', port); 16 | server.close(); 17 | done(); 18 | }); 19 | }); 20 | 21 | class ErrorClient extends Base { 22 | constructor() { 23 | super(); 24 | setImmediate(() => { 25 | this.ready(new Error('mock error')); 26 | }); 27 | } 28 | 29 | * getData() { 30 | return '123'; 31 | } 32 | 33 | close() {} 34 | } 35 | 36 | class APIClient extends APIClientBase { 37 | get DataClient() { 38 | return ErrorClient; 39 | } 40 | 41 | get clusterOptions() { 42 | return { 43 | port, 44 | }; 45 | } 46 | 47 | * getData() { 48 | return yield this._client.getData(); 49 | } 50 | 51 | close() { 52 | return this._client.close(); 53 | } 54 | } 55 | 56 | it('should ready failed', function* () { 57 | const client = new APIClient(); 58 | try { 59 | yield client.ready(); 60 | assert(false, 'should not run here'); 61 | } catch (err) { 62 | assert(err.message === 'mock error'); 63 | } 64 | yield client.close(); 65 | }); 66 | 67 | it('should invoke with error while client ready failed', function* () { 68 | const client_1 = new APIClient(); 69 | try { 70 | yield client_1.getData(); 71 | assert(false, 'should not run here'); 72 | } catch (err) { 73 | assert(err && err.message === 'mock error'); 74 | } 75 | const client_2 = new APIClient(); 76 | try { 77 | yield client_2.getData(); 78 | assert(false, 'should not run here'); 79 | } catch (err) { 80 | assert(err && err.message === 'mock error'); 81 | } 82 | yield client_2.close(); 83 | yield client_1.close(); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /test/close.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const net = require('net'); 5 | const path = require('path'); 6 | const cluster = require('../'); 7 | const Base = require('sdk-base'); 8 | const assert = require('assert'); 9 | const CloseClient = require('./supports/close_client'); 10 | const RegistyClient = require('./supports/registry_client'); 11 | 12 | describe('test/close.test.js', () => { 13 | let port; 14 | before(done => { 15 | const server = net.createServer(); 16 | server.listen(0, () => { 17 | port = server.address().port; 18 | server.close(); 19 | done(); 20 | }); 21 | }); 22 | 23 | it('should delegate close ok', async function() { 24 | const leader = cluster(CloseClient, { port }) 25 | .delegate('destroy', 'close') 26 | .create(); 27 | 28 | await leader.ready(); 29 | assert(fs.existsSync(path.join(__dirname, `supports/${process.version}.bin`))); 30 | await leader.destroy(); 31 | assert(!fs.existsSync(path.join(__dirname, `supports/${process.version}.bin`))); 32 | }); 33 | 34 | it('should APIClient has default close', async function() { 35 | class APIClient extends cluster.APIClientBase { 36 | get DataClient() { 37 | return CloseClient; 38 | } 39 | 40 | get clusterOptions() { 41 | return { port }; 42 | } 43 | } 44 | 45 | let client = new APIClient(); 46 | await client.ready(); 47 | await client.close(); 48 | 49 | class APIClient2 extends cluster.APIClientBase { 50 | get DataClient() { 51 | return RegistyClient; 52 | } 53 | 54 | get clusterOptions() { 55 | return { port }; 56 | } 57 | } 58 | 59 | client = new APIClient2(); 60 | await client.ready(); 61 | await client.close(); 62 | }); 63 | 64 | it('should handle error event after closed', async function() { 65 | class DataClient extends Base { 66 | constructor(options) { 67 | super(options); 68 | this.ready(true); 69 | } 70 | 71 | close() { 72 | setTimeout(() => { 73 | this.emit('error', new Error('mock error')); 74 | }, 2000); 75 | } 76 | } 77 | 78 | const leader = cluster(DataClient, { port }) 79 | .create(); 80 | 81 | await leader.ready(); 82 | await leader.close(); 83 | 84 | try { 85 | await leader.await('error'); 86 | } catch (err) { 87 | assert(err.message === 'mock error'); 88 | } 89 | 90 | // close again should work 91 | await leader.close(); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /test/subscribe.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { sleep } = require('../lib/utils'); 3 | const ApiClient = require('./supports/sub_timeout/api_client'); 4 | 5 | describe('test/subscrib.test.js', () => { 6 | describe('timeout case', () => { 7 | it('should timeout', async () => { 8 | const leader = new ApiClient({ 9 | singleMode: false, 10 | isLeader: true, 11 | }); 12 | const follower = new ApiClient({ 13 | clusterOptions: { 14 | subscribeTimeout: 1000, 15 | }, 16 | singleMode: false, 17 | isLeader: false, 18 | }); 19 | await follower.ready(); 20 | const errors = []; 21 | const values = []; 22 | follower.on('error', err => { 23 | errors.push(err); 24 | }); 25 | follower.subscribe({ 26 | key: 'timeout:500', 27 | }, value => { 28 | values.push(value); 29 | }); 30 | follower.subscribe({ 31 | key: 'timeout:1500', 32 | }, value => { 33 | values.push(value); 34 | }); 35 | await sleep(2000); 36 | assert.deepStrictEqual(values, [ 37 | 'hello:500', 38 | 'hello:1500', 39 | ]); 40 | assert(errors.length === 1); 41 | assert(/subscribe timeout for/.test(errors[0])); 42 | await follower.close(); 43 | await leader.close(); 44 | }); 45 | 46 | it('should not timeout when already have data', async () => { 47 | const leader = new ApiClient({ 48 | singleMode: false, 49 | isLeader: true, 50 | }); 51 | const follower = new ApiClient({ 52 | clusterOptions: { 53 | subscribeTimeout: 1000, 54 | }, 55 | singleMode: false, 56 | isLeader: false, 57 | }); 58 | await follower.ready(); 59 | const errors = []; 60 | const values = []; 61 | follower.on('error', err => { 62 | errors.push(err); 63 | }); 64 | // ensure has data 65 | follower.subscribe({ 66 | key: 'timeout:500', 67 | }, value => { 68 | values.push(value); 69 | }); 70 | await sleep(1000); 71 | // second subscribe 72 | follower.subscribe({ 73 | key: 'timeout:600', 74 | }, value => { 75 | values.push(value); 76 | }); 77 | await sleep(1000); 78 | assert.deepStrictEqual(values, [ 79 | 'hello:500', 80 | 'hello:600', 81 | ]); 82 | assert(errors.length === 0); 83 | await follower.close(); 84 | await leader.close(); 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /test/cluster.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const coffee = require('coffee'); 5 | const pedding = require('pedding'); 6 | 7 | describe('test/cluster.test.js', () => { 8 | it('should subscibe & publish ok', commit => { 9 | const count = 4; 10 | const pub = coffee.fork(path.join(__dirname, 'supports/pub.js')); 11 | pub.end((err, meta) => { 12 | if (err) { 13 | commit(err); 14 | } 15 | console.log(meta.stdout); 16 | console.log('publish finish'); 17 | }); 18 | 19 | setTimeout(() => { 20 | const done = pedding(err => { 21 | console.log('all subscibe finish'); 22 | pub.proc.kill(); 23 | commit(err); 24 | }, count); 25 | 26 | for (let i = 0; i < count; ++i) { 27 | coffee.fork(path.join(__dirname, 'supports/sub.js'), [ true ]) 28 | .expect('stdout', /receive val/) 29 | .end(err => { 30 | console.log('subscribe finish'); 31 | done(err); 32 | }); 33 | } 34 | }, 1000); 35 | }); 36 | 37 | it('should subscibe & publish ok after leader die', done => { 38 | done = pedding(done, 2); 39 | const leader = coffee.fork(path.join(__dirname, 'supports/sub.js'), [ false ]); 40 | leader.end(); 41 | 42 | setTimeout(() => { 43 | const pub = coffee.fork(path.join(__dirname, 'supports/pub.js')); 44 | pub.end(); 45 | 46 | let received = false; 47 | const follower = coffee.fork(path.join(__dirname, 'supports/sub.js'), [ false ]); 48 | follower 49 | .expect('stdout', /receive val/) 50 | .end(done); 51 | 52 | setImmediate(() => { 53 | follower.proc.on('message', () => { 54 | if (received) { 55 | follower.proc.kill(); 56 | pub.proc.kill(); 57 | done(); 58 | } else { 59 | leader.proc.kill(); 60 | received = true; 61 | } 62 | }); 63 | }); 64 | }, 1000); 65 | }); 66 | 67 | it('should invoke with special arguments', done => { 68 | coffee.fork(path.join(__dirname, 'supports/invoke')) 69 | .expect('stdout', /success/) 70 | .end(done); 71 | }); 72 | 73 | it('should work on cluster module', () => { 74 | return coffee.fork(path.join(__dirname, 'supports/cluster_server.js')) 75 | // .debug(0) 76 | // make sure leader and follower exists 77 | .expect('stdout', /, leader: true/) 78 | .expect('stdout', /, leader: false/) 79 | .expect('stdout', /client get val: bar/) 80 | .expect('code', 0) 81 | .end(); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const co = require('co'); 4 | const is = require('is-type-of'); 5 | const stringify = require('json-stringify-safe'); 6 | 7 | const MAX_REQUEST_ID = Math.pow(2, 30); // avoid write big integer 8 | const empty = () => {}; 9 | 10 | let id = 0; 11 | 12 | function nextId() { 13 | id += 1; 14 | if (id >= MAX_REQUEST_ID) { 15 | id = 1; 16 | } 17 | return id; 18 | } 19 | 20 | /** 21 | * generate requestId 22 | * 23 | * @return {Number} requestId 24 | */ 25 | exports.nextId = nextId; 26 | 27 | // for unittest 28 | exports.setId = val => { 29 | id = val; 30 | }; 31 | 32 | /** 33 | * event delegate 34 | * 35 | * @param {EventEmitter} from - from object 36 | * @param {EventEmitter} to - to object 37 | * @return {void} 38 | */ 39 | exports.delegateEvents = (from, to) => { 40 | // ignore the sdk-base defaultErrorHandler 41 | // https://github.com/node-modules/sdk-base/blob/master/index.js#L131 42 | if (from.listeners('error').length <= 1) { 43 | from.on('error', empty); 44 | } 45 | 46 | from.emit = new Proxy(from.emit, { 47 | apply(target, thisArg, args) { 48 | target.apply(from, args); 49 | to.emit.apply(to, args); 50 | return thisArg; 51 | }, 52 | }); 53 | }; 54 | 55 | function formatKey(reg) { 56 | return stringify(reg); 57 | } 58 | 59 | /** 60 | * normalize object to string 61 | * 62 | * @param {Object} reg - reg object 63 | * @return {String} key 64 | */ 65 | exports.formatKey = formatKey; 66 | 67 | /** 68 | * call a function, support common function, generator function, or a function returning promise 69 | * 70 | * @param {Function} fn - common function, generator function, or a function returning promise 71 | * @param {Array} args - args as fn() paramaters 72 | * @return {*} data returned by fn 73 | */ 74 | exports.callFn = async function(fn, args) { 75 | args = args || []; 76 | if (!is.function(fn)) return; 77 | if (is.generatorFunction(fn)) { 78 | return await co(function* () { 79 | return yield fn(...args); 80 | }); 81 | } 82 | const r = fn(...args); 83 | if (is.promise(r)) { 84 | return await r; 85 | } 86 | return r; 87 | }; 88 | 89 | exports.findMethodName = (descriptors, type) => { 90 | for (const method of descriptors.keys()) { 91 | const descriptor = descriptors.get(method); 92 | if (descriptor.type === 'delegate' && descriptor.to === type) { 93 | return method; 94 | } 95 | } 96 | return null; 97 | }; 98 | 99 | exports.sleep = ms => { 100 | return new Promise(resolve => { 101 | setTimeout(resolve, ms); 102 | }); 103 | }; 104 | -------------------------------------------------------------------------------- /test/utils.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mm = require('mm'); 4 | const assert = require('assert'); 5 | const Base = require('sdk-base'); 6 | const utils = require('../lib/utils'); 7 | 8 | describe('test/utils.test.js', () => { 9 | 10 | it('should call nextId ok', () => { 11 | const id = utils.nextId(); 12 | assert(typeof id === 'number'); 13 | assert((id + 1) === utils.nextId()); 14 | utils.setId(Math.pow(2, 30)); 15 | assert(utils.nextId() === 1); 16 | }); 17 | 18 | it('should callFn ok', async function() { 19 | await utils.callFn(null); 20 | const ret = await utils.callFn(function* (a, b) { 21 | return a + b; 22 | }, [ 1, 2 ]); 23 | assert(ret === 3); 24 | await utils.callFn(function(a, b) { 25 | return Promise.resolve(a + b); 26 | }, [ 1, 2 ]); 27 | assert(ret === 3); 28 | await utils.callFn(function(a, b) { 29 | return a + b; 30 | }, [ 1, 2 ]); 31 | assert(ret === 3); 32 | }); 33 | 34 | it('should delegateEvents ok', done => { 35 | const from = new Base(); 36 | const to = new Base(); 37 | 38 | utils.delegateEvents(from, to); 39 | 40 | to.once('foo', val => { 41 | assert(val === 'bar'); 42 | done(); 43 | }); 44 | from.emit('foo', 'bar'); 45 | }); 46 | 47 | it('should support nesting delegate', done => { 48 | const obj1 = new Base(); 49 | const obj2 = new Base(); 50 | const obj3 = new Base(); 51 | const obj4 = new Base(); 52 | 53 | utils.delegateEvents(obj1, obj2); 54 | utils.delegateEvents(obj2, obj3); 55 | utils.delegateEvents(obj3, obj4); 56 | 57 | let triggered = false; 58 | obj4.on('foo', val => { 59 | if (triggered) { 60 | done(new Error('should not triggered multi-times')); 61 | } else { 62 | assert(val === 'bar'); 63 | triggered = true; 64 | setImmediate(done); 65 | } 66 | }); 67 | obj1.emit('foo', 'bar'); 68 | }); 69 | 70 | it('should check duplcate error handler', done => { 71 | mm(console, 'error', () => { 72 | done(new Error('should not run here')); 73 | }); 74 | 75 | const obj1 = new Base(); 76 | const obj2 = new Base(); 77 | const obj3 = new Base(); 78 | const obj4 = new Base(); 79 | 80 | utils.delegateEvents(obj1, obj2); 81 | utils.delegateEvents(obj2, obj3); 82 | utils.delegateEvents(obj3, obj4); 83 | 84 | let triggered = false; 85 | obj4.on('error', err => { 86 | if (triggered) { 87 | done(new Error('should not triggered multi-times')); 88 | } else { 89 | assert(err && err.message === 'mock error'); 90 | triggered = true; 91 | setImmediate(done); 92 | } 93 | }); 94 | obj1.emit('error', new Error('mock error')); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /test/lazy.test.js: -------------------------------------------------------------------------------- 1 | const net = require('net'); 2 | const mm = require('egg-mock'); 3 | const assert = require('assert'); 4 | const Base = require('sdk-base'); 5 | const originCluster = require('../'); 6 | const { sleep } = require('../lib/utils'); 7 | const APIClient = require('./supports/api_client'); 8 | 9 | describe('test/lazy.test.js', () => { 10 | let port; 11 | let isLeader = false; 12 | 13 | function cluster(clientClass, options) { 14 | options = options || {}; 15 | options.port = port; 16 | options.isLeader = isLeader; 17 | const client = originCluster(clientClass, options); 18 | return client; 19 | } 20 | 21 | before(done => { 22 | const server = net.createServer(); 23 | server.listen(0, () => { 24 | const address = server.address(); 25 | port = address.port; 26 | console.log('using port =>', port); 27 | server.close(); 28 | done(); 29 | }); 30 | }); 31 | 32 | afterEach(mm.restore); 33 | 34 | it('should support follower create before leader', function* () { 35 | const follower = new APIClient({ cluster }); 36 | yield sleep(3000); 37 | 38 | isLeader = true; 39 | const leader = new APIClient({ cluster }); 40 | 41 | yield Promise.all([ 42 | leader.ready(), 43 | follower.ready(), 44 | ]); 45 | 46 | const ret = yield follower.echo('hello'); 47 | assert(ret === 'hello'); 48 | 49 | yield Promise.all([ 50 | follower.close(), 51 | leader.close(), 52 | ]); 53 | }); 54 | 55 | it('should follower ready failed if leader is failed', function* () { 56 | class ErrorClient extends Base { 57 | constructor() { 58 | super({ initMethod: '_init' }); 59 | } 60 | 61 | * _init() { 62 | throw new Error('init failed'); 63 | } 64 | } 65 | 66 | class APIErrorClient extends APIClient { 67 | get DataClient() { 68 | return ErrorClient; 69 | } 70 | 71 | get clusterOptions() { 72 | return { 73 | name: 'error_client_test', 74 | }; 75 | } 76 | } 77 | 78 | isLeader = false; 79 | const follower = new APIErrorClient({ cluster }); 80 | yield sleep(3000); 81 | 82 | isLeader = true; 83 | const leader = new APIErrorClient({ cluster }); 84 | 85 | try { 86 | yield follower.ready(); 87 | assert(false, 'should not run here'); 88 | } catch (err) { 89 | assert(err.message === 'init failed'); 90 | } 91 | 92 | try { 93 | yield leader.ready(); 94 | assert(false, 'should not run here'); 95 | } catch (err) { 96 | assert(err.message === 'init failed'); 97 | } 98 | 99 | yield Promise.all([ 100 | follower.close(), 101 | leader.close(), 102 | ]); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /test/edge_case.test.js: -------------------------------------------------------------------------------- 1 | const mm = require('mm'); 2 | const assert = require('assert'); 3 | const { detectPort: detect } = require('detect-port'); 4 | const { sleep } = require('../lib/utils'); 5 | const ApiClient = require('./supports/case_1/api_client'); 6 | 7 | describe('test/edge_case.test.js', () => { 8 | afterEach(mm.restore); 9 | 10 | [ 11 | 'single', 12 | 'cluster', 13 | ].forEach(scene => { 14 | describe(scene, () => { 15 | beforeEach(async () => { 16 | mm(process.env, 'NODE_CLUSTER_CLIENT_PORT', await detect()); 17 | mm(process.env, 'NODE_CLUSTER_CLIENT_SINGLE_MODE', scene === 'single' ? '1' : '0'); 18 | }); 19 | 20 | it('should not side effect', async () => { 21 | const client1 = new ApiClient(); 22 | await client1.ready(); 23 | 24 | assert.deepEqual(client1.foo, { bar: 'bar', xxx: 'yyy' }); 25 | 26 | const client2 = new ApiClient(); 27 | await client2.ready(); 28 | 29 | assert.deepEqual(client2.foo, { bar: 'bar', xxx: 'yyy' }); 30 | 31 | await client1.close(); 32 | await client2.close(); 33 | }); 34 | 35 | it('should trigger event ok', async () => { 36 | const client1 = new ApiClient(); 37 | await client1.ready(); 38 | 39 | assert.deepEqual(client1.foo, { bar: 'bar', xxx: 'yyy' }); 40 | 41 | mm(ApiClient.prototype, '_setFoo', () => { 42 | throw new Error('mock error'); 43 | }); 44 | 45 | const errors = []; 46 | const client2 = new ApiClient(); 47 | client2.on('error', err => { 48 | errors.push(err); 49 | }); 50 | await sleep(100); 51 | 52 | assert(errors.length === 1); 53 | assert(errors[0].message === 'mock error'); 54 | 55 | await client1.close(); 56 | await client2.close(); 57 | }); 58 | }); 59 | }); 60 | 61 | it('should delegate events', async () => { 62 | mm(process.env, 'NODE_CLUSTER_CLIENT_SINGLE_MODE', '1'); 63 | const client1 = new ApiClient(); 64 | await client1.ready(); 65 | 66 | client1.publish(null); 67 | try { 68 | await client1.await('error'); 69 | } catch (err) { 70 | assert(err.name === 'EmptyRegError'); 71 | } 72 | 73 | const client2 = new ApiClient(); 74 | await client2.ready(); 75 | 76 | client2.publish(null); 77 | try { 78 | await client2.await('error'); 79 | } catch (err) { 80 | assert(err.name === 'EmptyRegError'); 81 | } 82 | 83 | client1.publish(null); 84 | try { 85 | await client2.await('error'); 86 | } catch (err) { 87 | assert(err.name === 'EmptyRegError'); 88 | } 89 | 90 | client2.publish(null); 91 | try { 92 | await client1.await('error'); 93 | } catch (err) { 94 | assert(err.name === 'EmptyRegError'); 95 | } 96 | 97 | await client1.close(); 98 | await client2.close(); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /test/connection.test.js: -------------------------------------------------------------------------------- 1 | const net = require('net'); 2 | const assert = require('assert'); 3 | const awaitEvent = require('await-event'); 4 | const { sleep } = require('../lib/utils'); 5 | const Connection = require('../lib/connection'); 6 | const Request = require('../lib/protocol/request'); 7 | const transcode = require('../lib/default_transcode'); 8 | 9 | describe('test/connection.test.js', () => { 10 | let port; 11 | let server; 12 | const conns = new Map(); 13 | before(done => { 14 | server = net.createServer(socket => { 15 | const conn = new Connection({ 16 | socket, 17 | transcode, 18 | name: 'test', 19 | logger: console, 20 | requestTimeout: 1000, 21 | }); 22 | console.log('new connection', conn.key); 23 | conns.set(conn.key, conn); 24 | conn.once('close', () => { 25 | conns.delete(conn.key); 26 | }); 27 | }); 28 | server.listen(0, () => { 29 | port = server.address().port; 30 | console.log('server listen on %s', port); 31 | done(); 32 | }); 33 | }); 34 | after(done => { 35 | server.close(); 36 | for (const conn of conns.values()) { 37 | conn.close(); 38 | } 39 | server.once('close', done); 40 | }); 41 | 42 | it('should throw error if send timeout', async function() { 43 | const socket = net.connect(port, '127.0.0.1'); 44 | await awaitEvent(socket, 'connect'); 45 | await sleep(100); 46 | assert(conns.has(socket.localPort)); 47 | 48 | const conn = conns.get(socket.localPort); 49 | try { 50 | await new Promise((resolve, reject) => { 51 | conn.send(new Request({ 52 | connObj: { foo: 'bar' }, 53 | timeout: 1000, 54 | }), err => { 55 | if (err) { reject(err); } else { resolve(); } 56 | }); 57 | }); 58 | assert(false, 'no here'); 59 | } catch (err) { 60 | assert(err && err.name === 'ClusterConnectionResponseTimeoutError'); 61 | assert(err.message === `[ClusterClient] no response in 1000ms, remotePort#${socket.localPort}`); 62 | } 63 | socket.destroy(); 64 | await awaitEvent(socket, 'close'); 65 | await sleep(100); 66 | assert(!conns.has(socket.localPort)); 67 | }); 68 | 69 | it('should handle request ok', async function() { 70 | const socket = net.connect(port, '127.0.0.1'); 71 | await awaitEvent(socket, 'connect'); 72 | await sleep(100); 73 | assert(conns.has(socket.localPort)); 74 | 75 | const conn = conns.get(socket.localPort); 76 | 77 | socket.write(new Request({ 78 | connObj: { foo: 'bar' }, 79 | timeout: 1000, 80 | }).encode()); 81 | 82 | const req = await conn.await('request'); 83 | assert(req && !req.isResponse); 84 | assert(req.timeout === 1000); 85 | assert.deepEqual(req.connObj, { foo: 'bar' }); 86 | assert(!req.data); 87 | 88 | await Promise.all([ 89 | conn.close(), 90 | conn.close(), // close second time 91 | awaitEvent(socket, 'close'), 92 | ]); 93 | await sleep(100); 94 | assert(!conns.has(socket.localPort)); 95 | }); 96 | 97 | it('should close connection if decode error', async function() { 98 | const socket = net.connect(port, '127.0.0.1'); 99 | await awaitEvent(socket, 'connect'); 100 | await sleep(100); 101 | assert(conns.has(socket.localPort)); 102 | 103 | socket.write(Buffer.from('010000000000000000000001000003e80000000d000000007b22666f6f223a22626172227c', 'hex')); 104 | 105 | await awaitEvent(socket, 'close'); 106 | await sleep(100); 107 | assert(!conns.has(socket.localPort)); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /test/supports/cluster_server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const cluster = require('cluster'); 4 | const http = require('http'); 5 | const net = require('net'); 6 | // let numCPUs = require('os').cpus().length; 7 | const APIClientBase = require('../..').APIClientBase; 8 | 9 | const numCPUs = 2; 10 | 11 | function startServer(port) { 12 | class TestClient extends APIClientBase { 13 | get DataClient() { 14 | return require('./client'); 15 | } 16 | 17 | get delegates() { 18 | return { 19 | unPublish: 'invokeOneway', 20 | }; 21 | } 22 | 23 | get clusterOptions() { 24 | return { 25 | port, 26 | responseTimeout: 10000, 27 | name: `cluster-server-test-${process.version}`, 28 | }; 29 | } 30 | 31 | subscribe(...args) { 32 | return this._client.subscribe(...args); 33 | } 34 | 35 | unSubscribe(...args) { 36 | return this._client.unSubscribe(...args); 37 | } 38 | 39 | publish(...args) { 40 | return this._client.publish(...args); 41 | } 42 | 43 | unPublish(...args) { 44 | return this._client.unPublish(...args); 45 | } 46 | 47 | close() { 48 | return this._client.close(); 49 | } 50 | } 51 | 52 | if (cluster.isMaster) { 53 | console.log(`Master ${process.pid} is running`); 54 | const workerSet = new Set(); 55 | 56 | // Fork workers. 57 | for (let i = 0; i < numCPUs; i++) { 58 | cluster.fork({ 59 | CLUSTER_PORT: port, 60 | }); 61 | } 62 | 63 | cluster.on('exit', (worker, code, signal) => { 64 | console.log(`worker ${worker.process.pid} died, code: ${code}, signal: ${signal}`); 65 | }); 66 | cluster.on('message', worker => { 67 | workerSet.add(worker.id); 68 | if (workerSet.size === numCPUs) { 69 | process.exit(0); 70 | } 71 | }); 72 | // setTimeout(() => { 73 | // process.exit(0); 74 | // }, 10000); 75 | } else { 76 | const client = new TestClient(); 77 | console.log('NODE_CLUSTER_CLIENT_SINGLE_MODE =>', process.env.NODE_CLUSTER_CLIENT_SINGLE_MODE); 78 | client.ready(err => { 79 | if (err) { 80 | console.log(`Worker ${process.pid} client ready failed, leader: ${client.isClusterClientLeader}, errMsg: ${err.message}`); 81 | } else { 82 | console.log(`Worker ${process.pid} client ready, leader: ${client.isClusterClientLeader}`); 83 | } 84 | }); 85 | let latestVal; 86 | client.subscribe({ key: 'foo' }, val => { 87 | latestVal = val; 88 | console.log(`Worker ${process.pid} client get val: ${val}, leader: ${client.isClusterClientLeader}`); 89 | }); 90 | 91 | setInterval(() => { 92 | client.publish({ key: 'foo', value: 'bar ' + Date() }); 93 | }, 200); 94 | 95 | setTimeout(() => { 96 | process.send(cluster.worker.id); 97 | }, 5000); 98 | 99 | // Workers can share any TCP connection 100 | // In this case it is an HTTP server 101 | http.createServer((req, res) => { 102 | res.writeHead(200); 103 | res.end(`hello cluster client, data: ${latestVal}`); 104 | }).listen(port + 1); 105 | console.log(`Worker ${process.pid} started, listen at ${port + 1}`); 106 | } 107 | } 108 | 109 | const server = net.createServer(); 110 | if (cluster.isMaster) { 111 | server.listen(0, () => { 112 | const address = server.address(); 113 | console.log('using port =>', address.port); 114 | server.close(() => { 115 | startServer(address.port); 116 | }); 117 | }); 118 | } else { 119 | console.log('child process using port =>', process.env.CLUSTER_PORT); 120 | startServer(+process.env.CLUSTER_PORT); 121 | } 122 | -------------------------------------------------------------------------------- /lib/protocol/packet.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Constant = require('../const'); 4 | const byteBuffer = require('./byte_buffer'); 5 | const Long = require('long'); 6 | 7 | /** 8 | * 0 1 2 4 12 9 | * +---------+---------+-------------------+-------------------------------------------------------------------------------+ 10 | * | version | req/res | reserved | request id | 11 | * +---------------------------------------+---------------------------------------+---------------------------------------+ 12 | * | timeout | connection object length | application object length | 13 | * +---------------------------------------+-------------------+-------------------+---------------------------------------+ 14 | * | conn object (JSON format) ... | app object | 15 | * +-----------------------------------------------------------+ | 16 | * | ... | 17 | * +-----------------------------------------------------------------------------------------------------------------------+ 18 | * 19 | * packet protocol: 20 | * (1B): protocol version 21 | * (1B): req/res 22 | * (2B): reserved 23 | * (8B): request id 24 | * (4B): timeout 25 | * (4B): connection object length 26 | * (4B): application object length 27 | * -------------------------------- 28 | * conn object (JSON format) 29 | * -------------------------------- 30 | * app object 31 | */ 32 | class Packet { 33 | /** 34 | * cluster protocol packet 35 | * 36 | * @param {Object} options 37 | * - @param {Number} id - The identifier 38 | * - @param {Number} type - req/res 39 | * - @param {Number} timeout - The timeout 40 | * - @param {Object} connObj - connection object 41 | * - @param {Buffer} data - app data 42 | * @class 43 | */ 44 | constructor(options) { 45 | this.id = options.id; 46 | this.type = options.type; 47 | this.timeout = options.timeout; 48 | this.connObj = options.connObj; 49 | this.data = typeof options.data === 'string' ? Buffer.from(options.data) : options.data; 50 | } 51 | 52 | get isResponse() { 53 | return this.type === Constant.RESPONSE; 54 | } 55 | 56 | encode() { 57 | const header = Buffer.from([ Constant.VERSION, this.type, 0, 0 ]); 58 | const connBuf = Buffer.from(JSON.stringify(this.connObj)); 59 | const appLen = this.data ? this.data.length : 0; 60 | 61 | byteBuffer.reset(); 62 | byteBuffer.put(header); 63 | byteBuffer.putLong(this.id); 64 | byteBuffer.putInt(this.timeout); 65 | byteBuffer.putInt(connBuf.length); 66 | byteBuffer.putInt(appLen); 67 | byteBuffer.put(connBuf); 68 | if (appLen) { 69 | byteBuffer.put(this.data); 70 | } 71 | return byteBuffer.array(); 72 | } 73 | 74 | static decode(buf) { 75 | const isResponse = buf[1] === Constant.RESPONSE; 76 | const id = new Long( 77 | buf.readInt32BE(8), // low, high 78 | buf.readInt32BE(4) 79 | ).toNumber(); 80 | const timeout = buf.readInt32BE(12); 81 | const connLength = buf.readInt32BE(16); 82 | const appLength = buf.readInt32BE(20); 83 | 84 | const connBuf = Buffer.alloc(connLength); 85 | buf.copy(connBuf, 0, 24, 24 + connLength); 86 | const connObj = JSON.parse(connBuf); 87 | 88 | let data; 89 | if (appLength) { 90 | data = Buffer.alloc(appLength); 91 | buf.copy(data, 0, 24 + connLength, 24 + connLength + appLength); 92 | } 93 | return { 94 | id, 95 | isResponse, 96 | timeout, 97 | connObj, 98 | data, 99 | }; 100 | } 101 | } 102 | 103 | module.exports = Packet; 104 | -------------------------------------------------------------------------------- /lib/connection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const is = require('is-type-of'); 4 | const Base = require('sdk-base'); 5 | const Packet = require('./protocol/packet'); 6 | const Response = require('./protocol/response'); 7 | 8 | class Connection extends Base { 9 | /** 10 | * Socket Connection among Leader and Follower 11 | * 12 | * @param {Object} options 13 | * - {Socket} socket - the socket instance 14 | * - {Number} responseTimeout - the response timeout 15 | * - {Transcode} transcode - serialze / deserialze methods 16 | * @class 17 | */ 18 | constructor(options) { 19 | super(options); 20 | this._socket = options.socket; 21 | this._invokes = new Map(); 22 | this.key = this._socket.remotePort; 23 | this._lastActiveTime = Date.now(); 24 | this._transcode = options.transcode; 25 | this._lastError = null; 26 | 27 | // listen socket events 28 | this._socket.on('readable', () => { this._handleReadable(); }); 29 | this._socket.on('error', err => { this._handleSocketError(err); }); 30 | this._socket.on('close', () => { this._handleClose(); }); 31 | 32 | // try read data from buffer at first 33 | this._handleReadable(); 34 | } 35 | 36 | get isOk() { 37 | return this._socket && this._socket.writable; 38 | } 39 | 40 | get logger() { 41 | return this.options.logger; 42 | } 43 | 44 | get lastActiveTime() { 45 | return this._lastActiveTime; 46 | } 47 | 48 | set lastActiveTime(val) { 49 | this._lastActiveTime = val; 50 | } 51 | 52 | /** 53 | * send packet 54 | * 55 | * @param {Packet} packet - the packet 56 | * @param {Function} [callback] - callback function 57 | * @return {void} 58 | */ 59 | send(packet, callback) { 60 | this._write(packet.encode()); 61 | if (!packet.isResponse) { 62 | const id = packet.id; 63 | const timeout = packet.timeout; 64 | this._invokes.set(id, { 65 | id, 66 | timer: setTimeout(() => { 67 | const err = new Error(`[ClusterClient] no response in ${timeout}ms, remotePort#${this.key}`); 68 | err.name = 'ClusterConnectionResponseTimeoutError'; 69 | callback(err, timeout); 70 | this._invokes.delete(id); 71 | }, timeout), 72 | callback, 73 | }); 74 | } 75 | } 76 | 77 | close(err) { 78 | if (!this._socket) { 79 | return Promise.resolve(); 80 | } 81 | this._socket.destroy(err); 82 | return this.await('close'); 83 | } 84 | 85 | _handleReadable() { 86 | try { 87 | let remaining = false; 88 | do { 89 | remaining = this._readPacket(); 90 | } 91 | while (remaining); 92 | } catch (err) { 93 | this.close(err); 94 | } 95 | } 96 | 97 | _handleSocketError(err) { 98 | this._lastError = err; 99 | if (err.code === 'ECONNRESET') { 100 | this.logger.warn('[ClusterClient:Connection] socket is closed by other side while there were still unhandled data in the socket buffer'); 101 | } else { 102 | this.emit('error', err); 103 | } 104 | } 105 | 106 | _handleClose() { 107 | this._cleanInvokes(this._lastError); 108 | this.emit('close'); 109 | } 110 | 111 | _cleanInvokes(err) { 112 | if (!err) { 113 | err = new Error('The socket was closed.'); 114 | err.name = 'ClusterSocketCloseError'; 115 | } 116 | for (const req of this._invokes.values()) { 117 | clearTimeout(req.timer); 118 | req.callback(err); 119 | } 120 | this._invokes.clear(); 121 | } 122 | 123 | _read(n) { 124 | return this._socket.read(n); 125 | } 126 | 127 | _write(bytes) { 128 | if (!this.isOk) { 129 | return false; 130 | } 131 | return this._socket.write(bytes); 132 | } 133 | 134 | _getHeader() { 135 | return this._read(24); 136 | } 137 | 138 | _getBodyLength(header) { 139 | return header.readInt32BE(16) + header.readInt32BE(20); 140 | } 141 | 142 | _readPacket() { 143 | if (is.nullOrUndefined(this._bodyLength)) { 144 | this._header = this._getHeader(); 145 | if (!this._header) { 146 | return false; 147 | } 148 | this._bodyLength = this._getBodyLength(this._header); 149 | } 150 | 151 | let body; 152 | // body may be emtry 153 | if (this._bodyLength > 0) { 154 | body = this._read(this._bodyLength); 155 | if (!body) { 156 | return false; 157 | } 158 | } 159 | this._bodyLength = null; 160 | const packet = Packet.decode(Buffer.concat([ this._header, body ])); 161 | const id = packet.id; 162 | 163 | if (packet.isResponse) { 164 | const info = this._invokes.get(id); 165 | if (info) { 166 | clearTimeout(info.timer); 167 | info.callback(null, packet.data); 168 | this._invokes.delete(id); 169 | } 170 | } else { 171 | process.nextTick(() => this.emit('request', packet, new Response({ 172 | id, 173 | timeout: packet.timeout, 174 | }))); 175 | } 176 | return true; 177 | } 178 | } 179 | 180 | module.exports = Connection; 181 | -------------------------------------------------------------------------------- /lib/wrapper/single.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const co = require('co'); 4 | const Base = require('./base'); 5 | const is = require('is-type-of'); 6 | const utils = require('../utils'); 7 | const SdkBase = require('sdk-base'); 8 | const random = require('utility').random; 9 | 10 | // Symbol 11 | const { 12 | logger, 13 | createClient, 14 | singleMode, 15 | } = require('../symbol'); 16 | const _instances = new Map(); 17 | 18 | class InnerClient extends SdkBase { 19 | constructor(options = {}) { 20 | super(options); 21 | 22 | this._subData = new Map(); // 23 | this._subSet = new Set(); 24 | this._subListeners = new Map(); // > 25 | this._transcode = options.transcode; 26 | this._realClient = options.createRealClient(); 27 | this._closeMethodName = utils.findMethodName(options.descriptors, 'close'); 28 | this._subscribeMethodName = utils.findMethodName(options.descriptors, 'subscribe'); 29 | this._publishMethodName = utils.findMethodName(options.descriptors, 'publish'); 30 | this._isReady = false; 31 | this._closeByUser = false; 32 | this._refCount = 1; 33 | 34 | // event delegate 35 | utils.delegateEvents(this._realClient, this); 36 | 37 | if (is.function(this._realClient.ready)) { 38 | this._realClient.ready(err => { 39 | if (err) { 40 | this.ready(err); 41 | } else { 42 | this._isReady = true; 43 | this.ready(true); 44 | } 45 | }); 46 | } else { 47 | this._isReady = true; 48 | this.ready(true); 49 | } 50 | } 51 | 52 | ref() { 53 | this._refCount++; 54 | } 55 | 56 | get isLeader() { 57 | return true; 58 | } 59 | 60 | formatKey(reg) { 61 | return '$$inner$$__' + this.options.formatKey(reg); 62 | } 63 | 64 | subscribe(reg, listener) { 65 | const key = this.formatKey(reg); 66 | const transcode = this._transcode; 67 | const isBroadcast = this.options.isBroadcast; 68 | 69 | const listeners = this._subListeners.get(key) || []; 70 | listeners.push(listener); 71 | this._subListeners.set(key, listeners); 72 | this.on(key, listener); 73 | 74 | if (!this._subSet.has(key)) { 75 | this._subSet.add(key); 76 | this._realClient[this._subscribeMethodName](reg, result => { 77 | const data = transcode.encode(result); 78 | this._subData.set(key, data); 79 | 80 | let fns = this._subListeners.get(key); 81 | if (!fns) { 82 | return; 83 | } 84 | 85 | const len = fns.length; 86 | // if isBroadcast equal to false, random pick one to notify 87 | if (!isBroadcast) { 88 | fns = [ fns[random(len)] ]; 89 | } 90 | 91 | for (const fn of fns) { 92 | fn(transcode.decode(data)); 93 | } 94 | }); 95 | } else if (this._subData.has(key) && isBroadcast) { 96 | process.nextTick(() => { 97 | const data = this._subData.get(key); 98 | listener(transcode.decode(data)); 99 | }); 100 | } 101 | } 102 | 103 | unSubscribe(reg, listener) { 104 | const key = this.formatKey(reg); 105 | 106 | if (!listener) { 107 | this._subListeners.delete(key); 108 | } else { 109 | const listeners = this._subListeners.get(key) || []; 110 | const newListeners = []; 111 | 112 | for (const fn of listeners) { 113 | if (fn === listener) { 114 | continue; 115 | } 116 | newListeners.push(fn); 117 | } 118 | this._subListeners.set(key, newListeners); 119 | } 120 | } 121 | 122 | publish(reg) { 123 | this._realClient[this._publishMethodName](reg); 124 | } 125 | 126 | invoke(methodName, args, callback) { 127 | let method = this._realClient[methodName]; 128 | // compatible with generatorFunction 129 | if (is.generatorFunction(method)) { 130 | method = co.wrap(method); 131 | } 132 | args.push(callback); 133 | const ret = method.apply(this._realClient, args); 134 | if (callback && is.promise(ret)) { 135 | ret.then(result => callback(null, result), err => callback(err)) 136 | // to avoid uncaught exception in callback function, then cause unhandledRejection 137 | .catch(err => { this._errorHandler(err); }); 138 | } 139 | } 140 | 141 | // emit error asynchronously 142 | _errorHandler(err) { 143 | setImmediate(() => { 144 | if (!this._closeByUser) { 145 | this.emit('error', err); 146 | } 147 | }); 148 | } 149 | 150 | async close() { 151 | if (this._refCount > 0) { 152 | this._refCount--; 153 | } 154 | if (this._refCount > 0) return; 155 | 156 | this._closeByUser = true; 157 | 158 | if (this._realClient) { 159 | if (this._closeMethodName) { 160 | // support common function, generatorFunction, and function returning a promise 161 | await utils.callFn(this._realClient[this._closeMethodName].bind(this._realClient)); 162 | } 163 | } 164 | this.emit('close'); 165 | } 166 | } 167 | 168 | 169 | class SingleClient extends Base { 170 | get [singleMode]() { 171 | return true; 172 | } 173 | 174 | async [createClient]() { 175 | const options = this.options; 176 | let client; 177 | if (_instances.has(options.name)) { 178 | client = _instances.get(options.name); 179 | client.ref(); 180 | return client; 181 | } 182 | client = new InnerClient(options); 183 | client.once('close', () => { 184 | _instances.delete(options.name); 185 | this[logger].info('[cluster#SingleClient] %s is closed.', options.name); 186 | }); 187 | _instances.set(options.name, client); 188 | return client; 189 | } 190 | } 191 | 192 | module.exports = SingleClient; 193 | -------------------------------------------------------------------------------- /lib/wrapper/base.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('util').debuglog('cluster-client:lib:wrapper:base'); 4 | const is = require('is-type-of'); 5 | const Base = require('sdk-base'); 6 | const assert = require('assert'); 7 | const utils = require('../utils'); 8 | // Symbols 9 | const { 10 | init, 11 | logger, 12 | isReady, 13 | innerClient, 14 | subscribe, 15 | unSubscribe, 16 | publish, 17 | invoke, 18 | subInfo, 19 | pubInfo, 20 | closeHandler, 21 | close, 22 | singleMode, 23 | createClient, 24 | } = require('../symbol'); 25 | 26 | class WrapperBase extends Base { 27 | /** 28 | * Share Connection among Multi-Process Mode 29 | * 30 | * @param {Object} options 31 | * - {Number} port - the port 32 | * - {Transcode} transcode - serialze / deseriaze methods 33 | * - {Boolean} isLeader - wehether is leader or follower 34 | * - {Number} maxWaitTime - leader startup max time (ONLY effective on isLeader is true) 35 | * - {Function} createRealClient - to create the real client instance 36 | * @class 37 | */ 38 | constructor(options) { 39 | super(options); 40 | debug('new WrapperBase({ port: %j, isLeader: %j })', options.port, options.isLeader); 41 | this[subInfo] = new Map(); 42 | this[pubInfo] = new Map(); 43 | this[init]().catch(err => { this.ready(err); }); 44 | } 45 | 46 | get isClusterClientLeader() { 47 | return this[innerClient] && this[innerClient].isLeader; 48 | } 49 | 50 | get [singleMode]() { 51 | return false; 52 | } 53 | 54 | /** 55 | * log instance 56 | * @property {Logger} ClusterClient#[logger] 57 | */ 58 | get [logger]() { 59 | return this.options.logger; 60 | } 61 | 62 | async [createClient]() { 63 | throw new Error('not implement'); 64 | } 65 | 66 | /** 67 | * initialize, to leader or follower 68 | * 69 | * @return {void} 70 | */ 71 | async [init]() { 72 | this[innerClient] = await this[createClient](); 73 | 74 | // events delegate 75 | utils.delegateEvents(this[innerClient], this); 76 | 77 | // re init when connection is close 78 | if (this[closeHandler]) { 79 | this[innerClient].on('close', this[closeHandler]); 80 | } 81 | 82 | // wait leader/follower ready 83 | await this[innerClient].ready(); 84 | 85 | // subscribe all 86 | for (const registrations of this[subInfo].values()) { 87 | for (const args of registrations) { 88 | this[innerClient].subscribe(args[0], args[1]); 89 | } 90 | } 91 | // publish all 92 | for (const reg of this[pubInfo].values()) { 93 | this[innerClient].publish(reg); 94 | } 95 | 96 | if (!this[isReady]) { 97 | this[isReady] = true; 98 | this.ready(true); 99 | } 100 | } 101 | 102 | /** 103 | * do subscribe 104 | * 105 | * @param {Object} reg - subscription info 106 | * @param {Function} listener - callback function 107 | * @return {void} 108 | */ 109 | [subscribe](reg, listener) { 110 | assert(is.function(listener), `[ClusterClient:${this.options.name}] subscribe(reg, listener) listener should be a function`); 111 | 112 | debug('[ClusterClient:%s] subscribe %j', this.options.name, reg); 113 | const key = this.options.formatKey(reg); 114 | const registrations = this[subInfo].get(key) || []; 115 | registrations.push([ reg, listener ]); 116 | this[subInfo].set(key, registrations); 117 | 118 | if (this[isReady]) { 119 | this[innerClient].subscribe(reg, listener); 120 | } 121 | } 122 | 123 | /** 124 | * do unSubscribe 125 | * 126 | * @param {Object} reg - subscription info 127 | * @param {Function} listener - callback function 128 | * @return {void} 129 | */ 130 | [unSubscribe](reg, listener) { 131 | debug('[ClusterClient:%s] unSubscribe %j', this.options.name, reg); 132 | const key = this.options.formatKey(reg); 133 | const registrations = this[subInfo].get(key) || []; 134 | const newRegistrations = []; 135 | if (listener) { 136 | for (const arr of registrations) { 137 | if (arr[1] !== listener) { 138 | newRegistrations.push(arr); 139 | } 140 | } 141 | } 142 | this[subInfo].set(key, newRegistrations); 143 | 144 | if (this[isReady]) { 145 | this[innerClient].unSubscribe(reg, listener); 146 | } 147 | } 148 | 149 | /** 150 | * do publish 151 | * 152 | * @param {Object} reg - publish info 153 | * @return {void} 154 | */ 155 | [publish](reg) { 156 | debug('[ClusterClient:%s] publish %j', this.options.name, reg); 157 | const key = this.options.formatKey(reg); 158 | this[pubInfo].set(key, reg); 159 | 160 | if (this[isReady]) { 161 | this[innerClient].publish(reg); 162 | } 163 | } 164 | 165 | /** 166 | * invoke a method asynchronously 167 | * 168 | * @param {String} method - the method name 169 | * @param {Array} args - the arguments list 170 | * @param {Function} callback - callback function 171 | * @return {void} 172 | */ 173 | [invoke](method, args, callback) { 174 | if (!this[isReady]) { 175 | this.ready(err => { 176 | if (err) { 177 | callback && callback(err); 178 | return; 179 | } 180 | this[innerClient].invoke(method, args, callback); 181 | }); 182 | return; 183 | } 184 | 185 | debug('[ClusterClient:%s] invoke method: %s, args: %j', this.options.name, method, args); 186 | this[innerClient].invoke(method, args, callback); 187 | } 188 | 189 | async [close]() { 190 | try { 191 | // close after ready, in case of innerClient is initializing 192 | await this.ready(); 193 | } catch (err) { 194 | // ignore 195 | } 196 | 197 | const client = this[innerClient]; 198 | if (client) { 199 | // prevent re-initializing 200 | if (this[closeHandler]) { 201 | client.removeListener('close', this[closeHandler]); 202 | } 203 | if (client.close) { 204 | await utils.callFn(client.close.bind(client)); 205 | } 206 | } 207 | } 208 | } 209 | 210 | module.exports = WrapperBase; 211 | -------------------------------------------------------------------------------- /test/supports/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const net = require('net'); 4 | const Base = require('sdk-base'); 5 | const Packet = require('../../lib/protocol/packet'); 6 | const Response = require('../../lib/protocol/response'); 7 | 8 | // share memory in current process 9 | let serverMap; 10 | if (global.serverMap) { 11 | serverMap = global.serverMap; 12 | } else { 13 | global.serverMap = serverMap = new Map(); 14 | } 15 | let typeSet; 16 | if (global.typeSet) { 17 | typeSet = global.typeSet; 18 | } else { 19 | global.typeSet = typeSet = new Set(); 20 | } 21 | 22 | const empty = () => {}; 23 | const sleep = timeout => cb => setTimeout(cb, timeout); 24 | 25 | function claimServer(port) { 26 | return cb => { 27 | const server = net.createServer(); 28 | server.listen(port); 29 | 30 | function onError(err) { 31 | if (err.code === 'EADDRINUSE') { 32 | server.removeAllListeners(); 33 | cb(err); 34 | } 35 | } 36 | 37 | server.on('error', onError); 38 | server.on('listening', () => { 39 | server.removeAllListeners(); 40 | cb(null, server); 41 | }); 42 | }; 43 | } 44 | 45 | function tryToConnect(port) { 46 | return cb => { 47 | const socket = net.connect(port, '127.0.0.1'); 48 | socket.on('connect', () => { 49 | cb(null, true); 50 | // disconnect 51 | socket.removeAllListeners(); 52 | socket.end(); 53 | }); 54 | // close event occurred after ECONNREFUSED error 55 | socket.on('error', empty); 56 | socket.on('close', () => { 57 | cb(null, false); 58 | socket.removeAllListeners(); 59 | }); 60 | }; 61 | } 62 | 63 | class ClusterServer extends Base { 64 | /** 65 | * Manage all TCP Connections,assign them to proper channel 66 | * 67 | * @class 68 | * @param {Object} options 69 | * - {net.Server} server - the server 70 | * - {Number} port - the port 71 | */ 72 | constructor(options) { 73 | super(); 74 | 75 | this._sockets = new Map(); 76 | this._server = options.server; 77 | this._port = options.port; 78 | this._isClosed = false; 79 | this._server.on('connection', socket => this._handleSocket(socket)); 80 | this._server.once('close', () => { 81 | this._isClosed = true; 82 | serverMap.delete(this._port); 83 | this.emit('close'); 84 | this._server.removeAllListeners(); 85 | this.removeAllListeners(); 86 | }); 87 | this._server.once('error', err => { 88 | this.emit('error', err); 89 | this.close(); 90 | }); 91 | } 92 | 93 | get isClosed() { 94 | return this._isClosed; 95 | } 96 | 97 | close() { 98 | return new Promise((resolve, reject) => { 99 | if (this.isClosed) return resolve(); 100 | 101 | this._server.close(err => { 102 | if (err) return reject(err); 103 | 104 | for (const socket of this._sockets.values()) { 105 | socket.destroy(); 106 | } 107 | resolve(); 108 | }); 109 | }); 110 | } 111 | 112 | _handleSocket(socket) { 113 | let header; 114 | let bodyLength; 115 | let body; 116 | const server = this; 117 | const key = socket.remotePort; 118 | this._sockets.set(key, socket); 119 | 120 | function onReadable() { 121 | if (!header) { 122 | header = socket.read(24); 123 | if (!header) { 124 | return; 125 | } 126 | } 127 | if (!bodyLength) { 128 | bodyLength = header.readInt32BE(16) + header.readInt32BE(20); 129 | } 130 | body = socket.read(bodyLength); 131 | if (!body) { 132 | return; 133 | } 134 | // first packet to register to channel 135 | const packet = Packet.decode(Buffer.concat([ header, body ])); 136 | if (packet.connObj && packet.connObj.type === 'register_channel') { 137 | const channelName = packet.connObj.channelName; 138 | 139 | socket.removeListener('readable', onReadable); 140 | 141 | const response = new Response({ 142 | id: packet.id, 143 | timeout: packet.timeout, 144 | connObj: { type: 'register_channel_res' }, 145 | }); 146 | socket.write(response.encode()); 147 | 148 | // assign to proper channel 149 | server.emit(`${channelName}_connection`, socket); 150 | } 151 | } 152 | 153 | socket.on('readable', onReadable); 154 | socket.once('close', () => this._sockets.delete(key)); 155 | } 156 | 157 | /** 158 | * Occupy the port 159 | * 160 | * @param {String} name - the client name 161 | * @param {Number} port - the port 162 | * @return {ClusterServer} server 163 | */ 164 | static* create(name, port) { 165 | const key = `${name}@${port}`; 166 | let instance = serverMap.get(port); 167 | if (instance && !instance.isClosed) { 168 | if (typeSet.has(key)) { 169 | return null; 170 | } 171 | typeSet.add(key); 172 | return instance; 173 | } 174 | // compete for the local port, if got => leader, otherwise follower 175 | try { 176 | const server = yield claimServer(port); 177 | instance = new ClusterServer({ server, port }); 178 | typeSet.add(key); 179 | serverMap.set(port, instance); 180 | return instance; 181 | } catch (err) { 182 | // if exception, that mean compete for port failed, then double check 183 | instance = serverMap.get(port); 184 | if (instance && !instance.isClosed) { 185 | if (typeSet.has(key)) { 186 | return null; 187 | } 188 | typeSet.add(key); 189 | return instance; 190 | } 191 | return null; 192 | } 193 | } 194 | 195 | static* close(name, server) { 196 | typeSet.delete(`${name}@${server._port}`); 197 | // TODO calculate close 198 | } 199 | 200 | /** 201 | * Wait for Leader Startup 202 | * 203 | * @param {Number} port - the port 204 | * @param {Number} timeout - the max wait time 205 | * @return {void} 206 | */ 207 | static* waitFor(port, timeout) { 208 | const start = Date.now(); 209 | let connect = false; 210 | while (!connect) { 211 | connect = yield tryToConnect(port); 212 | 213 | // if timeout, throw error 214 | if (Date.now() - start > timeout) { 215 | throw new Error(`[ClusterClient] leader does not be active in ${timeout}ms on port:${port}`); 216 | } 217 | if (!connect) { 218 | yield sleep(3000); 219 | } 220 | } 221 | } 222 | } 223 | 224 | module.exports = ClusterServer; 225 | -------------------------------------------------------------------------------- /test/supports/registry_client.js: -------------------------------------------------------------------------------- 1 | const dgram = require('dgram'); 2 | const { format: URLFormat } = require('url'); 3 | const Base = require('sdk-base'); 4 | 5 | const pid = process.pid; 6 | const localIp = require('address').ip(); 7 | 8 | class RegistryClient extends Base { 9 | constructor(multicastPort, multicastAddress) { 10 | super(); 11 | 12 | this.multicastPort = multicastPort || 1234; 13 | this.multicastAddress = multicastAddress || '224.5.6.7'; 14 | 15 | this._registered = new Map(); 16 | this._subscribed = new Map(); 17 | 18 | this._socket = dgram.createSocket({ 19 | reuseAddr: true, 20 | type: 'udp4', 21 | }); 22 | 23 | this._socket.on('error', err => this.emit('error', err)); 24 | this._socket.on('message', buf => { 25 | const msg = buf.toString(); 26 | 27 | if (msg.startsWith('register ')) { 28 | const url = msg.substring(9); 29 | const parsed = new URL(url); 30 | const key = parsed.searchParams.get('interface'); 31 | if (this._subscribed.has(key)) { 32 | const subData = this._subscribed.get(key); 33 | const category = parsed.searchParams.get('category') || 'providers'; 34 | // const enabled = parsed.query.enabled || true; 35 | 36 | if (subData.urlObj.query.category.split(',').indexOf(category) >= 0) { 37 | subData.value = subData.value || new Map(); 38 | if (!subData.value.has(parsed.host)) { 39 | subData.value.set(parsed.host, { host: parsed.host }); 40 | this.emit(key, Array.from(subData.value.values())); 41 | } 42 | } 43 | } 44 | } else if (msg.startsWith('unregister ')) { 45 | // TODO: 46 | } else if (msg.startsWith('subscribe ')) { 47 | const consumerUrl = msg.substring(10); 48 | const parsed = new URL(consumerUrl); 49 | const key = parsed.searchParams.get('interface'); 50 | 51 | if (this._registered.has(key)) { 52 | const urls = this._registered.get(key); 53 | 54 | for (const url of urls) { 55 | const obj = new URL(url); 56 | const category = obj.searchParams.get('category') || 'providers'; 57 | // const enabled = obj.query.enabled || true; 58 | if (parsed.searchParams.get('category').split(',').indexOf(category) >= 0) { 59 | this._broadcast(`register ${url}`); 60 | } 61 | } 62 | } 63 | } 64 | }); 65 | 66 | this._socket.bind(this.multicastPort, () => { 67 | this._socket.addMembership(this.multicastAddress); 68 | setTimeout(() => { 69 | this.ready(true); 70 | }, 500); 71 | }); 72 | 73 | this._inited = false; 74 | this.ready(() => { 75 | this._inited = true; 76 | }); 77 | } 78 | 79 | _broadcast(msg) { 80 | if (!this._inited) { 81 | this.ready(() => { 82 | this._broadcast(msg); 83 | }); 84 | return; 85 | } 86 | 87 | const buf = Buffer.from(msg); 88 | this._socket.send( 89 | buf, 90 | 0, 91 | buf.length, 92 | this.multicastPort, 93 | this.multicastAddress, 94 | err => { 95 | if (err) { 96 | this.emit('error', err); 97 | } 98 | } 99 | ); 100 | } 101 | 102 | /** 103 | * subscribe 104 | * 105 | * @param {Object} reg 106 | * - {String} dataId - the dataId 107 | * @param {Function} listener - the listener 108 | */ 109 | subscribe(reg, listener) { 110 | const key = reg.dataId; 111 | const subData = this._subscribed.get(key); 112 | this.on(key, listener); 113 | 114 | if (!subData) { 115 | const urlObj = { 116 | protocol: 'consumer:', 117 | slashes: true, 118 | auth: null, 119 | host: `${localIp}:20880`, 120 | port: '20880', 121 | hash: null, 122 | query: { 123 | application: 'demo-consumer', 124 | category: 'consumers', 125 | check: false, 126 | dubbo: '2.0.0', 127 | generic: 'false', 128 | interface: key, // 'com.alibaba.dubbo.demo.DemoService', 129 | loadbalance: 'roundrobin', 130 | methods: 'sayHello', 131 | pid, 132 | side: 'consumer', 133 | timestamp: Date.now(), 134 | }, 135 | pathname: `/${key}`, 136 | }; 137 | this._broadcast(`register ${URLFormat(urlObj)}`); 138 | 139 | urlObj.query = { 140 | application: 'demo-consumer', 141 | category: 'providers,configurators,routers', 142 | dubbo: '2.0.0', 143 | generic: 'false', 144 | interface: key, // 'com.alibaba.dubbo.demo.DemoService', 145 | loadbalance: 'roundrobin', 146 | methods: 'sayHello', 147 | pid, 148 | side: 'consumer', 149 | timestamp: Date.now(), 150 | }; 151 | this._broadcast(`subscribe ${URLFormat(urlObj)}`); 152 | 153 | this._subscribed.set(key, { 154 | urlObj, 155 | value: null, 156 | }); 157 | } else if (subData.value) { 158 | process.nextTick(() => listener(Array.from(subData.value.values()))); 159 | } 160 | } 161 | 162 | /** 163 | * publish 164 | * 165 | * @param {Object} reg 166 | * - {String} dataId - the dataId 167 | * - {String} publishData - the publish data 168 | */ 169 | publish(reg) { 170 | // register dubbo://30.20.78.300:20880/com.alibaba.dubbo.demo.DemoService?anyhost=true&application=demo-provider&dubbo=2.0.0&generic=false&interface=com.alibaba.dubbo.demo.DemoService&loadbalance=roundrobin&methods=sayHello&owner=william&pid=81281&side=provider×tamp=1481613276143 171 | this._broadcast(`register ${reg.publishData}`); 172 | const urlObject = new URL(reg.publishData); 173 | const key = urlObject.searchParams.get('interface'); 174 | 175 | if (this._registered.has(key)) { 176 | this._registered.get(key).push(reg.publishData); 177 | } else { 178 | this._registered.set(key, [ reg.publishData ]); 179 | } 180 | 181 | urlObject.protocol = 'provider:'; 182 | urlObject.search = null; 183 | urlObject.searchParams.set('category', 'configurators'); 184 | urlObject.searchParams.set('check', 'fase'); 185 | const providerUrl = urlObject.toString(); 186 | console.log(providerUrl); 187 | this._broadcast(`subscribe ${providerUrl}`); 188 | } 189 | 190 | close() { 191 | return new Promise(resolve => { 192 | setTimeout(() => { 193 | this.closed = true; 194 | resolve(); 195 | }, 100); 196 | }); 197 | } 198 | } 199 | 200 | module.exports = RegistryClient; 201 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const is = require('is-type-of'); 4 | const symbols = require('./symbol'); 5 | const logger = require('./default_logger'); 6 | const transcode = require('./default_transcode'); 7 | const SingleClient = require('./wrapper/single'); 8 | const ClusterClient = require('./wrapper/cluster'); 9 | const { formatKey } = require('./utils'); 10 | 11 | const defaultOptions = { 12 | port: parseInt(process.env.NODE_CLUSTER_CLIENT_PORT) || 7777, 13 | singleMode: process.env.NODE_CLUSTER_CLIENT_SINGLE_MODE === '1', 14 | maxWaitTime: 30000, 15 | connectTimeout: parseInt(process.env.NODE_CLUSTER_CLIENT_CONNECT_TIMEOUT) || 10000, 16 | responseTimeout: 3000, 17 | heartbeatInterval: 20000, 18 | isCheckHeartbeat: true, 19 | autoGenerate: true, 20 | isBroadcast: true, 21 | logger, 22 | transcode, 23 | formatKey, 24 | }; 25 | const autoGenerateMethods = [ 26 | 'subscribe', 27 | 'unSubscribe', 28 | 'publish', 29 | 'close', 30 | ]; 31 | 32 | class ClientGenerator { 33 | /** 34 | * Cluster Client Generator 35 | * 36 | * @param {Function} clientClass - the client class 37 | * @param {Object} options 38 | * - {Number} responseTimeout - response timeout, default is 3000 39 | * - {Boolean} autoGenerate - whether generate delegate rule automatically, default is true 40 | * - {Boolean} isBroadcast - whether broadcast subscrption result to all followers or just one, default is true 41 | * - {Logger} logger - log instance 42 | * - {Transcode} [transcode|JSON.stringify/parse] 43 | * - {Function} encode - custom serialize method 44 | * - {Function} decode - custom deserialize method 45 | * - {Boolean} [isLeader|null] - specify whether current instance is leader 46 | * - {Number} [maxWaitTime|30000] - leader startup max time (ONLY effective on isLeader is true) 47 | * @class 48 | */ 49 | constructor(clientClass, options) { 50 | this._clientClass = clientClass; 51 | this._options = Object.assign({ 52 | name: clientClass.prototype.constructor.name, 53 | }, defaultOptions, options); 54 | 55 | // wrapper descptions 56 | this._descriptors = new Map(); 57 | } 58 | 59 | /** 60 | * override the property 61 | * 62 | * @param {String} name - property name 63 | * @param {Object} value - property value 64 | * @return {ClientGenerator} self 65 | */ 66 | override(name, value) { 67 | this._descriptors.set(name, { 68 | type: 'override', 69 | value, 70 | }); 71 | return this; 72 | } 73 | 74 | /** 75 | * delegate methods 76 | * 77 | * @param {String} from - method name 78 | * @param {String} to - delegate to subscribe|publish|invoke 79 | * @return {ClientGenerator} self 80 | */ 81 | delegate(from, to) { 82 | to = to || 'invoke'; 83 | this._descriptors.set(from, { 84 | type: 'delegate', 85 | to, 86 | }); 87 | return this; 88 | } 89 | 90 | /** 91 | * create cluster client instance 92 | * 93 | * @param {...any} args arguments 94 | * @return {Object} instance 95 | */ 96 | create(...args) { 97 | const clientClass = this._clientClass; 98 | const proto = clientClass.prototype; 99 | const descriptors = this._descriptors; 100 | 101 | // auto generate description 102 | if (this._options.autoGenerate) { 103 | this._generateDescriptors(); 104 | } 105 | 106 | function createRealClient() { 107 | return Reflect.construct(clientClass, args); 108 | } 109 | 110 | const ClientWrapper = this._options.singleMode ? SingleClient : ClusterClient; 111 | const client = new ClientWrapper(Object.assign({ 112 | createRealClient, 113 | descriptors: this._descriptors, 114 | }, this._options)); 115 | 116 | for (const name of descriptors.keys()) { 117 | let value; 118 | const descriptor = descriptors.get(name); 119 | switch (descriptor.type) { 120 | case 'override': 121 | value = descriptor.value; 122 | break; 123 | case 'delegate': 124 | if (/^invoke|invokeOneway$/.test(descriptor.to)) { 125 | if (is.generatorFunction(proto[name])) { 126 | value = function* (...args) { 127 | return yield cb => { client[symbols.invoke](name, args, cb); }; 128 | }; 129 | } else if (is.function(proto[name])) { 130 | if (descriptor.to === 'invoke') { 131 | value = (...args) => { 132 | let cb; 133 | if (is.function(args[args.length - 1])) { 134 | cb = args.pop(); 135 | } 136 | // whether callback or promise 137 | if (cb) { 138 | client[symbols.invoke](name, args, cb); 139 | } else { 140 | return new Promise((resolve, reject) => { 141 | client[symbols.invoke](name, args, function(err) { 142 | if (err) { 143 | reject(err); 144 | } else { 145 | resolve.apply(null, Array.from(arguments).slice(1)); 146 | } 147 | }); 148 | }); 149 | } 150 | }; 151 | } else { 152 | value = (...args) => { 153 | client[symbols.invoke](name, args); 154 | }; 155 | } 156 | } else { 157 | throw new Error(`[ClusterClient] api: ${name} not implement in client`); 158 | } 159 | } else { 160 | value = client[Symbol.for(`ClusterClient#${descriptor.to}`)]; 161 | } 162 | break; 163 | default: 164 | break; 165 | } 166 | Object.defineProperty(client, name, { 167 | value, 168 | writable: true, 169 | enumerable: true, 170 | configurable: true, 171 | }); 172 | } 173 | 174 | return client; 175 | } 176 | 177 | _generateDescriptors() { 178 | const clientClass = this._clientClass; 179 | const proto = clientClass.prototype; 180 | 181 | const needGenerateMethods = new Set(autoGenerateMethods); 182 | for (const entry of this._descriptors.entries()) { 183 | const key = entry[0]; 184 | const value = entry[1]; 185 | if (needGenerateMethods.has(key) || 186 | (value.type === 'delegate' && needGenerateMethods.has(value.to))) { 187 | needGenerateMethods.delete(key); 188 | } 189 | } 190 | for (const method of needGenerateMethods.values()) { 191 | if (is.function(proto[method])) { 192 | this.delegate(method, method); 193 | } 194 | } 195 | 196 | const keys = Reflect.ownKeys(proto) 197 | .filter(key => typeof key !== 'symbol' && 198 | !key.startsWith('_') && 199 | !this._descriptors.has(key)); 200 | 201 | for (const key of keys) { 202 | const descriptor = Reflect.getOwnPropertyDescriptor(proto, key); 203 | if (descriptor.value && 204 | (is.generatorFunction(descriptor.value) || is.asyncFunction(descriptor.value))) { 205 | this.delegate(key); 206 | } 207 | } 208 | } 209 | } 210 | 211 | module.exports = function(clientClass, options) { 212 | return new ClientGenerator(clientClass, options); 213 | }; 214 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | const debug = require('util').debuglog('cluster-client:lib:server'); 2 | const net = require('net'); 3 | const Base = require('sdk-base'); 4 | const { sleep } = require('./utils'); 5 | const Packet = require('./protocol/packet'); 6 | 7 | // share memory in current process 8 | let serverMap; 9 | if (global.serverMap) { 10 | serverMap = global.serverMap; 11 | } else { 12 | global.serverMap = serverMap = new Map(); 13 | } 14 | let typeSet; 15 | if (global.typeSet) { 16 | typeSet = global.typeSet; 17 | } else { 18 | global.typeSet = typeSet = new Set(); 19 | } 20 | 21 | function claimServer(port) { 22 | return new Promise((resolve, reject) => { 23 | const server = net.createServer(); 24 | server.listen({ 25 | port, 26 | host: '127.0.0.1', 27 | // When exclusive is true, the handle is not shared, and attempted port sharing results in an error. 28 | exclusive: true, 29 | }); 30 | 31 | function onError(err) { 32 | debug('listen %s error: %s', port, err); 33 | reject(err); 34 | } 35 | 36 | server.on('error', onError); 37 | server.on('listening', () => { 38 | server.removeListener('error', onError); 39 | debug('listen %s success', port); 40 | resolve(server); 41 | }); 42 | }); 43 | } 44 | 45 | function tryToConnect(port) { 46 | return new Promise(resolve => { 47 | debug('try to connecting port:%j', port); 48 | const socket = net.connect(port, '127.0.0.1'); 49 | let success = false; 50 | socket.on('connect', () => { 51 | success = true; 52 | resolve(true); 53 | // disconnect 54 | socket.end(); 55 | debug('test connected %s success, end now', port); 56 | }); 57 | socket.on('error', err => { 58 | debug('test connect %s error: %s, success: %s', port, err, success); 59 | // if success before, ignore it 60 | if (success) return; 61 | resolve(false); 62 | }); 63 | }); 64 | } 65 | 66 | class ClusterServer extends Base { 67 | /** 68 | * Manage all TCP Connections,assign them to proper channel 69 | * 70 | * @class 71 | * @param {Object} options 72 | * @param {net.Server} options.server - the server 73 | * @param {Number} options.port - the port 74 | * @param {Number} [options.maxListeners] maximum of listeners, default is 20 75 | */ 76 | constructor(options) { 77 | super(); 78 | debug('new ClusterServer(%j)', options); 79 | this._sockets = new Map(); 80 | this._server = options.server; 81 | this._port = options.port; 82 | this._isClosed = false; 83 | this._server.on('connection', socket => this._handleSocket(socket)); 84 | this._server.once('close', () => { 85 | this._isClosed = true; 86 | serverMap.delete(this._port); 87 | this.emit('close'); 88 | }); 89 | this._server.once('error', err => { 90 | this.emit('error', err); 91 | }); 92 | this.setMaxListeners(options.maxListeners || 20); 93 | } 94 | 95 | get isClosed() { 96 | return this._isClosed; 97 | } 98 | 99 | close() { 100 | return new Promise((resolve, reject) => { 101 | if (this.isClosed) return resolve(); 102 | 103 | this._server.close(err => { 104 | if (err) return reject(err); 105 | resolve(); 106 | }); 107 | 108 | // sockets must be closed manually, otherwise server.close callback will never be called 109 | for (const socket of this._sockets.values()) { 110 | socket.destroy(); 111 | } 112 | }); 113 | } 114 | 115 | _handleSocket(socket) { 116 | let header; 117 | let bodyLength; 118 | let body; 119 | const server = this; 120 | const key = socket.remotePort; 121 | this._sockets.set(key, socket); 122 | 123 | function onReadable() { 124 | if (!header) { 125 | header = socket.read(24); 126 | if (!header) { 127 | return; 128 | } 129 | } 130 | if (!bodyLength) { 131 | bodyLength = header.readInt32BE(16) + header.readInt32BE(20); 132 | } 133 | body = socket.read(bodyLength); 134 | if (!body) { 135 | return; 136 | } 137 | // first packet to register to channel 138 | const packet = Packet.decode(Buffer.concat([ header, body ], 24 + bodyLength)); 139 | header = null; 140 | bodyLength = null; 141 | body = null; 142 | if (packet.connObj && packet.connObj.type === 'register_channel') { 143 | const channelName = packet.connObj.channelName; 144 | const eventKey = `${channelName}_connection`; 145 | 146 | // that means leader already there 147 | if (server.listenerCount(eventKey)) { 148 | socket.removeListener('readable', onReadable); 149 | // assign to proper channel 150 | debug('new %s_connection %s connected', channelName, socket.remotePort); 151 | server.emit(`${channelName}_connection`, socket, packet); 152 | } 153 | } 154 | } 155 | 156 | socket.on('readable', onReadable); 157 | socket.once('close', () => { 158 | debug('socket %s close', key); 159 | this._sockets.delete(key); 160 | }); 161 | debug('new socket %s from follower', socket.remotePort); 162 | } 163 | 164 | /** 165 | * Occupy the port 166 | * 167 | * @param {String} name - the client name 168 | * @param {Number} port - the port 169 | * @param {Object} [options] - create instance options 170 | * @param {Number} [options.maxListeners] maximum of listeners, default is 20 171 | * @return {ClusterServer} server 172 | */ 173 | static async create(name, port, options) { 174 | const key = `${name}@${port}`; 175 | if (typeof port !== 'number') { 176 | throw new Error(`[ClusterClient.server.create:${name}] port should be a number, but got ${JSON.stringify(port)}`); 177 | } 178 | let instance = serverMap.get(port); 179 | if (instance && !instance.isClosed) { 180 | debug('create %j instance exists', key); 181 | if (typeSet.has(key)) { 182 | debug('%j instance in typeSet', key); 183 | return null; 184 | } 185 | typeSet.add(key); 186 | return instance; 187 | } 188 | debug('create %j instance not exists, try to create a new one with port: %j', key, port); 189 | // compete for the local port, if got => leader, otherwise follower 190 | try { 191 | const server = await claimServer(port); 192 | instance = new ClusterServer({ server, port, ...options }); 193 | typeSet.add(key); 194 | serverMap.set(port, instance); 195 | return instance; 196 | } catch (err) { 197 | debug('create %j instance error: %s', key, err); 198 | // if exception, that mean compete for port failed, then double check 199 | instance = serverMap.get(port); 200 | if (instance && !instance.isClosed) { 201 | if (typeSet.has(key)) { 202 | return null; 203 | } 204 | typeSet.add(key); 205 | return instance; 206 | } 207 | return null; 208 | } 209 | } 210 | 211 | static async close(name, server) { 212 | const port = server._port; 213 | 214 | // remove from typeSet, so other client can occupy 215 | typeSet.delete(`${name}@${port}`); 216 | 217 | let listening = false; 218 | for (const key of typeSet.values()) { 219 | if (key.endsWith(`@${port}`)) { 220 | listening = true; 221 | break; 222 | } 223 | } 224 | 225 | // close server if no one is listening on this port any more 226 | if (!listening) { 227 | const server = serverMap.get(port); 228 | if (server) await server.close(); 229 | } 230 | } 231 | 232 | /** 233 | * Wait for Leader Startup 234 | * 235 | * @param {Number} port - the port 236 | * @param {Number} timeout - the max wait time 237 | * @return {void} 238 | */ 239 | static async waitFor(port, timeout) { 240 | const start = Date.now(); 241 | let connect = false; 242 | while (!connect) { 243 | connect = await tryToConnect(port); 244 | 245 | // if timeout, throw error 246 | if (Date.now() - start > timeout) { 247 | throw new Error(`[ClusterClient] leader does not be active in ${timeout}ms on port:${port}`); 248 | } 249 | if (!connect) { 250 | await sleep(3000); 251 | } 252 | } 253 | } 254 | } 255 | 256 | module.exports = ClusterServer; 257 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [3.7.0](https://github.com/node-modules/cluster-client/compare/v3.6.0...v3.7.0) (2024-05-25) 4 | 5 | 6 | ### Features 7 | 8 | * use egg-logger@3, sdk-base@4, utility@2 ([#67](https://github.com/node-modules/cluster-client/issues/67)) ([71dff9a](https://github.com/node-modules/cluster-client/commit/71dff9aec7b11be3037181c54518a31701f332d4)) 9 | 10 | ## [3.6.0](https://github.com/node-modules/cluster-client/compare/v3.5.0...v3.6.0) (2024-01-24) 11 | 12 | 13 | ### Features 14 | 15 | * set ClusterServer default maximum listeners up to 20 ([#66](https://github.com/node-modules/cluster-client/issues/66)) ([384b7b4](https://github.com/node-modules/cluster-client/commit/384b7b48c9a79203a1b7639fa4b7475434cfa8b4)) 16 | 17 | ## [3.5.0](https://github.com/node-modules/cluster-client/compare/v3.4.1...v3.5.0) (2023-08-30) 18 | 19 | 20 | ### Features 21 | 22 | * add tips for tcp-base ([#64](https://github.com/node-modules/cluster-client/issues/64)) ([f3068e0](https://github.com/node-modules/cluster-client/commit/f3068e0b6900b595e91a630e0f4e3bea7e84c3de)) 23 | 24 | ## [3.4.1](https://github.com/node-modules/cluster-client/compare/v3.4.0...v3.4.1) (2023-06-20) 25 | 26 | 27 | ### Bug Fixes 28 | 29 | * avoid serialize-json encoder error ([#63](https://github.com/node-modules/cluster-client/issues/63)) ([73c44d2](https://github.com/node-modules/cluster-client/commit/73c44d235c3bc6015263c98a4c5bf961af08cb55)) 30 | 31 | ## [3.4.0](https://github.com/node-modules/cluster-client/compare/v3.3.3...v3.4.0) (2023-01-11) 32 | 33 | 34 | ### Features 35 | 36 | * throw error when create server port is missing ([#62](https://github.com/node-modules/cluster-client/issues/62)) ([eb890c3](https://github.com/node-modules/cluster-client/commit/eb890c3607d118f6c8493fc99e3cd74c46d3fd79)) 37 | 38 | ## [3.3.3](https://github.com/node-modules/cluster-client/compare/v3.3.2...v3.3.3) (2023-01-10) 39 | 40 | 41 | ### Bug Fixes 42 | 43 | * add more debug log ([#61](https://github.com/node-modules/cluster-client/issues/61)) ([698105f](https://github.com/node-modules/cluster-client/commit/698105f8016d8cb4f127cfb46aa01ac64c185453)) 44 | 45 | ## [3.3.2](https://github.com/node-modules/cluster-client/compare/v3.3.1...v3.3.2) (2022-12-17) 46 | 47 | 48 | ### Bug Fixes 49 | 50 | * Auto release on action ([#59](https://github.com/node-modules/cluster-client/issues/59)) ([eb1655e](https://github.com/node-modules/cluster-client/commit/eb1655e196aa196413e3a169f9ff636cbf83d2c9)) 51 | 52 | --- 53 | 54 | 55 | 3.3.1 / 2022-11-23 56 | ================== 57 | 58 | **fixes** 59 | * [[`9d9b965`](http://github.com/node-modules/cluster-client/commit/9d9b965a50e148bff78c8763bcd4dea7e2998e57)] - fix: should not emit timeout when has cache (#58) (killa <>) 60 | 61 | 3.3.0 / 2022-11-14 62 | ================== 63 | 64 | **features** 65 | * [[`8ed198d`](http://github.com/node-modules/cluster-client/commit/8ed198db8d64eb76e018699093faed7c88f8c412)] - feat: add options isCheckHeartbeat of whether check heartbeat (#57) (sinkhaha <<1468709106@qq.com>>) 66 | 67 | 3.2.0 / 2022-10-18 68 | ================== 69 | 70 | **features** 71 | * [[`c9f389d`](http://github.com/node-modules/cluster-client/commit/c9f389d277abf75a5b3274626b326b6642b208f1)] - feat: impl subscribeTimeout (#56) (killa <>) 72 | 73 | 3.1.1 / 2022-06-22 74 | ================== 75 | 76 | **fixes** 77 | * [[`f4cc11b`](http://github.com/node-modules/cluster-client/commit/f4cc11bb539f58cc4926f1ce10db6b4ae1e7769c)] - fix: subscribe is not working on single thread mode (#51) (钟典 Desmond <>) 78 | 79 | 3.1.0 / 2021-11-21 80 | ================== 81 | 82 | **features** 83 | * [[`a21dde4`](http://github.com/node-modules/cluster-client/commit/a21dde4e7e20b232fe62c44266a0391b4eb4e46a)] - feat: connectTimeout support env, default 10s (#55) (mansonchor.github.com <>) 84 | 85 | 3.0.1 / 2019-03-01 86 | ================== 87 | 88 | **fixes** 89 | * [[`6f9bf8e`](http://github.com/node-modules/cluster-client/commit/6f9bf8e300386f28c2a208aa8c394d441045c03c)] - fix: single mode bugs (#49) (zōng yǔ <>) 90 | 91 | 3.0.0 / 2019-02-26 92 | ================== 93 | 94 | **features** 95 | * [[`7f3765d`](http://github.com/node-modules/cluster-client/commit/7f3765ded8877758c9a0bf0c1b23ab4e2c932c21)] - feat: support cpu single mode (#48) (zōng yǔ <>) 96 | 97 | 2.1.2 / 2018-11-23 98 | ================== 99 | 100 | **others** 101 | * [[`4c5093b`](http://github.com/node-modules/cluster-client/commit/4c5093b878ee8018524b1b0a514865e647a8f38c)] - chore: upgrade deps (#47) (zōng yǔ <>) 102 | 103 | 2.1.1 / 2018-06-12 104 | ================== 105 | 106 | **fixes** 107 | * [[`9fe3849`](http://github.com/node-modules/cluster-client/commit/9fe38494d41afca76491c78cf54b2717508f94b9)] - fix: follower should not retry register channel if socket already disconnected (#43) (zōng yǔ <>) 108 | 109 | 2.1.0 / 2018-03-26 110 | ================== 111 | 112 | **features** 113 | * [[`dce3fee`](http://github.com/node-modules/cluster-client/commit/dce3fee89a5c3d617b09e129f3ee68214fa90ccd)] - feat: response with whole error object instead of only stack, message (#38) (killa <>) 114 | 115 | 2.0.0 / 2018-03-06 116 | ================== 117 | 118 | **others** 119 | * [[`2574eae`](http://github.com/node-modules/cluster-client/commit/2574eae6fdbe603b74a3c27a57e3b545aec54314)] - [BREAKING] feat: migrating from generators to async/await (#36) (zōng yǔ <>) 120 | * [[`e32f0ef`](http://github.com/node-modules/cluster-client/commit/e32f0eff5fe5dfb4187386d928b2fa8cdc65cfa5)] - chore: release 1.7.1 (xiaochen.gaoxc <>), 121 | 122 | 1.7.1 / 2017-09-21 123 | ================== 124 | 125 | * fix: error occured while calling invokeOneway method if ready failed (#33) 126 | 127 | 1.7.0 / 2017-08-18 128 | ================== 129 | 130 | * feat: support custom port by env NODE_CLUSTER_CLIENT_PORT (#31) 131 | 132 | 1.6.8 / 2017-08-18 133 | ================== 134 | 135 | * fix: make sure leader ready before follower in egg (#32) 136 | 137 | 1.6.7 / 2017-07-31 138 | ================== 139 | 140 | * fix: only close server when server exists (#30) 141 | 142 | 1.6.6 / 2017-07-28 143 | ================== 144 | 145 | * fix: set exclusive to true on listen (#29) 146 | 147 | 1.6.5 / 2017-06-24 148 | ================== 149 | 150 | * fix: ignore error after close & register channel issue (#28) 151 | 152 | 1.6.4 / 2017-05-08 153 | ================== 154 | 155 | * chore: remove unnecessary log, using debug instead (#27) 156 | 157 | 1.6.3 / 2017-04-25 158 | ================== 159 | 160 | * fix: make sure follower test socket end (#25) 161 | 162 | 1.6.2 / 2017-04-25 163 | ================== 164 | 165 | * fix: ignore ECONNRESET error (#24) 166 | 167 | 1.6.1 / 2017-04-20 168 | ================== 169 | 170 | * fix: invoke before client ready issue (#23) 171 | * fix: fix symbol property error (#22) 172 | 173 | 1.6.0 / 2017-04-18 174 | ================== 175 | 176 | * feat: make clustClient method writable to support mock or spy (#21) 177 | 178 | 1.5.4 / 2017-04-12 179 | ================== 180 | 181 | * fix: avoid event memory leak warning (#20) 182 | 183 | 1.5.3 / 2017-03-17 184 | ================== 185 | 186 | * fix: make sure subscribe listener triggered asynchronized (#19) 187 | 188 | 1.5.2 / 2017-03-14 189 | ================== 190 | 191 | * fix: event delegate & leader ready bug (#18) 192 | 193 | 1.5.1 / 2017-03-13 194 | ================== 195 | 196 | * fix: don't auto ready when initMethod exists (#17) 197 | 198 | 1.5.0 / 2017-03-10 199 | ================== 200 | 201 | * feat: add APIClientBase to help you create your api client (#16) 202 | 203 | 1.4.0 / 2017-03-08 204 | ================== 205 | 206 | * feat: support unSubscribe, invokeOneway & close self (#14) 207 | 208 | 1.3.2 / 2017-03-08 209 | ================== 210 | 211 | * fix: fix leader subscribe issue & heartbeat timeout issue (#15) 212 | 213 | 1.3.1 / 2017-03-07 214 | ================== 215 | 216 | * chore: better notice (#13) 217 | * test: fix failed case (#12) 218 | 219 | 1.3.0 / 2017-02-22 220 | ================== 221 | 222 | * fix: block all remote connection (#11) 223 | 224 | 1.2.0 / 2017-02-20 225 | ================== 226 | 227 | * feat: use serialize-json to support encode/decode buffer, date, undef… (#10) 228 | 229 | 1.1.0 / 2017-02-07 230 | ================== 231 | 232 | * feat: close (#7) 233 | * fix: no more need harmony-reflect on node >= 6 (#8) 234 | * refactor: improve utils.delegateEvents() (#6) 235 | 236 | 1.0.3 / 2017-02-04 237 | ================== 238 | 239 | * fix: adjust serialize algorithm for invoke arguments (#3) 240 | 241 | 1.0.2 / 2017-01-25 242 | ================== 243 | 244 | * fix: log error if error exist (#5) 245 | * docs: fix typo subsribe -> subscribe (#4) 246 | 247 | 1.0.1 / 2016-12-26 248 | ================== 249 | 250 | * fix: fix shared memory issue (#2) 251 | 252 | 1.0.0 / 2016-12-22 253 | ================== 254 | 255 | * feat: implement cluster-client 256 | -------------------------------------------------------------------------------- /lib/follower.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('util').debuglog('cluster-client#follower'); 4 | const is = require('is-type-of'); 5 | const Base = require('tcp-base'); 6 | const Packet = require('./protocol/packet'); 7 | const Request = require('./protocol/request'); 8 | const Response = require('./protocol/response'); 9 | 10 | class Follower extends Base { 11 | /** 12 | * "Fake" Client, forward request to leader 13 | * 14 | * @param {Object} options 15 | * - {Number} port - the port 16 | * - {Map} descriptors - interface descriptors 17 | * - {Transcode} transcode - serialze / deserialze methods 18 | * - {Number} responseTimeout - the timeout 19 | * - {Number} subscribeTimeout - the first subscribe callback timeout 20 | * @class 21 | */ 22 | constructor(options) { 23 | // local address 24 | options.host = '127.0.0.1'; 25 | super(options); 26 | this._publishMethodName = this._findMethodName('publish'); 27 | this._subInfo = new Set(); 28 | this._subData = new Map(); 29 | this._transcode = options.transcode; 30 | this._closeByUser = false; 31 | this._subscribeTimeoutListeners = new Map(); 32 | 33 | this.on('request', req => this._handleRequest(req)); 34 | // avoid warning message 35 | this.setMaxListeners(100); 36 | } 37 | get isLeader() { 38 | return false; 39 | } 40 | 41 | get logger() { 42 | return this.options.logger; 43 | } 44 | 45 | get heartBeatPacket() { 46 | const heartbeat = new Request({ 47 | connObj: { 48 | type: 'heartbeat', 49 | }, 50 | timeout: this.options.responseTimeout, 51 | }); 52 | return heartbeat.encode(); 53 | } 54 | 55 | getHeader() { 56 | return this.read(24); 57 | } 58 | 59 | getBodyLength(header) { 60 | return header.readInt32BE(16) + header.readInt32BE(20); 61 | } 62 | 63 | close(err) { 64 | this._closeByUser = true; 65 | return super.close(err); 66 | } 67 | 68 | decode(body, header) { 69 | const buf = Buffer.concat([ header, body ]); 70 | const packet = Packet.decode(buf); 71 | const connObj = packet.connObj; 72 | if (connObj && connObj.type === 'invoke_result') { 73 | let data; 74 | if (packet.data) { 75 | data = this.options.transcode.decode(packet.data); 76 | } 77 | if (connObj.success) { 78 | return { 79 | id: packet.id, 80 | isResponse: packet.isResponse, 81 | data, 82 | }; 83 | } 84 | const error = new Error(data.message); 85 | Object.assign(error, data); 86 | return { 87 | id: packet.id, 88 | isResponse: packet.isResponse, 89 | error, 90 | }; 91 | } 92 | return { 93 | id: packet.id, 94 | isResponse: packet.isResponse, 95 | connObj: packet.connObj, 96 | data: packet.data, 97 | }; 98 | } 99 | 100 | send(...args) { 101 | // just ignore after close 102 | if (this._closeByUser) { 103 | return; 104 | } 105 | return super.send(...args); 106 | } 107 | 108 | formatKey(reg) { 109 | return '$$inner$$__' + this.options.formatKey(reg); 110 | } 111 | 112 | _clearSubscribeTimeout(key) { 113 | if (!this._subscribeTimeoutListeners.has(key)) { 114 | return; 115 | } 116 | const timeout = this._subscribeTimeoutListeners.get(key); 117 | clearTimeout(timeout); 118 | this._subscribeTimeoutListeners.delete(key); 119 | } 120 | 121 | _setSubscribeTimeout(key) { 122 | this._clearSubscribeTimeout(key); 123 | const start = Date.now(); 124 | const subscribeTimeout = this.options.subscribeTimeout; 125 | const timeout = setTimeout(() => { 126 | const error = new Error(`subscribe timeout for ${key}, cost: ${Date.now() - start}ms, expect is ${subscribeTimeout}ms`); 127 | this.emit('error', error); 128 | }, subscribeTimeout); 129 | this._subscribeTimeoutListeners.set(key, timeout); 130 | } 131 | 132 | _listenSubscribeTimeout(key) { 133 | this._setSubscribeTimeout(key); 134 | const dataListener = () => { 135 | this.removeListener(key, dataListener); 136 | this._clearSubscribeTimeout(key); 137 | }; 138 | this.on(key, dataListener); 139 | } 140 | 141 | subscribe(reg, listener) { 142 | const key = this.formatKey(reg); 143 | this.on(key, listener); 144 | if (this.options.subscribeTimeout && !this._subData.has(key)) { 145 | this._listenSubscribeTimeout(key); 146 | } 147 | 148 | // no need duplicate subscribe 149 | if (!this._subInfo.has(key)) { 150 | debug('[Follower:%s] subscribe %j for first time', this.options.name, reg); 151 | const req = new Request({ 152 | connObj: { type: 'subscribe', key, reg }, 153 | timeout: this.options.responseTimeout, 154 | }); 155 | 156 | // send subscription 157 | this.send({ 158 | id: req.id, 159 | oneway: true, 160 | data: req.encode(), 161 | tips: `subscribe key: ${key}`, 162 | }); 163 | this._subInfo.add(key); 164 | } else if (this._subData.has(key)) { 165 | debug('[Follower:%s] subscribe %j', this.options.name, reg); 166 | process.nextTick(() => { 167 | listener(this._subData.get(key)); 168 | }); 169 | } 170 | return this; 171 | } 172 | 173 | unSubscribe(reg, listener) { 174 | const key = this.formatKey(reg); 175 | if (listener) { 176 | this.removeListener(key, listener); 177 | } else { 178 | this.removeAllListeners(key); 179 | } 180 | if (this.listeners(key).length === 0) { 181 | debug('[Follower:%s] no more subscriber for %j, send unSubscribe req to leader', this.options.name, reg); 182 | this._subInfo.delete(key); 183 | 184 | const req = new Request({ 185 | connObj: { type: 'unSubscribe', key, reg }, 186 | timeout: this.options.responseTimeout, 187 | }); 188 | // send subscription 189 | this.send({ 190 | id: req.id, 191 | oneway: true, 192 | data: req.encode(), 193 | tips: `unSubscribe key ${key}`, 194 | }); 195 | } 196 | } 197 | 198 | publish(reg) { 199 | this.invoke(this._publishMethodName, [ reg ]); 200 | return this; 201 | } 202 | 203 | invoke(method, args, callback) { 204 | const oneway = !is.function(callback); // if no callback, means oneway 205 | const argLength = args.length; 206 | let data; 207 | // data: 208 | // +-----+---------------+-----+---------------+ 209 | // | len | arg1 body | len | arg2 body | ... 210 | // +-----+---------------+-----+---------------+ 211 | if (argLength > 0) { 212 | let argsBufLength = 0; 213 | const arr = []; 214 | for (const arg of args) { 215 | const argBuf = this._transcode.encode(arg); 216 | const len = argBuf.length; 217 | const buf = Buffer.alloc(4 + len); 218 | buf.writeInt32BE(len, 0); 219 | argBuf.copy(buf, 4, 0, len); 220 | arr.push(buf); 221 | argsBufLength += (len + 4); 222 | } 223 | data = Buffer.concat(arr, argsBufLength); 224 | } 225 | const req = new Request({ 226 | connObj: { 227 | type: 'invoke', 228 | method, 229 | argLength, 230 | oneway, 231 | }, 232 | data, 233 | timeout: this.options.responseTimeout, 234 | }); 235 | // send invoke request 236 | this.send({ 237 | id: req.id, 238 | oneway, 239 | data: req.encode(), 240 | tips: `invoke method:${method}, oneway:${oneway}, argLength:${argLength}`, 241 | }, callback); 242 | } 243 | 244 | _registerChannel() { 245 | const channelName = this.options.name; 246 | const req = new Request({ 247 | connObj: { 248 | type: 'register_channel', 249 | channelName, 250 | }, 251 | timeout: this.options.responseTimeout, 252 | }); 253 | // send invoke request 254 | this.send({ 255 | id: req.id, 256 | oneway: false, 257 | data: req.encode(), 258 | tips: `register_channel channelName:${channelName}`, 259 | }, (err, data) => { 260 | if (err) { 261 | // if socket alive, do retry 262 | if (this._socket) { 263 | err.message = `register to channel: ${channelName} failed, will retry after 3s, ${err.message}`; 264 | this.logger.warn(err); 265 | // if exception, retry after 3s 266 | setTimeout(() => this._registerChannel(), 3000); 267 | } else { 268 | this.ready(err); 269 | } 270 | return; 271 | } 272 | const res = this._transcode.decode(data); 273 | if (res.success) { 274 | debug('[Follower:%s] register to channel: %s success', this.options.name, this.options.name); 275 | this.ready(true); 276 | } else { 277 | const error = new Error(res.error.message); 278 | Object.assign(error, res.error); 279 | this.ready(error); 280 | } 281 | }); 282 | } 283 | 284 | _findMethodName(type) { 285 | for (const method of this.options.descriptors.keys()) { 286 | const descriptor = this.options.descriptors.get(method); 287 | if (descriptor.type === 'delegate' && descriptor.to === type) { 288 | return method; 289 | } 290 | } 291 | return null; 292 | } 293 | 294 | _handleRequest(req) { 295 | debug('[Follower:%s] receive req: %j from leader', this.options.name, req); 296 | const connObj = req.connObj || {}; 297 | if (connObj.type === 'subscribe_result') { 298 | const result = this._transcode.decode(req.data); 299 | this.emit(connObj.key, result); 300 | this._subData.set(connObj.key, result); 301 | // feedback 302 | const res = new Response({ 303 | id: req.id, 304 | timeout: req.timeout, 305 | connObj: { type: 'subscribe_result_res' }, 306 | }); 307 | this.send({ 308 | id: req.id, 309 | oneway: true, 310 | data: res.encode(), 311 | tips: `subscribe_result_res key:${connObj.key}`, 312 | }); 313 | } 314 | } 315 | 316 | _connect(done) { 317 | if (!done) { 318 | done = err => { 319 | if (err) { 320 | this.ready(err); 321 | } else { 322 | // register to proper channel, difference type of client into difference channel 323 | this._registerChannel(); 324 | } 325 | }; 326 | } 327 | super._connect(done); 328 | } 329 | } 330 | 331 | module.exports = Follower; 332 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cluster-client 2 | 3 | Sharing Connection among Multi-Process Nodejs 4 | 5 | [![NPM version][npm-image]][npm-url] 6 | [![CI](https://github.com/node-modules/cluster-client/actions/workflows/nodejs.yml/badge.svg)](https://github.com/node-modules/cluster-client/actions/workflows/nodejs.yml) 7 | [![Test coverage][codecov-image]][codecov-url] 8 | [![Known Vulnerabilities][snyk-image]][snyk-url] 9 | [![npm download][download-image]][download-url] 10 | 11 | [npm-image]: https://img.shields.io/npm/v/cluster-client.svg?style=flat-square 12 | [npm-url]: https://npmjs.org/package/cluster-client 13 | [codecov-image]: https://codecov.io/gh/node-modules/cluster-client/branch/master/graph/badge.svg 14 | [codecov-url]: https://codecov.io/gh/node-modules/cluster-client 15 | [snyk-image]: https://snyk.io/test/npm/cluster-client/badge.svg?style=flat-square 16 | [snyk-url]: https://snyk.io/test/npm/cluster-client 17 | [download-image]: https://img.shields.io/npm/dm/cluster-client.svg?style=flat-square 18 | [download-url]: https://npmjs.org/package/cluster-client 19 | 20 | As we know, each Node.js process runs in a single thread. Usually, we split a single process into multiple processes to take advantage of multi-core systems. On the other hand, it brings more system overhead, sush as maintaining more TCP connections between servers. 21 | 22 | This module is designed to share connections among multi-process Nodejs. 23 | 24 | ## Theory 25 | 26 | - Inspired by [Leader/Follower pattern](http://www.cs.wustl.edu/~schmidt/PDF/lf.pdf). 27 | - Allow ONLY one process "the Leader" to communicate with server. Other processes "the Followers" act as "Proxy" client, and forward all requests to Leader. 28 | - The Leader is selected by "Port Competition". Every process try to listen on a certain port (for example 7777), but ONLY one can occupy the port, then it becomes the Leader, the others become Followers. 29 | - TCP socket connections are maintained between Leader and Followers. And I design a simple communication protocol to exchange data between them. 30 | - If old Leader dies, one of processes will be selected as the new Leader. 31 | 32 | ## Diagram 33 | 34 | normal (without using cluster client) 35 | 36 | ```js 37 | +--------+ +--------+ 38 | | Client | | Client | ... 39 | +--------+ +--------+ 40 | | \ / | 41 | | \ / | 42 | | / \ | 43 | | / \ | 44 | +--------+ +--------+ 45 | | Server | | Server | ... 46 | +--------+ +--------+ 47 | 48 | ``` 49 | 50 | using cluster-client 51 | 52 | ```js 53 | +-------+ 54 | | start | 55 | +---+---+ 56 | | 57 | +--------+---------+ 58 | __| port competition |__ 59 | win / +------------------+ \ lose 60 | / \ 61 | +--------+ tcp conn +----------+ 62 | | Leader |<---------------->| Follower | 63 | +--------+ +----------+ 64 | | 65 | +--------+ 66 | | Client | 67 | +--------+ 68 | | \ 69 | | \ 70 | | \ 71 | | \ 72 | +--------+ +--------+ 73 | | Server | | Server | ... 74 | +--------+ +--------+ 75 | 76 | ``` 77 | 78 | ## Protocol 79 | 80 | - Packet structure 81 | 82 | ```js 83 | 0 1 2 4 12 84 | +-------+-------+---------------+---------------------------------------------------------------+ 85 | |version|req/res| reserved | request id | 86 | +-------------------------------+-------------------------------+-------------------------------+ 87 | | timeout | connection object length | application object length | 88 | +-------------------------------+---------------------------------------------------------------+ 89 | | conn object (JSON format) ... | app object | 90 | +-----------------------------------------------------------+ | 91 | | ... | 92 | +-----------------------------------------------------------------------------------------------+ 93 | ``` 94 | 95 | - Protocol Type 96 | - Register Channel 97 | - Subscribe/Publish 98 | - Invoke 99 | - Sequence diagram 100 | 101 | ```js 102 | +----------+ +---------------+ +---------+ 103 | | Follower | | local server | | Leader | 104 | +----------+ +---------------+ +---------+ 105 | | register channel | assign to | 106 | + -----------------------> | --------------------> | 107 | | | | 108 | | subscribe | 109 | + ------------------------------------------------> | 110 | | subscribe result | 111 | | <------------------------------------------------ + 112 | | | 113 | | invoke | 114 | + ------------------------------------------------> | 115 | | invoke result | 116 | | <------------------------------------------------ + 117 | | | 118 | ``` 119 | 120 | ## Install 121 | 122 | ```bash 123 | npm install cluster-client --save 124 | ``` 125 | 126 | Node.js >= 6.0.0 required 127 | 128 | ## Usage 129 | 130 | ```js 131 | 'use strict'; 132 | 133 | const co = require('co'); 134 | const Base = require('sdk-base'); 135 | const cluster = require('cluster-client'); 136 | 137 | /** 138 | * Client Example 139 | */ 140 | class YourClient extends Base { 141 | constructor(options) { 142 | super(options); 143 | 144 | this.options = options; 145 | this.ready(true); 146 | } 147 | 148 | subscribe(reg, listener) { 149 | // subscribe logic 150 | } 151 | 152 | publish(reg) { 153 | // publish logic 154 | } 155 | 156 | * getData(id) { 157 | // invoke api 158 | } 159 | 160 | getDataCallback(id, cb) { 161 | // ... 162 | } 163 | 164 | getDataPromise(id) { 165 | // ... 166 | } 167 | } 168 | 169 | // create some client instances, but only one instance will connect to server 170 | const client_1 = cluster(YourClient) 171 | .delegate('getData') 172 | .delegate('getDataCallback') 173 | .delegate('getDataPromise') 174 | .create({ foo: 'bar' }); 175 | const client_2 = cluster(YourClient) 176 | .delegate('getData') 177 | .delegate('getDataCallback') 178 | .delegate('getDataPromise') 179 | .create({ foo: 'bar' }); 180 | const client_3 = cluster(YourClient) 181 | .delegate('getData') 182 | .delegate('getDataCallback') 183 | .delegate('getDataPromise') 184 | .create({ foo: 'bar' }); 185 | 186 | // subscribe information 187 | client_1.subscribe('some thing', result => console.log(result)); 188 | client_2.subscribe('some thing', result => console.log(result)); 189 | client_3.subscribe('some thing', result => console.log(result)); 190 | 191 | // publish data 192 | client_2.publish('some data'); 193 | 194 | // invoke method 195 | client_3.getDataCallback('some thing', (err, val) => console.log(val)); 196 | client_2.getDataPromise('some thing').then(val => console.log(val)); 197 | 198 | co(function*() { 199 | const ret = yield client_1.getData('some thing'); 200 | console.log(ret); 201 | }).catch(err => console.error(err)); 202 | ``` 203 | 204 | ## API 205 | 206 | - `delegate(from, to)`: 207 | create delegate method, `from` is the method name your want to create, and `to` have 6 possible values: [ `subscribe`, `unSubscribe`, `publish`, `invoke`, `invokeOneway`, `close` ], and the default value is invoke 208 | - `override(name, value)`: 209 | override one property 210 | - `create(…)` 211 | create the client instance 212 | - `close(client)` 213 | close the client 214 | - `APIClientBase` a base class to help you create your api client 215 | 216 | ## Best Practice 217 | 218 | 1. DataClient 219 | 220 | - Only provider data API, interact with server and maintain persistent connections etc. 221 | - No need to concern `cluster` issue 222 | 223 | 1. APIClient 224 | 225 | - Using `cluster-client` to wrap DataClient 226 | - Put your bussiness logic here 227 | 228 | ### DataClient 229 | 230 | ```js 231 | const Base = require('sdk-base'); 232 | 233 | class DataClient extends Base { 234 | constructor(options) { 235 | super(options); 236 | this.ready(true); 237 | } 238 | 239 | subscribe(info, listener) { 240 | // subscribe data from server 241 | } 242 | 243 | publish(info) { 244 | // publish data to server 245 | } 246 | 247 | * getData(id) { 248 | // asynchronous API 249 | } 250 | } 251 | ``` 252 | 253 | ### APIClient 254 | 255 | ```js 256 | const DataClient = require('./your-data-client'); 257 | const { APIClientBase } = require('cluster-client'); 258 | 259 | class APIClient extends APIClientBase { 260 | constructor(options) { 261 | super(options); 262 | this._cache = new Map(); 263 | } 264 | get DataClient() { 265 | return DataClient; 266 | } 267 | get delegates() { 268 | return { 269 | getData: 'invoke', 270 | }; 271 | } 272 | get clusterOptions() { 273 | return { 274 | name: 'MyClient', 275 | }; 276 | } 277 | subscribe(...args) { 278 | return this._client.subscribe(...args); 279 | } 280 | publish(...args) { 281 | return this._client.publish(...args); 282 | } 283 | * getData(id) { 284 | // write your business logic & use data client API 285 | if (this._cache.has(id)) { 286 | return this._cache.get(id); 287 | } 288 | const data = yield this._client.getData(id); 289 | this._cache.set(id, data); 290 | return datal 291 | } 292 | } 293 | ``` 294 | 295 | ```js 296 | |------------------------------------------------| 297 | | APIClient | 298 | | |----------------------------------------| 299 | | | ClusterClient | 300 | | | |---------------------------------| 301 | | | | DataClient | 302 | |-------|------|---------------------------------| 303 | ``` 304 | 305 | For more information, you can refer to the [discussion](https://github.com/eggjs/egg/issues/322) 306 | 307 | [MIT](LICENSE) 308 | 309 | ## Contributors 310 | 311 | [![Contributors](https://contrib.rocks/image?repo=node-modules/cluster-client)](https://github.com/node-modules/cluster-client/graphs/contributors) 312 | 313 | Made with [contributors-img](https://contrib.rocks). 314 | -------------------------------------------------------------------------------- /test/client.test.js: -------------------------------------------------------------------------------- 1 | const co = require('co'); 2 | const mm = require('mm'); 3 | const net = require('net'); 4 | const Base = require('sdk-base'); 5 | const is = require('is-type-of'); 6 | const assert = require('assert'); 7 | const symbols = require('../lib/symbol'); 8 | const { sleep } = require('../lib/utils'); 9 | const EventEmitter = require('events').EventEmitter; 10 | const APIClientBase = require('..').APIClientBase; 11 | 12 | describe('test/client.test.js', () => { 13 | let port; 14 | before(done => { 15 | const server = net.createServer(); 16 | server.listen(0, () => { 17 | const address = server.address(); 18 | port = address.port; 19 | console.log('using port =>', port); 20 | server.close(); 21 | done(); 22 | }); 23 | }); 24 | 25 | class ClusterClient extends APIClientBase { 26 | get DataClient() { 27 | return require('./supports/client'); 28 | } 29 | 30 | get delegates() { 31 | return { 32 | unPublish: 'invokeOneway', 33 | }; 34 | } 35 | 36 | get clusterOptions() { 37 | return { 38 | responseTimeout: 1000, 39 | port, 40 | singleMode: process.env.NODE_CLUSTER_CLIENT_SINGLE_MODE === '1', 41 | }; 42 | } 43 | 44 | subscribe(...args) { 45 | return this._client.subscribe(...args); 46 | } 47 | 48 | unSubscribe(...args) { 49 | return this._client.unSubscribe(...args); 50 | } 51 | 52 | publish(...args) { 53 | return this._client.publish(...args); 54 | } 55 | 56 | unPublish(...args) { 57 | return this._client.unPublish(...args); 58 | } 59 | 60 | close() { 61 | return this._client.close(); 62 | } 63 | } 64 | 65 | class ErrorClient extends Base { 66 | constructor() { 67 | super({ initMethod: '_init' }); 68 | 69 | this.data = ''; 70 | } 71 | 72 | async _init() { 73 | await sleep(1000); 74 | const error = new Error('mock error'); 75 | error.code = 'ERROR_CODE'; 76 | throw error; 77 | } 78 | 79 | send(data) { 80 | console.log('send', data); 81 | } 82 | 83 | async getData() { 84 | return this.data; 85 | } 86 | } 87 | 88 | class APIClient extends APIClientBase { 89 | get DataClient() { 90 | return ErrorClient; 91 | } 92 | 93 | get delegates() { 94 | return { 95 | send: 'invokeOneway', 96 | }; 97 | } 98 | 99 | get clusterOptions() { 100 | return { 101 | name: 'test_invokeOneway_ready_error', 102 | port, 103 | singleMode: process.env.NODE_CLUSTER_CLIENT_SINGLE_MODE === '1', 104 | }; 105 | } 106 | 107 | async _init() { 108 | await sleep(1000); 109 | throw new Error('mock error'); 110 | } 111 | 112 | send(data) { 113 | this._client.send(data); 114 | } 115 | 116 | async getData() { 117 | return await this._client.getData(); 118 | } 119 | } 120 | 121 | let version = 0; 122 | class DataClient extends EventEmitter { 123 | constructor() { 124 | super(); 125 | version++; 126 | } 127 | 128 | * echo(val) { 129 | return val; 130 | } 131 | 132 | getVersion() { 133 | return Promise.resolve(version); 134 | } 135 | 136 | getError() { 137 | this.emit('close'); 138 | return Promise.reject(new Error('mock error')); 139 | } 140 | } 141 | 142 | class APIClient2 extends APIClientBase { 143 | get DataClient() { 144 | return DataClient; 145 | } 146 | 147 | get delegates() { 148 | return { 149 | getVersion: 'invoke', 150 | getError: 'invoke', 151 | }; 152 | } 153 | 154 | get clusterOptions() { 155 | return { 156 | port, 157 | singleMode: process.env.NODE_CLUSTER_CLIENT_SINGLE_MODE === '1', 158 | }; 159 | } 160 | 161 | async echo(val) { 162 | return await co(function* () { 163 | return yield this._client.echo(val); 164 | }.bind(this)); 165 | } 166 | 167 | getVersion() { 168 | return this._client.getVersion(); 169 | } 170 | 171 | getError() { 172 | return this._client.getError(); 173 | } 174 | } 175 | 176 | [ 177 | 'cluster', 178 | 'single', 179 | ].forEach(scene => { 180 | describe(`ClusterClient on scene: ${scene}`, () => { 181 | before(() => { 182 | version = 0; 183 | }); 184 | beforeEach(() => { 185 | if (scene === 'single') { 186 | mm(process.env, 'NODE_CLUSTER_CLIENT_SINGLE_MODE', '1'); 187 | } 188 | }); 189 | afterEach(mm.restore); 190 | it('should work ok', async function() { 191 | const client_1 = new ClusterClient(); 192 | const client_2 = new ClusterClient(); 193 | 194 | assert(client_1._client[symbols.singleMode] === (scene === 'single')); 195 | assert(client_2._client[symbols.singleMode] === (scene === 'single')); 196 | 197 | const listener_1 = val => { 198 | client_1.emit('foo_received_1', val); 199 | }; 200 | const listener_2 = val => { 201 | client_2.emit('foo_received_2', val); 202 | }; 203 | 204 | // subscribe 205 | client_1.subscribe({ key: 'foo' }, val => { 206 | client_1.emit('foo_received', val); 207 | }); 208 | client_1.subscribe({ key: 'foo' }, listener_1); 209 | 210 | let ret = await client_1.await('foo_received'); 211 | assert(is.array(ret) && ret.length === 0); 212 | 213 | client_2.subscribe({ key: 'foo' }, val => { 214 | client_2.emit('foo_received', val); 215 | }); 216 | client_2.subscribe({ key: 'foo' }, listener_2); 217 | ret = await client_2.await('foo_received'); 218 | assert(is.array(ret) && ret.length === 0); 219 | 220 | client_2.subscribe({ key: 'foo' }, val => { 221 | client_2.emit('foo_received_again', val); 222 | }); 223 | ret = await client_2.await('foo_received_again'); 224 | assert(is.array(ret) && ret.length === 0); 225 | 226 | // publish 227 | client_2.publish({ key: 'foo', value: 'bar' }); 228 | 229 | let rs = await Promise.all([ 230 | client_1.await('foo_received'), 231 | client_2.await('foo_received'), 232 | ]); 233 | assert(is.array(rs[0]) && rs[0].length === 1); 234 | assert(rs[0][0] === 'bar'); 235 | assert(is.array(rs[1]) && rs[1].length === 1); 236 | assert(rs[1][0] === 'bar'); 237 | 238 | // unPublish 239 | client_2.unPublish({ key: 'foo', value: 'bar' }); 240 | 241 | rs = await Promise.all([ 242 | client_1.await('foo_received_1'), 243 | client_2.await('foo_received_2'), 244 | ]); 245 | assert(is.array(rs[0]) && rs[0].length === 0); 246 | assert(is.array(rs[1]) && rs[1].length === 0); 247 | 248 | // unSubscribe 249 | client_1.unSubscribe({ key: 'foo' }, listener_1); 250 | client_2.unSubscribe({ key: 'foo' }, listener_2); 251 | 252 | // publish again 253 | client_2.publish({ key: 'foo', value: 'bar_1' }); 254 | 255 | await Promise.all([ 256 | new Promise((resolve, reject) => { 257 | setTimeout(resolve, 3000); 258 | client_1.once('foo_received_1', () => { reject(new Error('should not run here')); }); 259 | }), 260 | new Promise((resolve, reject) => { 261 | setTimeout(resolve, 3000); 262 | client_2.once('foo_received_2', () => { reject(new Error('should not run here')); }); 263 | }), 264 | ]); 265 | 266 | client_1.unSubscribe({ key: 'foo' }); 267 | client_2.unSubscribe({ key: 'foo' }); 268 | 269 | client_2.publish({ key: 'foo', value: 'bar_2' }); 270 | 271 | await Promise.all([ 272 | new Promise((resolve, reject) => { 273 | setTimeout(resolve, 3000); 274 | client_1.once('foo_received', () => { reject(new Error('should not run here')); }); 275 | }), 276 | new Promise((resolve, reject) => { 277 | setTimeout(resolve, 3000); 278 | client_2.once('foo_received', () => { reject(new Error('should not run here')); }); 279 | }), 280 | ]); 281 | 282 | client_1.close(); 283 | client_2.close(); 284 | }); 285 | 286 | it('should subscribe for second time', async function() { 287 | const client = new ClusterClient(); 288 | client.publish({ key: 'foo', value: 'bar' }); 289 | 290 | client.subscribe({ key: 'foo' }, val => { 291 | client.emit('foo_received_1', val); 292 | }); 293 | 294 | let ret = await client.await('foo_received_1'); 295 | assert.deepEqual(ret, [ 'bar' ]); 296 | 297 | client.subscribe({ key: 'foo' }, val => { 298 | client.emit('foo_received_2', val); 299 | }); 300 | ret = await client.await('foo_received_2'); 301 | assert.deepEqual(ret, [ 'bar' ]); 302 | 303 | await client.close(); 304 | }); 305 | 306 | it('should invoke with ready err', async function() { 307 | const leader = new APIClient(); 308 | try { 309 | await leader.getData(); 310 | assert(false); 311 | } catch (err) { 312 | assert(err && err.message === 'mock error'); 313 | assert.strictEqual(err.code, 'ERROR_CODE'); 314 | } 315 | 316 | const follower = new APIClient(); 317 | 318 | try { 319 | await follower.getData(); 320 | assert(false); 321 | } catch (err) { 322 | assert(err && err.message === 'mock error'); 323 | assert.strictEqual(err.code, 'ERROR_CODE'); 324 | } 325 | 326 | await follower.close(); 327 | await follower.close(); 328 | }); 329 | 330 | it('invokeOneway + ready error', async function() { 331 | const client = new APIClient(); 332 | client.send(123); 333 | try { 334 | await client.ready(); 335 | } catch (err) { 336 | assert(err.message === 'mock error'); 337 | } 338 | 339 | const client2 = new APIClient(); 340 | client2.send(321); 341 | try { 342 | await client2.ready(); 343 | } catch (err) { 344 | assert(err.message === 'mock error'); 345 | } 346 | 347 | client.send(123); 348 | client2.send(321); 349 | 350 | await sleep(2000); 351 | 352 | await client.close(); 353 | await client2.close(); 354 | }); 355 | 356 | it('should isClusterClientLeader ok', async () => { 357 | const client_1 = new ClusterClient(); 358 | await client_1.ready(); 359 | const client_2 = new ClusterClient(); 360 | await client_2.ready(); 361 | 362 | assert.equal(client_1.isClusterClientLeader, true); 363 | assert.equal(client_2.isClusterClientLeader, scene === 'single'); 364 | 365 | await client_1.close(); 366 | await client_2.close(); 367 | }); 368 | 369 | it('should getVersion & getError & echo', async () => { 370 | let client_1 = new APIClient2(); 371 | let client_2 = new APIClient2(); 372 | 373 | let v = await client_1.getVersion(); 374 | assert.equal(v, 1); 375 | v = await client_2.getVersion(); 376 | assert.equal(v, 1); 377 | 378 | await Promise.all([ 379 | client_1.close(), 380 | client_2.close(), 381 | ]); 382 | 383 | client_1 = new APIClient2(); 384 | client_2 = new APIClient2(); 385 | 386 | v = await client_1.getVersion(); 387 | assert.equal(v, 2); 388 | v = await client_2.getVersion(); 389 | assert.equal(v, 2); 390 | 391 | v = await client_1.echo('hello'); 392 | assert.equal(v, 'hello'); 393 | v = await client_2.echo('hello'); 394 | assert.equal(v, 'hello'); 395 | 396 | try { 397 | await client_1.getError(); 398 | assert(false); 399 | } catch (err) { 400 | assert.equal(err.message, 'mock error'); 401 | } 402 | 403 | try { 404 | await client_2.getError(); 405 | assert(false); 406 | } catch (err) { 407 | assert.equal(err.message, 'mock error'); 408 | } 409 | 410 | await Promise.all([ 411 | client_1.close(), 412 | client_2.close(), 413 | ]); 414 | }); 415 | }); 416 | }); 417 | }); 418 | -------------------------------------------------------------------------------- /lib/leader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('util').debuglog('cluster-client#leader'); 4 | const co = require('co'); 5 | const is = require('is-type-of'); 6 | const Base = require('sdk-base'); 7 | const utils = require('./utils'); 8 | const random = require('utility').random; 9 | const ClusterServer = require('./server'); 10 | const Connection = require('./connection'); 11 | const Request = require('./protocol/request'); 12 | const Response = require('./protocol/response'); 13 | 14 | class Leader extends Base { 15 | /** 16 | * The Leader hold the real client 17 | * 18 | * @param {Object} options 19 | * - {String} name - client name, default is the class name 20 | * - {ClusterServer} server - the cluster server 21 | * - {Boolean} isBroadcast - whether broadcast subscrption result to all followers or just one, default is true 22 | * - {Number} heartbeatInterval - the heartbeat interval 23 | * - {Function} createRealClient - to create the real client 24 | * - {Boolean} isCheckHeartbeat - whether check heartbeat, default is true 25 | * @class 26 | */ 27 | constructor(options) { 28 | super(options); 29 | this._connections = new Map(); 30 | this._subListeners = new Map(); // subscribe key => listener 31 | this._subConnMap = new Map(); // subscribe key => conn key 32 | this._subData = new Map(); 33 | // local socket server 34 | this._server = this.options.server; 35 | this._transcode = this.options.transcode; 36 | this._isReady = false; 37 | this._closeByUser = false; 38 | // the real client 39 | this._realClient = this.options.createRealClient(); 40 | this._subscribeMethodName = this._findMethodName('subscribe'); 41 | this._publishMethodName = this._findMethodName('publish'); 42 | 43 | // event delegate 44 | utils.delegateEvents(this._realClient, this); 45 | 46 | if (is.function(this._realClient.ready)) { 47 | this._realClient.ready(err => { 48 | if (err) { 49 | this.ready(err); 50 | } else { 51 | this._isReady = true; 52 | this.ready(true); 53 | } 54 | }); 55 | } else { 56 | this._isReady = true; 57 | this.ready(true); 58 | } 59 | 60 | this._handleConnection = this._handleConnection.bind(this); 61 | 62 | // subscribe its own channel 63 | this._server.on(`${this.options.name}_connection`, this._handleConnection); 64 | this._server.once('close', () => { this.emit('server_closed'); }); 65 | this.on('server_closed', () => { 66 | this._handleClose().catch(err => { this.emit('error', err); }); 67 | }); 68 | 69 | if (this.options.isCheckHeartbeat) { 70 | // maxIdleTime is 3 times of heartbeatInterval 71 | const heartbeatInterval = this.options.heartbeatInterval; 72 | const maxIdleTime = this.options.heartbeatInterval * 3; 73 | 74 | this._heartbeatTimer = setInterval(() => { 75 | const now = Date.now(); 76 | for (const conn of this._connections.values()) { 77 | const dur = now - conn.lastActiveTime; 78 | if (dur > maxIdleTime) { 79 | const err = new Error(`client no response in ${dur}ms exceeding maxIdleTime ${maxIdleTime}ms, maybe the connection is close on other side.`); 80 | err.name = 'ClusterClientNoResponseError'; 81 | conn.close(err); 82 | } 83 | } 84 | }, heartbeatInterval); 85 | } 86 | } 87 | 88 | get isLeader() { 89 | return true; 90 | } 91 | 92 | get logger() { 93 | return this.options.logger; 94 | } 95 | 96 | formatKey(reg) { 97 | return '$$inner$$__' + this.options.formatKey(reg); 98 | } 99 | 100 | subscribe(reg, listener) { 101 | const transcode = this._transcode; 102 | const conn = Object.create(Base.prototype, { 103 | isMock: { value: true }, 104 | key: { value: `${this.options.name}_mock_conn_${utils.nextId()}` }, 105 | lastActiveTime: { 106 | get() { 107 | return Date.now(); 108 | }, 109 | }, 110 | listener: { 111 | get() { 112 | return listener; 113 | }, 114 | }, 115 | send: { 116 | value(req) { 117 | const result = transcode.decode(req.data); 118 | process.nextTick(() => { 119 | listener(result); 120 | }); 121 | }, 122 | }, 123 | close: { value() {} }, 124 | }); 125 | conn.once('close', () => { 126 | this._connections.delete(conn.key); 127 | for (const connKeySet of this._subConnMap.values()) { 128 | connKeySet.delete(conn.key); 129 | } 130 | }); 131 | 132 | this._connections.set(conn.key, conn); 133 | this._doSubscribe(reg, conn); 134 | } 135 | 136 | unSubscribe(reg, listener) { 137 | const key = this.formatKey(reg); 138 | const connKeySet = this._subConnMap.get(key) || new Set(); 139 | const newConnKeySet = new Set(); 140 | for (const connKey of connKeySet.values()) { 141 | const conn = this._connections.get(connKey); 142 | if (!conn) { 143 | continue; 144 | } 145 | if (conn.isMock && (!listener || conn.listener === listener)) { 146 | this._connections.delete(connKey); 147 | continue; 148 | } 149 | newConnKeySet.add(connKey); 150 | } 151 | this._subConnMap.set(key, newConnKeySet); 152 | } 153 | 154 | publish(reg) { 155 | this._realClient[this._publishMethodName](reg); 156 | } 157 | 158 | invoke(methodName, args, callback) { 159 | let method = this._realClient[methodName]; 160 | // compatible with generatorFunction 161 | if (is.generatorFunction(method)) { 162 | method = co.wrap(method); 163 | } 164 | args.push(callback); 165 | const ret = method.apply(this._realClient, args); 166 | if (callback && is.promise(ret)) { 167 | ret.then(result => callback(null, result), err => callback(err)) 168 | // to avoid uncaught exception in callback function, then cause unhandledRejection 169 | .catch(err => { this._errorHandler(err); }); 170 | } 171 | } 172 | 173 | _doSubscribe(reg, conn) { 174 | const key = this.formatKey(reg); 175 | const callback = err => { 176 | if (err) { 177 | this._errorHandler(err); 178 | } 179 | }; 180 | const isBroadcast = this.options.isBroadcast; 181 | const timeout = this.options.responseTimeout; 182 | 183 | const connKeySet = this._subConnMap.get(key) || new Set(); 184 | connKeySet.add(conn.key); 185 | this._subConnMap.set(key, connKeySet); 186 | 187 | // only subscribe once in cluster mode, and broadcast to all followers 188 | if (!this._subListeners.has(key)) { 189 | const listener = result => { 190 | const data = this._transcode.encode(result); 191 | this._subData.set(key, data); 192 | 193 | const connKeySet = this._subConnMap.get(key); 194 | if (!connKeySet) { 195 | return; 196 | } 197 | let keys = Array.from(connKeySet.values()); 198 | // if isBroadcast equal to false, random pick one to notify 199 | if (!isBroadcast) { 200 | keys = [ keys[random(keys.length)] ]; 201 | } 202 | 203 | for (const connKey of keys) { 204 | const conn = this._connections.get(connKey); 205 | if (conn) { 206 | debug('[Leader:%s] push subscribe data to cluster client#%s', this.options.name, connKey); 207 | conn.send(new Request({ 208 | timeout, 209 | connObj: { 210 | type: 'subscribe_result', 211 | key, 212 | }, 213 | data, 214 | }), callback); 215 | } 216 | } 217 | }; 218 | this._subListeners.set(key, listener); 219 | this._realClient[this._subscribeMethodName](reg, listener); 220 | } else if (this._subData.has(key) && isBroadcast) { 221 | conn.send(new Request({ 222 | timeout, 223 | connObj: { 224 | type: 'subscribe_result', 225 | key, 226 | }, 227 | data: this._subData.get(key), 228 | }), callback); 229 | } 230 | } 231 | 232 | _findMethodName(type) { 233 | return utils.findMethodName(this.options.descriptors, type); 234 | } 235 | 236 | // handle new socket connect 237 | _handleConnection(socket, req) { 238 | debug('[Leader:%s] socket connected, port: %d', this.options.name, socket.remotePort); 239 | 240 | const conn = new Connection({ 241 | socket, 242 | name: this.options.name, 243 | logger: this.logger, 244 | transcode: this.options.transcode, 245 | requestTimeout: this.options.requestTimeout, 246 | }); 247 | this._connections.set(conn.key, conn); 248 | conn.once('close', () => { 249 | this._connections.delete(conn.key); 250 | for (const connKeySet of this._subConnMap.values()) { 251 | connKeySet.delete(conn.key); 252 | } 253 | }); 254 | conn.on('error', err => this._errorHandler(err)); 255 | conn.on('request', (req, res) => this._handleRequest(req, res, conn)); 256 | 257 | // handle register channel request 258 | const res = new Response({ 259 | id: req.id, 260 | timeout: req.timeout, 261 | }); 262 | this._handleRequest(req, res, conn); 263 | } 264 | 265 | _handleSubscribe(req, conn) { 266 | const connObj = req.connObj || {}; 267 | this._doSubscribe(connObj.reg, conn); 268 | } 269 | 270 | _handleUnSubscribe(req, conn) { 271 | const connObj = req.connObj || {}; 272 | const key = this.formatKey(connObj.reg); 273 | const connKeySet = this._subConnMap.get(key) || new Set(); 274 | connKeySet.delete(conn.key); 275 | this._subConnMap.set(key, connKeySet); 276 | } 277 | 278 | // handle request from followers 279 | _handleRequest(req, res, conn) { 280 | const connObj = req.connObj || {}; 281 | // update last active time to make sure not kick out by leader 282 | conn.lastActiveTime = Date.now(); 283 | 284 | switch (connObj.type) { 285 | case 'subscribe': 286 | debug('[Leader:%s] received subscribe request from follower, req: %j, conn: %s', this.options.name, req, conn.key); 287 | this._handleSubscribe(req, conn); 288 | break; 289 | case 'unSubscribe': 290 | debug('[Leader:%s] received unSubscribe request from follower, req: %j, conn: %s', this.options.name, req, conn.key); 291 | this._handleUnSubscribe(req, conn); 292 | break; 293 | case 'invoke': 294 | { 295 | debug('[Leader:%s] received invoke request from follower, req: %j, conn: %s', this.options.name, req, conn.key); 296 | const argLength = connObj.argLength; 297 | const args = []; 298 | if (argLength > 0) { 299 | const data = req.data; 300 | for (let i = 0, offset = 0; i < argLength; ++i) { 301 | const len = data.readUInt32BE(offset); 302 | const arg = this._transcode.decode(data.slice(offset + 4, offset + 4 + len)); 303 | args.push(arg); 304 | offset += (4 + len); 305 | } 306 | } 307 | 308 | if (connObj.oneway) { 309 | this.invoke(connObj.method, args); 310 | } else { 311 | const startTime = Date.now(); 312 | this.invoke(connObj.method, args, (err, result) => { 313 | // no response if processing timeout, just record error 314 | if (req.timeout && Date.now() - startTime > req.timeout) { 315 | const err = new Error(`[Leader:${this.options.name}] invoke method:${connObj.method} timeout for req#${req.id}`); 316 | err.name = 'ClusterLeaderTimeoutError'; 317 | err.method = connObj.method; 318 | err.args = args; 319 | this._errorHandler(err); 320 | return; 321 | } 322 | 323 | if (err) { 324 | const data = { 325 | stack: err.stack, 326 | name: err.name, 327 | message: err.message, 328 | code: err.code, 329 | }; 330 | err.method = connObj.method; 331 | err.args = connObj.args; 332 | this._errorHandler(err); 333 | 334 | res.connObj = { 335 | type: 'invoke_result', 336 | success: false, 337 | }; 338 | res.data = this._transcode.encode(data); 339 | } else { 340 | debug('[Leader:%s] send method:%s result to follower, result: %j', this.options.name, connObj.method, result); 341 | const data = this._transcode.encode(result); 342 | res.connObj = { 343 | type: 'invoke_result', 344 | success: true, 345 | }; 346 | res.data = data; 347 | } 348 | conn.send(res); 349 | }); 350 | } 351 | break; 352 | } 353 | case 'heartbeat': 354 | debug('[Leader:%s] received heartbeat request from follower, req: %j, conn: %s', this.options.name, req, conn.key); 355 | res.connObj = { type: 'heartbeat_res' }; 356 | conn.send(res); 357 | break; 358 | case 'register_channel': 359 | // make sure response after leader is ready 360 | this.ready(err => { 361 | if (err) { 362 | res.connObj = { type: 'register_channel_res' }; 363 | const data = { 364 | message: err.message, 365 | stack: err.stack, 366 | name: err.name, 367 | code: err.code, 368 | }; 369 | res.data = this._transcode.encode({ 370 | success: false, 371 | error: data, 372 | }); 373 | } else { 374 | res.connObj = { type: 'register_channel_res' }; 375 | res.data = this._transcode.encode({ success: true }); 376 | } 377 | conn.send(res); 378 | }); 379 | break; 380 | default: 381 | { 382 | const err = new Error(`unsupport data type: ${connObj.type}`); 383 | err.name = 'ClusterRequestTypeError'; 384 | this._errorHandler(err); 385 | break; 386 | } 387 | } 388 | } 389 | 390 | // emit error asynchronously 391 | _errorHandler(err) { 392 | setImmediate(() => { 393 | if (!this._closeByUser) { 394 | this.emit('error', err); 395 | } 396 | }); 397 | } 398 | 399 | async _handleClose() { 400 | debug('[Leader:%s] leader server is closed', this.options.name); 401 | // close the real client 402 | if (this._realClient) { 403 | const originClose = this._findMethodName('close'); 404 | if (originClose) { 405 | // support common function, generatorFunction, and function returning a promise 406 | await utils.callFn(this._realClient[originClose].bind(this._realClient)); 407 | } 408 | } 409 | this._heartbeatTimer && clearInterval(this._heartbeatTimer); 410 | this._heartbeatTimer = null; 411 | this.emit('close'); 412 | } 413 | 414 | async close() { 415 | this._closeByUser = true; 416 | debug('[Leader:%s] try to close leader', this.options.name); 417 | // 1. stop listening to server channel 418 | this._server.removeListener(`${this.options.name}_connection`, this._handleConnection); 419 | 420 | // 2. close all mock connections 421 | for (const conn of this._connections.values()) { 422 | if (conn.isMock) { 423 | conn.emit('close'); 424 | } 425 | } 426 | 427 | // 3. close server 428 | // CANNOT close server directly by server.close(), other cluster clients may be using it 429 | this.removeAllListeners('server_closed'); 430 | await ClusterServer.close(this.options.name, this._server); 431 | 432 | // 5. close real client 433 | await this._handleClose(); 434 | } 435 | } 436 | 437 | module.exports = Leader; 438 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | const spy = require('spy'); 2 | const net = require('net'); 3 | const mm = require('egg-mock'); 4 | const cluster = require('../'); 5 | const is = require('is-type-of'); 6 | const Base = require('sdk-base'); 7 | const assert = require('assert'); 8 | const pedding = require('pedding'); 9 | const serverMap = global.serverMap; 10 | const symbols = require('../lib/symbol'); 11 | const ClusterServer = require('../lib/server'); 12 | const NotifyClient = require('./supports/notify_client'); 13 | const RegistryClient = require('./supports/registry_client'); 14 | const portDelta = Number(process.versions.node.slice(0, 1)); 15 | 16 | describe('test/index.test.js', () => { 17 | 18 | afterEach(mm.restore); 19 | 20 | it('should throw if port is occupied by other, while new Leader', async function() { 21 | mm(ClusterServer, 'create', async () => null); 22 | 23 | const server = net.createServer(); 24 | const port = await new Promise(resolve => { 25 | server.listen(0, () => { 26 | const address = server.address(); 27 | // console.log('using port =>', port); 28 | // server.close(); 29 | resolve(address.port); 30 | }); 31 | }); 32 | const leader = cluster(RegistryClient, { port, isLeader: true }) 33 | .delegate('subscribe', 'subscribe') 34 | .delegate('publish', 'publish') 35 | .override('foo', 'bar') 36 | .create(); 37 | 38 | try { 39 | await leader.ready(); 40 | assert(false); 41 | } catch (err) { 42 | assert(err.message === `create "RegistryClient" leader failed, the port:${port} is occupied by other`); 43 | } 44 | 45 | await leader.close(); 46 | server.close(); 47 | }); 48 | 49 | [ 50 | [ 'cluster', false ], 51 | [ 'single', true ], 52 | ].forEach(item => { 53 | const scence = item[0]; 54 | const singleMode = item[1]; 55 | describe(scence, () => { 56 | describe('RegistryClient', () => { 57 | const port = 8880 + portDelta; 58 | let leader; 59 | let follower; 60 | beforeEach(() => { 61 | leader = cluster(RegistryClient, { port, isLeader: true, singleMode }) 62 | .delegate('subscribe', 'subscribe') 63 | .delegate('publish', 'publish') 64 | .override('foo', 'bar') 65 | .create(); 66 | follower = cluster(RegistryClient, { port, isLeader: false, singleMode }).create(); 67 | }); 68 | 69 | afterEach(async function() { 70 | if (scence === 'cluster') { 71 | assert(serverMap.has(port) === true); 72 | } 73 | await Promise.race([ 74 | cluster.close(follower), 75 | follower.await('error'), 76 | ]); 77 | await Promise.race([ 78 | cluster.close(leader), 79 | leader.await('error'), 80 | ]); 81 | assert(leader[symbols.innerClient]._realClient.closed === true); // make sure real client is closed 82 | assert(!serverMap.has(port)); // make sure net.Server is closed 83 | }); 84 | 85 | it('should have subscribe/publish method', () => { 86 | assert(is.function(leader.subscribe)); 87 | assert(is.function(leader.publish)); 88 | assert(is.function(follower.subscribe)); 89 | assert(is.function(follower.publish)); 90 | assert(leader.foo === 'bar'); 91 | }); 92 | 93 | it('should subscribe ok', done => { 94 | done = pedding(done, 3); 95 | let leader_trigger = false; 96 | let follower_trigger = false; 97 | let follower_trigger_2 = false; 98 | 99 | leader.subscribe({ 100 | dataId: 'com.alibaba.dubbo.demo.DemoService', 101 | }, val => { 102 | assert(val && val.length > 0); 103 | if (val.length === 2 && !leader_trigger) { 104 | assert(val.some(url => url.host === '30.20.78.299:20880')); 105 | assert(val.some(url => url.host === '30.20.78.300:20880')); 106 | leader_trigger = true; 107 | done(); 108 | } 109 | }); 110 | 111 | follower.subscribe({ 112 | dataId: 'com.alibaba.dubbo.demo.DemoService', 113 | }, val => { 114 | assert(val && val.length > 0); 115 | if (val.length === 2 && !follower_trigger) { 116 | assert(val.some(url => url.host === '30.20.78.299:20880')); 117 | assert(val.some(url => url.host === '30.20.78.300:20880')); 118 | follower_trigger = true; 119 | done(); 120 | } 121 | }); 122 | 123 | setTimeout(() => { 124 | // double subscribe 125 | follower.subscribe({ 126 | dataId: 'com.alibaba.dubbo.demo.DemoService', 127 | }, val => { 128 | assert(val && val.length > 0); 129 | if (val.length === 2 && !follower_trigger_2) { 130 | assert(val.some(url => url.host === '30.20.78.299:20880')); 131 | assert(val.some(url => url.host === '30.20.78.300:20880')); 132 | follower_trigger_2 = true; 133 | done(); 134 | } 135 | }); 136 | }, 3000); 137 | 138 | leader.publish({ 139 | dataId: 'com.alibaba.dubbo.demo.DemoService', 140 | publishData: 'dubbo://30.20.78.299:20880/com.alibaba.dubbo.demo.DemoService?anyhost=true&application=demo-provider&dubbo=2.0.0&generic=false&interface=com.alibaba.dubbo.demo.DemoService&loadbalance=roundrobin&methods=sayHello&owner=william&pid=81281&side=provider×tamp=1481613276143', 141 | }); 142 | follower.publish({ 143 | dataId: 'com.alibaba.dubbo.demo.DemoService', 144 | publishData: 'dubbo://30.20.78.300:20880/com.alibaba.dubbo.demo.DemoService?anyhost=true&application=demo-provider&dubbo=2.0.0&generic=false&interface=com.alibaba.dubbo.demo.DemoService&loadbalance=roundrobin&methods=sayHello&owner=william&pid=81281&side=provider×tamp=1481613276143', 145 | }); 146 | }); 147 | 148 | if (scence === 'single') return; 149 | 150 | it('should should not close net.Server if other client is using same port', async function() { 151 | class AnotherClient extends Base { 152 | constructor() { 153 | super(); 154 | this.ready(true); 155 | } 156 | } 157 | const anotherleader = cluster(AnotherClient, { port, isLeader: true }).create(); 158 | await anotherleader.ready(); 159 | 160 | // assert has problem with global scope virable 161 | // assert(serverMap.has(port) === true); 162 | if (!serverMap.has(port)) throw new Error(); 163 | await cluster.close(anotherleader); 164 | 165 | // leader is using the same port, so anotherleader.close should not close the net.Server 166 | if (!serverMap.has(port)) throw new Error(); 167 | }); 168 | 169 | it('should realClient.close be a generator function ok', async function() { 170 | class RealClientWithGeneratorClose extends Base { 171 | constructor() { 172 | super(); 173 | this.ready(true); 174 | } 175 | 176 | // async close() { 177 | // this.closed = true; 178 | // } 179 | * close() { 180 | this.closed = true; 181 | } 182 | } 183 | const anotherleader = cluster(RealClientWithGeneratorClose, { port, isLeader: true }).create(); 184 | await anotherleader.ready(); 185 | await cluster.close(anotherleader); 186 | // make sure real client is closed; 187 | // assert has problem with global scope virable 188 | if (anotherleader[symbols.innerClient]._realClient.closed !== true) { 189 | throw new Error('close not work'); 190 | } 191 | }); 192 | 193 | it('should realClient.close be a normal function ok', async function() { 194 | class RealClientWithNormalClose extends Base { 195 | constructor() { 196 | super(); 197 | this.ready(true); 198 | } 199 | close() { 200 | this.closed = true; 201 | } 202 | } 203 | const anotherleader = cluster(RealClientWithNormalClose, { port, isLeader: true }).create(); 204 | await anotherleader.ready(); 205 | await cluster.close(anotherleader); 206 | // make sure real client is closed; 207 | // assert has problem with global scope virable 208 | if (anotherleader[symbols.innerClient]._realClient.closed !== true) { 209 | throw new Error(); 210 | } 211 | }); 212 | 213 | it('should realClient.close be a function returning promise ok', async function() { 214 | class RealClientWithCloseReturningPromise extends Base { 215 | constructor() { 216 | super(); 217 | this.ready(true); 218 | } 219 | close() { 220 | this.closed = true; 221 | } 222 | } 223 | const anotherleader = cluster(RealClientWithCloseReturningPromise, { port, isLeader: true }).create(); 224 | await anotherleader.ready(); 225 | await cluster.close(anotherleader); 226 | // make sure real client is closed; 227 | // assert has problem with global scope virable 228 | if (anotherleader[symbols.innerClient]._realClient.closed !== true) { 229 | throw new Error(); 230 | } 231 | }); 232 | }); 233 | }); 234 | }); 235 | 236 | 237 | describe('heartbeat', () => { 238 | it('should close connection if long time no heartbeat', done => { 239 | done = pedding(done, 2); 240 | const port = 7770 + portDelta; 241 | const leader = cluster(RegistryClient, { 242 | port, 243 | isLeader: true, 244 | heartbeatInterval: 3000, 245 | }).create(); 246 | const follower = cluster(RegistryClient, { 247 | port, 248 | isLeader: false, 249 | heartbeatInterval: 30000, 250 | }).create(); 251 | cluster(RegistryClient, { 252 | port, 253 | isLeader: false, 254 | heartbeatInterval: 2000, 255 | }).create(); 256 | 257 | const start = Date.now(); 258 | leader.once('error', err => { 259 | assert(err); 260 | assert(/client no response in \d+ms exceeding maxIdleTime \d+ms, maybe the connection is close on other side\./.test(err.message)); 261 | assert(err.name === 'ClusterClientNoResponseError'); 262 | done(); 263 | }); 264 | follower.once('close', () => { 265 | console.log('follower closed'); 266 | const dur = Date.now() - start; 267 | assert(dur > 3000); 268 | done(); 269 | }); 270 | }); 271 | }); 272 | 273 | describe('invoke', () => { 274 | const SYMBOL_FN = Symbol('MockClient#symbolFN'); 275 | 276 | class MockClient extends Base { 277 | constructor() { 278 | super(); 279 | this.ready(true); 280 | } 281 | 282 | * get(id) { 283 | return yield cb => this.getCallback(id, cb); 284 | } 285 | 286 | getCallback(id, cb) { 287 | setTimeout(() => { 288 | if (id === 'error') { 289 | cb(new Error('mock error')); 290 | } else if (id === 'timeout') { 291 | // do nothing 292 | } else { 293 | cb(null, id); 294 | } 295 | }, 500); 296 | } 297 | 298 | getPromise(id) { 299 | return new Promise((resolve, reject) => { 300 | this.getCallback(id, (err, data) => { 301 | if (err) { 302 | reject(err); 303 | } else { 304 | resolve(data); 305 | } 306 | }); 307 | }); 308 | } 309 | 310 | [SYMBOL_FN]() { 311 | return 'symboFn!'; 312 | } 313 | 314 | timeout(cb) { 315 | setTimeout(cb, 6000); 316 | } 317 | } 318 | 319 | const port = 6660 + portDelta; 320 | const leader = cluster(MockClient, { port }) 321 | .delegate('get') 322 | .delegate('getCallback') 323 | .delegate('getPromise') 324 | .delegate('timeout') 325 | .create(); 326 | const follower = cluster(MockClient, { port }) 327 | .delegate('get') 328 | .delegate('getCallback') 329 | .delegate('getPromise') 330 | .delegate('timeout') 331 | .create(); 332 | 333 | it('should invoke generator function ok', function* () { 334 | let ret = yield leader.get('123'); 335 | assert(ret === '123'); 336 | ret = yield follower.get('123'); 337 | assert(ret === '123'); 338 | 339 | try { 340 | yield leader.get('error'); 341 | } catch (err) { 342 | assert(err.message === 'mock error'); 343 | } 344 | 345 | try { 346 | yield follower.get('error'); 347 | } catch (err) { 348 | assert(err.message === 'mock error'); 349 | } 350 | }); 351 | 352 | it('should symbol function not delegated', () => { 353 | assert(!leader[SYMBOL_FN]); 354 | assert(!follower[SYMBOL_FN]); 355 | }); 356 | 357 | it('should be mocked', function* () { 358 | mm(leader, 'get', function* () { 359 | return '456'; 360 | }); 361 | mm(follower, 'get', function* () { 362 | return '456'; 363 | }); 364 | 365 | let ret = yield leader.get('123'); 366 | assert(ret === '456'); 367 | ret = yield follower.get('123'); 368 | assert(ret === '456'); 369 | 370 | }); 371 | 372 | it('should be spied', function* () { 373 | const leaderGet = spy(leader, 'get'); 374 | const followerGet = spy(follower, 'get'); 375 | 376 | yield leader.get('123'); 377 | yield follower.get('123'); 378 | assert(leaderGet.callCount === 1); 379 | assert(followerGet.callCount === 1); 380 | }); 381 | 382 | it('should invoke callback function ok', done => { 383 | done = pedding(done, 5); 384 | leader.getCallback('123', (err, data) => { 385 | assert.ifError(err); 386 | assert(data === '123'); 387 | done(); 388 | }); 389 | follower.getCallback('123', (err, data) => { 390 | assert.ifError(err); 391 | assert(data === '123'); 392 | done(); 393 | }); 394 | leader.getCallback('error', err => { 395 | assert(err.message === 'mock error'); 396 | done(); 397 | }); 398 | follower.getCallback('error', err => { 399 | assert(err.message === 'mock error'); 400 | done(); 401 | }); 402 | 403 | follower.getCallback('timeout', err => { 404 | assert(err.message.startsWith('Server no response in 3000ms, address#127.0.0.1')); 405 | done(); 406 | }); 407 | }); 408 | 409 | it('should invoke promise function ok', done => { 410 | done = pedding(done, 4); 411 | leader.getPromise('123').then(data => { 412 | assert(data === '123'); 413 | done(); 414 | }); 415 | follower.getPromise('123').then(data => { 416 | assert(data === '123'); 417 | done(); 418 | }); 419 | leader.getPromise('error').catch(err => { 420 | assert(err.message === 'mock error'); 421 | done(); 422 | }); 423 | follower.getPromise('error').catch(err => { 424 | assert(err.message === 'mock error'); 425 | done(); 426 | }); 427 | }); 428 | 429 | it('should invoke timeout ok', done => { 430 | follower.timeout(err => { 431 | assert(err && err.name === 'ResponseTimeoutError'); 432 | done(); 433 | }); 434 | }); 435 | }); 436 | 437 | describe('event delegate', () => { 438 | 439 | class MockClient extends Base { 440 | constructor() { 441 | super(); 442 | this.ready(true); 443 | 444 | setTimeout(() => { 445 | this.emit('foo', 'bar'); 446 | }, 2000); 447 | 448 | setTimeout(() => { 449 | this.emit('ready'); 450 | }, 500); 451 | } 452 | } 453 | 454 | it('should delegate all events', done => { 455 | done = pedding(done, 2); 456 | const port = 5550 + portDelta; 457 | const leader = cluster(MockClient, { port }).create(); 458 | 459 | leader.ready(() => { 460 | leader.on('ready', done) 461 | .once('foo', bar => { 462 | assert(bar === 'bar'); 463 | done(); 464 | }); 465 | }); 466 | }); 467 | }); 468 | 469 | describe('Custom Transcode', () => { 470 | const port = 5550 + portDelta; 471 | const transcode = { 472 | encode(urls) { 473 | return Buffer.from(JSON.stringify(urls)); 474 | }, 475 | decode(buf) { 476 | const arr = JSON.parse(buf); 477 | return arr; 478 | }, 479 | }; 480 | const leader = cluster(RegistryClient, { port, transcode }).create(4321, '224.5.6.8'); 481 | const follower = cluster(RegistryClient, { port, transcode }).create(4321, '224.5.6.8'); 482 | 483 | it('should subscribe ok', done => { 484 | done = pedding(done, 2); 485 | 486 | leader.subscribe({ 487 | dataId: 'com.alibaba.dubbo.demo.DemoService', 488 | }, val => { 489 | console.log('leader', val); 490 | assert(val && val.length > 0); 491 | if (val.length === 2) { 492 | // assert(val.every(url => url instanceof URL.Url)); 493 | assert(val.some(url => url.host === '30.20.78.299:20880')); 494 | assert(val.some(url => url.host === '30.20.78.300:20880')); 495 | done(); 496 | } 497 | }); 498 | 499 | follower.subscribe({ 500 | dataId: 'com.alibaba.dubbo.demo.DemoService', 501 | }, val => { 502 | console.log('follower', val, val.map(item => item.host)); 503 | assert(val && val.length > 0); 504 | if (val.length === 2) { 505 | // assert(val.every(url => url instanceof URL.Url)); 506 | assert(val.some(url => url.host === '30.20.78.299:20880')); 507 | assert(val.some(url => url.host === '30.20.78.300:20880')); 508 | done(); 509 | } 510 | }); 511 | 512 | leader.publish({ 513 | dataId: 'com.alibaba.dubbo.demo.DemoService', 514 | publishData: 'dubbo://30.20.78.299:20880/com.alibaba.dubbo.demo.DemoService?anyhost=true&application=demo-provider&dubbo=2.0.0&generic=false&interface=com.alibaba.dubbo.demo.DemoService&loadbalance=roundrobin&methods=sayHello&owner=william&pid=81281&side=provider×tamp=1481613276143', 515 | }); 516 | follower.publish({ 517 | dataId: 'com.alibaba.dubbo.demo.DemoService', 518 | publishData: 'dubbo://30.20.78.300:20880/com.alibaba.dubbo.demo.DemoService?anyhost=true&application=demo-provider&dubbo=2.0.0&generic=false&interface=com.alibaba.dubbo.demo.DemoService&loadbalance=roundrobin&methods=sayHello&owner=william&pid=81281&side=provider×tamp=1481613276143', 519 | }); 520 | }); 521 | }); 522 | 523 | describe('not broadcast', () => { 524 | const port = 4440 + portDelta; 525 | let leader; 526 | let follower; 527 | let follower2; 528 | before(() => { 529 | leader = cluster(RegistryClient, { isLeader: true, port, isBroadcast: false }).create(4322, '224.5.6.9'); 530 | follower = cluster(RegistryClient, { isLeader: false, port, isBroadcast: false }).create(4322, '224.5.6.9'); 531 | follower2 = cluster(RegistryClient, { isLeader: false, port, isBroadcast: false }).create(4322, '224.5.6.9'); 532 | }); 533 | after(async function() { 534 | await follower.close(); 535 | await follower2.close(); 536 | await leader.close(); 537 | }); 538 | 539 | 540 | it('should subscribe ok', done => { 541 | let trigger = false; 542 | 543 | leader.subscribe({ 544 | dataId: 'com.alibaba.dubbo.demo.DemoService', 545 | }, val => { 546 | assert(!trigger); 547 | trigger = true; 548 | assert(val && val.length > 0); 549 | assert(val.some(url => url.host === '30.20.78.299:20880')); 550 | done(); 551 | }); 552 | 553 | follower.subscribe({ 554 | dataId: 'com.alibaba.dubbo.demo.DemoService', 555 | }, val => { 556 | assert(!trigger); 557 | trigger = true; 558 | assert(val && val.length > 0); 559 | assert(val.some(url => url.host === '30.20.78.299:20880')); 560 | done(); 561 | }); 562 | 563 | follower2.subscribe({ 564 | dataId: 'com.alibaba.dubbo.demo.DemoService', 565 | }, val => { 566 | assert(!trigger); 567 | trigger = true; 568 | assert(val && val.length > 0); 569 | assert(val.some(url => url.host === '30.20.78.299:20880')); 570 | done(); 571 | }); 572 | 573 | leader.publish({ 574 | dataId: 'com.alibaba.dubbo.demo.DemoService', 575 | publishData: 'dubbo://30.20.78.299:20880/com.alibaba.dubbo.demo.DemoService?anyhost=true&application=demo-provider&dubbo=2.0.0&generic=false&interface=com.alibaba.dubbo.demo.DemoService&loadbalance=roundrobin&methods=sayHello&owner=william&pid=81281&side=provider×tamp=1481613276143', 576 | }); 577 | }); 578 | }); 579 | 580 | describe('server close', () => { 581 | const port = 3330 + portDelta; 582 | const innerClient = Symbol.for('ClusterClient#innerClient'); 583 | let client_1; 584 | let client_2; 585 | let client_3; 586 | before(async function() { 587 | client_1 = cluster(RegistryClient, { port }).create(4323, '224.5.6.10'); 588 | client_2 = cluster(RegistryClient, { port }).create(4323, '224.5.6.10'); 589 | client_3 = cluster(RegistryClient, { port }).create(4323, '224.5.6.10'); 590 | await client_1.ready(); 591 | await client_2.ready(); 592 | await client_3.ready(); 593 | }); 594 | 595 | after(async () => { 596 | await cluster.close(client_3); 597 | await cluster.close(client_2); 598 | await cluster.close(client_1); 599 | }); 600 | 601 | it('should re subscribe / publish ok', done => { 602 | done = pedding(done, 3); 603 | let trigger_1 = false; 604 | let trigger_2 = false; 605 | let trigger_3 = false; 606 | client_1.subscribe({ 607 | dataId: 'com.alibaba.dubbo.demo.DemoService', 608 | }, val => { 609 | if (trigger_1) return; 610 | 611 | trigger_1 = true; 612 | assert(val && val.length > 0); 613 | console.log('1', val.map(url => url.host)); 614 | assert(val.some(url => url.host === '30.20.78.299:20880')); 615 | done(); 616 | }); 617 | 618 | client_2.subscribe({ 619 | dataId: 'com.alibaba.dubbo.demo.DemoService', 620 | }, val => { 621 | if (trigger_2) return; 622 | 623 | trigger_2 = true; 624 | assert(val && val.length > 0); 625 | console.log('2', val.map(url => url.host)); 626 | assert(val.some(url => url.host === '30.20.78.299:20880')); 627 | done(); 628 | }); 629 | 630 | client_2.publish({ 631 | dataId: 'com.alibaba.dubbo.demo.DemoService', 632 | publishData: 'dubbo://30.20.78.299:20880/com.alibaba.dubbo.demo.DemoService?anyhost=true&application=demo-provider&dubbo=2.0.0&generic=false&interface=com.alibaba.dubbo.demo.DemoService&loadbalance=roundrobin&methods=sayHello&owner=william&pid=81281&side=provider×tamp=1481613276143', 633 | }); 634 | 635 | setTimeout(() => { 636 | let master; 637 | for (const client of [ client_1, client_2, client_3 ]) { 638 | if (client[innerClient].isLeader) { 639 | if (master) { 640 | done(new Error('should only one leader')); 641 | return; 642 | } 643 | master = client; 644 | } 645 | } 646 | if (!master) { 647 | done(new Error('should have leader')); 648 | return; 649 | } 650 | // close inner server 651 | master[innerClient]._server.close(); 652 | 653 | client_3.subscribe({ 654 | dataId: 'com.alibaba.dubbo.demo.DemoService', 655 | }, val => { 656 | if (trigger_3) return; 657 | 658 | trigger_3 = true; 659 | assert(val && val.length > 0); 660 | console.log('3', val.map(url => url.host)); 661 | if (val.length === 2) { 662 | assert(val.some(url => url.host === '30.20.78.300:20880')); 663 | } 664 | done(); 665 | }); 666 | 667 | master.publish({ 668 | dataId: 'com.alibaba.dubbo.demo.DemoService', 669 | publishData: 'dubbo://30.20.78.300:20880/com.alibaba.dubbo.demo.DemoService?anyhost=true&application=demo-provider&dubbo=2.0.0&generic=false&interface=com.alibaba.dubbo.demo.DemoService&loadbalance=roundrobin&methods=sayHello&owner=william&pid=81281&side=provider×tamp=1481613276143', 670 | }); 671 | }, 5000); 672 | }); 673 | }); 674 | 675 | describe('wait for Leader', () => { 676 | const port = 2220 + portDelta; 677 | 678 | it('should subscribe ok', done => { 679 | const follower = cluster(RegistryClient, { port, isLeader: false, maxWaitTime: 3000 }).create(4322, '224.5.6.9'); 680 | follower.once('error', err => { 681 | assert(err.message === `[ClusterClient] leader does not be active in 3000ms on port:${port}`); 682 | done(); 683 | }); 684 | }); 685 | }); 686 | 687 | describe('leader subscribe', () => { 688 | let port; 689 | before(done => { 690 | const server = net.createServer(); 691 | server.listen(0, () => { 692 | port = server.address().port; 693 | console.log('using port =>', port); 694 | server.close(); 695 | done(); 696 | }); 697 | }); 698 | 699 | it('should subscribe mutli data at same time', function* () { 700 | const client = cluster(NotifyClient, { port }) 701 | .delegate('publish', 'invoke') 702 | .create(); 703 | client.subscribe({ dataId: 'foo' }, val => { 704 | client.emit('foo_1', val); 705 | }); 706 | client.subscribe({ dataId: 'foo' }, val => { 707 | client.emit('foo_2', val); 708 | }); 709 | client.subscribe({ dataId: 'bar' }, val => { 710 | client.emit('bar_1', val); 711 | }); 712 | 713 | let result = yield [ 714 | client.publish({ dataId: 'foo', publishData: 'xxx' }), 715 | client.await('foo_1'), 716 | client.await('foo_2'), 717 | ]; 718 | assert(result && result.length === 3); 719 | assert(result[1] === 'xxx'); 720 | assert(result[2] === 'xxx'); 721 | 722 | result = yield [ 723 | client.publish({ dataId: 'bar', publishData: 'yyy' }), 724 | client.await('bar_1'), 725 | ]; 726 | assert(result && result.length === 2); 727 | assert(result[1] === 'yyy'); 728 | 729 | cluster.close(client); 730 | }); 731 | }); 732 | 733 | describe('error', () => { 734 | it('should throw error if delegate to not implement method', () => { 735 | assert.throws(() => { 736 | cluster(NotifyClient) 737 | .delegate('not-exist') 738 | .create(); 739 | }, /\[ClusterClient\] api\: not-exist not implement in client/); 740 | }); 741 | }); 742 | }); 743 | --------------------------------------------------------------------------------