├── .eslintrc ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── examples ├── broker.js ├── config.json ├── echo │ ├── client-flood.js │ ├── client.js │ └── worker.js └── stocks │ ├── client.js │ └── worker.js ├── index.js ├── lib ├── Broker.js ├── BrokerController.js ├── Client.js ├── Worker.js ├── mdp.js └── utils.js ├── package.json ├── services ├── Base.js └── Directory.js └── test ├── client.js ├── concurrency.js ├── directory.js ├── index.js ├── requestStream.js ├── semver.js ├── startStop.js ├── target.js ├── timeout.js ├── wildcards.js ├── worker.js └── z9_system.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "indent": [ 4 | 2, 5 | 2, 6 | { 7 | "SwitchCase": 1 8 | } 9 | ], 10 | "quotes": [ 11 | 2, 12 | "single" 13 | ], 14 | "linebreak-style": [ 15 | 2, 16 | "unix" 17 | ], 18 | "semi": [ 19 | 2, 20 | "always" 21 | ], 22 | "space-before-function-paren": [ 23 | 2, 24 | "never" 25 | ] 26 | }, 27 | "env": { 28 | "node": true, 29 | "mocha": true 30 | }, 31 | "extends": "eslint:recommended" 32 | } 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.swp 3 | npm-debug.log 4 | # webstorm stuff 5 | .idea 6 | atlassian-ide-plugin.xml -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | env: 2 | - ZMQ="git://github.com/zeromq/zeromq4-1.git" ZMQ_VERSION="v4.1.2" SODIUM="git://github.com/jedisct1/libsodium.git" SODIUM_VERSION="1.0.3" CXX=g++-4.8 3 | addons: 4 | apt: 5 | sources: 6 | - ubuntu-toolchain-r-test 7 | packages: 8 | - g++-4.8 9 | before_install: 10 | - sudo apt-get install uuid-dev 11 | - '[ -z "$SODIUM" ] || git clone --depth 1 --no-single-branch $SODIUM libsodium' 12 | - '[ -z "$SODIUM" ] || cd libsodium' 13 | - '[ -z "$SODIUM" ] || git checkout $SODIUM_VERSION' 14 | - '[ -z "$SODIUM" ] || ./autogen.sh' 15 | - '[ -z "$SODIUM" ] || ./configure' 16 | - '[ -z "$SODIUM" ] || make' 17 | - '[ -z "$SODIUM" ] || sudo make install' 18 | - '[ -z "$SODIUM" ] || cd ..' 19 | - git clone --depth 1 --no-single-branch $ZMQ zmqlib 20 | - cd zmqlib 21 | - git checkout $ZMQ_VERSION 22 | - ./autogen.sh 23 | - ./configure 24 | - make 25 | - sudo make install 26 | - sudo /sbin/ldconfig 27 | - cd .. 28 | language: node_js 29 | node_js: 30 | - "4.2" 31 | - "5.5" 32 | script: travis_retry npm test 33 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # PIGATO CHANGELOG 2 | 3 | ### v0.0.45 4 | * Minor fix 5 | 6 | ### v0.0.44 7 | * Improved internal structures. 8 | * Added support for Worker liveness refresh via Client. 9 | 10 | ### v0.0.43 11 | * Js lint 12 | * BREAKING: Worker and Client now must specificy 'prefix' instead of 'name'. The socket identifier is generated using 'prefix' followed by a random uuid. This fixes issues in socket identifier overlapping. 13 | * Support for Worker semver 14 | 15 | ### v0.0.42 16 | * Upgraded to zmq-2.13.0 to support iojs >= 3.0 17 | * Removed cache support from Broker (shoulbe be refactored) 18 | * Broker minor refactoring 19 | * Switched to use an improved performance queue library 20 | 21 | ### v0.0.41 22 | * Minor performance improvements 23 | * Test fixes 24 | 25 | ### v0.0.40 26 | * Worker can see Client request options 27 | * Broker stop timeout (flush sockets) 28 | * Fix Worker call reply.end() without arguments 29 | * Added error emitter when Broker receives invalid messages 30 | * Worker heartbeating logic improvement 31 | 32 | ### v0.0.39 33 | * Minor perf improvements 34 | * Load and rand broker policies 35 | * Wildcard fixes (maxired) 36 | * Symmetric behaviour for Client/Worker/Broker (maxired) 37 | * Add Broker onStart/onStop callbacks and start/stop events (maxired) 38 | * Add Worker onConnect/onDisconnect callbacks and connect/disconnect/start/stop events (maxired) 39 | * Add Client onConnect/onDisconnect callbacks and connect/disconnect/start/stop events (maxired) 40 | * More tests (maxired) 41 | * A bit of refactoring 42 | 43 | ### v.0.0.38 44 | * Minor perf improvements 45 | 46 | ### v.0.0.37 47 | * Minor changes 48 | 49 | ### v.0.0.36 50 | * Minor changes 51 | 52 | ### v.0.0.35 53 | * Core Services exported 54 | * Core Services documentation 55 | 56 | ### v.0.0.34 57 | * Test suite refactoring for improved execution speed 58 | * Support for targeting a Client Request to a specific Worker using the Request option workerId 59 | * Minor Broker code refactoring 60 | * Broker dispatcher improvements 61 | * Refactored test directory structure 62 | * New test for file descriptors management 63 | * Fixed Broker internal request-map memory leak 64 | 65 | ### v.0.0.33 66 | * Minor fixes 67 | * Stress test for file descriptors 68 | * Changelog moved to its own file 69 | 70 | ### v.0.0.32 71 | * Minor fix in `client.requestStream` 72 | * Changelog and protocol specs 73 | 74 | ### v.0.0.31 75 | * Support for `opts.nocache` flag in `client.request` : Client requests a fresh uncached reply 76 | 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Paolo Ardoino 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 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @mocha 3 | 4 | .PHONY: test 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PIGATO 2 | ======== 3 | 4 | Checkout **[Grenache](https://github.com/bitfinexcom/grenache)** , a DHT based high-performance microservices framework for Node.js, Ruby and Go. Supports ZeroMQ and WebSocket transports. 5 | 6 | **PIGATO - an high-performance Node.js microservices framework based on ZeroMQ** 7 | 8 | PIGATO aims to offer an high-performance, reliable, scalable and extensible service-oriented framework supporting multiple programming languages: Node.js/Io.js and Ruby. 9 | 10 | [![Travis Build Status](https://travis-ci.org/prdn/pigato.svg?style=flat)](https://travis-ci.org/prdn/pigato) 11 | [![NPM version](http://img.shields.io/npm/v/pigato.svg?style=flat)](https://www.npmjs.com/package/pigato) 12 | 13 | **Supported Programming Languages** 14 | 15 | * [PIGATO](https://github.com/prdn/pigato) : PIGATO Client/Worker/Broker for Node.js / Io.js 16 | * [PIGATO-RUBY](https://github.com/prdn/pigato-ruby) : PIGATO Client/Worker for Ruby 17 | * [PIGATO-GO](https://github.com/prdn/pigato-go) : PIGATO Client for Go 18 | 19 | 20 | ## Structure and Protocol 21 | 22 | ### Actors 23 | * Worker : receives requests, does something and replies. A Worker offers a Service, should be a functionality as atomic as possible 24 | * Client : creates, pushes Requests and waits for results. A request always includes a service name and data for the Worker 25 | * Broker : handles Requests queueing and routing 26 | 27 | ### Benefits 28 | * High-performance 29 | * Realiable, Distributed and Scalable 30 | * Load Balancing 31 | * No central point of failure 32 | * Multi-Worker : infinite Services and infinite Workers for each Service 33 | * Multi-Client : infinite Clients 34 | * Multi-Broker : infinite Brokers to avoid bottlenecks and improve network reliability 35 | 36 | ### Features 37 | * Request / Reply protocol 38 | * Support for partial Replies 39 | * Client concurrent Requests 40 | * Client streaming Requests 41 | * Worker concurrent Requests 42 | * Worker dynamic load balancing 43 | * Client heartbeating for long running requests. Allows Workers to dected whenever Clients disconnect or lose interest in some request. This feature is very useful to stop long-running partial requests (i.e data streaming). 44 | 45 | ## Examples 46 | 47 | Start a **Broker** 48 | ``` 49 | node examples/broker 50 | ``` 51 | 52 | 1) **echo** : simple echo request-reply 53 | ``` 54 | node examples/echo/worker 55 | node examples/echo/client 56 | ``` 57 | 58 | 2) **stocks** : get stocks data from yahoo 59 | ``` 60 | node examples/stocks/worker 61 | node examples/stocks/client 62 | ``` 63 | 64 | 65 | More examples 66 | 67 | [PIGATO-EXAMPLES](https://github.com/fincluster/pigato-examples) : a collection of multi-purpose useful examples. 68 | 69 | ### Performance 70 | 71 | [PIGATO-PERF](https://github.com/prdn/pigato-perf) : a command-line tool to test PIGATO performances in different scenarios. 72 | i 73 | ## API 74 | 75 | ### Broker 76 | #### `PIGATO.Broker(addr, conf)` 77 | * `addr` - Broker address (string, i.e: 'tcp://*:12345') 78 | * `conf` - configuration override (type=object, i.e { concurrency: 20 }) 79 | * `onStart`: function to be called when the Broker start 80 | * `onStop`: function to be called when the Broker stop 81 | 82 | Simply starts up a broker. 83 | 84 | ``` 85 | var Broker = require('./../index').Broker; 86 | 87 | var broker = new Broker("tcp://*:55555"); 88 | broker.start(function() { 89 | console.log("Broker started"); 90 | }); 91 | ``` 92 | 93 | #### Events 94 | * `start` : on Client start 95 | * `stop` : on Client stop 96 | 97 | ### Worker 98 | #### `PIGATO.Worker(addr, serviceName, conf)` 99 | * `addr` - Broker address (type=string, i.e: 'tcp://localhost:12345') 100 | * `serviceName` - service implemented by the Worker (type=string, i.e: 'echo') 101 | * wildcards * are supported (i.e: 'ech*') 102 | * `conf` - configuration override (type=object, i.e { concurrency: 20 }) 103 | * `prefix` - sets the Worker identifier prefix 104 | * `concurrency` - sets max number of concurrent requests (type=int, -1 = no limit) 105 | * `onConnect`: function to be called when the Worker connects to the Broker 106 | * `onDisconnnect`: function to be called when the Worker disconnects from the Broker 107 | 108 | #### Methods 109 | 110 | ##### `on` 111 | Worker receives `request` events with 3 arguments: 112 | * `data` - data sent from the Client (type=string/object/array). 113 | * `reply` - extended writable stream (type=object) 114 | * `opts` - client request options (type=object) 115 | 116 | `reply` writable stream exposes also following methods and attributes: 117 | 118 | * `write()` - sends partial data to the Client 119 | * `end()` - sends last data to the Client and completes/closes current Request 120 | * `reject()` - rejects a Request. 121 | * `heartbeat()` - forces sending heartbeat to the Broker 122 | * `active()` - returns the status of the Request (type=boolean). A Request becomes inactive when the Worker disconnects from the Broker or it has been discarded by the Client or the Client disconnects from the Broker. This is useful for long running tasks so the Worker can monitor whether or not continue processing a Request. 123 | * `ended` - tells if the Request has been ended (type=boolean). 124 | 125 | **Example** 126 | 127 | ``` 128 | var worker = new PIGATO.Worker('tcp://localhost:12345', 'my-service'); 129 | worker.start(); 130 | 131 | worker.on('request', function(data, reply, copts) { 132 | for (var i = 0; i < 1000; i++) { 133 | reply.write('PARTIAL DATA ' + i); 134 | } 135 | reply.end('FINAL DATA'); 136 | }); 137 | 138 | // or 139 | worker.on('request', function(data, reply, copts) { 140 | fs.createReadStream(data).pipe(reply); 141 | }); 142 | ``` 143 | 144 | Worker can change concurrency level updating its configuration. This information is carried with the heartbeat message. 145 | 146 | **Example** 147 | 148 | ``` 149 | worker.conf.concurrency = 2; 150 | ``` 151 | 152 | Take note: due to the framing protocol of `zmq` only the data supplied to `response.end(data)` will be given to the client's final callback. 153 | 154 | #### Events 155 | * `start` : on Worker start 156 | * `stop` : on Worker stop 157 | * `connect` : on Worker connect 158 | * `disconnect` : on Worker disconnect 159 | 160 | 161 | ### Client 162 | #### `PIGATO.Client(addr, conf)` 163 | * `addr` - Broker address (type=string, i.e: 'tcp://localhost:12345') 164 | * `conf` 165 | * `prefix` - sets the Client identifier prefix 166 | * `autostart`: automatically starts the Client (type=boolean, default=false) 167 | * `onConnect`: function to be called when the Client connects to the Broker 168 | * `onDisconnnect`: function to be called when the Client disconnects from the Broker 169 | 170 | #### Methods 171 | 172 | ##### `start` 173 | 174 | Start the Client 175 | 176 | ##### `request` 177 | 178 | Send a Request 179 | 180 | * `serviceName` - name of the Service we wish to connect to (type=string) 181 | * `data` - data to give to the Service (type=string/object/buffer) 182 | * `opts` - options for the Request (type=object) 183 | * `timeout`: timeout in milliseconds (type=number, default=60000, -1 for infinite timeout) 184 | * `retry`: if a Worker dies before replying, the Request is automatically requeued. (type=number, values=0|1, default=0) 185 | * `nocache`: skip Broker's cache 186 | * `workerId`: ID of the Worker that must handle the Request (type=string) 187 | 188 | 189 | **Example** 190 | 191 | ``` 192 | var client = new PIGATO.Client('tcp://localhost:12345'); 193 | client.start() 194 | 195 | client.request('my-service', { foo: 'bar' }, { timeout: 120000 }) 196 | .on('data', function(data) { 197 | console.log("DATA", data); 198 | }) 199 | .on('end', function() { 200 | console.log("END"); 201 | }); 202 | 203 | // or 204 | client.request('my-service', 'foo', { timeout: 120000 }).pipe(process.stdout); 205 | ``` 206 | 207 | Clients may also make request with partial and final callbacks instead of using streams. 208 | 209 | * `serviceName` 210 | * `data` 211 | * `partialCallback(err, data)` - called whenever the request does not end but emits data 212 | * `finalCallback(err, data)` - called when the request will emit no more data 213 | * `opts` 214 | 215 | **Example** 216 | 217 | ``` 218 | client.request('my-service', 'foo', function (err, data) { 219 | // frames sent prior to final frame 220 | console.log('PARTIAL', data); 221 | }, function (err, data) { 222 | // this is the final frame sent 223 | console.log('FINAL', data); 224 | }, { timeout: 30000 }); 225 | 226 | ``` 227 | 228 | #### Events 229 | * `start` : on Client start 230 | * `stop` : on Client stop 231 | * `connect` : on Client connect 232 | * `disconnect` : on Client disconnect 233 | 234 | ### Core Services 235 | Core services are a set of Services that interact with a Broker via a dedicated PUB/SUB channel to extend its core functionalities. 236 | 237 | #### Initialization 238 | 239 | ``` 240 | var broker = new PIGATO.Broker(bhost); 241 | var csrv = new PIGATO.services.ExampleCoreService(bhost, { 242 | intch: broker.conf.intch // internal pub/sub channel 243 | }); 244 | broker.start(); 245 | csrv.start(); 246 | ``` 247 | 248 | #### Directory 249 | ##### `PIGATO.services.Directory` 250 | 251 | Directory service ($dir) replies to Requests with the list of available Workers for a selected service. 252 | 253 | **Example** 254 | 255 | ``` 256 | // Broadcast a message to all Workers that offer 'echo' Service 257 | client.request( 258 | '$dir', 'echo', undefined, 259 | function(err, workers) { 260 | workers.forEach(function(wid) { 261 | client.request('echo', 'foo', { workerId: wid }); 262 | }); 263 | } 264 | ); 265 | ``` 266 | 267 | ### Notes 268 | * when using a `inproc` socket the broker *must* become active before any queued messages. 269 | 270 | ## Specification (good for RFC) 271 | * Worker <-> Broker heartbeating. 272 | * Broker tracks Worker/Client/Request relation. 273 | * Client MAY send heartbeat for active request. If the request is being processed by Worker, Broker forwards heartbeat to Worker. 274 | * Worker MAY decide to stop an inactive Request (tracks liveness for Request). 275 | * Client MAY assign a timeout to a Request. 276 | * Worker SHALL NOT send more W_REPLY (for a Request) after sending first W_REPLY message. 277 | * Broker SHALL force disconnect Worker if any error occurs. 278 | 279 | ## Protocol 280 | 281 | #### Common 282 | * Frame 0: Side tag (MDP.CLIENT/MDP.WORKER) 283 | * Frame 1: Message type (MDP.W_REQUEST, MDP.W_REPLY, MDP.W_REPLY_REJECT, ...) 284 | * Frame 2: Service name 285 | * Frame 3: Request ID (uuid) 286 | 287 | #### Client request 288 | * Frame 4: JSON encode request data 289 | * Frame 5: JSON encode request options 290 | 291 | #### Worker reply 292 | * Frame 4: Numeric status (0=OK, -1=ERROR) 293 | * Frame 5: JSON encode request data / error 294 | * Frame 6: JSON encode request options 295 | 296 | ## Changelog 297 | 298 | [CHANGELOG](CHANGELOG.md) 299 | 300 | ## Roadmap 301 | * Add authentication support through [zmq-zap](https://github.com/msealand/zmq-zap.node) ZeroMQ ZAP to trust Clients and Workers. 302 | 303 | ## Follow me 304 | 305 | * My personal blog : [ardoino.com](http://ardoino.com) / [@paoloardoino](https://twitter.com/paoloardoino) 306 | 307 | ## Contributors 308 | * [bmeck](https://github.com/bmeck) 309 | * [maxired](https://github.com/maxired) 310 | * [leonpegg](https://github.com/leonpegg) 311 | -------------------------------------------------------------------------------- /examples/broker.js: -------------------------------------------------------------------------------- 1 | var Broker = require('./../index').Broker; 2 | var conf = require('./config.json'); 3 | 4 | var broker = new Broker(conf.broker.host); 5 | broker.start(); 6 | -------------------------------------------------------------------------------- /examples/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "broker": { 3 | "host": "tcp://127.0.0.1:55555" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/echo/client-flood.js: -------------------------------------------------------------------------------- 1 | var Client = require('./../../index').Client; 2 | var conf = require('../config.json'); 3 | 4 | var client = new Client(conf.broker.host); 5 | client.start(); 6 | 7 | client.on('error', function(e) { 8 | if (e === 'ERR_REQ_INVALID') { 9 | inv++; 10 | } 11 | console.log('ERROR', e); 12 | }); 13 | 14 | var d1 = new Date(); 15 | var reqs = 100000; 16 | 17 | var rcnt = 0; 18 | var inv = 0; 19 | 20 | for (var i = 0; i < reqs; i++) { 21 | client.request( 22 | 'echo', 'foo', 23 | function(err, data) {}, 24 | function(err, data) { 25 | rcnt++; 26 | if (rcnt === reqs) { 27 | console.log(reqs + ' requests/replies processed (' + ((new Date()).getTime() - d1.getTime()) + ' milliseconds)', 'inv=' + inv); 28 | process.exit(0); 29 | } 30 | }, { timeout: 10000 } 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /examples/echo/client.js: -------------------------------------------------------------------------------- 1 | var Client = require('./../../index').Client; 2 | var conf = require('../config.json'); 3 | 4 | var client = new Client(conf.broker.host); 5 | client.start(); 6 | 7 | client.on('error', function(e) { 8 | console.log('ERROR', e); 9 | }); 10 | 11 | // STREAM MODE 12 | console.log("CLIENT SEND REQUEST (stream mode)"); 13 | client.request( 14 | 'echo', 'foo-stream', 15 | { timeout: 10000 } 16 | ) 17 | .on('data', function(data) { 18 | console.log("DATA", data); 19 | }) 20 | .on('end', function() { 21 | console.log("END"); 22 | }); 23 | 24 | // CALLBACK MODE 25 | console.log("CLIENT SEND REQUEST (callback mode)"); 26 | client.request( 27 | 'echo', 'foo-callback', 28 | function(err, data) { 29 | console.log("PARTIAL", err, data); 30 | }, 31 | function(err, data) { 32 | console.log("FINAL", err, data); 33 | }, { timeout: 10000 } 34 | ); 35 | -------------------------------------------------------------------------------- /examples/echo/worker.js: -------------------------------------------------------------------------------- 1 | var Worker = require('./../../index').Worker; 2 | var conf = require('../config.json'); 3 | 4 | var worker = new Worker(conf.broker.host, 'echo') 5 | worker.start(); 6 | 7 | worker.on('error', function(e) { 8 | console.log('ERROR', e); 9 | }); 10 | 11 | worker.on('request', function(inp, rep, opts) { 12 | rep.opts.cache = 10000; 13 | rep.end(inp); 14 | }); 15 | -------------------------------------------------------------------------------- /examples/stocks/client.js: -------------------------------------------------------------------------------- 1 | var Client = require('./../../index').Client; 2 | var conf = require('../config.json'); 3 | 4 | var client = new Client(conf.broker.host); 5 | client.start(); 6 | 7 | client.on( 8 | 'error', 9 | function(err) { 10 | console.log("CLIENT ERROR", err); 11 | } 12 | ); 13 | 14 | // Streaming implementation 15 | 16 | var res = client.request( 17 | 'stock', 18 | { 19 | ticker:'AAPL', 20 | startDay:'1', 21 | startMonth:'6', 22 | startYear:'2013', 23 | endDay:'1', 24 | endMonth:'6', 25 | endYear:'2014', 26 | freq:'d' 27 | }, 28 | { timeout: 90000 } 29 | ); 30 | 31 | var body = ''; 32 | res.on('data', function(data) { 33 | body += data; 34 | }).on('end', function() { 35 | console.log(body); 36 | }); 37 | -------------------------------------------------------------------------------- /examples/stocks/worker.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var fs = require('fs'); 3 | var Worker = require('./../../index').Worker; 4 | 5 | var conf = JSON.parse(fs.readFileSync(__dirname + '/../config.json', 'UTF-8')); 6 | 7 | var worker = new Worker(conf.broker.host, 'stock') 8 | worker.start(); 9 | 10 | worker.on('error',function(err) { 11 | console.log("WORKER ERROR", err); 12 | }); 13 | 14 | worker.on('request',function(inp, rep){ 15 | var url="http://ichart.finance.yahoo.com/table.csv?s=" + inp.ticker + 16 | "&a=" + inp.startDay + "&b=" + inp.startMonth+"&c=" + inp.startYear + "&d=" + 17 | inp.endYear + "&e=" + inp.endMonth + "&f=" + inp.endYear + "&g=" + inp.freq + "&ignore=.csv"; 18 | 19 | //console.log(url); 20 | http.get(url, function(res) { 21 | res.on('data', function(chunk) { 22 | rep.write(String(chunk)); 23 | }).on('end', function() { 24 | rep.end(''); 25 | }); 26 | }).on('error', function(e) { 27 | console.log("Got error: " + e.message); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports.Broker = require('./lib/Broker'); 2 | module.exports.Worker = require('./lib/Worker'); 3 | module.exports.Client = require('./lib/Client'); 4 | module.exports.services = { 5 | Directory: require('./services/Directory') 6 | }; 7 | -------------------------------------------------------------------------------- /lib/Broker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('es6-shim'); 4 | 5 | var util = require('util'); 6 | var debug = require('debug')('pigato:Broker'); 7 | var uuid = require('node-uuid'); 8 | var events = require('events'); 9 | var zmq = require('zmq'); 10 | var async = require('async'); 11 | var _ = require('lodash'); 12 | var semver = require('semver'); 13 | var MDP = require('./mdp'); 14 | var putils = require('./utils'); 15 | 16 | var BaseController = require('./BrokerController'); 17 | 18 | function Broker(endpoint, conf) { 19 | this.services = {}; 20 | this.workers = new Map(); 21 | this.rmap = new Map(); 22 | 23 | this.conf = { 24 | heartbeat: 2500, 25 | heartbeatLiveness: 3, 26 | dmode: 'load', 27 | name: 'B' + uuid.v4(), 28 | intch: 'tcp://127.0.0.1:55550' 29 | }; 30 | 31 | _.extend(this.conf, conf); 32 | 33 | this.endpoint = endpoint; 34 | 35 | if (this.conf.ctrl) { 36 | this.ctrl = this.conf.ctrl; 37 | } else { 38 | this.ctrl = new BaseController(); 39 | } 40 | 41 | events.EventEmitter.call(this); 42 | } 43 | util.inherits(Broker, events.EventEmitter); 44 | 45 | Broker.prototype.start = function() { 46 | this.socket = zmq.socket('router'); 47 | this.socket.identity = this.conf.name; 48 | this.socket.setsockopt('linger', 1); 49 | 50 | this.pub = zmq.socket('pub'); 51 | this.pub.identity = this.conf.name + '/pub'; 52 | 53 | var self = this; 54 | 55 | this.socket.on('message', function() { 56 | var args = putils.args(arguments); 57 | self.onMsg.call(self, args); 58 | }); 59 | 60 | this.hbTimer = setInterval(function() { 61 | self.workersCheck(); 62 | }, this.conf.heartbeat); 63 | 64 | this.srvTimer = setInterval(function() { 65 | var supd = false; 66 | 67 | _.each(self.services, function(service, srvName) { 68 | if (!service.workers.size) { 69 | supd = true; 70 | delete self.services[srvName]; 71 | } 72 | }); 73 | 74 | if (supd) { 75 | self.servicesUpdate(); 76 | } 77 | 78 | self.rmap.forEach(function(req) { 79 | var vld = self.requestValidate(req); 80 | if (!vld) { 81 | self.requestDelete(req); 82 | } 83 | }); 84 | }, 60000); 85 | 86 | var queue = []; 87 | 88 | queue.push(function(next) { 89 | self.socket.bind(self.endpoint, function() { 90 | next(); 91 | }); 92 | }); 93 | 94 | queue.push(function(next) { 95 | self.pub.bind(self.conf.intch, function() { 96 | next(); 97 | }); 98 | }); 99 | 100 | queue.push(function(next) { 101 | self.ctrl.rgetall(function(err, reqs) { 102 | _.each(reqs, function(req) { 103 | self.requestProcess(req); 104 | }); 105 | next(); 106 | }); 107 | }); 108 | 109 | async.series(queue, function() { 110 | debug('B: broker started on %s', self.endpoint); 111 | setImmediate(self.onStart.bind(self)); 112 | }); 113 | }; 114 | 115 | Broker.prototype.stop = function() { 116 | var self = this; 117 | 118 | clearInterval(this.hbTimer); 119 | clearInterval(this.srvTimer); 120 | 121 | var queue = []; 122 | 123 | queue.push(function(next) { 124 | setTimeout(function() { 125 | next(); 126 | }, 100); 127 | }); 128 | 129 | if (self.socket) { 130 | queue.push(function(next) { 131 | self.socket.unbind(self.endpoint, function() { 132 | self.socket.close(); 133 | delete self.socket; 134 | next(); 135 | }); 136 | }); 137 | } 138 | 139 | if (self.pub) { 140 | queue.push(function(next) { 141 | self.pub.unbind(self.conf.intch, function() { 142 | self.pub.close(); 143 | delete self.pub; 144 | next(); 145 | }); 146 | }); 147 | } 148 | 149 | async.series(queue, function() { 150 | delete self.socket; 151 | delete self.pub; 152 | 153 | setImmediate(self.onStop.bind(self)); 154 | }); 155 | }; 156 | 157 | Broker.prototype.onStart = function() { 158 | if (this.conf.onStart) { 159 | this.conf.onStart(); 160 | } 161 | this.emit('start'); 162 | }; 163 | 164 | Broker.prototype.onStop = function() { 165 | if (this.conf.onStop) { 166 | this.conf.onStop(); 167 | } 168 | this.emit('stop'); 169 | }; 170 | 171 | Broker.prototype.send = function(msg) { 172 | if (!this.socket) { 173 | return; 174 | } 175 | 176 | this.socket.send(msg); 177 | }; 178 | 179 | Broker.prototype.onMsg = function(_msg) { 180 | var msg = putils.mparse(_msg); 181 | 182 | var header = msg[1]; 183 | 184 | if (header == MDP.CLIENT) { 185 | this.onClient(msg); 186 | } else if (header == MDP.WORKER) { 187 | this.onWorker(msg); 188 | } else { 189 | this.emitErr('ERR_MSG_HEADER'); 190 | } 191 | 192 | this.workersCheck(); 193 | }; 194 | 195 | Broker.prototype.emitErr = function(msg) { 196 | this.emit.apply(this, ['error', msg]); 197 | }; 198 | 199 | Broker.prototype.onClient = function(msg) { 200 | var clientId = msg[0]; 201 | var type = msg[2]; 202 | 203 | var req; 204 | var rid; 205 | if (type == MDP.W_REQUEST) { 206 | var srvName = msg[3] || 'UNK'; 207 | rid = msg[4]; 208 | debug('B: REQUEST from clientId: %s, service: %s', clientId, srvName); 209 | 210 | var opts = msg[6]; 211 | 212 | try { 213 | opts = JSON.parse(opts); 214 | } catch (e) { 215 | // Ignore 216 | } 217 | if (!_.isObject(opts)) { 218 | opts = {}; 219 | } 220 | 221 | req = { 222 | service: srvName, 223 | clientId: clientId, 224 | attempts: 0, 225 | rid: rid, 226 | timeout: opts.timeout || 60000, 227 | ts: (new Date()).getTime(), 228 | rejects: new Map(), 229 | msg: msg, 230 | opts: opts 231 | }; 232 | 233 | this.requestProcess(req); 234 | } else if (type == MDP.W_HEARTBEAT) { 235 | if (msg.length === 5) { 236 | var type = msg[3]; 237 | if (type === 'req') { 238 | rid = req[4]; 239 | req = this.rmap.get(rid); 240 | if (req && req.workerId) { 241 | this.send([req.workerId, MDP.WORKER, MDP.W_HEARTBEAT, clientId, '', rid]); 242 | } 243 | } else if (type === 'worker') { 244 | var worker = this.workers.get(msg[4]); 245 | if (worker) { 246 | worker.liveness = this.conf.heartbeatLiveness; 247 | } 248 | } 249 | } else { 250 | this.send([clientId, MDP.CLIENT, MDP.W_HEARTBEAT]); 251 | } 252 | } 253 | }; 254 | 255 | Broker.prototype.onWorker = function(msg) { 256 | var workerId = msg[0]; 257 | var type = msg[2]; 258 | 259 | var wready = this.workers.has(workerId); 260 | var worker = this.workerRequire(workerId); 261 | 262 | var opts; 263 | 264 | 265 | if (type == MDP.W_READY) { 266 | var srvName = msg[3]; 267 | 268 | debug('B: register worker: %s, service: %s', workerId, srvName, wready ? 'R' : ''); 269 | 270 | if (!srvName) { 271 | this.workerDelete(workerId, true); 272 | 273 | } else if (!wready) { 274 | this.serviceRequire(srvName); 275 | this.serviceWorkerAdd(srvName, workerId); 276 | 277 | this.send([workerId, MDP.WORKER, MDP.W_READY]); 278 | } 279 | return; 280 | } 281 | 282 | if (!wready) { 283 | this.workerDelete(workerId, true); 284 | return; 285 | } 286 | 287 | worker.liveness = this.conf.heartbeatLiveness; 288 | 289 | if (type == MDP.W_REPLY || type == MDP.W_REPLY_PARTIAL || type == MDP.W_REPLY_REJECT) { 290 | var rid = msg[5]; 291 | 292 | if (!worker.rids.has(rid)) { 293 | debug('B: FATAL from worker \'%s\' (%s), rid not found mismatch \'%s\'', workerId, worker.service, rid); 294 | this.workerDelete(workerId, true); 295 | return; 296 | } 297 | 298 | var req = this.rmap.get(rid); 299 | if (!req) { 300 | debug('B: FATAL from worker \'%s\' (%s), req not found', workerId, worker.service, rid); 301 | this.workerDelete(workerId, true); 302 | return; 303 | } 304 | 305 | var service = this.serviceRequire(req.service); 306 | 307 | if (type == MDP.W_REPLY_REJECT) { 308 | debug('B: REJECT from worker \'%s\' (%s) for req \'%s\'', workerId, worker.service, rid); 309 | 310 | req.rejects.set(workerId, 1); 311 | delete req.workerId; 312 | worker.rids.delete(rid); 313 | 314 | service.q.push(req.rid); 315 | this.dispatch(req.service); 316 | 317 | } else if (type == MDP.W_REPLY || type == MDP.W_REPLY_PARTIAL) { 318 | debug('B: REPLY from worker \'%s\' (%s)', workerId, worker.service); 319 | 320 | opts = msg[8]; 321 | try { 322 | opts = JSON.parse(opts); 323 | } catch (e) { 324 | // Ignore 325 | } 326 | if (!_.isObject(opts)) { 327 | opts = {}; 328 | } 329 | 330 | var obj = msg.slice(6); 331 | 332 | this.reply(type, req, obj); 333 | 334 | if (type == MDP.W_REPLY) { 335 | worker.rids.delete(rid); 336 | this.requestDelete(req); 337 | this.dispatch(req.service); 338 | if (req.service !== worker.service) { 339 | this.dispatch(worker.service); 340 | } 341 | } 342 | } 343 | 344 | } else if (type == MDP.W_HEARTBEAT) { 345 | opts = msg[4]; 346 | try { 347 | opts = JSON.parse(opts); 348 | } catch (e) { 349 | opts = {}; 350 | } 351 | 352 | _.each(['concurrency'], function(fld) { 353 | if (!_.isUndefined(opts[fld])) { 354 | worker.opts[fld] = opts[fld]; 355 | } 356 | }); 357 | 358 | worker.liveness++; 359 | this.send([workerId, MDP.WORKER, MDP.W_HEARTBEAT]); 360 | 361 | if (worker.service) { 362 | if (worker.service.indexOf('$') === 0 && opts.update) { 363 | this.notify(worker.service); 364 | } 365 | } 366 | 367 | } else if (type == MDP.W_DISCONNECT) { 368 | this.workerDelete(workerId, true); 369 | } 370 | }; 371 | 372 | Broker.prototype.reply = function(type, req, msg) { 373 | this.send([req.clientId, MDP.CLIENT, type, '', req.rid].concat(msg)); 374 | }; 375 | 376 | Broker.prototype.requestProcess = function(req) { 377 | var service = this.serviceRequire(req.service); 378 | 379 | this.rmap.set(req.rid, req); 380 | if (req.opts.persist) { 381 | this.ctrl.rset(req); 382 | } 383 | 384 | service.q.push(req.rid); 385 | this.dispatch(req.service); 386 | }; 387 | 388 | Broker.prototype.requestDelete = function(req) { 389 | if (!req) { 390 | return; 391 | } 392 | 393 | this.rmap.delete(req.rid); 394 | if (req.opts.persist) { 395 | this.ctrl.rdel(req); 396 | } 397 | }; 398 | 399 | Broker.prototype.requestValidate = function(req) { 400 | if (!req) { 401 | return false; 402 | } 403 | 404 | if (req.timeout > -1 && ((new Date()).getTime() > req.ts + req.timeout)) { 405 | return false; 406 | } 407 | 408 | return true; 409 | }; 410 | 411 | Broker.prototype.workerRequire = function(workerId) { 412 | if (this.workers.has(workerId)) { 413 | return this.workers.get(workerId); 414 | } 415 | 416 | var worker = { 417 | workerId: workerId, 418 | liveness: this.conf.heartbeatLiveness, 419 | rids: new Map(), 420 | opts: { 421 | concurrency: 100 422 | }, 423 | rcnt: 0 424 | }; 425 | 426 | this.workers.set(workerId, worker); 427 | 428 | return worker; 429 | }; 430 | 431 | Broker.prototype.workerDelete = function(workerId, disconnect) { 432 | var self = this; 433 | 434 | var worker = this.workers.get(workerId); 435 | 436 | if (!worker) { 437 | this.workers.delete(workerId); 438 | return; 439 | } 440 | 441 | debug('B: Worker delete \'%s\' (%s)', workerId, disconnect); 442 | 443 | if (disconnect) { 444 | this.send([workerId, MDP.WORKER, MDP.W_DISCONNECT]); 445 | } 446 | 447 | var service = null; 448 | var dservices = []; 449 | if (worker.service) { 450 | service = this.serviceRequire(worker.service); 451 | service.workers.delete(workerId); 452 | dservices.push(worker.service); 453 | } 454 | 455 | this.workers.delete(workerId); 456 | 457 | worker.rids.forEach(function(__, rid) { 458 | var req = self.rmap.get(rid); 459 | 460 | if (!req) { 461 | return; 462 | } 463 | 464 | delete req.workerId; 465 | 466 | var crd = true; 467 | 468 | if (worker.service) { 469 | if (req.opts.retry) { 470 | service.q.push(req.rid); 471 | crd = false; 472 | } 473 | } 474 | 475 | if (crd) { 476 | self.requestDelete(req); 477 | } 478 | 479 | if (_.indexOf(dservices, req.service) === -1) { 480 | dservices.push(req.service); 481 | } 482 | }); 483 | 484 | this.notify('$dir'); 485 | 486 | if (dservices.length) { 487 | for (var s = 0; s < dservices.length; s++) { 488 | this.dispatch(dservices[s]); 489 | } 490 | } 491 | }; 492 | 493 | Broker.prototype.workersCheck = function() { 494 | var self = this; 495 | 496 | if (this._wcheck) { 497 | if ((new Date()).getTime() - this._wcheck < this.conf.heartbeat) { 498 | return; 499 | } 500 | } 501 | 502 | this._wcheck = (new Date()).getTime(); 503 | 504 | this.workers.forEach(function(worker, workerId) { 505 | if (!worker) { 506 | self.workerDelete(workerId, true); 507 | return; 508 | } 509 | 510 | worker.liveness--; 511 | 512 | if (worker.liveness < 0) { 513 | debug('B: Worker purge \'%s\'', workerId); 514 | self.workerDelete(workerId, true); 515 | return; 516 | } 517 | 518 | if (worker.liveness < self.conf.heartbeatLiveness) { 519 | self.send([workerId, MDP.WORKER, MDP.W_HEARTBEAT]); 520 | } 521 | }); 522 | }; 523 | 524 | Broker.prototype.workerAvailable = function(workerId) { 525 | if (!workerId) { 526 | return false; 527 | } 528 | 529 | var worker = this.workers.get(workerId); 530 | 531 | if (!worker) { 532 | return false; 533 | } 534 | 535 | if (worker.opts.concurrency === -1) { 536 | return true; 537 | } 538 | 539 | if (worker.rids.size < worker.opts.concurrency) { 540 | return true; 541 | } 542 | 543 | return false; 544 | }; 545 | 546 | Broker.prototype.serviceRequire = function(srvName) { 547 | if (this.services[srvName]) { 548 | return this.services[srvName]; 549 | } 550 | 551 | var service = { 552 | name: srvName, 553 | workers: new Map(), 554 | q: [], 555 | aux: [] 556 | }; 557 | 558 | this.services[srvName] = service; 559 | this.servicesUpdate(); 560 | return service; 561 | }; 562 | 563 | Broker.prototype.servicesUpdate = function() { 564 | var self = this; 565 | 566 | _.each(this.services, function(service, srvName) { 567 | service.aux = [srvName]; 568 | }); 569 | 570 | _.each(this.services, function(service, srvName) { 571 | if (srvName.indexOf('@') > 0 && !srvName.match(/@\d+[.]\d+[.]\d+$/)) { 572 | // find all satisfying services 573 | 574 | var prefix = srvName.replace(/@([^@]+)$/, '@'); 575 | var range = srvName.replace(/(^.*@)([^@]+)$/, '$2'); 576 | var satisfying = _.filter(self.services, function(aService, aSrvName) { 577 | if (aSrvName == srvName || aSrvName.indexOf(prefix) !== 0 || !aSrvName.match(/@\d+[.]\d+[.]\d+$/)) { 578 | return false; 579 | } 580 | 581 | var version = aSrvName.replace(/^.+@([^@]+)$/, '$1'); 582 | return semver.satisfies(version, range); 583 | }); 584 | 585 | service.aux = _.map(satisfying, 'name'); 586 | } 587 | }); 588 | 589 | _.each(this.services, function(service, srvName) { 590 | if (!service.workers.size && srvName[srvName.length - 1] !== '*' && srvName.indexOf('@') === -1) { 591 | // let's find the best matching widlcard services 592 | 593 | var bestMatching = _.reduce(self.services, function(acc, aService, aSrvName) { 594 | 595 | if (aSrvName == srvName || aSrvName[aSrvName.length - 1] !== '*' || srvName.indexOf(aSrvName.slice(0, -1)) !== 0) { 596 | return acc; 597 | } 598 | 599 | return aSrvName.length > acc.length ? aSrvName : acc; 600 | }, ''); 601 | 602 | if (bestMatching) { 603 | service.aux.push(bestMatching); 604 | } 605 | } 606 | }); 607 | 608 | _.each(this.services, function(service, srvName) { 609 | service.aux = _.uniq(service.aux); 610 | if (!service.aux.length) { 611 | delete self.services[srvName]; 612 | } 613 | }); 614 | }; 615 | 616 | Broker.prototype.serviceWorkerAdd = function(srvName, workerId) { 617 | var service = this.serviceRequire(srvName); 618 | var worker = this.workerRequire(workerId); 619 | 620 | if (!worker) { 621 | this.workerDelete(workerId, true); 622 | return; 623 | } 624 | 625 | if (!service.workers.has(workerId)) { 626 | worker.service = srvName; 627 | service.workers.set(workerId, 1); 628 | } 629 | 630 | this.notify('$dir'); 631 | this.dispatch(srvName); 632 | }; 633 | 634 | function _rhandle(srvName, workerIds) { 635 | var self = this; 636 | 637 | var service = this.serviceRequire(srvName); 638 | 639 | var rid = service.q.shift(); 640 | if (!rid) { 641 | return 0; 642 | } 643 | 644 | var req = this.rmap.get(rid); 645 | if (!req) { 646 | return 0; 647 | } 648 | 649 | var vld = this.requestValidate(req); 650 | 651 | if (!vld) { 652 | this.rmap.delete(req.rid); 653 | if (req.opts.persist) { 654 | this.ctrl.rdel(req); 655 | } 656 | 657 | return service.q.length; 658 | } 659 | 660 | req.attempts++; 661 | 662 | var workerId = null; 663 | var wcret = 0; 664 | 665 | if (workerIds.length) { 666 | if (req.opts.workerId) { 667 | wcret = 1; 668 | if (self.workerAvailable(req.opts.workerId)) { 669 | workerId = req.opts.workerId; 670 | } 671 | } else { 672 | for (var wi = 0; wi < workerIds.length; wi++) { 673 | var _workerId = workerIds[wi]; 674 | if (self.workerAvailable(_workerId)) { 675 | wcret++; 676 | if (!req.rejects.has(workerId)) { 677 | workerId = _workerId; 678 | break; 679 | } 680 | } 681 | } 682 | } 683 | } 684 | 685 | if (!workerId) { 686 | service.q.push(req.rid); 687 | return wcret; 688 | } 689 | 690 | var worker = this.workerRequire(workerId); 691 | 692 | req.workerId = worker.workerId; 693 | worker.rids.set(req.rid, req); 694 | worker.rcnt++; 695 | 696 | if (req.opts.persist) { 697 | this.ctrl.rset(req); 698 | } 699 | 700 | var obj = [ 701 | worker.workerId, MDP.WORKER, MDP.W_REQUEST, 702 | req.clientId, req.service, '' 703 | ].concat(req.msg.slice(4)); 704 | 705 | this.send(obj); 706 | 707 | return 1; 708 | } 709 | 710 | Broker.prototype.dispatch = function(srvName) { 711 | var self = this; 712 | var service = this.serviceRequire(srvName); 713 | var qlen = service.q.length; 714 | 715 | if (!qlen) { 716 | return; 717 | } 718 | 719 | var workerIds = []; 720 | for (var s = 0; s < service.aux.length; s++) { 721 | var aService = this.serviceRequire(service.aux[s]); 722 | workerIds = workerIds.concat(Array.from(aService.workers.keys())); 723 | } 724 | 725 | if (this.conf.dmode === 'load' && workerIds.length > 1) { 726 | workerIds.sort(function(a, b) { 727 | var wa = self.workers.get(a); 728 | var wb = self.workers.get(b); 729 | var arn = wa.rids.size; 730 | var brn = wb.length; 731 | 732 | if (arn < brn) { 733 | return -1; 734 | } else if (brn < arn) { 735 | return 1; 736 | } 737 | 738 | return wa.rcnt <= wb.rcnt ? -1 : 1; 739 | }); 740 | } else if (this.conf.dmode === 'rand' && workerIds.length > 1) { 741 | workerIds = putils.shuffle(workerIds); 742 | } 743 | 744 | for (var r = 0; r < qlen; r++) { 745 | var ret = _rhandle.call(this, srvName, workerIds); 746 | if (!ret) { 747 | break; 748 | } 749 | } 750 | }; 751 | 752 | Broker.prototype.notify = function(channel) { 753 | switch (channel) { 754 | case '$dir': 755 | this.pub.send('$dir ' + JSON.stringify( 756 | _.reduce(this.services, function(acc, service, srvName) { 757 | acc[srvName] = Array.from(service.workers.keys()); 758 | return acc; 759 | }, {}) 760 | )); 761 | break; 762 | } 763 | }; 764 | 765 | module.exports = Broker; 766 | -------------------------------------------------------------------------------- /lib/BrokerController.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function BrokerController() { 4 | this.__reqs = {}; 5 | this.__srq = {}; 6 | 7 | this.init(); 8 | } 9 | 10 | BrokerController.prototype.init = function() {}; 11 | 12 | BrokerController.prototype._srq = function(srv) { 13 | if (this.__srq[srv]) { 14 | return this.__srq[srv]; 15 | } 16 | 17 | var srq = this.__srq[srv] = []; 18 | return srq; 19 | }; 20 | 21 | BrokerController.prototype.rset = function(req, callback) { 22 | this.__reqs[req.rid] = req; 23 | 24 | if (callback) { 25 | callback(); 26 | } 27 | }; 28 | 29 | BrokerController.prototype.rgetall = function(callback) { 30 | var self = this; 31 | 32 | setImmediate(function() { 33 | callback(null, self.__reqs); 34 | }); 35 | }; 36 | 37 | BrokerController.prototype.rget = function(rid, callback) { 38 | var self = this; 39 | 40 | setImmediate(function() { 41 | callback(null, self.__reqs[rid]); 42 | }); 43 | }; 44 | 45 | BrokerController.prototype.rdel = function(req) { 46 | delete this.__reqs[req.rid]; 47 | }; 48 | 49 | module.exports = BrokerController; 50 | -------------------------------------------------------------------------------- /lib/Client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('es6-shim'); 4 | 5 | var zmq = require('zmq'); 6 | var Readable = require('readable-stream').Readable; 7 | var debug = require('debug')('pigato:Client'); 8 | var uuid = require('node-uuid'); 9 | var util = require('util'); 10 | var events = require('events'); 11 | var _ = require('lodash'); 12 | var MDP = require('./mdp'); 13 | var putils = require('./utils'); 14 | 15 | var HEARTBEAT_LIVENESS = 3; 16 | 17 | function Client(broker, conf) { 18 | 19 | this.broker = broker; 20 | 21 | this.conf = { 22 | autostart: false, 23 | reconnect: 1000, 24 | heartbeat: 2500, 25 | timeout: 60000, 26 | retry: 0, 27 | prefix: 'C' + uuid.v4() 28 | }; 29 | 30 | this.reqs = new Map(); 31 | 32 | _.extend(this.conf, conf); 33 | 34 | events.EventEmitter.call(this); 35 | 36 | if (this.conf.autostart) { 37 | this.start(); 38 | } 39 | } 40 | util.inherits(Client, events.EventEmitter); 41 | 42 | Client.prototype.onConnect = function() { 43 | this.emit.apply(this, ['connect']); 44 | if (this.conf.onConnect) { 45 | this.conf.onConnect(); 46 | } 47 | 48 | debug('C: connected'); 49 | }; 50 | 51 | Client.prototype.onDisconnect = function() { 52 | this.emit.apply(this, ['disconnect']); 53 | if (this.conf.onDisconnect) { 54 | this.conf.onDisconnect(); 55 | } 56 | 57 | debug('C: disconnected'); 58 | }; 59 | 60 | Client.prototype.start = function() { 61 | var self = this; 62 | 63 | this.stop(); 64 | 65 | this._mcnt = 0; 66 | 67 | this.socketId = this.conf.prefix + '-' + uuid.v4(); 68 | this.socket = zmq.socket('dealer'); 69 | this.socket.identity = new Buffer(this.socketId); 70 | this.socket.setsockopt('linger', 1); 71 | 72 | this.socket.on('message', function() { 73 | self.onMsg.call(self, putils.args(arguments)); 74 | }); 75 | 76 | this.socket.on('error', function(err) { 77 | self.emitErr(err); 78 | }); 79 | 80 | this.socket.connect(this.broker); 81 | this.liveness = HEARTBEAT_LIVENESS; 82 | 83 | debug('C: starting'); 84 | 85 | this.hbTimer = setInterval(function() { 86 | self.heartbeat(); 87 | 88 | self.reqs.forEach(function(req, rid) { 89 | if (req.timeout > -1 && ((new Date()).getTime() > req.lts + req.timeout)) { 90 | self.onMsg([ 91 | MDP.CLIENT, MDP.W_REPLY, '', new Buffer(rid), new Buffer('-1'), 92 | new Buffer(JSON.stringify('C_TIMEOUT')) 93 | ]); 94 | } 95 | }); 96 | 97 | self.liveness--; 98 | 99 | if (self.liveness <= 0) { 100 | debug('C: liveness=0'); 101 | self.stop(); 102 | setTimeout(function() { 103 | self.start(); 104 | }, self.conf.reconnect); 105 | } 106 | 107 | }, this.conf.heartbeat); 108 | 109 | this.heartbeat(); 110 | this.emit.apply(this, ['start']); 111 | }; 112 | 113 | Client.prototype.stop = function() { 114 | clearInterval(this.hbTimer); 115 | 116 | if (this.socket) { 117 | debug('C: stopping'); 118 | 119 | var socket = this.socket; 120 | delete this.socket; 121 | delete this.socketId; 122 | 123 | if (socket._zmq.state != zmq.STATE_CLOSED) { 124 | socket.close(); 125 | } 126 | 127 | this.onDisconnect(); 128 | this.emit.apply(this, ['stop']); 129 | } 130 | }; 131 | 132 | Client.prototype.send = function(msg) { 133 | if (!this.socket) { 134 | return; 135 | } 136 | 137 | this.socket.send(msg); 138 | }; 139 | 140 | Client.prototype.onMsg = function(msg) { 141 | msg = putils.mparse(msg); 142 | 143 | var header = msg[0]; 144 | var type = msg[1]; 145 | 146 | this.liveness = HEARTBEAT_LIVENESS; 147 | 148 | if (header != MDP.CLIENT) { 149 | this.emitErr('ERR_MSG_HEADER'); 150 | return; 151 | } 152 | 153 | this._mcnt++; 154 | 155 | if (this._mcnt === 1) { 156 | this.onConnect(); 157 | } 158 | 159 | if (type == MDP.W_HEARTBEAT) { 160 | debug('C: HEARTBEAT'); 161 | return; 162 | } 163 | 164 | if (msg.length < 3) { 165 | this.emitErr('ERR_MSG_LENGTH'); 166 | return; 167 | } 168 | 169 | var rid = msg[3]; 170 | 171 | var req = this.reqs.get(rid); 172 | if (!req) { 173 | this.emitErr('ERR_REQ_INVALID'); 174 | return; 175 | } 176 | 177 | var err = +msg[4] || 0; 178 | var data = msg[5] || null; 179 | 180 | if (data) { 181 | data = JSON.parse(data); 182 | } 183 | 184 | if (err === -1) { 185 | err = data; 186 | data = null; 187 | } 188 | 189 | if (type == MDP.W_REPLY || type == MDP.W_REPLY_PARTIAL) { 190 | req.lts = new Date().getTime(); 191 | 192 | if (type == MDP.W_REPLY) { 193 | req._finalMsg = [err, data]; 194 | req.ended = true; 195 | this.reqs.delete(rid); 196 | } 197 | 198 | if (err) { 199 | req.stream.emit('error', err); 200 | } 201 | 202 | req.stream.push(data); 203 | 204 | if (type == MDP.W_REPLY) { 205 | req.stream.push(null); 206 | } 207 | } else { 208 | this.emitErr('ERR_MSG_TYPE'); 209 | } 210 | }; 211 | 212 | Client.prototype.emitErr = function(msg) { 213 | this.emit.apply(this, ['error', msg]); 214 | }; 215 | 216 | function noop() {} 217 | 218 | function _request(serviceName, data, _opts) { 219 | var self = this; 220 | var rid = uuid.v4(); 221 | var opts = _.isObject(_opts) ? _opts : {}; 222 | 223 | _.each(['timeout', 'retry'], function(fld) { 224 | opts[fld] = opts[fld] !== undefined ? opts[fld] : self.conf[fld]; 225 | }); 226 | 227 | var req = { 228 | rid: rid, 229 | timeout: opts.timeout, 230 | ts: new Date().getTime(), 231 | opts: opts, 232 | heartbeat: function() { 233 | self.heartbeat(rid); 234 | }, 235 | _finalMsg: null, 236 | ended: false 237 | }; 238 | 239 | req.lts = req.ts; 240 | 241 | this.reqs.set(rid, req); 242 | 243 | var stream = new Readable({ 244 | objectMode: true 245 | }); 246 | 247 | stream._read = noop; 248 | stream.heartbeat = req.heartbeat; 249 | 250 | req.stream = stream; 251 | 252 | debug('C: send request', serviceName, rid); 253 | 254 | this.send([ 255 | MDP.CLIENT, MDP.W_REQUEST, serviceName, rid, 256 | JSON.stringify(data), JSON.stringify(opts) 257 | ]); 258 | 259 | return req; 260 | } 261 | 262 | Client.prototype.requestStream = function(serviceName, data, opts) { 263 | return this.request(serviceName, data, opts); 264 | }; 265 | 266 | Client.prototype.request = function() { 267 | var mode = 'stream'; 268 | var serviceName = arguments[0]; 269 | var data = arguments[1]; 270 | var opts, partialCb, finalCb; 271 | 272 | if (arguments.length >= 4) { 273 | mode = 'callback'; 274 | partialCb = arguments[2]; 275 | finalCb = arguments[3]; 276 | opts = arguments[4]; 277 | } else { 278 | opts = arguments[2]; 279 | } 280 | 281 | var req = _request.call(this, serviceName, data, opts); 282 | 283 | if (mode === 'callback') { 284 | req.stream.on('data', function(data) { 285 | if (req.ended) { 286 | return; 287 | } 288 | 289 | if (partialCb) { 290 | partialCb(null, data); 291 | } 292 | }); 293 | 294 | req.stream.on('end', function() { 295 | var msg = req._finalMsg; 296 | 297 | if (finalCb) { 298 | finalCb(msg[0], msg[1]); 299 | } 300 | }); 301 | 302 | req.stream.on('error', noop); 303 | 304 | } else { 305 | return req.stream; 306 | } 307 | }; 308 | 309 | Client.prototype.heartbeat = function(rid) { 310 | var msg = [MDP.CLIENT, MDP.W_HEARTBEAT]; 311 | if (rid) { 312 | msg.push('request', rid); 313 | } 314 | this.send(msg); 315 | }; 316 | 317 | module.exports = Client; 318 | -------------------------------------------------------------------------------- /lib/Worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('es6-shim'); 4 | 5 | var zmq = require('zmq'); 6 | var Writable = require('readable-stream').Writable; 7 | var debug = require('debug')('pigato:Worker'); 8 | var uuid = require('node-uuid'); 9 | var util = require('util'); 10 | var events = require('events'); 11 | var _ = require('lodash'); 12 | var MDP = require('./mdp'); 13 | var putils = require('./utils'); 14 | 15 | var HEARTBEAT_LIVENESS = 3; 16 | 17 | function Worker(broker, service, conf) { 18 | this.broker = broker; 19 | this.service = service; 20 | 21 | this.conf = { 22 | heartbeat: 2500, 23 | reconnect: 1000, 24 | concurrency: 100, 25 | prefix: 'W' + uuid.v4() 26 | }; 27 | 28 | _.extend(this.conf, conf); 29 | 30 | this.reqs = new Map(); 31 | 32 | events.EventEmitter.call(this); 33 | } 34 | util.inherits(Worker, events.EventEmitter); 35 | 36 | Worker.prototype.onConnect = function() { 37 | this.emit.apply(this, ['connect']); 38 | if (this.conf.onConnect) { 39 | this.conf.onConnect(); 40 | } 41 | 42 | debug('W(' + this.conf.prefix + ') connected to B(%s)', this.broker); 43 | }; 44 | 45 | Worker.prototype.onDisconnect = function() { 46 | this.emit.apply(this, ['disconnect']); 47 | if (this.conf.onDisconnect) { 48 | this.conf.onDisconnect(); 49 | } 50 | 51 | debug('W(' + this.conf.prefix + ') disconnected from B(%s)', this.broker); 52 | }; 53 | 54 | Worker.prototype.start = function() { 55 | var self = this; 56 | 57 | this.stop(); 58 | 59 | this._mcnt = 0; 60 | 61 | this.socket = zmq.socket('dealer'); 62 | 63 | this.socketId = this.conf.prefix + '-' + uuid.v4(); 64 | this.socket.identity = new Buffer(this.socketId); 65 | this.socket.setsockopt('linger', 1); 66 | 67 | this.socket.on('message', function() { 68 | self.onMsg.call(self, putils.args(arguments)); 69 | }); 70 | 71 | this.socket.on('error', function(err) { 72 | self.emitErr(err); 73 | }); 74 | 75 | this.socket.connect(this.broker); 76 | this.liveness = HEARTBEAT_LIVENESS; 77 | 78 | debug('Worker ' + this.conf.prefix + ' connected to %s', this.broker); 79 | 80 | this.sendReady(); 81 | 82 | debug('W: starting'); 83 | 84 | this.hbTimer = setInterval(function() { 85 | self.liveness--; 86 | 87 | if (self.liveness <= 0) { 88 | debug('W: liveness=0'); 89 | self.stop(); 90 | setTimeout(function() { 91 | self.start(); 92 | }, self.conf.reconnect); 93 | return; 94 | } 95 | 96 | self.heartbeat(); 97 | 98 | self.reqs.forEach(function(req) { 99 | req.liveness--; 100 | }); 101 | }, this.conf.heartbeat); 102 | 103 | this.heartbeat(); 104 | this.emit.apply(this, ['start']); 105 | }; 106 | 107 | Worker.prototype.stop = function() { 108 | clearInterval(this.hbTimer); 109 | 110 | if (this.socket) { 111 | debug('W: stopping'); 112 | 113 | this.sendDisconnect(); 114 | 115 | var socket = this.socket; 116 | delete this.socket; 117 | delete this.socketId; 118 | 119 | setImmediate(function() { 120 | if (socket._zmq.state != zmq.STATE_CLOSED) { 121 | socket.close(); 122 | } 123 | }); 124 | 125 | this.onDisconnect(); 126 | this.emit.apply(this, ['stop']); 127 | } 128 | }; 129 | 130 | Worker.prototype.send = function(msg) { 131 | if (!this.socket) { 132 | return; 133 | } 134 | 135 | this.socket.send(msg); 136 | }; 137 | 138 | // process message from broker 139 | Worker.prototype.onMsg = function(msg) { 140 | this._mcnt++; 141 | 142 | if (this._mcnt === 1) { 143 | this.onConnect(); 144 | } 145 | 146 | msg = putils.mparse(msg); 147 | 148 | var header = msg[0]; 149 | var type = msg[1]; 150 | 151 | if (header != MDP.WORKER) { 152 | this.emitErr('ERR_MSG_HEADER'); 153 | // send error 154 | return; 155 | } 156 | 157 | this.liveness = HEARTBEAT_LIVENESS; 158 | 159 | var clientId; 160 | var rid; 161 | var service; 162 | 163 | if (type == MDP.W_REQUEST) { 164 | clientId = msg[2]; 165 | service = msg[3]; 166 | rid = msg[5]; 167 | debug('W: W_REQUEST:', clientId, rid); 168 | this.onRequest(clientId, service, rid, msg[6], msg[7]); 169 | } else if (type == MDP.W_HEARTBEAT) { 170 | if (msg.length === 5) { 171 | clientId = msg[2]; 172 | rid = msg[4]; 173 | if (rid && this.reqs.has(rid)) { 174 | var req = this.reqs.get(rid); 175 | req.liveness = HEARTBEAT_LIVENESS; 176 | } 177 | } 178 | } else if (type == MDP.W_DISCONNECT) { 179 | debug('W: W_DISCONNECT'); 180 | this.start(); 181 | } else if (type == MDP.W_READY) { 182 | debug('W: W_READY'); 183 | } else { 184 | this.emitErr('ERR_MSG_TYPE_INVALID'); 185 | } 186 | }; 187 | 188 | Worker.prototype.emitReq = function(req, rep) { 189 | this.emit.apply(this, ['request', req.data, rep, req.opts]); 190 | }; 191 | 192 | Worker.prototype.emitErr = function(msg) { 193 | this.emit.apply(this, ['error', msg]); 194 | }; 195 | 196 | Worker.prototype.onRequest = function(clientId, service, rid, data, opts) { 197 | var self = this; 198 | 199 | var req = { 200 | clientId: clientId, 201 | rid: rid, 202 | liveness: HEARTBEAT_LIVENESS, 203 | service: service, 204 | data: null, 205 | opts: {} 206 | }; 207 | 208 | try { req.data = JSON.parse(data); } catch(e) { 209 | // Ignore 210 | } 211 | try { req.opts = JSON.parse(opts); } catch(e) { 212 | // Ignore 213 | } 214 | 215 | this.reqs.set(rid, req); 216 | 217 | var reply = new Writable({ 218 | objectMode: true 219 | }); 220 | 221 | reply.ended = false; 222 | 223 | var _write = reply.write; 224 | reply.write = function(chunk, encoding, cb) { 225 | return _write.call(reply, chunk, encoding, cb); 226 | }; 227 | 228 | reply._write = function(chunk, encoding, cb) { 229 | var rf = self.replyPartial; 230 | if (this.ended) { 231 | rf = self.replyFinal; 232 | } 233 | if (this.endNull) { 234 | chunk = null; 235 | } 236 | rf.apply(self, [clientId, rid, chunk, reply.opts]); 237 | cb(null); 238 | }; 239 | 240 | reply.opts = {}; 241 | 242 | reply.active = function() { 243 | return self.reqs.has(rid) && !req.ended && req.liveness > 0; 244 | }; 245 | 246 | reply.heartbeat = function() { 247 | self.heartbeat(); 248 | }; 249 | 250 | var _end = reply.end; 251 | 252 | reply.end = function(chunk, encoding, cb) { 253 | this.ended = true; 254 | 255 | if (chunk === undefined || chunk === null) { 256 | chunk = ''; 257 | this.endNull = true; 258 | } 259 | 260 | var ret = _end.apply(reply, [chunk, encoding, cb]); 261 | 262 | self.dreq(rid); 263 | return ret; 264 | }; 265 | 266 | reply.reject = function(err) { 267 | self.replyReject(clientId, rid, err); 268 | self.dreq(rid); 269 | }; 270 | 271 | reply.error = function(err) { 272 | self.replyError(clientId, rid, err); 273 | self.dreq(rid); 274 | }; 275 | 276 | this.emitReq(req, reply); 277 | }; 278 | 279 | Worker.prototype.dreq = function(rid) { 280 | this.reqs.delete(rid); 281 | }; 282 | 283 | Worker.prototype.sendReady = function() { 284 | this.send([MDP.WORKER, MDP.W_READY, this.service]); 285 | }; 286 | 287 | Worker.prototype.sendDisconnect = function() { 288 | this.send([MDP.WORKER, MDP.W_DISCONNECT]); 289 | }; 290 | 291 | Worker.prototype.heartbeat = function(opts) { 292 | this.send([ 293 | MDP.WORKER, MDP.W_HEARTBEAT, '', 294 | JSON.stringify(_.extend({}, opts, { 295 | concurrency: this.conf.concurrency 296 | })) 297 | ]); 298 | }; 299 | 300 | Worker.prototype.reply = function(type, clientId, rid, code, data, opts) { 301 | this.send([MDP.WORKER, type, clientId, '', rid, code, JSON.stringify(data), JSON.stringify(opts)]); 302 | }; 303 | 304 | Worker.prototype.replyPartial = function(clientId, rid, data, opts) { 305 | this.reply(MDP.W_REPLY_PARTIAL, clientId, rid, 0, data, opts); 306 | }; 307 | 308 | Worker.prototype.replyFinal = function(clientId, rid, data, opts) { 309 | this.reply(MDP.W_REPLY, clientId, rid, 0, data, opts); 310 | }; 311 | 312 | Worker.prototype.replyReject = function(clientId, rid, err) { 313 | this.reply(MDP.W_REPLY_REJECT, clientId, rid, 0, err); 314 | }; 315 | 316 | Worker.prototype.replyError = function(clientId, rid, err) { 317 | this.reply(MDP.W_REPLY, clientId, rid, -1, err); 318 | }; 319 | 320 | module.exports = Worker; 321 | -------------------------------------------------------------------------------- /lib/mdp.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | WORKER: 'W', 3 | CLIENT: 'C', 4 | 5 | W_READY: '1', 6 | W_REQUEST: '2', 7 | W_REPLY: '3', 8 | W_HEARTBEAT: '4', 9 | W_DISCONNECT: '5', 10 | W_REPLY_PARTIAL: '6', 11 | W_REPLY_REJECT: '7' 12 | }; 13 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mparse: function(_msg) { 3 | var msg = []; 4 | for (var i = 0; i < _msg.length; i++) { 5 | if (_msg[i]) { 6 | msg[i] = _msg[i].toString(); 7 | } 8 | } 9 | return msg; 10 | }, 11 | args: function(_args) { 12 | var args = []; 13 | for (var i = 0; i < _args.length; i++) { 14 | args.push(_args[i]); 15 | } 16 | return args; 17 | }, 18 | shuffle: function(array) { 19 | var currentIndex = array.length, temporaryValue, randomIndex ; 20 | 21 | while (0 !== currentIndex) { 22 | randomIndex = Math.floor(Math.random() * currentIndex); 23 | currentIndex -= 1; 24 | 25 | temporaryValue = array[currentIndex]; 26 | array[currentIndex] = array[randomIndex]; 27 | array[randomIndex] = temporaryValue; 28 | } 29 | 30 | return array; 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pigato", 3 | "version": "0.0.47", 4 | "private": false, 5 | "description": "A high-performance Node.js microservices framework based on ZeroMQ", 6 | "author": "prdn (http://ardoino.com/)", 7 | "keywords": [ 8 | "pigato", 9 | "nodejs", 10 | "zmq", 11 | "zeromq", 12 | "0mq", 13 | "request-reply", 14 | "amqp", 15 | "mdp", 16 | "micro-services" 17 | ], 18 | "dependencies": { 19 | "async": "~1.5.2", 20 | "debug": "~2.2.0", 21 | "es6-shim": "^0.33.13", 22 | "lodash": "~4.2.1", 23 | "node-uuid": "~1.4.7", 24 | "readable-stream": "2.0.4", 25 | "semver": "^5.0.3", 26 | "zmq": "~2.15.2" 27 | }, 28 | "engine": { 29 | "node": ">=0.10" 30 | }, 31 | "main": "index.js", 32 | "directories": { 33 | "example": "examples", 34 | "test": "test" 35 | }, 36 | "devDependencies": { 37 | "chai": "~3.2.0", 38 | "mocha": "~2.2.5" 39 | }, 40 | "scripts": { 41 | "test": "make test", 42 | "lint": "eslint --config .eslintrc --fix index.js lib/ test/" 43 | }, 44 | "license": "MIT", 45 | "repository": { 46 | "type": "git", 47 | "url": "https://github.com/prdn/pigato.git" 48 | }, 49 | "bugs": { 50 | "url": "https://github.com/prdn/pigato/issues" 51 | }, 52 | "homepage": "https://github.com/prdn/pigato", 53 | "optionalDependencies": { 54 | "eslint": "~1.6.0" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /services/Base.js: -------------------------------------------------------------------------------- 1 | var zmq = require('zmq'); 2 | var _ = require('lodash'); 3 | var Worker = require('./../index').Worker; 4 | 5 | function Base(endpoint, conf) { 6 | this.endpoint = endpoint; 7 | this.conf = _.extend({}, conf); 8 | }; 9 | 10 | Base.prototype.start = function() { 11 | this.wrk = new Worker(this.endpoint, this.service); 12 | this.wrk.start(); 13 | 14 | this.sub = zmq.socket('sub'); 15 | this.sub.identity = this.wrk.name + '/sub'; 16 | 17 | this.sub.connect(this.conf.intch); 18 | }; 19 | 20 | Base.prototype.onStart = function() { 21 | if (this.conf.onStart) { 22 | this.conf.onStart(); 23 | } 24 | }; 25 | 26 | Base.prototype.stop = function() { 27 | if (this.wrk) { 28 | this.wrk.stop(); 29 | delete this.wrk; 30 | } 31 | 32 | if (this.sub) { 33 | this.sub.close(); 34 | delete this.sub; 35 | } 36 | }; 37 | 38 | Base.prototype.onStop = function() { 39 | if (this.conf.onStop) { 40 | this.conf.onStop(); 41 | } 42 | }; 43 | 44 | module.exports = Base; 45 | -------------------------------------------------------------------------------- /services/Directory.js: -------------------------------------------------------------------------------- 1 | var zmq = require('zmq'); 2 | var _ = require('lodash'); 3 | var util = require('util'); 4 | var Base = require('./Base'); 5 | 6 | function Directory(endpoint, conf) { 7 | this.service = '$dir'; 8 | this._dir = {}; 9 | 10 | Base.call(this, endpoint, conf); 11 | }; 12 | util.inherits(Directory, Base); 13 | 14 | Directory.prototype.start = function() { 15 | Base.prototype.start.call(this); 16 | 17 | var self = this; 18 | 19 | this.wrk.on('request', function(inp, rep) { 20 | rep.end(inp ? (self._dir[inp] || []) : self._dir); 21 | }); 22 | 23 | this.sub.on('message', function(data) { 24 | data = data.toString(); 25 | if (data.indexOf(self.service) === 0) { 26 | var msg = data.substr(5); 27 | msg = JSON.parse(msg); 28 | self._dir = msg; 29 | } 30 | }); 31 | 32 | this.sub.subscribe(this.service); 33 | this.onStart(); 34 | 35 | this.wrk.heartbeat({ update: 1 }, true); 36 | }; 37 | 38 | Directory.prototype.stop = function() { 39 | if (this.sub) { 40 | this.sub.unsubscribe(this.service); 41 | } 42 | 43 | Base.prototype.stop.call(this); 44 | this.onStop(); 45 | }; 46 | 47 | module.exports = Directory; 48 | -------------------------------------------------------------------------------- /test/client.js: -------------------------------------------------------------------------------- 1 | var zmq = require('zmq'); 2 | 3 | var MDP = require('../lib/mdp'); 4 | 5 | var PIGATO = require('../'); 6 | 7 | var chai = require('chai'); 8 | var assert = chai.assert; 9 | var uuid = require('node-uuid'); 10 | 11 | var location = 'inproc://#'; 12 | var bhost; 13 | 14 | var client, clientOpts; 15 | 16 | describe('Client', function() { 17 | var mockBroker; 18 | 19 | beforeEach(function() { 20 | bhost = location + uuid.v4(); 21 | client = new PIGATO.Client(bhost, clientOpts); 22 | mockBroker = zmq.socket('router'); 23 | mockBroker.bindSync(bhost); 24 | }); 25 | 26 | afterEach(function() { 27 | client.removeAllListeners(); 28 | client.stop(); 29 | mockBroker.unbind(bhost); 30 | }); 31 | 32 | it('connect to a zmq endpoint and call callback once heartbeat made round trip', function(done) { 33 | 34 | var called = false; 35 | 36 | mockBroker.on('message', function(a, b, c) { 37 | assert.include(a.toString(), client.conf.prefix); 38 | assert.equal(MDP.CLIENT, b.toString()); 39 | assert.equal(MDP.W_HEARTBEAT, c.toString()); 40 | 41 | mockBroker.send([a, b, c]); 42 | called = true; 43 | }); 44 | 45 | client.conf.onConnect = function() { 46 | assert.equal(true, called); 47 | delete client.conf.onConnect; 48 | done(); 49 | }; 50 | 51 | client.start(); 52 | }); 53 | 54 | it('connect to a zmq endpoint and emit connect once heartbeat made round trip', function(done) { 55 | 56 | var called = false; 57 | 58 | mockBroker.on('message', function(a, b, c) { 59 | assert.include(a.toString(), client.conf.prefix); 60 | assert.equal(MDP.CLIENT, b.toString()); 61 | assert.equal(MDP.W_HEARTBEAT, c.toString()); 62 | 63 | mockBroker.send([a, b, c]); 64 | called = true; 65 | }); 66 | 67 | client.on('connect', function() { 68 | assert.equal(true, called); 69 | done(); 70 | }); 71 | client.start(); 72 | }); 73 | 74 | 75 | it('doesn\'t call callback if no heartbeat response', function(done) { 76 | 77 | var called = false; 78 | var cbCalled = false; 79 | 80 | mockBroker.on('message', function(a, b, c) { 81 | assert.include(a.toString(), client.conf.prefix); 82 | assert.equal(MDP.CLIENT, b.toString()); 83 | assert.equal(MDP.W_HEARTBEAT, c.toString()); 84 | called = true; 85 | }); 86 | 87 | client.on('connect', function() { 88 | cbCalled = true; 89 | }); 90 | 91 | client.start(); 92 | 93 | setTimeout(function() { 94 | assert.equal(true, called); 95 | assert.equal(false, cbCalled); 96 | done(); 97 | }, 20); 98 | }); 99 | 100 | 101 | it('emit an error if answer with bad header', function(done) { 102 | 103 | var called = false; 104 | var cbCalled = false; 105 | 106 | mockBroker.on('message', function(a, b, c) { 107 | assert.include(a.toString(), client.conf.prefix); 108 | assert.equal(MDP.CLIENT, b.toString()); 109 | assert.equal(MDP.W_HEARTBEAT, c.toString()); 110 | 111 | mockBroker.send([a, MDP.WORKER, c]); 112 | called = true; 113 | }); 114 | 115 | client.on('error', function(err) { 116 | assert.equal('ERR_MSG_HEADER', err); 117 | assert.equal(true, called); 118 | assert.equal(false, cbCalled); 119 | done(); 120 | }); 121 | 122 | client.on('connect', function() { 123 | cbCalled = true; 124 | }); 125 | 126 | client.start(); 127 | }); 128 | 129 | 130 | it('doesn\'t call callback if answer with bad id', function(done) { 131 | 132 | var called = false; 133 | var cbCalled = false; 134 | 135 | mockBroker.on('message', function(a, b, c) { 136 | assert.include(a.toString(), client.conf.prefix); 137 | assert.equal(MDP.CLIENT, b.toString()); 138 | assert.equal(MDP.W_HEARTBEAT, c.toString()); 139 | mockBroker.send([a + '' + uuid.v4(), b, MDP.W_HEARTBEAT]); 140 | called = true; 141 | }); 142 | 143 | client.on('connect', function() { 144 | cbCalled = true; 145 | }); 146 | 147 | client.start(); 148 | 149 | setTimeout(function() { 150 | assert.equal(true, called); 151 | assert.equal(false, cbCalled); 152 | done(); 153 | }, 20); 154 | }); 155 | 156 | 157 | it('can do callback request with no partial', function(done) { 158 | var toAnswer = uuid.v4(); 159 | 160 | mockBroker.on('message', function(id, clazz, type, topic, rid) { 161 | if (type.toString() == MDP.W_HEARTBEAT) { 162 | mockBroker.send([id, clazz, MDP.W_HEARTBEAT]); 163 | return; 164 | } 165 | 166 | if (type.toString() == MDP.W_REQUEST) { 167 | mockBroker.send([id, clazz, MDP.W_REPLY, '', rid, null, JSON.stringify(toAnswer)]); 168 | } 169 | 170 | }); 171 | 172 | var partial = false; 173 | 174 | client.on('connect', function() { 175 | client.request('foo', 'bar', function() { 176 | partial = true; 177 | }, function(err, data) { 178 | assert.equal(false, partial); 179 | assert.equal(err, 0); 180 | assert.equal(data, toAnswer); 181 | done(); 182 | }); 183 | }); 184 | client.start(); 185 | }); 186 | 187 | it('can do stream request with no partial', function(done) { 188 | var toAnswer = uuid.v4(); 189 | 190 | mockBroker.on('message', function(id, clazz, type, topic, rid) { 191 | if (type.toString() == MDP.W_HEARTBEAT) { 192 | mockBroker.send([id, clazz, MDP.W_HEARTBEAT]); 193 | return; 194 | } 195 | 196 | if (type.toString() == MDP.W_REQUEST) { 197 | mockBroker.send([id, clazz, MDP.W_REPLY, '', rid, null, JSON.stringify(toAnswer)]); 198 | } 199 | }); 200 | 201 | client.on('connect', function() { 202 | client.request('foo', 'bar').on('data', function(data) { 203 | assert.equal(data, toAnswer); 204 | }).on('end', done); 205 | }); 206 | client.start(); 207 | }); 208 | 209 | 210 | it('can do stream request with partial', function(done) { 211 | 212 | var reponses = ['one', 'two', 'three']; 213 | 214 | mockBroker.on('message', function(id, clazz, type, topic, rid) { 215 | if (type.toString() == MDP.W_HEARTBEAT) { 216 | mockBroker.send([id, clazz, MDP.W_HEARTBEAT]); 217 | return; 218 | } 219 | 220 | if (type.toString() == MDP.W_REQUEST) { 221 | mockBroker.send([id, clazz, MDP.W_REPLY_PARTIAL, '', rid, null, JSON.stringify(reponses[0])]); 222 | mockBroker.send([id, clazz, MDP.W_REPLY_PARTIAL, '', rid, null, JSON.stringify(reponses[1])]); 223 | mockBroker.send([id, clazz, MDP.W_REPLY, '', rid, null, JSON.stringify(reponses[2])]); 224 | } 225 | }); 226 | 227 | var index = 0; 228 | 229 | client.on('connect', function() { 230 | client.request('foo', 'bar').on('data', function(data) { 231 | assert.equal(data, reponses[index]); 232 | index++; 233 | 234 | }).on('end', function() { 235 | assert.equal(3, index); 236 | done(); 237 | }); 238 | }); 239 | 240 | client.start(); 241 | }); 242 | 243 | it('can do callback request with partial', function(done) { 244 | 245 | var reponses = ['one', 'two', 'three']; 246 | 247 | mockBroker.on('message', function(id, clazz, type, topic, rid) { 248 | if (type.toString() == MDP.W_HEARTBEAT) { 249 | mockBroker.send([id, clazz, MDP.W_HEARTBEAT]); 250 | return; 251 | } 252 | 253 | if (type.toString() == MDP.W_REQUEST) { 254 | mockBroker.send([id, clazz, MDP.W_REPLY_PARTIAL, '', rid, null, JSON.stringify(reponses[0])]); 255 | mockBroker.send([id, clazz, MDP.W_REPLY_PARTIAL, '', rid, null, JSON.stringify(reponses[1])]); 256 | mockBroker.send([id, clazz, MDP.W_REPLY, '', rid, null, JSON.stringify(reponses[2])]); 257 | } 258 | }); 259 | 260 | var index = 0; 261 | client.on('connect', function() { 262 | client.request('foo', 'bar', function(err, data) { 263 | assert.equal(data, reponses[index]); 264 | index++; 265 | }, function(err, data) { 266 | assert.equal(2, index); 267 | assert.equal(data, reponses[index]); 268 | done(); 269 | }); 270 | }); 271 | client.start(); 272 | }); 273 | 274 | 275 | it('emit an error if ERR_MSG_LENGTH we send a message to short', function(done) { 276 | 277 | client.on('error', function(err) { 278 | assert.ok(err); 279 | assert.equal(err, 'ERR_MSG_LENGTH'); 280 | done(); 281 | }); 282 | 283 | 284 | mockBroker.on('message', function(id, clazz, type) { 285 | if (type.toString() == MDP.W_HEARTBEAT) { 286 | mockBroker.send([id, clazz, MDP.W_HEARTBEAT]); 287 | return; 288 | } 289 | }); 290 | client.on('connect', function() { 291 | mockBroker.send([client.socketId, MDP.CLIENT, MDP.W_REPLY]); 292 | 293 | }); 294 | client.start(); 295 | }); 296 | 297 | 298 | it('emit an ERR_REQ_INVALID error if we send a reply to an invalid request', function(done) { 299 | 300 | client.on('error', function(err) { 301 | assert.ok(err); 302 | assert.equal(err, 'ERR_REQ_INVALID'); 303 | done(); 304 | 305 | }); 306 | 307 | 308 | mockBroker.on('message', function(id, clazz, type) { 309 | if (type.toString() == MDP.W_HEARTBEAT) { 310 | mockBroker.send([id, clazz, MDP.W_HEARTBEAT]); 311 | return; 312 | } 313 | }); 314 | client.on('connect', function() { 315 | mockBroker.send([client.socketId, MDP.CLIENT, MDP.W_REPLY, '', 'MYUNKNOWREQUEST', null, JSON.stringify('bar')]); 316 | 317 | }); 318 | client.start(); 319 | }); 320 | 321 | 322 | it('emit an error if answer with bad type', function(done) { 323 | 324 | var called = false; 325 | 326 | mockBroker.on('message', function(id, clazz, type, topic, rid) { 327 | if (type.toString() == MDP.W_HEARTBEAT) { 328 | mockBroker.send([id, clazz, MDP.W_HEARTBEAT]); 329 | return; 330 | } 331 | 332 | if (type.toString() == MDP.W_REQUEST) { 333 | called = true; 334 | mockBroker.send([id, clazz, '999', '', rid, null, JSON.stringify('DATA')]); 335 | } 336 | }); 337 | 338 | client.on('error', function(err) { 339 | assert.ok(err); 340 | assert.equal(err, 'ERR_MSG_TYPE'); 341 | assert.equal(true, called); 342 | done(); 343 | }); 344 | 345 | client.on('connect', function() { 346 | client.request('foo', 'bar', function() { 347 | assert.ok(false); 348 | }, function() { 349 | assert.ok(false); 350 | }); 351 | }); 352 | client.start(); 353 | }); 354 | 355 | 356 | describe('when timeout exceeded with heartbeat short ', function() { 357 | 358 | before(function() { 359 | clientOpts = { 360 | heartbeat: 20 361 | }; 362 | }); 363 | 364 | it('emit an error when timeout exceeded', function(done) { 365 | 366 | mockBroker.on('message', function(id, clazz, type) { 367 | if (type.toString() == MDP.W_HEARTBEAT) { 368 | mockBroker.send([id, clazz, MDP.W_HEARTBEAT]); 369 | return; 370 | } 371 | }); 372 | 373 | client.on('connect', function() { 374 | client.request('foo', 'bar', function() { 375 | assert.ok(false); 376 | }, function(err, data) { 377 | assert.ok(err); 378 | assert.equal(data, undefined); 379 | assert.equal('C_TIMEOUT', err); 380 | done(); 381 | }, { 382 | timeout: 40 383 | }); 384 | }); 385 | client.start(); 386 | }); 387 | 388 | after(function() { 389 | clientOpts = undefined; 390 | }); 391 | }); 392 | 393 | 394 | describe('when timeout exceeded with heartbeat long ', function() { 395 | 396 | before(function() { 397 | clientOpts = { 398 | heartbeat: 50 399 | }; 400 | }); 401 | 402 | it('wait for the heartbeat to expire before the error is returned', function(done) { 403 | 404 | var cbCalled = false; 405 | 406 | mockBroker.on('message', function(id, clazz, type) { 407 | if (type.toString() == MDP.W_HEARTBEAT) { 408 | mockBroker.send([id, clazz, MDP.W_HEARTBEAT]); 409 | return; 410 | } 411 | }); 412 | 413 | client.on('connect', function() { 414 | 415 | client.request('foo', 'bar', function() { 416 | assert.ok(false); 417 | }, function(err, data) { 418 | cbCalled = true; 419 | assert.ok(err); 420 | assert.equal(data, undefined); 421 | assert.equal('C_TIMEOUT', err); 422 | }, { 423 | timeout: 10 424 | }); 425 | }); 426 | 427 | client.start(); 428 | 429 | setTimeout(function() { 430 | assert.equal(false, cbCalled); 431 | }, 30); 432 | 433 | setTimeout(function() { 434 | assert.equal(true, cbCalled); 435 | done(); 436 | }, 60); 437 | }); 438 | 439 | after(function() { 440 | clientOpts = undefined; 441 | }); 442 | }); 443 | 444 | 445 | describe('when heartbeat is setted', function() { 446 | 447 | before(function() { 448 | clientOpts = { 449 | heartbeat: 25 450 | }; 451 | }); 452 | 453 | it('send heartbeat regularly', function(done) { 454 | 455 | var heartbeatCount = 0; 456 | 457 | mockBroker.on('message', function(id, clazz, type) { 458 | if (type.toString() == MDP.W_HEARTBEAT) { 459 | mockBroker.send([id, clazz, MDP.W_HEARTBEAT]); 460 | heartbeatCount++; 461 | return; 462 | } 463 | }); 464 | 465 | client.on('connect', function() { 466 | assert.equal(1, heartbeatCount); 467 | 468 | setTimeout(function() { 469 | assert.equal(3, heartbeatCount); 470 | done(); 471 | }, 60); 472 | }); 473 | 474 | client.start(); 475 | }); 476 | 477 | after(function() { 478 | clientOpts = undefined; 479 | }); 480 | }); 481 | 482 | 483 | it('can send heartbeat from a request, and it will send the good requestId', function(done) { 484 | 485 | var heartbeatCount = 0; 486 | var requestId; 487 | 488 | mockBroker.on('message', function(id, clazz, type, topic, rid) { 489 | if (type.toString() == MDP.W_HEARTBEAT && topic == undefined) { 490 | mockBroker.send([id, clazz, MDP.W_HEARTBEAT]); 491 | return; 492 | } 493 | 494 | if (type.toString() == MDP.W_REQUEST) { 495 | requestId = rid.toString(); 496 | } else if (type.toString() == MDP.W_HEARTBEAT) { 497 | heartbeatCount++; 498 | 499 | assert.ok(requestId); 500 | assert.include(id.toString(), client.conf.prefix); 501 | assert.equal(MDP.CLIENT, clazz.toString()); 502 | assert.equal(MDP.W_HEARTBEAT, type.toString()); 503 | assert.equal(topic, "request"); 504 | assert.equal(requestId, rid.toString()); 505 | } 506 | 507 | if (heartbeatCount == 1) { 508 | done(); 509 | } 510 | 511 | }); 512 | 513 | client.on('connect', function() { 514 | var req = client.request('foo', 'bar'); 515 | req.heartbeat(); 516 | }); 517 | 518 | client.start(); 519 | }); 520 | 521 | 522 | it('can send manual heartbeat for an unknown request', function(done) { 523 | 524 | var heartbeatCount = 0; 525 | var heartbeatContent = ['ONE', 'TWO', 'THREE', 'THREE']; 526 | 527 | mockBroker.on('message', function(id, clazz, type, topic, rid) { 528 | if (type.toString() == MDP.W_HEARTBEAT && topic == undefined) { 529 | mockBroker.send([id, clazz, MDP.W_HEARTBEAT]); 530 | return; 531 | } 532 | 533 | if (type.toString() == MDP.W_HEARTBEAT) { 534 | assert.include(id.toString(), client.conf.prefix); 535 | assert.equal(MDP.CLIENT, clazz.toString()); 536 | assert.equal(MDP.W_HEARTBEAT, type.toString()); 537 | assert.equal(heartbeatContent[heartbeatCount], rid.toString()); 538 | heartbeatCount++; 539 | } 540 | 541 | if (heartbeatCount == 4) { 542 | done(); 543 | } 544 | 545 | }); 546 | 547 | client.on('connect', function() { 548 | client.heartbeat(heartbeatContent[0]); 549 | client.heartbeat(heartbeatContent[1]); 550 | client.heartbeat(heartbeatContent[2]); 551 | client.heartbeat(heartbeatContent[3]); 552 | }); 553 | 554 | client.start(); 555 | }); 556 | 557 | }); 558 | -------------------------------------------------------------------------------- /test/concurrency.js: -------------------------------------------------------------------------------- 1 | var PIGATO = require('../'); 2 | var chai = require('chai'); 3 | var uuid = require('node-uuid'); 4 | 5 | var bhost = 'inproc://#' + uuid.v4(); 6 | //var bhost = 'tcp://0.0.0.0:2020'; 7 | 8 | var broker = new PIGATO.Broker(bhost); 9 | 10 | describe('CONCURRENCY', function() { 11 | 12 | before(function(done) { 13 | broker.conf.onStart = done; 14 | broker.start(); 15 | }); 16 | 17 | after(function(done) { 18 | broker.conf.onStop = done; 19 | broker.stop(); 20 | }); 21 | 22 | it('Base', function(done) { 23 | var ns = uuid.v4(); 24 | var chunk = 'bar'; 25 | 26 | var cc = 20; 27 | 28 | var worker = new PIGATO.Worker(bhost, ns, { concurrency: cc }); 29 | 30 | var reqIx = 0; 31 | 32 | worker.on('request', function(inp, res) { 33 | ++reqIx; 34 | 35 | var it = setInterval(function() { 36 | if (reqIx < cc) { 37 | return; 38 | } 39 | 40 | clearInterval(it); 41 | 42 | res.end(chunk); 43 | }, 10); 44 | }); 45 | 46 | worker.start(); 47 | 48 | var client = new PIGATO.Client(bhost); 49 | client.start(); 50 | 51 | var repIx = 0; 52 | 53 | for (var i = 0; i < cc; i++) { 54 | client.request( 55 | ns, chunk, 56 | undefined, 57 | function(err, data) { 58 | chai.assert.deepEqual(data, chunk); 59 | ++repIx; 60 | if (repIx === cc) { 61 | stop(); 62 | } 63 | } 64 | ); 65 | } 66 | 67 | function stop() { 68 | worker.stop(); 69 | client.stop(); 70 | done(); 71 | } 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/directory.js: -------------------------------------------------------------------------------- 1 | var PIGATO = require('../'); 2 | var chai = require('chai'); 3 | var uuid = require('node-uuid'); 4 | 5 | var bhost = 'inproc://#' + uuid.v4(); 6 | //var bhost = 'tcp://0.0.0.0:2020'; 7 | 8 | var broker; 9 | var ds; 10 | var brokerConf = {}; 11 | 12 | 13 | 14 | describe('DIRECTORY', function() { 15 | 16 | beforeEach(function(done) { 17 | brokerConf.intch = 'ipc:///tmp/' + uuid.v4(); 18 | 19 | broker = new PIGATO.Broker(bhost, brokerConf); 20 | 21 | broker.conf.onStart = function() { 22 | ds = new PIGATO.services.Directory(bhost, { 23 | intch: broker.conf.intch 24 | }); 25 | ds.conf.onStart = done; 26 | ds.start(); 27 | }; 28 | 29 | broker.start(); 30 | }); 31 | 32 | afterEach(function(done) { 33 | broker.conf.onStop = done; 34 | ds.stop(); 35 | broker.stop(); 36 | }); 37 | 38 | describe('when asking with a specific service', function() { 39 | it('Respond with worker handling this serviceName', function(done) { 40 | var nsGood = uuid.v4(); 41 | var nsBad = uuid.v4(); 42 | var client = new PIGATO.Client(bhost); 43 | 44 | var workers = {}; 45 | 46 | workers[nsGood] = []; 47 | workers[nsBad] = []; 48 | 49 | function spawn(ns) { 50 | var worker = new PIGATO.Worker(bhost, ns); 51 | worker.on('request', function(inp, rep) { 52 | rep.end(worker.socketId); 53 | }); 54 | worker.start(); 55 | return worker; 56 | } 57 | 58 | client.start(); 59 | 60 | var samples = 3; 61 | 62 | for (var wi = 0; wi < samples; wi++) { 63 | workers[nsGood].push(spawn(nsGood)); 64 | workers[nsBad].push(spawn(nsBad)); 65 | } 66 | 67 | var workerIds = workers[nsGood].map(function(wrk) { 68 | return wrk.socketId; 69 | }); 70 | 71 | workerIds.sort(function(a, b) { 72 | return a < b ? -1 : 1; 73 | }); 74 | 75 | setTimeout(function() { 76 | client.request('$dir', nsGood) 77 | .on('data', function(data) { 78 | chai.assert.isArray(data); 79 | chai.assert.equal(3, data.length); 80 | data.sort(function(a, b) { 81 | return a < b ? -1 : 1; 82 | }); 83 | 84 | chai.assert.deepEqual(data, workerIds); 85 | }) 86 | .on('error', function(err) { 87 | stop(err); 88 | }) 89 | .on('end', function() { 90 | stop(); 91 | }); 92 | }, 100); 93 | 94 | function stop(err) { 95 | workers[nsGood].forEach(function(worker) { 96 | worker.stop(); 97 | }); 98 | workers[nsBad].forEach(function(worker) { 99 | worker.stop(); 100 | }); 101 | client.stop(); 102 | done(err); 103 | } 104 | }); 105 | }); 106 | 107 | 108 | describe('when asking without a specific service', function() { 109 | it('Respond with all workers', function(done) { 110 | var nsGood = uuid.v4(); 111 | var nsBad = uuid.v4(); 112 | var client = new PIGATO.Client(bhost); 113 | 114 | var workers = {}; 115 | 116 | workers[nsGood] = []; 117 | workers[nsBad] = []; 118 | 119 | function spawn(ns) { 120 | var worker = new PIGATO.Worker(bhost, ns); 121 | worker.on('request', function(inp, rep) { 122 | rep.end(worker.conf.prefix); 123 | }); 124 | worker.start(); 125 | return worker; 126 | } 127 | 128 | client.start(); 129 | 130 | var samples = 3; 131 | 132 | for (var wi = 0; wi < samples; wi++) { 133 | workers[nsGood].push(spawn(nsGood)); 134 | workers[nsBad].push(spawn(nsBad)); 135 | } 136 | 137 | var workerIds = workers[nsGood].map(function(wrk) { 138 | return wrk.conf.prefix; 139 | }); 140 | 141 | workerIds.sort(function(a, b) { 142 | return a < b ? -1 : 1; 143 | }); 144 | 145 | setTimeout(function() { 146 | client.request('$dir') 147 | .on('data', function(data) { 148 | chai.assert.isObject(data); 149 | 150 | chai.assert.isArray(data[nsGood]); 151 | chai.assert.equal(3, data[nsGood].length); 152 | chai.assert.isArray(data[nsBad]); 153 | chai.assert.equal(3, data[nsBad].length); 154 | chai.assert.isAbove(Object.keys(data).length, 2); 155 | chai.assert.equal(1, data['$dir'].length); 156 | chai.assert.include(data['$dir'][0], ds.wrk.conf.prefix); 157 | }) 158 | .on('error', function(err) { 159 | stop(err); 160 | }) 161 | .on('end', function() { 162 | stop(); 163 | }); 164 | }, 10); 165 | 166 | function stop(err) { 167 | workers[nsGood].forEach(function(worker) { 168 | worker.stop(); 169 | }); 170 | workers[nsBad].forEach(function(worker) { 171 | worker.stop(); 172 | }); 173 | client.stop(); 174 | done(err); 175 | } 176 | }); 177 | }); 178 | 179 | }); 180 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var PIGATO = require('../'); 2 | var chai = require('chai'); 3 | var uuid = require('node-uuid'); 4 | 5 | var bhost = 'inproc://#' + uuid.v4(); 6 | //var bhost = 'tcp://0.0.0.0:2020'; 7 | 8 | var broker = new PIGATO.Broker(bhost); 9 | 10 | describe('BASE', function() { 11 | 12 | before(function(done) { 13 | broker.conf.onStart = done; 14 | broker.start(); 15 | }); 16 | 17 | after(function(done) { 18 | broker.conf.onStop = done; 19 | broker.stop(); 20 | }); 21 | 22 | it('Client partial/final Request (stream)', function(done) { 23 | var ns = uuid.v4(); 24 | var chunk = 'foo'; 25 | 26 | var worker = new PIGATO.Worker(bhost, ns); 27 | 28 | worker.on('request', function(inp, res) { 29 | for (var i = 0; i < 5; i++) { 30 | res.write(inp + i); 31 | } 32 | res.end(inp + (i)); 33 | }); 34 | 35 | worker.start(); 36 | 37 | var client = new PIGATO.Client(bhost); 38 | client.start(); 39 | 40 | var repIx = 0; 41 | 42 | client.request( 43 | ns, chunk 44 | ).on('data', function(data) { 45 | chai.assert.equal(data, String(chunk + (repIx++))); 46 | }).on('end', function() { 47 | stop(); 48 | }); 49 | 50 | function stop() { 51 | worker.stop(); 52 | client.stop(); 53 | done(); 54 | } 55 | }); 56 | 57 | it('Client partial/final Request (callback)', function(done) { 58 | var ns = uuid.v4(); 59 | var chunk = 'foo'; 60 | 61 | var worker = new PIGATO.Worker(bhost, ns); 62 | 63 | worker.on('request', function(inp, res) { 64 | for (var i = 0; i < 5; i++) { 65 | res.write(inp + i); 66 | } 67 | res.end(inp + 'FINAL' + (++i)); 68 | }); 69 | 70 | worker.start(); 71 | 72 | var client = new PIGATO.Client(bhost); 73 | client.start(); 74 | 75 | var repIx = 0; 76 | 77 | client.request( 78 | ns, chunk, 79 | function(err, data) { 80 | chai.assert.equal(data, chunk + (repIx++)); 81 | }, 82 | function(err, data) { 83 | chai.assert.equal(data, chunk + 'FINAL' + (++repIx)); 84 | stop(); 85 | } 86 | ); 87 | 88 | function stop() { 89 | worker.stop(); 90 | client.stop(); 91 | done(); 92 | } 93 | }); 94 | 95 | it('JSON Client partial/final Request (callback)', function(done) { 96 | var ns = uuid.v4(); 97 | var chunk = { 98 | foo: 'bar' 99 | }; 100 | 101 | var worker = new PIGATO.Worker(bhost, ns); 102 | 103 | worker.on('request', function(inp, res) { 104 | res.end(chunk); 105 | }); 106 | 107 | worker.start(); 108 | 109 | var client = new PIGATO.Client(bhost); 110 | client.start(); 111 | 112 | client.request( 113 | ns, 'foo', 114 | undefined, 115 | function(err, data) { 116 | chai.assert.deepEqual(data, chunk); 117 | stop(); 118 | } 119 | ); 120 | 121 | function stop() { 122 | worker.stop(); 123 | client.stop(); 124 | done(); 125 | } 126 | }); 127 | 128 | it('Worker reject', function(done) { 129 | var ns = uuid.v4(); 130 | 131 | var chunk = 'NOT_MY_JOB'; 132 | var chunk_2 = 'DID_MY_JOB'; 133 | 134 | var workers = []; 135 | 136 | function spawn(fn) { 137 | var worker = new PIGATO.Worker(bhost, ns); 138 | worker.on('request', fn); 139 | 140 | worker.start(); 141 | workers.push(worker); 142 | } 143 | 144 | spawn(function(inp, res) { 145 | res.reject(chunk); 146 | spawn(function(inp, res) { 147 | res.end(chunk_2); 148 | }); 149 | }); 150 | 151 | var client = new PIGATO.Client(bhost); 152 | client.start(); 153 | 154 | client.request( 155 | ns, chunk 156 | ).on('data', function(data) { 157 | chai.assert.equal(data, chunk_2); 158 | }).on('end', function() { 159 | stop(); 160 | }); 161 | 162 | function stop() { 163 | workers.forEach(function(worker) { 164 | worker.stop(); 165 | }); 166 | client.stop(); 167 | done(); 168 | } 169 | }); 170 | 171 | it('Broker Request retry', function(done) { 172 | var ns = uuid.v4(); 173 | 174 | var chunk = 'foo'; 175 | 176 | var workers = []; 177 | 178 | function spawn(id) { 179 | var worker = new PIGATO.Worker(bhost, ns); 180 | 181 | worker.on('request', function(inp, res) { 182 | if (id == 'w1') { 183 | firstRequestDone(); 184 | } else { 185 | res.end(chunk + '/' + id); 186 | } 187 | }); 188 | 189 | worker.start(); 190 | workers.push(worker); 191 | return worker; 192 | } 193 | 194 | var client = new PIGATO.Client(bhost); 195 | client.start(); 196 | 197 | client.request( 198 | ns, chunk, { 199 | retry: 1 200 | } 201 | ).on('data', function(data) { 202 | chai.assert.equal(data, chunk + '/w2'); 203 | }).on('end', function() { 204 | stop(); 205 | }); 206 | 207 | spawn('w1'); 208 | 209 | function firstRequestDone() { 210 | setTimeout(function() { 211 | spawn('w2'); 212 | workers[0].stop(); 213 | }, 10); 214 | } 215 | 216 | function stop() { 217 | workers.forEach(function(worker) { 218 | worker.stop(); 219 | }); 220 | client.stop(); 221 | done(); 222 | } 223 | }); 224 | 225 | it('Client error request (stream)', function(done) { 226 | var ns = uuid.v4(); 227 | var chunk = 'SOMETHING_FAILED'; 228 | 229 | var worker = new PIGATO.Worker(bhost, ns); 230 | 231 | worker.on('request', function(inp, res) { 232 | res.error(chunk); 233 | }); 234 | 235 | worker.start(); 236 | 237 | var client = new PIGATO.Client(bhost); 238 | client.start(); 239 | 240 | client.request(ns, chunk) 241 | .on('error', function(err) { 242 | chai.assert.equal(err, chunk); 243 | stop(); 244 | }); 245 | 246 | function stop() { 247 | worker.stop(); 248 | client.stop(); 249 | done(); 250 | } 251 | }); 252 | 253 | it('Client error request (callback)', function(done) { 254 | var ns = uuid.v4(); 255 | var chunk = 'SOMETHING_FAILED'; 256 | 257 | var worker = new PIGATO.Worker(bhost, ns); 258 | 259 | worker.on('request', function(inp, res) { 260 | res.error(chunk); 261 | }); 262 | 263 | worker.start(); 264 | 265 | var client = new PIGATO.Client(bhost); 266 | client.start(); 267 | 268 | client.request( 269 | ns, chunk, 270 | undefined, 271 | function(err, data) { 272 | chai.assert.equal(err, chunk); 273 | chai.assert.equal(data, null); 274 | stop(); 275 | } 276 | ); 277 | 278 | function stop() { 279 | worker.stop(); 280 | client.stop(); 281 | done(); 282 | } 283 | }); 284 | 285 | it('Client error timeout (stream)', function(done) { 286 | var ns = uuid.v4(); 287 | 288 | var client = new PIGATO.Client(bhost, { 289 | heartbeat: 25 290 | }); 291 | client.start(); 292 | 293 | client.request(ns, 'foo', { 294 | timeout: 60 295 | }) 296 | .on('error', function(err) { 297 | chai.assert.equal(err, 'C_TIMEOUT'); 298 | stop(); 299 | }); 300 | 301 | function stop() { 302 | client.stop(); 303 | done(); 304 | } 305 | }); 306 | 307 | it('Client error timeout (callback)', function(done) { 308 | var ns = uuid.v4(); 309 | 310 | var client = new PIGATO.Client(bhost, { 311 | heartbeat: 25 312 | }); 313 | client.start(); 314 | 315 | client.request( 316 | ns, 'foo', 317 | undefined, 318 | function(err, data) { 319 | chai.assert.equal(err, 'C_TIMEOUT'); 320 | chai.assert.equal(data, undefined); 321 | stop(); 322 | }, { 323 | timeout: 60 324 | } 325 | ); 326 | 327 | function stop() { 328 | client.stop(); 329 | done(); 330 | } 331 | }); 332 | }); 333 | -------------------------------------------------------------------------------- /test/requestStream.js: -------------------------------------------------------------------------------- 1 | var PIGATO = require('../'); 2 | var chai = require('chai'); 3 | var uuid = require('node-uuid'); 4 | 5 | var bhost = 'inproc://#' + uuid.v4(); 6 | //var bhost = 'tcp://0.0.0.0:2020'; 7 | 8 | var broker = new PIGATO.Broker(bhost); 9 | 10 | describe('STREAM SPECS', function() { 11 | 12 | before(function(done) { 13 | broker.conf.onStart = done; 14 | broker.start(); 15 | }); 16 | 17 | after(function(done) { 18 | broker.conf.onStop = done; 19 | broker.stop(); 20 | }); 21 | 22 | it('Client requestStream', function(done) { 23 | var ns = uuid.v4(); 24 | var chunk = 'foo'; 25 | 26 | var worker = new PIGATO.Worker(bhost, ns); 27 | 28 | worker.on('request', function(inp, res) { 29 | for (var i = 0; i < 5; i++) { 30 | res.write(inp + i); 31 | } 32 | res.end(inp + (i)); 33 | }); 34 | 35 | worker.start(); 36 | 37 | var client = new PIGATO.Client(bhost); 38 | client.start(); 39 | 40 | var repIx = 0; 41 | 42 | client.requestStream( 43 | ns, chunk 44 | ).on('data', function(data) { 45 | chai.assert.equal(data, String(chunk + (repIx++))); 46 | }).on('end', function() { 47 | stop(); 48 | }); 49 | 50 | function stop() { 51 | worker.stop(); 52 | client.stop(); 53 | done(); 54 | } 55 | }); 56 | 57 | }); 58 | -------------------------------------------------------------------------------- /test/semver.js: -------------------------------------------------------------------------------- 1 | var PIGATO = require('../'); 2 | var chai = require('chai'); 3 | var uuid = require('node-uuid'); 4 | 5 | var bhost = 'inproc://#' + uuid.v4(); 6 | //var bhost = 'tcp://0.0.0.0:2020'; 7 | 8 | var broker = new PIGATO.Broker(bhost); 9 | 10 | 11 | describe('SEMVER', function() { 12 | before(function(done) { 13 | broker.conf.onStart = done; 14 | broker.start(); 15 | }); 16 | 17 | after(function(done) { 18 | broker.conf.onStop = done; 19 | broker.stop(); 20 | }); 21 | 22 | it('allows services with semver in name', function(done) { 23 | var ns = uuid.v4() + '@1.2.3'; 24 | this.timeout(5000); 25 | 26 | var client = new PIGATO.Client(bhost); 27 | 28 | var worker = new PIGATO.Worker(bhost, ns); 29 | worker.on('request', function(inp, rep) { 30 | rep.end(worker.conf.name); 31 | }); 32 | worker.start(); 33 | 34 | client.start(); 35 | 36 | var workerId = worker.conf.name; 37 | client.request(ns, 'foo') 38 | .on('data', function(data) { 39 | chai.assert.equal(data, workerId); 40 | }) 41 | .on('error', function(err) { 42 | stop(err); 43 | }) 44 | .on('end', function() { 45 | stop(); 46 | }); 47 | 48 | function stop(err) { 49 | worker.stop(); 50 | client.stop(); 51 | done(err); 52 | } 53 | }); 54 | 55 | it('can resolve services using semver', function(done) { 56 | var namePrefix = uuid.v4(); 57 | var ns = namePrefix + '@1.2.3'; 58 | this.timeout(5000); 59 | 60 | var client = new PIGATO.Client(bhost); 61 | 62 | var worker = new PIGATO.Worker(bhost, ns); 63 | worker.on('request', function(inp, rep) { 64 | rep.end(worker.conf.name); 65 | }); 66 | worker.start(); 67 | 68 | client.start(); 69 | 70 | var workerId = worker.conf.name; 71 | client.request(namePrefix + '@^1.2.x', 'foo') 72 | .on('data', function(data) { 73 | chai.assert.equal(data, workerId); 74 | }) 75 | .on('error', function(err) { 76 | stop(err); 77 | }) 78 | .on('end', function() { 79 | stop(); 80 | }); 81 | 82 | function stop(err) { 83 | worker.stop(); 84 | client.stop(); 85 | done(err); 86 | } 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /test/startStop.js: -------------------------------------------------------------------------------- 1 | var PIGATO = require('../'); 2 | 3 | var uuid = require('node-uuid'); 4 | var bhost = 'inproc://#' + uuid.v4(); 5 | //var bhost = 'tcp://0.0.0.0:2020'; 6 | 7 | var broker = new PIGATO.Broker(bhost, { 8 | heartbeat: 10000 9 | }); 10 | var worker = new PIGATO.Worker(bhost, 'foo', { 11 | heartbeat: 10000 12 | }); 13 | 14 | var client = new PIGATO.Client(bhost); 15 | 16 | 17 | describe('StartStop', function() { 18 | 19 | describe('A broker', function() { 20 | it('call the callback on start', function(done) { 21 | broker.conf.onStart = done; 22 | broker.start(); 23 | }); 24 | 25 | it('call the callback on stop', function(done) { 26 | broker.conf.onStop = done; 27 | broker.stop(); 28 | }); 29 | }); 30 | 31 | describe('When I start a worker against a running broker', function() { 32 | 33 | before(function(done) { 34 | broker.conf.onStart = done; 35 | broker.start(); 36 | }); 37 | 38 | 39 | it('call the callback on start', function(done) { 40 | worker.conf.onConnect = done; 41 | worker.start(); 42 | }); 43 | 44 | it('call the callback on stop', function(done) { 45 | worker.conf.onDisconnect = done; 46 | worker.stop(); 47 | }); 48 | 49 | it('call the callback on (re)start', function(done) { 50 | worker.conf.onConnect = done; 51 | worker.start(); 52 | }); 53 | 54 | it('call the callback on (re)stop', function(done) { 55 | worker.conf.onDisconnect = done; 56 | worker.stop(); 57 | }); 58 | 59 | after(function(done) { 60 | broker.conf.onStop = done; 61 | broker.stop(); 62 | worker.conf.onConnect = null; 63 | worker.conf.onDisconnect = null; 64 | }); 65 | }); 66 | 67 | 68 | describe('When I start a client against a running broker', function() { 69 | 70 | before(function(done) { 71 | broker.conf.onStart = done; 72 | broker.start(); 73 | }); 74 | 75 | it('call the callback on start', function(done) { 76 | client.conf.onConnect = done; 77 | client.start(); 78 | }); 79 | 80 | it('call the callback on stop', function(done) { 81 | client.conf.onDisconnect = done; 82 | client.stop(); 83 | }); 84 | 85 | it('call the callback on (re)start', function(done) { 86 | client.conf.onConnect = done; 87 | client.start(); 88 | }); 89 | 90 | it('call the callback on (re)stop', function(done) { 91 | client.conf.onDisconnect = done; 92 | client.stop(); 93 | }); 94 | 95 | after(function(done) { 96 | broker.conf.onStop = done; 97 | broker.stop(); 98 | }); 99 | 100 | }); 101 | 102 | 103 | describe('When I start a client and a worker against a running broker', function() { 104 | 105 | before(function(done) { 106 | broker.conf.onStart = done; 107 | broker.start(); 108 | }); 109 | 110 | it('call the callback on start', function(done) { 111 | worker.conf.onConnect = function() { 112 | client.conf.onConnect = done; 113 | client.start(); 114 | }; 115 | worker.start(); 116 | }); 117 | 118 | it('call the callback on stop', function(done) { 119 | worker.conf.onDisconnect = function() { 120 | client.conf.onDisconnect = done; 121 | client.stop(); 122 | }; 123 | worker.stop(); 124 | }); 125 | 126 | 127 | after(function(done) { 128 | broker.conf.onStop = done; 129 | broker.stop(); 130 | }); 131 | }); 132 | }); 133 | -------------------------------------------------------------------------------- /test/target.js: -------------------------------------------------------------------------------- 1 | var PIGATO = require('../'); 2 | var chai = require('chai'); 3 | var uuid = require('node-uuid'); 4 | 5 | var bhost = 'inproc://#' + uuid.v4(); 6 | //var bhost = 'tcp://0.0.0.0:2020'; 7 | 8 | var broker = new PIGATO.Broker(bhost); 9 | 10 | 11 | describe('TARGET', function() { 12 | before(function(done) { 13 | broker.conf.onStart = done; 14 | broker.start(); 15 | }); 16 | 17 | after(function(done) { 18 | broker.conf.onStop = done; 19 | broker.stop(); 20 | }); 21 | 22 | it('Request for specific Worker', function(done) { 23 | var ns = uuid.v4(); 24 | this.timeout(5000); 25 | 26 | var client = new PIGATO.Client(bhost); 27 | 28 | var workers = []; 29 | 30 | function spawn() { 31 | var worker = new PIGATO.Worker(bhost, ns); 32 | worker.on('request', function(inp, rep) { 33 | rep.end(worker.conf.name); 34 | }); 35 | worker.start(); 36 | workers.push(worker); 37 | } 38 | 39 | client.start(); 40 | 41 | var samples = 10; 42 | 43 | for (var wi = 0; wi < samples; wi++) { 44 | spawn(); 45 | } 46 | 47 | var rcnt = 0; 48 | 49 | function request() { 50 | var workerId = workers[Math.round(Math.random() * 1000 % 9)].conf.name; 51 | client.request(ns, 'foo', { 52 | workerId: workerId 53 | }) 54 | .on('data', function(data) { 55 | chai.assert.equal(data, workerId); 56 | }) 57 | .on('error', function(err) { 58 | stop(err); 59 | }) 60 | .on('end', function() { 61 | rcnt++; 62 | if (rcnt === samples) { 63 | stop(); 64 | } 65 | }); 66 | } 67 | 68 | for (var i = 0; i < samples; i++) { 69 | request(); 70 | } 71 | 72 | function stop(err) { 73 | workers.forEach(function(worker) { 74 | worker.stop(); 75 | }); 76 | client.stop(); 77 | done(err); 78 | } 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /test/timeout.js: -------------------------------------------------------------------------------- 1 | var PIGATO = require('../'); 2 | var chai = require('chai'), 3 | assert = chai.assert; 4 | var uuid = require('node-uuid'); 5 | 6 | var location = 'inproc://#'; 7 | 8 | describe('TIMEOUT', function() { 9 | var bhost = location + uuid.v4(); 10 | var ns = uuid.v4(); 11 | var broker = new PIGATO.Broker(bhost); 12 | 13 | var client = new PIGATO.Client(bhost, { 14 | heartbeat: 25 15 | }); 16 | 17 | var chunk = 'foo'; 18 | var worker = new PIGATO.Worker(bhost, ns); 19 | var backupWorker = new PIGATO.Worker(bhost, ns); 20 | 21 | before(function(done) { 22 | client.start(); 23 | broker.conf.onStart = done; 24 | broker.start(); 25 | }); 26 | 27 | after(function(done) { 28 | broker.conf.onStop = done; 29 | broker.stop(); 30 | }); 31 | 32 | describe('When a server is launched', function() { 33 | 34 | before(function(done) { 35 | worker.start(); 36 | worker.on('request', function(inp, res) { 37 | res.end(inp + ':bar'); 38 | }); 39 | done(); 40 | }); 41 | 42 | it('can be reached', function(done) { 43 | client.request(ns, chunk, { 44 | timeout: 60 45 | }) 46 | .on('data', function(data) { 47 | chai.assert.equal(data, chunk + ':bar'); 48 | }) 49 | .on('end', done); 50 | }); 51 | }); 52 | 53 | 54 | describe('When a server is stop', function() { 55 | 56 | before(function(done) { 57 | worker.stop(); 58 | setImmediate(done); 59 | }); 60 | 61 | it('client get a timeout', function(done) { 62 | client.request(ns, chunk, { 63 | timeout: 50 64 | }) 65 | .on('error', function(err) { 66 | chai.assert.notEqual(null, err); 67 | chai.assert.equal('C_TIMEOUT', err); 68 | done(); 69 | }); 70 | }); 71 | }); 72 | 73 | 74 | describe('When we launch a server during a request', function() { 75 | 76 | it('can reach the server and answer', function(done) { 77 | 78 | var timeoutBeforeLaunch = 50; 79 | var startTime = +new Date(); 80 | client.request(ns, chunk, { 81 | timeout: 100 82 | }) 83 | .on('data', function(data) { 84 | chai.assert.equal(data, chunk + ':bar'); 85 | }) 86 | .on('error', function(err) { 87 | assert.ok(!err, 'Should not be here'); 88 | }) 89 | .on('end', function(){ 90 | 91 | var endTime = +new Date(); 92 | assert.ok( endTime - startTime > timeoutBeforeLaunch , 'response is to fast'); 93 | done(); 94 | }); 95 | 96 | setTimeout(function() { 97 | backupWorker.start(); 98 | backupWorker.on('request', function(inp, res) { 99 | res.end(inp + ':bar'); 100 | }); 101 | }, timeoutBeforeLaunch); 102 | }); 103 | }); 104 | 105 | 106 | describe('After that I can do something fast', function() { 107 | 108 | it('can reach the server and answer', function(done) { 109 | client.request(ns, chunk, { 110 | timeout: 60 111 | }) 112 | .on('data', function(data) { 113 | chai.assert.equal(data, chunk + ':bar'); 114 | }) 115 | .on('error', function(err) { 116 | assert.ok(!err, 'Should not be here'); 117 | }) 118 | .on('end', done); 119 | }); 120 | }); 121 | }); 122 | /* 123 | it('Can reach a launched server ', function(done) { 124 | 125 | 126 | }); 127 | 128 | 129 | it('Timeout once their is no more server', function(done) { 130 | 131 | 132 | }); 133 | 134 | describe('When I launch a server while a request is launch', function() { 135 | 136 | it('successfully wait between tiemout delay for a new server', function() { 137 | 138 | }); 139 | 140 | }); 141 | 142 | 143 | it('Resend Request on Worker death', function(done) { 144 | var chunk = 'foo'; 145 | 146 | 147 | worker.start(); 148 | 149 | worker.on('request', function(inp, res) { 150 | res.end(inp + ':bar'); 151 | }); 152 | 153 | backupWorker.on('request', function(inp, res) { 154 | res.end(inp + ':bar'); 155 | }); 156 | 157 | var counter = 0; 158 | 159 | var workerRuning = true; 160 | 161 | function request() { 162 | client.request(ns, chunk, { 163 | timeout: 60 164 | }) 165 | .on('data', function(data) { 166 | chai.assert.equal(data, chunk + ':bar'); 167 | }) 168 | .on('end', function() { 169 | if (counter >= 3) { 170 | stop(null); 171 | } else { 172 | setTimeout(function() { 173 | if (workerRuning) { 174 | worker.stop(null); 175 | workerRuning = false; 176 | } 177 | request(); 178 | }, 200); 179 | } 180 | counter++; 181 | }) 182 | .on('error', function(err) { 183 | if (err === 'C_TIMEOUT' && counter < 3) { 184 | request(); 185 | } else { 186 | stop('All workers died'); 187 | } 188 | }); 189 | } 190 | 191 | request(); 192 | setTimeout(function() { 193 | backupWorker.start(); 194 | }, 100); 195 | 196 | function stop(err) { 197 | client.stop(); 198 | worker.stop(); 199 | done(err); 200 | } 201 | }) 202 | }); */ 203 | -------------------------------------------------------------------------------- /test/wildcards.js: -------------------------------------------------------------------------------- 1 | var PIGATO = require('../'); 2 | var assert = require('chai').assert; 3 | var uuid = require('node-uuid'); 4 | 5 | var bhost = 'inproc://#' + uuid.v4(); 6 | //var bhost = 'tcp://0.0.0.0:2020'; 7 | var broker = new PIGATO.Broker(bhost); 8 | 9 | var client, worker, ns; 10 | 11 | describe('WILDCARDS', function() { 12 | 13 | beforeEach(function(done) { 14 | broker.conf.onStart = done; 15 | broker.start(); 16 | }); 17 | 18 | afterEach(function(done) { 19 | broker.conf.onStop = done; 20 | broker.stop(); 21 | }); 22 | 23 | 24 | describe('A wildcard worker', function() { 25 | var chunk = 'foo'; 26 | beforeEach(function() { 27 | ns = uuid.v4(); 28 | client = new PIGATO.Client(bhost); 29 | worker = new PIGATO.Worker(bhost, ns + '*'); 30 | 31 | worker.start(); 32 | 33 | client.start(); 34 | worker.on('request', function(inp, res) { 35 | res.end(inp + ':bar'); 36 | }); 37 | 38 | }); 39 | 40 | afterEach(function() { 41 | client.stop(); 42 | worker.stop(); 43 | }); 44 | 45 | it('can be reach several times using widlcard mecanisme', function(done) { 46 | this.timeout(5000); 47 | var rcnt = 0; 48 | 49 | function request() { 50 | client.request(ns + '-' + uuid.v4(), chunk, { 51 | timeout: 5000 52 | }) 53 | .on('data', function(data) { 54 | assert.equal(data, chunk + ':bar'); 55 | }) 56 | .on('error', function(err) { 57 | assert.equal(undefined, err); 58 | done(err); 59 | }) 60 | .on('end', function() { 61 | rcnt++; 62 | if (rcnt === 5) { 63 | done(); 64 | } 65 | }); 66 | } 67 | for (var i = 0; i < 5; i++) { 68 | request(); 69 | } 70 | 71 | }); 72 | 73 | it('Can reach a wildcard worker by using the wildcard name', function(done) { 74 | 75 | client.request(ns + '*', chunk, function() {}, function(type, data) { 76 | assert.equal(type, 0); 77 | assert.equal(data, chunk + ':bar'); 78 | done(); 79 | }); 80 | }); 81 | }); 82 | 83 | 84 | describe('When a worker with matching name exist', function() { 85 | 86 | var wildcardWorker, matchingworker, workerid; 87 | 88 | beforeEach(function() { 89 | ns = uuid.v4(); 90 | 91 | workerid = uuid.v4(); 92 | wildcardWorker = new PIGATO.Worker(bhost, ns + '-*'); 93 | matchingworker = new PIGATO.Worker(bhost, ns + '-' + workerid); 94 | 95 | wildcardWorker.on('request', function(inp, res) { 96 | res.end('WILDCARD'); 97 | }); 98 | 99 | matchingworker.on('request', function(inp, res) { 100 | res.end('MATCHING'); 101 | }); 102 | 103 | client = new PIGATO.Client(bhost); 104 | 105 | wildcardWorker.start(); 106 | matchingworker.start(); 107 | client.start(); 108 | 109 | }); 110 | 111 | afterEach(function() { 112 | matchingworker.stop(); 113 | wildcardWorker.stop(); 114 | client.stop(); 115 | }); 116 | 117 | it('use it instead of the wildcard', function(done) { 118 | client.request(ns + '-' + workerid, '', function() {}, function(type, data) { 119 | assert.equal(type, 0); 120 | assert.equal(data, 'MATCHING'); 121 | done(); 122 | }); 123 | }); 124 | }); 125 | 126 | 127 | describe('When several workers exists with different matching length', function() { 128 | 129 | var wildcardWorker, matchingworker, workerid; 130 | 131 | beforeEach(function() { 132 | workerid = uuid.v4(); 133 | wildcardWorker = new PIGATO.Worker(bhost, ns + '-*'); 134 | matchingworker = new PIGATO.Worker(bhost, ns + '-' + workerid + '-*'); 135 | 136 | wildcardWorker.on('request', function(inp, res) { 137 | res.end('WILDCARD'); 138 | }); 139 | 140 | matchingworker.on('request', function(inp, res) { 141 | res.end('BEST MATCHING'); 142 | }); 143 | 144 | client = new PIGATO.Client(bhost); 145 | 146 | wildcardWorker.start(); 147 | matchingworker.start(); 148 | client.start(); 149 | 150 | }); 151 | 152 | afterEach(function() { 153 | matchingworker.stop(); 154 | wildcardWorker.stop(); 155 | client.stop(); 156 | }); 157 | 158 | it('use the biggest matching wildcard node', function(done) { 159 | client.request(ns + '-' + workerid + '-aa', '', function() {}, function(type, data) { 160 | assert.equal(type, 0); 161 | assert.equal(data, 'BEST MATCHING'); 162 | done(); 163 | }); 164 | }); 165 | }); 166 | 167 | 168 | describe('A wildcard worker with a long name but not matching', function() { 169 | 170 | var wildcardWorker, matchingworker, workerid; 171 | 172 | beforeEach(function() { 173 | workerid = uuid.v4(); 174 | wildcardWorker = new PIGATO.Worker(bhost, ns + uuid.v4() + uuid.v4() + '-*'); 175 | matchingworker = new PIGATO.Worker(bhost, ns + '-' + workerid + '-*'); 176 | 177 | wildcardWorker.on('request', function(inp, res) { 178 | res.end('WILDCARD'); 179 | }); 180 | 181 | matchingworker.on('request', function(inp, res) { 182 | res.end('BEST MATCHING'); 183 | }); 184 | 185 | client = new PIGATO.Client(bhost); 186 | 187 | wildcardWorker.start(); 188 | matchingworker.start(); 189 | client.start(); 190 | 191 | }); 192 | 193 | afterEach(function() { 194 | matchingworker.stop(); 195 | wildcardWorker.stop(); 196 | client.stop(); 197 | }); 198 | 199 | it('is not used', function(done) { 200 | client.request(ns + '-' + workerid + '-aa', '', function() {}, function(type, data) { 201 | assert.equal(type, 0); 202 | assert.equal(data, 'BEST MATCHING'); 203 | done(); 204 | }); 205 | }); 206 | }); 207 | }); -------------------------------------------------------------------------------- /test/worker.js: -------------------------------------------------------------------------------- 1 | var zmq = require('zmq'); 2 | 3 | var MDP = require('../lib/mdp'); 4 | 5 | var PIGATO = require('../'); 6 | 7 | var chai = require('chai'); 8 | var assert = chai.assert; 9 | var uuid = require('node-uuid'); 10 | 11 | var location = 'inproc://#'; 12 | var bhost = location + uuid.v4(); 13 | 14 | var worker, workerOpts, workerTopic = 'worker'; 15 | 16 | describe('Worker', function() { 17 | var mockBroker; 18 | 19 | beforeEach(function() { 20 | bhost = location + uuid.v4(); 21 | worker = new PIGATO.Worker(bhost, workerTopic, workerOpts); 22 | mockBroker = zmq.socket('router'); 23 | mockBroker.bindSync(bhost); 24 | }); 25 | 26 | afterEach(function(done) { 27 | 28 | mockBroker.unbind(bhost); 29 | mockBroker.removeAllListeners('message'); 30 | 31 | worker.on('disconnect', function() { 32 | done(); 33 | }); 34 | 35 | worker.stop(); 36 | }); 37 | 38 | it('connect to a zmq endpoint and call callback once ready made round trip', function(done) { 39 | 40 | var called = false; 41 | var types = [MDP.W_READY, MDP.W_HEARTBEAT, MDP.W_DISCONNECT]; 42 | var typesIndex = 0; 43 | 44 | mockBroker.on('message', function(a, b, c) { 45 | assert.include(a.toString(), worker.conf.prefix); 46 | assert.equal(MDP.WORKER, b.toString()); 47 | assert.equal(types[typesIndex], c.toString()); 48 | typesIndex++; 49 | if (typesIndex == 2) { 50 | mockBroker.send([a, b, c]); 51 | } 52 | called = true; 53 | }); 54 | 55 | 56 | worker.conf.onConnect = function() { 57 | assert.equal(true, called); 58 | delete worker.conf.onConnect; 59 | done(); 60 | }; 61 | 62 | worker.start(); 63 | }); 64 | 65 | it('connect to a zmq endpoint and emit \'connect\' once ready made round trip', function(done) { 66 | 67 | var called = false; 68 | var types = [MDP.W_READY, MDP.W_HEARTBEAT, MDP.W_DISCONNECT]; 69 | var typesIndex = 0; 70 | 71 | mockBroker.on('message', function(a, b, c) { 72 | assert.include(a.toString(), worker.conf.prefix); 73 | assert.equal(MDP.WORKER, b.toString()); 74 | assert.equal(types[typesIndex], c.toString()); 75 | typesIndex++; 76 | mockBroker.send([a, b, c]); 77 | called = true; 78 | }); 79 | 80 | worker.on('connect', function() { 81 | assert.equal(true, called); 82 | done(); 83 | }); 84 | 85 | worker.start(); 86 | }); 87 | 88 | it('emit \'connect\' at reception of first ZMQ message (even if it is not a READY message)', function(done) { 89 | 90 | var countMessage = 0; 91 | 92 | worker.on('connect', function() { 93 | setTimeout(function() { 94 | assert.equal(1, countMessage); 95 | done(); 96 | }, 20); 97 | }); 98 | 99 | worker.start(); 100 | 101 | mockBroker.send([worker.socketId, MDP.WORKER, MDP.W_REQUEST]); 102 | 103 | worker.socket.on('message', function(a, b) { 104 | assert.equal(MDP.W_REQUEST, b.toString()); 105 | countMessage++; 106 | }); 107 | }); 108 | 109 | describe('emit hearbeat regularly', function() { 110 | 111 | before(function() { 112 | workerOpts = { 113 | heartbeat: 10 114 | }; 115 | }); 116 | 117 | after(function() { 118 | workerOpts = undefined; 119 | }); 120 | 121 | it('emit hearbeat regularly ', function(done) { 122 | 123 | var heartbeatCount = 0; 124 | var lastDate = new Date(); 125 | 126 | mockBroker.on('message', function(a, b, c) { 127 | assert.include(a.toString(), worker.conf.prefix); 128 | assert.equal(MDP.WORKER, b.toString()); 129 | 130 | mockBroker.send([a, b, c]); 131 | if (c.toString() == MDP.W_HEARTBEAT) { 132 | heartbeatCount++; 133 | 134 | var currentDate = new Date(); 135 | 136 | // should be around 10, but jitter can occur 137 | assert.ok(currentDate - lastDate < 20); 138 | lastDate = currentDate; 139 | 140 | if (heartbeatCount == 3) { 141 | done(); 142 | } 143 | } 144 | }); 145 | 146 | worker.on('connect', function() { 147 | lastDate = +new Date(); 148 | }); 149 | 150 | worker.start(); 151 | }); 152 | 153 | }); 154 | 155 | 156 | it('emit an error events when receiving a request with CLIENT as header', function(done) { 157 | 158 | worker.on('error', function(err) { 159 | assert.equal(err, 'ERR_MSG_HEADER'); 160 | done(); 161 | }); 162 | 163 | worker.start(); 164 | 165 | mockBroker.send([worker.socketId, MDP.CLIENT, MDP.W_REQUEST]); 166 | }); 167 | 168 | 169 | it('emit request events with no data when receiving a request with nothing', function(done) { 170 | 171 | worker.on('request', function(data, reply) { 172 | assert.equal(data, undefined); 173 | assert.equal(typeof reply, 'object'); 174 | done(); 175 | }); 176 | 177 | worker.start(); 178 | 179 | mockBroker.send([worker.socketId, MDP.WORKER, MDP.W_REQUEST]); 180 | }); 181 | 182 | it('emit request events with no data when receiving a request with a JSON String', function(done) { 183 | 184 | worker.on('request', function(data, reply) { 185 | assert.equal(data, 'foo'); 186 | assert.equal(typeof reply, 'object'); 187 | done(); 188 | }); 189 | 190 | worker.start(); 191 | 192 | mockBroker.send([worker.socketId, MDP.WORKER, MDP.W_REQUEST, 'clientId', workerTopic, '', 'requestId', '"foo"']); 193 | }); 194 | 195 | it('emit request events with no data when receiving a request with an empty JSON object', function(done) { 196 | 197 | worker.on('request', function(data, reply) { 198 | assert.equal(typeof data, 'object'); 199 | assert.equal(Object.keys(data).length, 0); 200 | assert.equal(typeof reply, 'object'); 201 | done(); 202 | }); 203 | 204 | worker.start(); 205 | 206 | mockBroker.send([worker.socketId, MDP.WORKER, MDP.W_REQUEST, 'clientId', 'service', '', 'requestId', JSON.stringify({})]); 207 | }); 208 | 209 | it('emit request events with no data when receiving a request with an complexe JSON object', function(done) { 210 | 211 | worker.on('request', function(data, reply) { 212 | assert.equal(typeof data, 'object'); 213 | assert.equal(Object.keys(data).length, 4); 214 | assert.equal(data.foo, 'bar'); 215 | assert.equal(data.foo, 'bar'); 216 | assert.equal(data.life, 42); 217 | assert.equal(typeof data, 'object'); 218 | assert.ok(data.obj); 219 | assert.equal(data.obj.foo, 'baz'); 220 | assert.equal(typeof reply, 'object'); 221 | done(); 222 | }); 223 | 224 | worker.start(); 225 | 226 | mockBroker.send([worker.socketId, MDP.WORKER, MDP.W_REQUEST, 'clientId', 'service', '', 'requestId', JSON.stringify({ 227 | foo: 'bar', 228 | life: 42, 229 | tables: [], 230 | obj: { 231 | foo: 'baz' 232 | } 233 | })]); 234 | }); 235 | 236 | 237 | it('messages sended to another worker is not received/handled', function(done) { 238 | 239 | var received = false; 240 | 241 | worker.on('request', function(data, reply) { 242 | assert.equal(typeof reply, 'object'); 243 | received = true; 244 | }); 245 | 246 | worker.start(); 247 | 248 | mockBroker.send([worker.socketId + 'LLLLL', MDP.WORKER, MDP.W_REQUEST, 'clientId', 'service', '', 'requestId']); 249 | 250 | setTimeout(function() { 251 | assert.equal(received, false); 252 | done(); 253 | }, 25); 254 | }); 255 | 256 | 257 | describe('For an request exchange', function() { 258 | var toCheck = undefined; 259 | var toAnswer = 'HELLO'; 260 | beforeEach(function() { 261 | worker.on('request', function(data, reply) { 262 | reply.end(toAnswer); 263 | }); 264 | 265 | mockBroker.on('message', function(a, side, type) { 266 | if (MDP.WORKER == side.toString() && type.toString() == MDP.W_REPLY) { 267 | assert.ok(toCheck); 268 | toCheck.apply(toCheck, arguments); 269 | } 270 | }); 271 | 272 | worker.start(); 273 | }); 274 | 275 | it('response keep the same clientId', function(done) { 276 | toCheck = function(a, side, type, clientId) { 277 | assert.ok(clientId); 278 | assert.equal(clientId.toString(), 'clientId'); 279 | done(); 280 | }; 281 | mockBroker.send([worker.socketId, MDP.WORKER, MDP.W_REQUEST, 'clientId', 'service', '', 'requestId']); 282 | }); 283 | 284 | it('empty is empty string ', function(done) { 285 | toCheck = function(a, side, type, clientId, empty) { 286 | assert.ok(empty); 287 | assert.equal(empty.length, 0); 288 | assert.equal(empty.toString().length, 0); 289 | done(); 290 | }; 291 | mockBroker.send([worker.socketId, MDP.WORKER, MDP.W_REQUEST, 'clientId', 'service', '', 'requestId']); 292 | }); 293 | 294 | it('empty is empty string event when not empty was sended', function(done) { 295 | toCheck = function(a, side, type, clientId, empty) { 296 | assert.ok(empty); 297 | assert.equal(empty.length, 0); 298 | assert.equal(empty.toString().length, 0); 299 | done(); 300 | }; 301 | mockBroker.send([worker.socketId, MDP.WORKER, MDP.W_REQUEST, 'clientId', 'service', 'NOTEMPTY', 'requestId']); 302 | }); 303 | 304 | it('response keep the same requestId', function(done) { 305 | var rid = Math.random(); 306 | toCheck = function(a, side, type, clientId, service, requestId) { 307 | assert.ok(requestId); 308 | assert.equal(requestId.toString(), 'requestId' + rid); 309 | done(); 310 | }; 311 | mockBroker.send([worker.socketId, MDP.WORKER, MDP.W_REQUEST, 'clientId', 'service', '', 'requestId' + rid]); 312 | }); 313 | 314 | it('answer status 0 for a response with no problem', function(done) { 315 | toCheck = function(a, side, type, clientId, service, requestId, status) { 316 | assert.ok(status); 317 | assert.equal(status.toString(), 0); 318 | done(); 319 | }; 320 | mockBroker.send([worker.socketId, MDP.WORKER, MDP.W_REQUEST, 'clientId', 'service', '', 'requestId']); 321 | }); 322 | 323 | it('string data are correctly sended', function(done) { 324 | toCheck = function(a, side, type, clientId, service, requestId, status, data) { 325 | assert.ok(data); 326 | assert.ok(data.toString()); 327 | assert.equal(JSON.parse(data.toString()), toAnswer); 328 | done(); 329 | }; 330 | mockBroker.send([worker.socketId, MDP.WORKER, MDP.W_REQUEST, 'clientId', 'service', '', 'requestId']); 331 | }); 332 | 333 | it('object data are correctly sended', function(done) { 334 | 335 | toAnswer = { 336 | foo: 'bar', 337 | toto: 42 338 | }; 339 | 340 | toCheck = function(a, side, type, clientId, service, requestId, status, data) { 341 | assert.ok(data); 342 | assert.ok(data.toString()); 343 | var parsed = JSON.parse(data.toString()); 344 | assert.ok(parsed); 345 | assert.equal(Object.keys(parsed).length, 2); 346 | 347 | assert.equal(parsed.foo, 'bar'); 348 | assert.equal(parsed.toto, 42); 349 | done(); 350 | }; 351 | 352 | mockBroker.send([worker.socketId, MDP.WORKER, MDP.W_REQUEST, 'clientId', 'service', '', 'requestId']); 353 | }); 354 | }); 355 | 356 | 357 | describe('when I set Concurency', function() { 358 | 359 | before(function() { 360 | workerOpts = { 361 | concurrency: Math.floor(Math.random() * 100), 362 | heartbeat: 10, 363 | reconnect: 10 364 | }; 365 | }); 366 | 367 | it('send the defined concurrency in the hearbeat message', function(done) { 368 | 369 | var finished = false; 370 | 371 | mockBroker.on('message', function(a, b, c, empty, data) { 372 | assert.include(a.toString(), worker.conf.prefix); 373 | assert.equal(MDP.WORKER, b.toString()); 374 | 375 | if (c.toString() == MDP.W_HEARTBEAT && !finished) { 376 | 377 | assert.ok(empty); 378 | assert.equal(0, empty.toString().length); 379 | 380 | assert.ok(data); 381 | var parsedConf = JSON.parse(data); 382 | assert.ok(parsedConf); 383 | assert.equal(parsedConf.concurrency, workerOpts.concurrency); 384 | 385 | finished = true; 386 | done(); 387 | } 388 | }); 389 | 390 | worker.start(); 391 | }); 392 | 393 | it('is not impacted by the current requests', function(done) { 394 | 395 | var heartbeatCount = 0; 396 | 397 | mockBroker.on('message', function(a, b, c, empty, data) { 398 | assert.include(a.toString(), worker.conf.prefix); 399 | assert.equal(MDP.WORKER, b.toString()); 400 | 401 | if (c.toString() == MDP.W_HEARTBEAT) { 402 | mockBroker.send([a, b, c]); 403 | 404 | assert.ok(empty); 405 | assert.equal(0, empty.toString().length); 406 | 407 | assert.ok(data); 408 | var parsedConf = JSON.parse(data); 409 | assert.ok(parsedConf); 410 | assert.equal(parsedConf.concurrency, workerOpts.concurrency); 411 | heartbeatCount++; 412 | 413 | if (heartbeatCount == 3) { 414 | done(); 415 | } 416 | } 417 | }); 418 | 419 | worker.start(); 420 | mockBroker.send([worker.socketId, MDP.WORKER, MDP.W_REQUEST, 'clientId', 'service', '', 'requestId' + Math.floor(Math.random() * 100)]); 421 | mockBroker.send([worker.socketId, MDP.WORKER, MDP.W_REQUEST, 'clientId', 'service', '', 'requestId' + Math.floor(Math.random() * 100)]); 422 | mockBroker.send([worker.socketId, MDP.WORKER, MDP.W_REQUEST, 'clientId', 'service', '', 'requestId' + Math.floor(Math.random() * 100)]); 423 | }); 424 | 425 | }); 426 | 427 | 428 | describe('when I send more request in // than conf.concurency', function() { 429 | var toCheck = undefined; 430 | 431 | before(function() { 432 | workerOpts = { 433 | concurrency: 1, 434 | heartbeat: 10, 435 | reconnect: 10 436 | }; 437 | }); 438 | 439 | beforeEach(function() { 440 | var toAnswer = 'HELLO'; 441 | 442 | var requestIndex = 0; 443 | worker.on('request', function(data, reply) { 444 | requestIndex++; 445 | setTimeout(function() { 446 | //we answer in reversed order to 3 first request 447 | reply.end(toAnswer); 448 | }, 30 - requestIndex * 10); 449 | }); 450 | 451 | mockBroker.on('message', function(a, side, type) { 452 | if (MDP.WORKER == side.toString() && type.toString() == MDP.W_REPLY) { 453 | assert.ok(toCheck); 454 | toCheck.apply(toCheck, arguments); 455 | } 456 | }); 457 | }); 458 | 459 | after(function() { 460 | workerOpts = {}; 461 | }); 462 | 463 | it('requests are still handled in //', function(done) { 464 | 465 | var replyIndex = 3; 466 | toCheck = function(a, side, type, clientId, service, requestId) { 467 | assert.include(a.toString(), worker.conf.prefix); 468 | assert.equal(MDP.WORKER, side.toString()); 469 | 470 | if (type.toString() == MDP.W_HEARTBEAT) { 471 | mockBroker.send([a, undefined, type]); 472 | 473 | } else if (type.toString() == MDP.W_REPLY) { 474 | 475 | assert.equal(requestId, '' + replyIndex); 476 | replyIndex--; 477 | if (replyIndex == 0) { 478 | done(); 479 | } 480 | } 481 | }; 482 | 483 | worker.start(); 484 | 485 | mockBroker.send([worker.socketId, MDP.WORKER, MDP.W_REQUEST, 'clientId', 'service', '', '1']); 486 | mockBroker.send([worker.socketId, MDP.WORKER, MDP.W_REQUEST, 'clientId', 'service', '', '2']); 487 | mockBroker.send([worker.socketId, MDP.WORKER, MDP.W_REQUEST, 'clientId', 'service', '', '3']); 488 | }); 489 | 490 | }); 491 | }); 492 | 493 | 494 | 495 | describe('Worker Disconnection', function() { 496 | 497 | var mockBroker; 498 | 499 | beforeEach(function() { 500 | bhost = location + uuid.v4(); 501 | worker = new PIGATO.Worker(bhost, workerTopic, workerOpts); 502 | mockBroker = zmq.socket('router'); 503 | mockBroker.bindSync(bhost); 504 | }); 505 | 506 | afterEach(function(done) { 507 | mockBroker.unbind(bhost); 508 | mockBroker.removeAllListeners('message'); 509 | done(); 510 | }); 511 | 512 | 513 | describe('when stop is called when connected', function() { 514 | it('send Disconnect', function(done) { 515 | 516 | var types = [MDP.W_READY, MDP.W_HEARTBEAT, MDP.W_DISCONNECT]; 517 | var typesIndex = 0; 518 | 519 | mockBroker.on('message', function(a, b, c) { 520 | assert.include(a.toString(), worker.conf.prefix); 521 | assert.equal(MDP.WORKER, b.toString()); 522 | assert.equal(types[typesIndex], c.toString()); 523 | typesIndex++; 524 | 525 | if (c.toString() == MDP.W_DISCONNECT) { 526 | done(); 527 | return; 528 | } 529 | 530 | setTimeout(function() { 531 | mockBroker.send([a, b, c]); 532 | }, 10); 533 | }); 534 | 535 | worker.on('connect', function() { 536 | worker.stop(); 537 | }); 538 | 539 | worker.start(); 540 | }); 541 | }); 542 | 543 | 544 | describe('when Broker doesn\'t anwser to heartbeat', function() { 545 | 546 | before(function() { 547 | workerOpts = { 548 | heartbeat: 20, 549 | reconnect: 200 550 | }; 551 | }); 552 | 553 | it('send 3 Heartbeat messages then reconnect', function(done) { 554 | 555 | var types = [MDP.W_READY, MDP.W_HEARTBEAT, MDP.W_HEARTBEAT, MDP.W_HEARTBEAT, MDP.W_DISCONNECT, MDP.W_READY, MDP.W_HEARTBEAT]; 556 | var typesIndex = -1; 557 | 558 | mockBroker.on('message', function(a, b, c) { 559 | typesIndex++; 560 | 561 | if (typesIndex == types.length) { 562 | done(); 563 | return; 564 | } 565 | 566 | if (typesIndex >= types.length) { 567 | return; 568 | } 569 | 570 | assert.include(a.toString(), worker.conf.prefix); 571 | assert.equal(MDP.WORKER, b.toString()); 572 | assert.equal(types[typesIndex], c.toString()); 573 | }); 574 | 575 | worker.start(); 576 | }); 577 | 578 | 579 | it('emit Disconnect when detecting it, after sending it', function(done) { 580 | 581 | var types = [MDP.W_READY, MDP.W_HEARTBEAT, MDP.W_HEARTBEAT, MDP.W_HEARTBEAT, MDP.W_DISCONNECT]; 582 | var typesIndex = -1; 583 | 584 | mockBroker.on('message', function(a, b, c) { 585 | typesIndex++; 586 | 587 | if (typesIndex >= types.length) { 588 | return; 589 | } 590 | 591 | assert.include(a.toString(), worker.conf.prefix); 592 | assert.equal(MDP.WORKER, b.toString()); 593 | assert.equal(types[typesIndex], c.toString()); 594 | }); 595 | 596 | worker.on('disconnect', function() { 597 | assert(typesIndex, 3); 598 | worker.removeAllListeners('disconnect'); 599 | done(); 600 | }); 601 | 602 | worker.start(); 603 | }); 604 | 605 | }); 606 | }); 607 | -------------------------------------------------------------------------------- /test/z9_system.js: -------------------------------------------------------------------------------- 1 | var PIGATO = require('../'); 2 | var zmq = require('zmq'); 3 | var chai = require('chai'); 4 | var assert = chai.assert; 5 | var uuid = require('node-uuid'); 6 | 7 | var bhost = 'inproc://#' + uuid.v4(); 8 | //var bhost = 'tcp://0.0.0.0:2020'; 9 | 10 | var broker = new PIGATO.Broker(bhost); 11 | 12 | describe('FILE DESCRIPTORS', function() { 13 | before(function(done) { 14 | broker.conf.onStart = done; 15 | broker.start(); 16 | }); 17 | 18 | after(function(done) { 19 | broker.conf.onStop = done; 20 | broker.stop(); 21 | }); 22 | 23 | var spawn = function(hm, callback) { 24 | var clients = []; 25 | try { 26 | for (var ci = 0; ci < hm; ci++) { 27 | var c = new PIGATO.Client(bhost); 28 | c.start(); 29 | clients.push(c); 30 | } 31 | } catch (err) { 32 | return callback(err, clients); 33 | } 34 | 35 | callback(null, clients); 36 | }; 37 | 38 | /* describe("When I create too many sockets", function() { 39 | it('return a \'Too many open files\' error', function(done) { 40 | spawn(zmq.Context.getMaxSockets() * 2, function(err, clients) { 41 | 42 | assert.ok(err); 43 | assert.ok(err.message); 44 | assert.equal('Too many open files', err.message); 45 | clients.forEach(function(client) { 46 | client.stop(); 47 | }); 48 | setTimeout(done, 100); 49 | }); 50 | }); 51 | });*/ 52 | 53 | 54 | describe('When I create lots of sockets', function() { 55 | 56 | before(function(done) { 57 | setTimeout(done, 100); 58 | }); 59 | 60 | it('still works if I close them in the meantime', function(done) { 61 | 62 | var cnt = 0; 63 | var step = function() { 64 | spawn(zmq.Context.getMaxSockets() - 100, function(err, clients) { 65 | assert.ok(!err); 66 | clients.forEach(function(client) { 67 | client.stop(); 68 | cnt++; 69 | }); 70 | 71 | setImmediate(next); 72 | }); 73 | }; 74 | 75 | var next = function() { 76 | if (cnt < zmq.Context.getMaxSockets() * 5) { 77 | setTimeout(step, 30); 78 | } else { 79 | done(); 80 | } 81 | }; 82 | next(); 83 | }); 84 | }); 85 | }); 86 | --------------------------------------------------------------------------------