├── .gitignore ├── LICENSE ├── README.md ├── example ├── testClient.js └── testServer.js ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | #intellij config 2 | .idea 3 | 4 | #npm dependency 5 | node_modules 6 | 7 | #bower dependency 8 | public/lib 9 | 10 | #jscoverage tmp file 11 | lib-cov 12 | 13 | #log4js logs 14 | log/*.log* 15 | log/access* 16 | 17 | #npm log 18 | npm-debug.log 19 | 20 | build -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Yuxuan Zhang 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Node-Microservice Package 2 | ============================ 3 | 4 | 5 | 6 | Clean, Direct, Easy to Scale solution to Node Microservice Framework. 7 | 8 | 9 | 10 | Inspired by [seneca.js][1], IBM's article on [microservice using seneca and MQLight][2], and RabbitMQ's [tutorial on RPC][3], 11 | we aim to create a one stop solution that will allow seneca-style communication between any number of servers and clients, with the 12 | ability of load balance offered by message brokers such as RabbitMQ. We decided to do this without 13 | any dependency on seneca, thus allowing us the maximum freedom in programming language, and set up RabbitMQ server 14 | as our own message broker. Using this package, you only need one function to set up the server, and two functions for 15 | the client. You can even separate the connection and send functions for the client, so you only need to connect once 16 | for your entire project, and send any number of requests. 17 | 18 | 19 | 20 | Special thanks to my mentor and great boss: [dingziran][4]@[Wecash][5]! This solution will not exist or even work without his help. 21 | 22 | 23 | 24 | 25 | **TL;DR**: freedom in language, minimum lines of code, over amqp message broker 26 | 27 | 28 | 29 | 30 | Advantage/Features: 31 | - **Timeout Mechanism**: During our production test, we found that having the timeout mechanism is extremely helpful especially when the internet 32 | connection is poor and the number of requests is huge. Both the server and the client take the parameter of timeout, 33 | so that when the time is due, the client will automatically timeout the requests, or the tasks will timeout in the queue 34 | before being sent to the server. 35 | - **Multiple Requests**: While testing all of the available choices, we found that none of them can handle the problem of having multiple 36 | requests from one client. By using our own function, we made sure that no matter which requesting client 37 | received the response, the request will be resolved and returned properly. 38 | - **Promise**: No more call back api! Everything is a Promise. 39 | 40 | 41 | 42 | 43 | 44 | *This package is tested and stable. The performance is great in production for over two months. More extensive testings are welcomed!* 45 | 46 | 47 | 48 | # What's new 49 | 50 | - Fixed client's timeout error behavior- it now throws an Error object, intead of a string of "Timeout" 51 | - Added ensureDone to server's option- the server will guarantee to complete the task by asking the MQ to requeue the error/failed 52 | message. 53 | - Fixed client's behavior when you don't care about server's response- it now returns undefined. 54 | 55 | 56 | 57 | # Install 58 | 59 | npm install node-microservice 60 | 61 | # Usage 62 | 63 | ## Server: 64 | Just add this one line of code at the end of your service file, pass options as an object, and you have a working server. 65 | 66 | 67 | 68 | exports.server_listen=function(amqp_url,service_name,pro,options) 69 | 70 | 71 | - amqp_url is the address of your MQ service. Such as:`"amqp://usr:password@128.11.22.230"` 72 | - service_name is the name of the request the server is listening to. The server will only listen to the request that the 73 | client sent with the same service name. 74 | - pro is the function that you want to pass the message/task to. This function **MUST** be a Promise. 75 | 76 | For options: 77 | 78 | 79 | 80 | **Must Have**: noAck (boolean) 81 | - if noAck = True: the queue will send tasks to server and then discard regardless of the server's 82 | state. **Warning**: when having two or more servers, setting noAck to True will risk loosing message if one of the server that gets the messages goes offline. 83 | - if noAck = False: the queue will make sure the server get and finish the task with acknowledgements. 84 | Warning: if the server gets the tasks but fails to finish, the message will 85 | - 1. Timeout as determined by messageTtl option, 86 | - 2. If the server goes offline, the tasks will be requeued and sent to the 87 | next available server. 88 | 89 | 90 | 91 | *We recommend setting noAck to False to guarantee message delivery and avoid loosing message when 92 | multiple servers are online and working. 93 | 94 | 95 | **Optional**: 96 | 97 | **messageTtl** (milliseconds) (must start a new queue if this option was just added, modified, or taken out) 98 | - Set timeout for messages in the queue. This option has proven to be extremely helpful when the 99 | server goes offline and comes back and the messages are requeued. If the messageTtl is set to 100 | the same milliseconds as the client's timeout, the queue will make sure the server do not get 101 | timed out messages that the client no longer cares about. 102 | 103 | 104 | 105 | *We recommend setting the messageTtl equal to the client's timeout parameter. 106 | 107 | 108 | **prefetch_num** (integer bigger than or equal to 1) 109 | - Set the maximum number of acknowledgements the queue can wait from the server. In other words, it 110 | is the maximum number of tasks one server can take each time. That said, this will only be 111 | effective when the noAck is set to False. If prefetch_num is not given, the server will 112 | simply take as many tasks as possible and this may cause a race condition. In our production 113 | experience, we find that a number of 10 or 100 works just fine. 114 | 115 | 116 | 117 | *Only effective when noAck is set to False. 118 | 119 | 120 | 121 | 122 | **ensureDone** (boolean) 123 | - **New**: When set to true, if your server return the task with an error, the MQ will take back and requeue the message. 124 | Great for production when you need to make huge and consistent queries. It will guarantee the completeness of each task 125 | until servers no longer send error and ack the message. 126 | 127 | 128 | 129 | *Only effective when noAck is set to False. 130 | 131 | 132 | 133 | 134 | **durable** (boolean) 135 | - Make the queue durable as stated on the RabbitMQ website. The default setting is false. 136 | 137 | 138 | **Example**: 139 | - Our Safest/Most Used Options: 140 | `{noAck:false, prefetch_num:10, messageTtl:60000}` 141 | - Simplest/Minimalist Options(best for just testing): 142 | `{noAck:true}` 143 | - When you need to make sure all tasks are completed with no error: 144 | `{noAck:false, prefetch_num:10, messageTtl:60000, ensureDone:true}` 145 | 146 | 147 | 148 | 149 | 150 | ## Client: 151 | This function will easily set up your client with your message broker over amqp protocol: 152 | 153 | 154 | 155 | exports.connect_amqp=function(amqp_url, [options]) 156 | 157 | 158 | - amqp_url is the address of your MQ service. Such as:`"amqp://usr:password@128.11.22.230"` 159 | - options is the object that contains your graylog server's setting. A typical and minimum setting is included in the 160 | testClient.js file in example folder. This is optional if you don't have or don't want to use the graylog system for your 161 | project. 162 | 163 | 164 | 165 | This function will send your message to your designated server, and timeout if a response is not received within the given time: 166 | 167 | 168 | 169 | exports.send=function(serviceName,message,timeout) 170 | 171 | 172 | - serviceName is the name of the server you want to send your message to. Make sure your server and client have the same serviceName! 173 | - message is an object that is sent to the message broker. Behind the scene, the message is first transformed to string and then a buffer. 174 | After the server received the message, it will first decode and parse it as a JSON object. 175 | - timeout is the milliseconds you want the client to wait before giving up 176 | 177 | 178 | 179 | We have decided that the client will not have an noAck option and it is set to true since we do not really care about whether 180 | the client has received the response. Welcome to fork if you would like to add this feature! 181 | 182 | 183 | 184 | 185 | ## Logging 186 | 187 | We had a long discussion with multiple tryouts, and we havedecided to use and only support the [graylog][6] system 188 | based on [graylog2][7] package for this project. It was not an 189 | easy decision for us, and we understand having to install both graylog and elasticsearch would take up to two hours of 190 | configurations. So we have made the logging options **optional**. If you would still like to use the common way `console.log`, 191 | simply uncomment the line in source code and everything will work exactly the same. For detailed description, see client function 192 | usage. 193 | 194 | 195 | 196 | 197 | 198 | ## Example 199 | 200 | You can find one test client and one test server file in the example folder. The amqp_url is fortified! Try with different options. 201 | 202 | 203 | 204 | ## Message Broker 205 | 206 | We recommend and use RabbitMQ for its popularity and wide range of support. For a simple intro to installation and tutorial, please refer 207 | to its [website][8]. Essentially, since we use `amqplib`, any message broker based on amqp protocol is just fine. 208 | 209 | 210 | 211 | 212 | ## Version Updates 213 | 214 | - Ver 0.6.0: Production performance is great. Welcome for extensive testings! 215 | - Ver 0.5.11: Timeout err is thrown with an Error object 216 | - Ver 0.5.10: minor improve on graylog message; server throw error after disconnection 217 | - Ver 0.5.9: see New section; added ensureDone for extreme production needs 218 | - Ver 0.5.8: see New section; fixed client's behavior with empty response and bug on timeout error; the correlationID is used to 219 | identify empty response 220 | - Ver 0.5.7: the logging system is now here with full support to graylog 221 | - Ver 0.5.6: fixed the problem of not parsing the response from server to JSON in package before passing back to client; 222 | no parsing is needed at all now! 223 | - Ver 0.5.5: fixed problems in server side on error handling including returning messages and shutting down when connection throws an error; 224 | updated license from ISC to MIT; minor fixes in readme; no longer prefer global installation 225 | 226 | 227 | 228 | 229 | ## TODO 230 | 231 | 232 | Definite: 233 | - Improve Readme with photos/interactions and the underlying mechanism 234 | - Include a simple RabbitMQ tutorial in README 235 | 236 | Maybe: 237 | - Implement the topic/fanout delivery options 238 | - Implement the exchange/clustering node options 239 | 240 | Any help/fork/issues/contribution is deeply appreciated and welcomed! 241 | 242 | 243 | 244 | 245 | July 2015 246 | 247 | 248 | [1]: http://senecajs.org/ 249 | [2]: https://developer.ibm.com/messaging/2015/05/06/microservices-with-seneca-and-mq-light/ 250 | [3]: https://github.com/squaremo/amqp.node/tree/master/examples/tutorials 251 | [4]: https://github.com/dingziran 252 | [5]: http://www.wecash.net/ 253 | [6]: https://www.graylog.org/ 254 | [7]: https://www.npmjs.com/package/graylog2 255 | [8]: https://www.rabbitmq.com/download.html -------------------------------------------------------------------------------- /example/testClient.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by root on 6/17/15. 3 | */ 4 | var amqpClient = require('./../index'); 5 | var amqp_url="amqp://usr:password@128.11.22.230";//Change to your own username, password, and address and/or vhost 6 | var options = { 7 | servers: [ 8 | { 'host': "42.11.11.123", port: 12201 }//Put your own graylog server here! 9 | ], 10 | hostname: "node-microservice" // the name of this host 11 | // (optional, default: os.hostname()) 12 | };//If you don't have a graylog system, simply comment and take out options. It still works without a logging system 13 | 14 | amqpClient.connect_amqp(amqp_url, options).then( 15 | function(){ 16 | amqpClient.send('testing',3,60000).then( 17 | function onFulfilled(result){ 18 | console.log(result); 19 | }, 20 | function onTimeout(err){ 21 | console.log(err); 22 | } 23 | ) 24 | amqpClient.send('testing',6,60000).then( 25 | function onFulfilled(result){ 26 | console.log(result); 27 | }, 28 | function onTimeout(err){ 29 | console.log(err); 30 | } 31 | ) 32 | } 33 | ); 34 | -------------------------------------------------------------------------------- /example/testServer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by root on 6/17/15. 3 | */ 4 | var amqpServer = require('./../index'); 5 | var amqp_url="amqp://usr:password@128.11.22.230";//Change to your own username, password, and address and/or vhost 6 | var options = {noAck:false, prefetch_num:10, messageTtl:60000}; 7 | 8 | function test(content){ 9 | return new Promise(function(resolve,reject){ 10 | var n = parseInt(content.toString()); 11 | console.log(' [.] got(%d)', n); 12 | var response = n+1; 13 | setTimeout(function(){ 14 | resolve(response); 15 | },n*1000); 16 | }); 17 | } 18 | amqpServer.server_listen(amqp_url,'testing',test,options); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var amqp = require('amqplib'); 2 | var uuid = require('node-uuid'); 3 | var graylog2 = require('graylog2'); 4 | var logger={log:function(){},error:function(){}}; 5 | var cacheTable={}; 6 | var mqService={}; 7 | 8 | //defer is discourage in most promise library so we define one for this particular situation 9 | function defer(timeout) { 10 | var resolve, reject; 11 | var promise = new Promise(function() { 12 | resolve = arguments[0]; 13 | reject = arguments[1]; 14 | setTimeout(reject, timeout, "Timeout"); 15 | }); 16 | return { 17 | resolve: resolve, 18 | reject: reject, 19 | promise: promise, 20 | timestamp:new Date().getTime() 21 | }; 22 | } 23 | 24 | exports.connect_amqp=function(amqp_url, options){ 25 | return amqp.connect(amqp_url).then(function (conn) { 26 | return conn.createChannel().then( 27 | function onFulfilled(ch) { 28 | if(options){ 29 | logger=new graylog2.graylog(options); 30 | } 31 | mqService.ch=ch; 32 | var ok = ch.assertQueue('', {exclusive: true}) 33 | .then(function (qok) { 34 | return qok.queue; 35 | }); 36 | mqService.ok=ok; 37 | return ok; 38 | }); 39 | }).then(null, function(err) { 40 | console.error("Exception handled, reconnecting...\nDetail:\n" + err); 41 | setTimeout(exports.connect_amqp(amqp_url), 5000); 42 | }); 43 | }; 44 | 45 | exports.send=function(serviceName,message,timeout){ 46 | var messageStr=JSON.stringify(message); 47 | var q = serviceName + "_queue"; 48 | var corrId = uuid(); 49 | var ok = mqService.ok; 50 | var ch = mqService.ch; 51 | //create a new Promise and waiting for resolve 52 | var df=defer(timeout); 53 | cacheTable[corrId]=df; 54 | ok = ok.then(function (queue) { 55 | return ch.consume(queue, maybeAnswer, {noAck: true}) 56 | .then(function () { 57 | return queue; 58 | }); 59 | }); 60 | ok = ok.then(function (queue) { 61 | //console.log(' [x] Requesting'+ message); 62 | ch.sendToQueue(q, new Buffer(messageStr), { 63 | correlationId: corrId, replyTo: queue 64 | }); 65 | }); 66 | return df.promise.then( 67 | function onFulfilled(rs){ 68 | if(JSON.parse(rs) == corrId) { 69 | rs = undefined; 70 | return rs; 71 | }else{ 72 | logger.log("Success","request@#$"+messageStr+"====response@#$"+rs,{duration:new Date().getTime()-df.timestamp, service:serviceName}); 73 | return JSON.parse(rs); 74 | } 75 | }, 76 | function onReject(err){ 77 | logger.error("Timeout","request@#$"+messageStr+"====response@#$"+err,{duration:new Date().getTime()-df.timestamp, service:serviceName}); 78 | throw new Error(err); 79 | } 80 | ); 81 | }; 82 | 83 | function maybeAnswer(msg) { 84 | var corrId=msg.properties.correlationId; 85 | var pro = cacheTable[corrId]; 86 | if(pro){ 87 | pro.resolve(msg.content.toString()); 88 | delete cacheTable[corrId]; 89 | } 90 | } 91 | 92 | /** 93 | * Server Side function 94 | **/ 95 | 96 | 97 | /** 98 | * 99 | * @param amqp_url 100 | * @param service_name 101 | * @param pro (this function has to be a promise) 102 | * @param options (object) 103 | * Must Have: noAck (boolean) 104 | * if noAck = True: the queue will send tasks to server and then discard regardless of the server's 105 | * state. 106 | * Warning: when having two or more servers, setting noAck to True will risk loosing 107 | * message if one of the server that gets the messages goes offline. 108 | * if noAck = False: the queue will make sure the server get and finish the task with acknowledgements. 109 | * Warning: if the server gets the tasks but fails to finish, the message will 110 | * 1. Timeout as determined by messageTtl option 111 | * 2. If the server goes offline, the tasks will be requeued and sent to the 112 | * next available server. 113 | * *We recommend setting noAck to False to guarantee message delivery and avoid loosing message when 114 | * multiple servers are online and working 115 | * Optional : 116 | * messageTtl (milliseconds) (must start a new queue if this option was just added, modified, or taken out) 117 | * Set Timeout for messages in the queue. This option has proven to be extremely helpful when the 118 | * server goes offline and comes back and the messages are requeued. If the messageTtl is set to 119 | * the same milliseconds as the client's timeout, the queue will make sure the server do not get 120 | * timed out messages that the client no longer cares about. 121 | * *We recommend setting the messageTtl equal to the client's timeout parameter. 122 | * 123 | * prefetch_num (integer bigger than or equal to 1) 124 | * Set the maximum number of acknowledgements the queue can wait from the server. In other words, it 125 | * is the maximum number of tasks one server can take each time. That said, this will only be 126 | * effective when the noAck is set to False. If prefetch_num is not passed in, the server will 127 | * simply take as many tasks as possible and this may cause a race condition. In our production 128 | * experience, we find that a number of 10 or 100 works just fine. 129 | * *Only effective when noAck == False 130 | * 131 | * durable (boolean) 132 | * Make the queue durable as stated on the RabbitMQ website. The default setting is false. 133 | * Example: 134 | * Our Safest/Most Used Options: {noAck:false, prefetch_num:10, messageTtl:60000} 135 | * Simplest/Minimalist Options(best for just testing): {noAck:true} 136 | * 137 | * 138 | * 139 | */ 140 | exports.server_listen=function(amqp_url,service_name,pro,options){//pro has to be a promise 141 | var durable; 142 | if(options.durable) { 143 | durable = options.durable; 144 | }else 145 | durable = false; 146 | 147 | var queueOptions={durable: durable}; 148 | 149 | if(options.messageTtl) 150 | queueOptions.messageTtl = options.messageTtl; 151 | 152 | amqp.connect(amqp_url).then(function(conn) { 153 | process.once('SIGINT', function() { conn.close(); }); 154 | return conn.createChannel().then(function(ch) { 155 | var q = service_name + "_queue"; 156 | var ok = ch.assertQueue(q, queueOptions); 157 | var ok = ok.then(function() { 158 | if(options.prefetch_num) { 159 | ch.prefetch(options.prefetch_num); 160 | } 161 | return ch.consume(q, reply,{noAck:options.noAck}); 162 | }); 163 | return ok.then(function() { 164 | console.log(' [x] Awaiting requests'); 165 | }); 166 | 167 | function reply(msg) { 168 | var content=JSON.parse(msg.content.toString()); 169 | pro(content).then( 170 | function onFulfilled(response){ 171 | if(!response) { 172 | response = msg.properties.correlationId; 173 | } 174 | ch.sendToQueue(msg.properties.replyTo, 175 | new Buffer(JSON.stringify(response)), 176 | {correlationId: msg.properties.correlationId}); 177 | if(!options.noAck) { 178 | ch.ack(msg); 179 | } 180 | }, 181 | function onReject(err){ 182 | console.log(err); 183 | ch.sendToQueue(msg.properties.replyTo, 184 | new Buffer(JSON.stringify(err)), 185 | {correlationId: msg.properties.correlationId}); 186 | if(!options.noAck) { 187 | if(options.ensureDone){ 188 | ch.nack(msg); 189 | }else { 190 | ch.ack(msg); 191 | } 192 | } 193 | } 194 | ); 195 | } 196 | }); 197 | }).then(null, function(err){ 198 | console.error("Server has problem connecting, shutting down...\nDetail:\n" + err); 199 | throw err; 200 | }); 201 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-microservice", 3 | "version": "0.6.0", 4 | "description": "Clean, Direct, Easy to Scale solution to Node Microservice Framework", 5 | "url": "https://github.com/richardzyx/node-microservice", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/richardzyx/node-microservice" 9 | }, 10 | "main": "index.js", 11 | "scripts": { 12 | "start": "node index.js" 13 | }, 14 | "keywords": [ 15 | "rabbitmq", 16 | "microservice", 17 | "amqp", 18 | "framework" 19 | ], 20 | "author": "Richard Yuxuan Zhang, Ziran Ding", 21 | "license": "MIT", 22 | "dependencies": { 23 | "amqplib": "^0.3.2", 24 | "graylog2": "^0.1.3", 25 | "node-uuid": "^1.4.3" 26 | } 27 | } 28 | --------------------------------------------------------------------------------