├── test ├── ssl │ ├── cleanSsl.sh │ ├── openssl.cnf │ └── setupSsl.sh ├── .DS_Store ├── ssl.test.coffee ├── rabbit.test.coffee ├── proxy.js ├── sslproxy.js ├── heartbeat.test.coffee ├── connection.test.coffee ├── exchange.test.coffee ├── publish.test.coffee └── queue.test.coffee ├── .npmignore ├── src ├── amqp.coffee ├── lib │ ├── TemporaryChannel.coffee │ ├── config.coffee │ ├── protocol.coffee │ ├── constants.coffee │ ├── ChannelManager.coffee │ ├── Exchange.coffee │ ├── plugins │ │ └── rabbit.coffee │ ├── Queue.coffee │ ├── defaults.coffee │ ├── parseHelpers.coffee │ ├── Publisher.coffee │ ├── AMQPParser.coffee │ ├── Channel.coffee │ ├── serializationHelpers.coffee │ ├── Consumer.coffee │ └── Connection.coffee ├── amqp-definitions-0-9-1.js └── jspack.js ├── .gitignore ├── .travis.yml ├── scripts ├── compile.sh ├── generateProtocolFile.coffee └── test.sh ├── examples ├── sslExample.coffee ├── multipleConsumers.coffee ├── publishConfirms.coffee └── basic.coffee ├── package.json ├── LICENSE.txt └── README.md /test/ssl/cleanSsl.sh: -------------------------------------------------------------------------------- 1 | rm -Rf client 2 | rm -Rf server 3 | rm -Rf testca 4 | -------------------------------------------------------------------------------- /test/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/amqp-coffee/master/test/.DS_Store -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /.DS_Store 3 | /coverage.html 4 | /coverage/ 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /src/amqp.coffee: -------------------------------------------------------------------------------- 1 | debug = require('debug')('amqp') 2 | 3 | Connection = require('./lib/Connection') 4 | 5 | module.exports = Connection -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /.DS_Store 3 | /bin/ 4 | /coverage.html 5 | /coverage/ 6 | npm-debug.log 7 | /test/ssl/testca 8 | /test/ssl/server 9 | /test/ssl/client 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "7" 5 | - "6" 6 | - "5" 7 | - "5.1" 8 | - "4" 9 | - "4.2" 10 | - "4.1" 11 | - "0.12" 12 | - "0.10" 13 | - "iojs" 14 | services: rabbitmq 15 | 16 | -------------------------------------------------------------------------------- /scripts/compile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | SCRIPT_PATH=`dirname $0` 3 | echo "amqp-coffee Compiling coffeescript to bin/" 4 | echo $SCRIPT_PATH 5 | 6 | rm -rf $SCRIPT_PATH/../bin 7 | mkdir $SCRIPT_PATH/../bin 8 | 9 | cp -r $SCRIPT_PATH/../src $SCRIPT_PATH/../bin/ 10 | 11 | # compile all coffeescript files 12 | find $SCRIPT_PATH/../bin -name "*.coffee" | xargs $SCRIPT_PATH/../node_modules/coffee-script/bin/coffee --compile 13 | # remove all coffeescript files 14 | find $SCRIPT_PATH/../bin -name "*.coffee" | xargs rm 15 | -------------------------------------------------------------------------------- /examples/sslExample.coffee: -------------------------------------------------------------------------------- 1 | AMQP = require('../bin/src/amqp') 2 | 3 | # amqpClient = new AMQP { ssl: true, host: 'localhost', sslOptions:{ ca: [require('fs').readFileSync('./test/ssl/testca/cacert.pem')]}}, (error)-> 4 | 5 | amqpClient = new AMQP { ssl: true, host: 'owl.rmq.cloudamqp.com', vhost:, login:, password:}, (error, connection)-> 6 | 7 | 8 | amqpClient.queue({name:'testQueue'}).declare().bind 'amq.direct','testRoutingKey', ()-> 9 | 10 | console.error "cool" 11 | 12 | 13 | amqpClient.on 'ready' , ()-> 14 | console.error "we're ready" 15 | -------------------------------------------------------------------------------- /examples/multipleConsumers.coffee: -------------------------------------------------------------------------------- 1 | AMQP = require('../bin/src/amqp') 2 | 3 | amqpClient = new AMQP {host: 'localhost'}, (error)-> 4 | # queue functinos can be chained 5 | amqpClient.queue({name:'testQueue'}).declare().bind 'amq.direct','testRoutingKey', ()-> 6 | amqpClient.queue({name:'testQueue2'}).declare().bind 'amq.direct','testRoutingKey2', ()-> 7 | 8 | consumer = amqpClient.consume 'testQueue', {}, (message)-> 9 | console.error "Got Message:", message 10 | consumer.close() 11 | , ()-> 12 | 13 | # consumer = amqpClient.consume 'testQueue2', {}, (message)-> 14 | # console.error "Got Message:", message 15 | 16 | -------------------------------------------------------------------------------- /src/lib/TemporaryChannel.coffee: -------------------------------------------------------------------------------- 1 | # Temporary Channel 2 | debug = require('./config').debug('amqp:TemporaryChannel') 3 | Channel = require('./Channel') 4 | 5 | # This is just a skeleton of a simple channel object to pass around 6 | 7 | class TemporaryChannel extends Channel 8 | constructor: (connection, channel, cb)-> 9 | super(connection, channel) 10 | @cb = cb 11 | @temporaryChannel() 12 | return @ 13 | 14 | _channelOpen: ()=> 15 | if @cb? then @cb(null, @) 16 | @cb = null 17 | 18 | _channelClosed: ()-> 19 | # do nothing 20 | 21 | _onMethod: ()-> 22 | # do nothing 23 | 24 | module.exports = TemporaryChannel 25 | -------------------------------------------------------------------------------- /src/lib/config.coffee: -------------------------------------------------------------------------------- 1 | constants = require('./constants') 2 | protocol = require('./protocol')('../amqp-definitions-0-9-1') 3 | DEBUG = require('debug') 4 | 5 | DEBUG_LEVEL = process.env.AMQP 6 | 7 | debuggers = {} 8 | debug = (name)-> 9 | if !DEBUG_LEVEL? 10 | return ()-> 11 | # do nothing 12 | else 13 | return (level, message)-> 14 | if !message? and level? 15 | message = level 16 | level = 1 17 | 18 | if level <= DEBUG_LEVEL 19 | if !debuggers[name] then debuggers[name] = DEBUG(name) 20 | if typeof message is 'function' then message = message() 21 | debuggers[name](message) 22 | else 23 | return ()-> 24 | # do nothing 25 | 26 | module.exports = { constants, protocol, debug } 27 | -------------------------------------------------------------------------------- /examples/publishConfirms.coffee: -------------------------------------------------------------------------------- 1 | AMQP = require('../bin/src/amqp') 2 | 3 | amqpClient = new AMQP {host: 'localhost'}, (error)-> 4 | # queue functinos can be chained 5 | amqpClient.queue({name:'testQueue'}).declare().bind 'amq.direct','testRoutingKey', ()-> 6 | 7 | 8 | amqpClient.publish 'amq.direct', 'testRoutingKey', 'testMessagewithConfirm', {confirm: true}, (err, res)-> 9 | if err? 10 | return console.error "Message pushish error", err 11 | 12 | console.error "Message published" 13 | 14 | consumer = amqpClient.consume 'testQueue', {}, (message)-> 15 | console.error "Got Message:", message.data 16 | 17 | consumer.close ()-> 18 | console.error "Closed the consumer" 19 | 20 | amqpClient.close ()-> 21 | console.error "Closed the connection" 22 | -------------------------------------------------------------------------------- /src/lib/protocol.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (protocolFile)-> 2 | protocol = require(protocolFile) 3 | 4 | methodTable = {} 5 | methods = {} 6 | classes = {} 7 | 8 | for classInfo in protocol.classes 9 | classes[classInfo.index] = classInfo 10 | for methodInfo in classInfo.methods 11 | # className + methodInfo.name.toCammelCase 12 | name = "#{classInfo.name}#{methodInfo.name[0].toUpperCase()}#{methodInfo.name.slice(1)}" 13 | method = { name, fields: methodInfo.fields, methodIndex: methodInfo.index, classIndex: classInfo.index } 14 | 15 | if !methodTable[method.classIndex]? then methodTable[method.classIndex] = {} 16 | methodTable[method.classIndex][method.methodIndex] = method 17 | 18 | methods[name] = method 19 | 20 | return {methods, methodTable, classes} 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/basic.coffee: -------------------------------------------------------------------------------- 1 | AMQP = require('../bin/src/amqp') 2 | 3 | amqpClient = new AMQP {host: 'localhost'}, (error)-> 4 | # queue functinos can be chained 5 | amqpClient.queue({name:'testQueue'}).declare().bind 'amq.direct','testRoutingKey', ()-> 6 | 7 | expectedMessages = 3 8 | recievedMessages = 0 9 | 10 | consumer = amqpClient.consume 'testQueue', {}, (message)-> 11 | console.error "Got Message:", message.data 12 | recievedMessages++ 13 | 14 | if recievedMessages == expectedMessages 15 | console.error "Received all expected messages" 16 | 17 | consumer.close ()-> 18 | console.error "Closed the consumer" 19 | 20 | amqpClient.close ()-> 21 | console.error "Closed the connection" 22 | 23 | amqpClient.publish 'amq.direct', 'testRoutingKey', 'testMessage' 24 | amqpClient.publish 'amq.direct', 'testRoutingKey', {json: 'this is a json object'} 25 | amqpClient.publish 'amq.direct', 'testRoutingKey', new Buffer(3) 26 | 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "amqp-coffee", 3 | "description": "AMQP driver for node", 4 | "keywords": [ 5 | "amqp", 6 | "rabbitmq" 7 | ], 8 | "version": "0.1.31", 9 | "author": { 10 | "name": "David Barshow" 11 | }, 12 | "licenses": [ 13 | { 14 | "type": "MIT", 15 | "url": "https://raw.github.com/dropbox/amqp-coffee/master/LICENSE.txt" 16 | } 17 | ], 18 | "main": "./bin/src/amqp.js", 19 | "scripts": { 20 | "test": "./scripts/test.sh", 21 | "prepublish": "./scripts/compile.sh" 22 | }, 23 | "dependencies": { 24 | "async": "*", 25 | "bson": "~0.4.21", 26 | "debug": "*", 27 | "underscore": "*" 28 | }, 29 | "devDependencies": { 30 | "coffee-script": "1.4.0", 31 | "istanbul": "0.1.43", 32 | "jscoverage": "0.3.8", 33 | "mocha": "2.5.3", 34 | "should": "*", 35 | "uuid": "^3.0.0" 36 | }, 37 | "repository": { 38 | "type": "git", 39 | "url": "https://github.com/dropbox/amqp-coffee.git" 40 | }, 41 | "engines": { 42 | "node": ">=0.10.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Dropbox, Inc., http://www.dropbox.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/lib/constants.coffee: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | CHANNEL_STATE: 3 | OPEN: 'open' 4 | CLOSED: 'closed' 5 | OPENING: 'opening' 6 | 7 | MaxFrameSize : 131072 8 | MaxEmptyFrameSize : 8 9 | 10 | AMQPTypes: Object.freeze({ 11 | STRING: 'S'.charCodeAt(0) 12 | INTEGER: 'I'.charCodeAt(0) 13 | HASH: 'F'.charCodeAt(0) 14 | TIME: 'T'.charCodeAt(0) 15 | DECIMAL: 'D'.charCodeAt(0) 16 | BOOLEAN: 't'.charCodeAt(0) 17 | SIGNED_8BIT: 'b'.charCodeAt(0) 18 | SIGNED_16BIT: 's'.charCodeAt(0) 19 | SIGNED_64BIT: 'l'.charCodeAt(0) 20 | _32BIT_FLOAT: 'f'.charCodeAt(0) 21 | _64BIT_FLOAT: 'd'.charCodeAt(0) 22 | VOID: 'v'.charCodeAt(0) 23 | BYTE_ARRAY: 'x'.charCodeAt(0) 24 | ARRAY: 'A'.charCodeAt(0) 25 | TEN: '10'.charCodeAt(0) 26 | BOOLEAN_TRUE: '\x01' 27 | BOOLEAN_FALSE:'\x00' 28 | }) 29 | 30 | Indicators: Object.freeze({ 31 | FRAME_END: 206 32 | }) 33 | 34 | FrameType: Object.freeze({ 35 | METHOD: 1 36 | HEADER: 2 37 | BODY: 3 38 | HEARTBEAT: 8 39 | }) 40 | 41 | HeartbeatFrame : new Buffer([8,0,0,0,0,0,0,206]) 42 | EndFrame : new Buffer([206]) 43 | } 44 | 45 | -------------------------------------------------------------------------------- /test/ssl/openssl.cnf: -------------------------------------------------------------------------------- 1 | [ ca ] 2 | default_ca = testca 3 | 4 | [ testca ] 5 | dir = . 6 | certificate = $dir/cacert.pem 7 | database = $dir/index.txt 8 | new_certs_dir = $dir/certs 9 | private_key = $dir/private/cakey.pem 10 | serial = $dir/serial 11 | 12 | default_crl_days = 7 13 | default_days = 365 14 | default_md = sha1 15 | 16 | policy = testca_policy 17 | x509_extensions = certificate_extensions 18 | 19 | [ testca_policy ] 20 | commonName = supplied 21 | stateOrProvinceName = optional 22 | countryName = optional 23 | emailAddress = optional 24 | organizationName = optional 25 | organizationalUnitName = optional 26 | 27 | [ certificate_extensions ] 28 | basicConstraints = CA:false 29 | 30 | [ req ] 31 | default_bits = 2048 32 | default_keyfile = ./private/cakey.pem 33 | default_md = sha1 34 | prompt = yes 35 | distinguished_name = root_ca_distinguished_name 36 | x509_extensions = root_ca_extensions 37 | 38 | [ root_ca_distinguished_name ] 39 | commonName = hostname 40 | 41 | [ root_ca_extensions ] 42 | basicConstraints = CA:true 43 | keyUsage = keyCertSign, cRLSign 44 | 45 | [ client_ca_extensions ] 46 | basicConstraints = CA:false 47 | keyUsage = digitalSignature 48 | extendedKeyUsage = 1.3.6.1.5.5.7.3.2 49 | 50 | [ server_ca_extensions ] 51 | basicConstraints = CA:false 52 | keyUsage = keyEncipherment 53 | extendedKeyUsage = 1.3.6.1.5.5.7.3.1 54 | -------------------------------------------------------------------------------- /test/ssl/setupSsl.sh: -------------------------------------------------------------------------------- 1 | # https://www.rabbitmq.com/ssl.html 2 | 3 | # setup certs for testing 4 | 5 | mkdir testca 6 | cd testca 7 | 8 | mkdir certs private 9 | chmod 700 private 10 | echo 01 > serial 11 | touch index.txt 12 | pwd 13 | openssl req -x509 -config ../openssl.cnf -newkey rsa:2048 -days 365 \ 14 | -out cacert.pem -outform PEM -subj /CN=MyTestCA/ -nodes 15 | 16 | openssl x509 -in cacert.pem -out cacert.cer -outform DER 17 | 18 | cd .. 19 | 20 | mkdir server 21 | cd server 22 | openssl genrsa -out key.pem 2048 23 | openssl req -new -key key.pem -out req.pem -outform PEM \ 24 | -subj /CN=localhost/O=server/ -nodes 25 | cd ../testca 26 | pwd 27 | openssl ca -config ../openssl.cnf -in ../server/req.pem -out \ 28 | ../server/cert.pem -notext -batch -extensions server_ca_extensions 29 | cd ../server 30 | openssl pkcs12 -export -out keycert.p12 -in cert.pem -inkey key.pem -passout pass: 31 | 32 | cd .. 33 | 34 | mkdir client 35 | cd client 36 | openssl genrsa -out key.pem 2048 37 | openssl req -new -key key.pem -out req.pem -outform PEM \ 38 | -subj /CN=localhost/O=client/ -nodes 39 | cd ../testca 40 | pwd 41 | openssl ca -config ../openssl.cnf -in ../client/req.pem -out \ 42 | ../client/cert.pem -notext -batch -extensions client_ca_extensions 43 | cd ../client 44 | openssl pkcs12 -export -out keycert.p12 -in cert.pem -inkey key.pem -passout pass: 45 | -------------------------------------------------------------------------------- /scripts/generateProtocolFile.coffee: -------------------------------------------------------------------------------- 1 | parseString = require('xml2js').parseString; 2 | ampqDefinition = require('fs').readFileSync("../amqp-0-9-1-rabbit.xml") 3 | 4 | constantsForExport = [] 5 | classesForExport = [] 6 | 7 | makeCamelCase = (string)-> 8 | string = string.replace(/-/g, ' ').replace(/\s(.)/g, (str)-> return str.toUpperCase()).replace(/\s/g,'') 9 | if string == "nowait" then string = "noWait" # special case for consumer confirms feels like a bug but correct in the spec 10 | return string 11 | 12 | parseString ampqDefinition, (err, res)-> 13 | constants = res.amqp.constant 14 | classes = res.amqp.class 15 | 16 | domains = {} 17 | 18 | for domain in res.amqp.domain 19 | domains[domain['$'].name] = domain['$'].type 20 | 21 | # CONSTANTS 22 | for constant in constants 23 | constantsForExport.push [parseInt(constant['$'].value), makeCamelCase(constant['$'].name)] 24 | 25 | 26 | # CLASSES 27 | for classDef in classes 28 | 29 | classDefForExport = {name: makeCamelCase(classDef['$'].name), index: parseInt(classDef['$'].index), fields: [], methods: []} 30 | 31 | if classDef.field? 32 | for field in classDef.field 33 | 34 | if field['$'].type? 35 | domain = field['$'].type 36 | 37 | else 38 | domain = domains[field['$'].domain] 39 | 40 | classDefForExport.fields.push {name: makeCamelCase(field['$'].name), domain: domain} 41 | 42 | 43 | 44 | for method in classDef.method 45 | methodDefForExport = {name: makeCamelCase(method['$'].name), index: parseInt(method['$'].index), fields: []} 46 | 47 | if method.field? 48 | for field in method.field 49 | 50 | if field['$'].type? 51 | domain = field['$'].type 52 | 53 | else 54 | domain = domains[field['$'].domain] 55 | 56 | methodDefForExport.fields.push {name: makeCamelCase(field['$'].name), domain: domain} 57 | 58 | classDefForExport.methods.push methodDefForExport 59 | 60 | classesForExport.push classDefForExport 61 | 62 | console.log "exports.constants = " + JSON.stringify constantsForExport 63 | console.log "exports.classes = " + JSON.stringify classesForExport 64 | 65 | -------------------------------------------------------------------------------- /src/lib/ChannelManager.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | 3 | Channel Manager 4 | 5 | we track and manage all the channels on a connection. 6 | we will dynamically add and remove publish channels... maybe 7 | we track confirm channels and non confirm channels separately. 8 | 9 | ### 10 | 11 | publisherPoolSize = 1 12 | 13 | Publisher = require('./Publisher') 14 | Consumer = require('./Consumer') 15 | TemporaryChannel = require('./TemporaryChannel') 16 | 17 | class ChannelManager 18 | constructor: (connection)-> 19 | @connection = connection 20 | @channels = @connection.channels 21 | 22 | @publisherConfirmChannels = [] 23 | @publisherChannels = [] 24 | 25 | @tempChannel = null 26 | @queue = null 27 | @exchange = null 28 | 29 | @channelCount = @connection.channelCount 30 | 31 | nextChannelNumber: ()=> 32 | @channelCount++ 33 | nextChannelNumber = @channelCount 34 | return nextChannelNumber 35 | 36 | publisherChannel: (confirm, cb)=> 37 | if typeof confirm is 'function' 38 | cb = confirm 39 | confirm = false 40 | 41 | if confirm 42 | pool = @publisherConfirmChannels 43 | else 44 | pool = @publisherChannels 45 | 46 | if pool.length < publisherPoolSize 47 | channel = @nextChannelNumber() 48 | p = new Publisher(@connection, channel, confirm) 49 | @channels[channel] = p 50 | pool.push p 51 | cb(null, p.channel) 52 | else 53 | i = Math.floor(Math.random() * pool.length) 54 | cb(null, pool[i].channel) 55 | 56 | temporaryChannel: (cb)=> 57 | if @tempChannel? 58 | cb?(null, @tempChannel) 59 | return @tempChannel 60 | 61 | channel = @nextChannelNumber() 62 | 63 | @tempChannel = new TemporaryChannel @connection, channel, (err, res)=> 64 | cb?(err, @tempChannel) 65 | 66 | @channels[channel] = @tempChannel 67 | return @tempChannel 68 | 69 | consumerChannel: (cb)-> 70 | channel = @nextChannelNumber() 71 | s = new Consumer(@connection, channel) 72 | @channels[channel] = s 73 | 74 | cb(null, channel) 75 | 76 | 77 | channelReassign: (channel)-> 78 | delete @channels[channel.channel] 79 | newChannelNumber = @nextChannelNumber() 80 | channel.channel = newChannelNumber 81 | @channels[newChannelNumber] = channel 82 | 83 | channelClosed: (channelNumber)-> 84 | delete @channels[channelNumber] 85 | 86 | isChannelClosed: (channelNumber)-> 87 | return !@channels.hasOwnProperty(channelNumber) 88 | 89 | module.exports = ChannelManager 90 | -------------------------------------------------------------------------------- /src/lib/Exchange.coffee: -------------------------------------------------------------------------------- 1 | # Exchange 2 | debug = require('./config').debug('amqp:Exchange') 3 | { methods } = require('./config').protocol 4 | defaults = require('./defaults') 5 | 6 | _ = require('underscore') 7 | 8 | class Exchange 9 | constructor: (channel, args, cb)-> 10 | if !args.exchange? and args.name? 11 | args.exchange = args.name 12 | delete args['name'] 13 | 14 | if !args.exchange? 15 | cb("args.exchange is requried") if cb? 16 | return 17 | 18 | @exchangeOptions = _.defaults args, defaults.exchange 19 | 20 | @channel = channel 21 | @taskPush = channel.taskPush 22 | 23 | if cb? then cb(null, @) 24 | 25 | declare: (args, cb)-> 26 | if !args? and !cb? 27 | declareOptions = @exchangeOptions 28 | 29 | else if typeof args is 'function' 30 | cb = args 31 | args = {} 32 | declareOptions = @exchangeOptions 33 | else 34 | declareOptions = _.defaults args, @exchangeOptions 35 | 36 | @taskPush methods.exchangeDeclare, declareOptions, methods.exchangeDeclareOk, cb 37 | return @ 38 | 39 | delete: (args, cb)=> 40 | if typeof args is 'function' 41 | cb = args 42 | args = {} 43 | 44 | exchangeDeleteOptions = _.defaults args, defaults.exchangeDelete, {exchange: @exchangeOptions.exchange} 45 | 46 | @taskPush methods.exchangeDelete, exchangeDeleteOptions, methods.exchangeDeleteOk, cb 47 | return @ 48 | 49 | bind: (destExchange, routingKey, sourceExchange, cb)=> 50 | if typeof sourceExchange is 'string' 51 | sourceExchangeName = sourceExchange 52 | else 53 | cb = sourceExchange 54 | sourceExchangeName = @exchangeOptions.exchange 55 | 56 | exchangeBindOptions = { 57 | destination: destExchange 58 | source: sourceExchangeName 59 | routingKey: routingKey 60 | arguments: {} 61 | } 62 | 63 | @taskPush methods.exchangeBind, exchangeBindOptions, methods.exchangeBindOk, cb 64 | return @ 65 | 66 | unbind: (destExchange, routingKey, sourceExchange, cb)=> 67 | if typeof sourceExchange is 'string' 68 | sourceExchangeName = sourceExchange 69 | else 70 | cb = sourceExchange 71 | sourceExchangeName = @exchangeOptions.exchange 72 | 73 | exchangeUnbindOptions = { 74 | destination: destExchange 75 | source: sourceExchangeName 76 | routingKey: routingKey 77 | arguments: {} 78 | } 79 | 80 | @taskPush methods.exchangeUnbind, exchangeUnbindOptions, methods.exchangeUnbindOk, cb 81 | return @ 82 | 83 | 84 | 85 | 86 | module.exports = Exchange 87 | -------------------------------------------------------------------------------- /src/lib/plugins/rabbit.coffee: -------------------------------------------------------------------------------- 1 | debug = require('../config').debug('amqp:plugins:rabbit') 2 | http = require('http') 3 | 4 | module.exports = 5 | masterNode : (connection, queue, callback)-> 6 | # only atempt if we have hosts 7 | if !connection.connectionOptions.hosts? then return callback() 8 | 9 | #TODO let the api host and port be specifically configured 10 | host = connection.connectionOptions.host 11 | port = connection.connectionOptions.port + 10000 # this is the default option, but should probably be configurable 12 | vhost = encodeURIComponent connection.connectionOptions.vhost 13 | 14 | requestOptions = { 15 | host: host 16 | port: port 17 | path: "/api/queues/#{vhost}/#{queue}" 18 | method: 'GET' 19 | headers: { 20 | Host: host 21 | Authorization: 'Basic ' + new Buffer(connection.connectionOptions.login + ':' + connection.connectionOptions.password).toString('base64') 22 | } 23 | agent: false 24 | } 25 | 26 | req = http.request requestOptions, (res)-> 27 | if res.statusCode is 404 then return callback(null, true) # if our queue doesn't exist then master node doesn't matter 28 | res.setEncoding('utf8') 29 | 30 | body = "" 31 | 32 | res.on 'data', (chunk)-> 33 | body += chunk 34 | 35 | res.on 'end', ()-> 36 | try 37 | response = JSON.parse body 38 | catch e 39 | response = {} 40 | 41 | if !response.node? 42 | debug 1, ()-> return ["No .node in the api response,",response] 43 | return callback("No response node") # if we have no node information we doesn't really know what to do here 44 | 45 | masternode = response.node.split('@')[1] if response.node.indexOf('@') isnt -1 46 | masternode = masternode.toLowerCase() 47 | 48 | if connection.connectionOptions.host is masternode 49 | return callback(null, true) 50 | 51 | # connection.connectionOptions.hosts.hosts is set as toLowerCase in Connection 52 | for host, i in connection.connectionOptions.hosts 53 | 54 | if host.host is masternode or (host.host.indexOf('.') isnt -1 and host.host.split('.')[0] is masternode) 55 | connection.connectionOptions.hosti = i 56 | connection.updateConnectionOptionsHostInformation() 57 | return callback(null, true) 58 | 59 | debug 1, ()-> return "we can not connection to the master node, its not in our valid hosts. Master : #{masternode} Hosts : #{JSON.stringify(connection.connectionOptions.hosts)}" 60 | callback("master node isn't in our hosts") 61 | 62 | req.on 'error', (e)-> 63 | return callback(e) 64 | 65 | req.end() 66 | -------------------------------------------------------------------------------- /test/ssl.test.coffee: -------------------------------------------------------------------------------- 1 | should = require('should') 2 | async = require('async') 3 | _ = require('underscore') 4 | SslProxy = require('./sslproxy') 5 | Proxy = require('./proxy') 6 | 7 | AMQP = require('src/amqp') 8 | 9 | describe 'SSL Connection', () -> 10 | sslProxyConnection = null 11 | before (done)-> 12 | sslProxyConnection = new SslProxy.route() 13 | done() 14 | 15 | it 'tests it can connect to localhost using ssl', (done) -> 16 | amqp = new AMQP {host:'localhost', ssl: true, sslOptions: {secureProtocol:"TLSv1_method", ca: [require('fs').readFileSync('./test/ssl/testca/cacert.pem')]}}, (e, r)-> 17 | should.not.exist e 18 | done() 19 | 20 | it 'we can reconnect if the connection fails ssl', (done)-> 21 | proxy = new Proxy.route(7051, 5671, "localhost") 22 | amqp = null 23 | 24 | async.series [ 25 | (next)-> 26 | amqp = new AMQP {host:'localhost', sslPort: 7051, ssl: true, sslOptions: {secureProtocol:"TLSv1_method", ca: [require('fs').readFileSync('./test/ssl/testca/cacert.pem')]}}, (e, r)-> 27 | should.not.exist e 28 | next() 29 | 30 | (next)-> 31 | proxy.interrupt() 32 | next() 33 | 34 | (next)-> 35 | amqp.queue {queue:"test"}, (e, q)-> 36 | should.not.exist e 37 | should.exist q 38 | next() 39 | 40 | ], ()-> 41 | amqp.close() 42 | proxy.close() 43 | done() 44 | 45 | it 'we emit only one close event ssl', (done)-> 46 | proxy = new Proxy.route(9010, 5671, "localhost") 47 | amqp = null 48 | closes = 0 49 | 50 | async.series [ 51 | (next)-> 52 | amqp = new AMQP {host:'localhost', sslPort: 9010, ssl: true, sslOptions: {secureProtocol:"TLSv1_method", ca: [require('fs').readFileSync('./test/ssl/testca/cacert.pem')]}}, (e, r)-> 53 | should.not.exist e 54 | next() 55 | 56 | (next)-> 57 | amqp.on 'close', ()-> 58 | closes++ 59 | amqp.close() 60 | 61 | _.delay ()-> 62 | closes.should.eql 1 63 | amqp.close() 64 | done() 65 | , 300 66 | 67 | 68 | proxy.close() 69 | next() 70 | 71 | ], (e,r)-> 72 | should.not.exist e 73 | 74 | 75 | it 'we disconnect ssl', (done)-> 76 | amqp = null 77 | 78 | async.series [ 79 | (next)-> 80 | amqp = new AMQP {host:'localhost', ssl: true, sslOptions: {secureProtocol:"TLSv1_method", ca: [require('fs').readFileSync('./test/ssl/testca/cacert.pem')]}}, (e, r)-> 81 | should.not.exist e 82 | next() 83 | 84 | (next)-> 85 | amqp.close() 86 | next() 87 | 88 | (next)-> 89 | setTimeout next, 100 90 | 91 | (next)-> 92 | amqp.state.should.eql 'destroyed' 93 | next() 94 | 95 | ], done 96 | 97 | -------------------------------------------------------------------------------- /test/rabbit.test.coffee: -------------------------------------------------------------------------------- 1 | should = require('should') 2 | async = require('async') 3 | _ = require('underscore') 4 | Proxy = require('./proxy') 5 | 6 | uuid = require('uuid').v4 7 | 8 | AMQP = require('src/amqp') 9 | 10 | describe 'Rabbit Plugin', () -> 11 | it 'tests we can connect with a master node for a non-existent queue', (done) -> 12 | this.timeout(5000) 13 | amqp = null 14 | queue = uuid() 15 | 16 | async.series [ 17 | (next)-> 18 | 19 | amqp = new AMQP {host:['127.0.0.1', 'localhost'], rabbitMasterNode:{queue}}, (e, r)-> 20 | should.not.exist e 21 | next() 22 | 23 | (next)-> 24 | amqp.queue {queue}, (e,q)-> 25 | q.declare ()-> 26 | q.bind "amq.direct", queue, next 27 | 28 | (next)-> 29 | messageProcessor = ()-> 30 | 31 | amqp.consume queue, {}, messageProcessor, (e,r)-> 32 | should.not.exist e 33 | next() 34 | ], done 35 | 36 | it 'tests we can try to connect to a with a masterNode with no api server', (done) -> 37 | amqp = null 38 | queue = uuid() 39 | 40 | async.series [ 41 | (next)-> 42 | 43 | amqp = new AMQP {host:['idontexist.testing'], rabbitMasterNode:{queue}}, (e, r)-> 44 | should.exist e 45 | next() 46 | 47 | ], done 48 | 49 | 50 | it 'tests we can not connect to the master node', (done) -> 51 | amqp = null 52 | queue = "masterNodeTest2" 53 | 54 | async.series [ 55 | (next)-> 56 | 57 | amqp = new AMQP {host:['localhost'], rabbitMasterNode:{queue}}, (e, r)-> 58 | should.not.exist e 59 | next() 60 | 61 | (next)-> 62 | amqp.queue {queue, autoDelete: false}, (e,q)-> 63 | q.declare ()-> 64 | q.bind "amq.direct", queue, next 65 | 66 | (next)-> 67 | amqp.close() 68 | next() 69 | 70 | (next)-> 71 | amqp = new AMQP {host:['127.0.0.1'], rabbitMasterNode:{queue}}, (e, r)-> 72 | should.exist e 73 | next() 74 | 75 | ], done 76 | 77 | 78 | it 'tests we can connect with a master node for a existing queue', (done) -> 79 | amqp = null 80 | queue = "masterNodeTest" 81 | 82 | async.series [ 83 | (next)-> 84 | 85 | amqp = new AMQP {host:['127.0.0.1', 'localhost'], rabbitMasterNode:{queue}}, (e, r)-> 86 | should.not.exist e 87 | next() 88 | 89 | (next)-> 90 | amqp.queue {queue, autoDelete: false}, (e,q)-> 91 | q.declare ()-> 92 | q.bind "amq.direct", queue, next 93 | 94 | (next)-> 95 | amqp.close() 96 | next() 97 | 98 | (next)-> 99 | amqp = new AMQP {host:['127.0.0.1', 'localhost'], rabbitMasterNode:{queue}}, (e, r)-> 100 | should.not.exist e 101 | next() 102 | 103 | (next)-> 104 | messageProcessor = ()-> 105 | 106 | amqp.consume queue, {}, messageProcessor, (e,r)-> 107 | should.not.exist e 108 | next() 109 | 110 | (next)-> 111 | amqp.connectionOptions.host.should.eql 'localhost' 112 | next() 113 | 114 | ], done 115 | 116 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # default to all the tests 4 | GREP="" 5 | # don't run coverage by default 6 | COVERAGE=false 7 | 8 | # disable nodejs debugging by default 9 | NODE_DEBUG=false 10 | 11 | ROOT=`git rev-parse --show-toplevel` 12 | # the default directory to test from 13 | FILES=$ROOT/test 14 | # try to find mocha, no matter where it is 15 | MOCHA=$(dirname $(/usr/bin/env node -e "console.log(require.resolve('mocha'))"))/bin/mocha 16 | PATTERN="*.test.coffee" 17 | 18 | USAGE='Usage: '$0' [options] [paths]\n\n' 19 | USAGE=$USAGE'Options:\n' 20 | USAGE=$USAGE' -g only run the tests whose names match this grep pattern\n' 21 | USAGE=$USAGE' -d enable the nodejs debugger\n' 22 | USAGE=$USAGE' -p only run the tests whole _files_ match this pattern\n' 23 | USAGE=$USAGE' -h display this help information\n' 24 | USAGE=$USAGE' -c display coverage output instead of pass/fail\n\n' 25 | USAGE=$USAGE'Example:\n' 26 | USAGE=$USAGE'# run only the sync.test.coffee test\n' 27 | USAGE=$USAGE' '$0' test/unit/sync.test.coffee\n\n' 28 | USAGE=$USAGE'# run only the unit tests matching hello \n' 29 | USAGE=$USAGE' '$0' -g hello test/unit\n\n' 30 | 31 | args=`getopt g:p:cdh $*` 32 | # this is used if getopt finds an invalid option 33 | if test $? != 0 34 | then 35 | echo $USAGE 36 | exit 1 37 | fi 38 | 39 | set -- $args 40 | 41 | while [ ! -z "$1" ] 42 | do 43 | case "$1" in 44 | -c) 45 | COVERAGE=true 46 | ;; 47 | -d) 48 | NODE_DEBUG=true 49 | ;; 50 | -g) 51 | GREP=$2 52 | # shift another parameter off of grep, since it requires 2 53 | shift 54 | ;; 55 | -p) 56 | PATTERN=$2 57 | # shift another parameter off, since pattern requires an argument 58 | shift 59 | ;; 60 | -h) 61 | echo $USAGE 62 | exit 1 63 | ;; 64 | --) 65 | shift 66 | break 67 | ;; 68 | *) 69 | echo "Invalid option $1" 70 | echo $USAGE 71 | exit 1 72 | ;; 73 | esac 74 | 75 | shift 76 | done 77 | 78 | if [ "$#" -ne 0 ];then 79 | FILES=$@ 80 | fi 81 | 82 | 83 | echo $PATTERN 84 | # find all the tests to run 85 | TESTS=`find $FILES -iname "$PATTERN"` 86 | 87 | #ulimit -n 10000 88 | 89 | cd $ROOT/test/ssl/ 90 | ./cleanSsl.sh 91 | ./setupSsl.sh 92 | cd $ROOT 93 | 94 | $ROOT/scripts/compile.sh 95 | 96 | if $COVERAGE; then 97 | _MOCHA=$(dirname $(/usr/bin/env node -e "console.log(require.resolve('mocha'))"))/bin/_mocha 98 | ISTANBUL=$(dirname $(/usr/bin/env node -e "console.log(require.resolve('istanbul'))"))/lib/cli.js 99 | AMQP_TEST=1 NODE_PATH=$ROOT/bin $ISTANBUL cover $_MOCHA -- --require 'coffee-script' --compilers coffee:coffee-script --reporter spec --ui bdd --grep "$GREP" $TESTS 100 | open $ROOT/coverage/lcov-report/index.html 101 | 102 | # rm -rf $ROOT/bin-cov 103 | # jscoverage $ROOT/bin $ROOT/bin-cov 104 | 105 | # AMQP_TEST=1 NODE_PATH=$ROOT/bin-cov $MOCHA --require 'coffee-script' --compilers coffee:coffee-script --reporter html-cov --ui bdd --grep "$GREP" $TESTS > $ROOT/coverage.html 106 | # open $ROOT/coverage.html 107 | else 108 | 109 | AMQP_TEST=1 NODE_PATH=$ROOT/bin $MOCHA --require 'coffee-script' --compilers coffee:coffee-script --reporter spec --ui bdd --timeout 10000 --grep "$GREP" $TESTS 110 | 111 | fi 112 | 113 | 114 | -------------------------------------------------------------------------------- /src/lib/Queue.coffee: -------------------------------------------------------------------------------- 1 | # Queues 2 | debug = require('./config').debug('amqp:Queue') 3 | Channel = require('./Channel') 4 | defaults = require('./defaults') 5 | 6 | { methodTable, classes, methods } = require('./config').protocol 7 | 8 | _ = require('underscore') 9 | 10 | class Queue 11 | ### 12 | @args.name(required) 13 | @cb function required 14 | ### 15 | constructor: (channel, args, cb)-> 16 | debug 3, ()->return ["New queue", JSON.stringify(args)] 17 | if !args.queue? and args.name? 18 | args.queue = args.name 19 | delete args['name'] 20 | 21 | if !args.queue? 22 | cb("args.queue is required") if cb? 23 | return 24 | 25 | @queueOptions = _.defaults args, defaults.queue 26 | 27 | @channel = channel 28 | @taskPush = channel.taskPush 29 | 30 | if cb? then cb(null, @) 31 | 32 | declare: (args={}, cb)-> 33 | queueNameSpecified = args.queue? and args.queue isnt "" 34 | 35 | if typeof args is 'function' 36 | cb = args 37 | args = {} 38 | declareOptions = @queueOptions 39 | else 40 | declareOptions = _.defaults args, @queueOptions 41 | 42 | @taskPush methods.queueDeclare, declareOptions, methods.queueDeclareOk, (err, res)=> 43 | if !queueNameSpecified and !err? and res.queue? 44 | @queueOptions.queue = res.queue 45 | cb?(err, res) 46 | 47 | return @ 48 | 49 | bind: (exchange, routingKey, queueName, cb)=> 50 | if typeof queueName is 'string' 51 | queueName = queueName 52 | else 53 | cb = queueName 54 | queueName = @queueOptions.queue 55 | 56 | queueBindOptions = { 57 | queue: queueName 58 | exchange: exchange 59 | routingKey: routingKey 60 | arguments: {} 61 | } 62 | @taskPush methods.queueBind, queueBindOptions, methods.queueBindOk, cb 63 | 64 | return @ 65 | 66 | unbind: (exchange, routingKey, queueName, cb)=> 67 | if typeof queueName is 'string' 68 | queueName = queueName 69 | else 70 | cb = queueName 71 | queueName = @queueOptions.queue 72 | 73 | queueUnbindOptions = { 74 | queue: queueName 75 | exchange: exchange 76 | routingKey: routingKey 77 | arguments: {} 78 | } 79 | @taskPush methods.queueUnbind, queueUnbindOptions, methods.queueUnbindOk, cb 80 | 81 | return @ 82 | 83 | messageCount: (args={}, cb)=> 84 | if typeof args is 'function' 85 | cb = args 86 | args = {} 87 | 88 | declareOptions = _.defaults args, @queueOptions 89 | 90 | @declare declareOptions, (err, res)-> 91 | return cb(err) if err? 92 | if res?.messageCount? 93 | cb(null, res.messageCount) 94 | else 95 | cb('messageCount not returned') 96 | 97 | consumerCount: (args={}, cb)-> 98 | if typeof args is 'function' 99 | cb = args 100 | args = {} 101 | 102 | declareOptions = _.defaults args, @queueOptions 103 | 104 | @declare declareOptions, (err, res)-> 105 | return cb(err) if err? 106 | if res?.consumerCount? 107 | cb(null, res.consumerCount) 108 | else 109 | cb('consumerCount not returned') 110 | 111 | delete: (args={}, cb)=> 112 | if typeof args is 'function' 113 | cb = args 114 | args = {} 115 | 116 | queueDeleteArgs = _.defaults args, defaults.queueDelete, {queue: @queueOptions.queue} 117 | @taskPush methods.queueDelete, queueDeleteArgs, methods.queueDeleteOk, cb 118 | 119 | return @ 120 | 121 | module.exports = Queue 122 | -------------------------------------------------------------------------------- /test/proxy.js: -------------------------------------------------------------------------------- 1 | // Stolen from Devendra Tewari 2 | // (http://delog.wordpress.com/2011/04/08/a-simple-tcp-proxy-in-node-js/) 3 | 4 | var net = require('net'); 5 | var debug = require('debug')('proxy'); 6 | 7 | module.exports.route = function (proxyPort, servicePort, serviceHost) { 8 | var proxyRoute = this; 9 | proxyRoute.proxyPort = proxyPort || 9001; 10 | var servicePort = servicePort || 5672; 11 | var serviceHost = serviceHost || '127.0.0.1'; 12 | 13 | proxyRoute.operational = true; 14 | proxyRoute.serviceSockets = []; 15 | proxyRoute.proxySockets = []; 16 | 17 | proxyRoute.server = net.createServer(function (proxySocket) { 18 | // If we're "experiencing trouble", immediately end the connection. 19 | if (!proxyRoute.operational) { 20 | proxySocket.end(); 21 | return; 22 | } 23 | 24 | // If we're operating normally, accept the connection and begin proxying traffic. 25 | proxyRoute.proxySockets.push(proxySocket); 26 | 27 | var connected = false; 28 | var buffers = []; 29 | var serviceSocket = new net.Socket(); 30 | proxyRoute.serviceSockets.push(serviceSocket); 31 | serviceSocket.connect(parseInt(servicePort), serviceHost); 32 | serviceSocket.on('connect', function() { 33 | connected = true; 34 | for (var i in buffers) { 35 | serviceSocket.write(buffers[i]); 36 | } 37 | buffers = []; 38 | }); 39 | proxySocket.on('error', function (e) { 40 | serviceSocket.end(); 41 | }); 42 | serviceSocket.on('error', function (e) { 43 | debug('Could not connect to service at host ' + serviceHost + ', port ' + servicePort); 44 | proxySocket.end(); 45 | }); 46 | proxySocket.on("data", function (data) { 47 | if (proxyRoute.operational) { 48 | if (connected) { 49 | serviceSocket.write(data); 50 | } else { 51 | buffers.push(data); 52 | } 53 | } 54 | }); 55 | serviceSocket.on("data", function(data) { 56 | if (proxyRoute.operational) { 57 | proxySocket.write(data); 58 | } 59 | }); 60 | proxySocket.on("close", function(had_error) { 61 | serviceSocket.end(); 62 | }); 63 | serviceSocket.on("close", function(had_error) { 64 | proxySocket.end(); 65 | }); 66 | }); 67 | proxyRoute.listen(); 68 | }; 69 | module.exports.route.prototype.listen = function () { 70 | var proxyRoute = this; 71 | debug('listening for proxy connection...'); 72 | proxyRoute.operational = true; 73 | proxyRoute.server.listen(proxyRoute.proxyPort); 74 | }; 75 | module.exports.route.prototype.close = function () { 76 | var proxyRoute = this; 77 | debug('closing proxy connection...'); 78 | proxyRoute.operational = false; 79 | for (var index in proxyRoute.serviceSockets) { 80 | proxyRoute.serviceSockets[index].destroy(); 81 | } 82 | proxyRoute.serviceSockets = []; 83 | for (var index in proxyRoute.proxySockets) { 84 | proxyRoute.proxySockets[index].destroy(); 85 | } 86 | proxyRoute.proxySockets = []; 87 | proxyRoute.server.close(); 88 | }; 89 | module.exports.route.prototype.interrupt = function (howLong) { 90 | var proxyRoute = this; 91 | debug('interrupting proxy connection...'); 92 | proxyRoute.close(); 93 | setTimeout(function () { 94 | proxyRoute.listen(); 95 | }, howLong || 50); 96 | }; 97 | 98 | if (!module.parent) { 99 | var proxyPort = process.argv[2]; 100 | var servicePort = process.argv[3]; 101 | var serviceHost = process.argv[4]; 102 | var proxyRoute = new module.exports.route(proxyPort, servicePort, serviceHost); 103 | // Don't exit until parent kills us. 104 | setInterval(function () { 105 | if (process.argv[5]) { 106 | proxyRoute.interrupt(); 107 | } 108 | }, parseInt(process.argv[5]) || 1000); 109 | } 110 | -------------------------------------------------------------------------------- /test/sslproxy.js: -------------------------------------------------------------------------------- 1 | // Stolen from Devendra Tewari 2 | // (http://delog.wordpress.com/2011/04/08/a-simple-tcp-proxy-in-node-js/) 3 | 4 | var net = require('net'); 5 | var tls = require('tls'); 6 | var debug = require('debug')('ssl-proxy'); 7 | var fs = require('fs'); 8 | 9 | 10 | module.exports.route = function (proxyPort, servicePort, serviceHost) { 11 | var proxyRoute = this; 12 | proxyRoute.proxyPort = proxyPort || 5671; 13 | var servicePort = servicePort || 5672; 14 | var serviceHost = serviceHost || '127.0.0.1'; 15 | 16 | proxyRoute.operational = true; 17 | proxyRoute.serviceSockets = []; 18 | proxyRoute.proxySockets = []; 19 | 20 | proxyRoute.server = tls.createServer({ca: [fs.readFileSync('./test/ssl/testca/cacert.pem')], cert: fs.readFileSync('./test/ssl/server/cert.pem'), key: fs.readFileSync('./test/ssl/server/key.pem')},function (proxySocket) { 21 | // If we're "experiencing trouble", immediately end the connection. 22 | if (!proxyRoute.operational) { 23 | proxySocket.end(); 24 | return; 25 | } 26 | 27 | // If we're operating normally, accept the connection and begin proxying traffic. 28 | proxyRoute.proxySockets.push(proxySocket); 29 | 30 | var connected = false; 31 | var buffers = []; 32 | var serviceSocket = new net.Socket(); 33 | proxyRoute.serviceSockets.push(serviceSocket); 34 | serviceSocket.connect(parseInt(servicePort), serviceHost); 35 | serviceSocket.on('connect', function() { 36 | connected = true; 37 | for (var i in buffers) { 38 | serviceSocket.write(buffers[i]); 39 | } 40 | buffers = []; 41 | }); 42 | proxySocket.on('error', function (e) { 43 | serviceSocket.end(); 44 | }); 45 | serviceSocket.on('error', function (e) { 46 | debug('Could not connect to service at host ' + serviceHost + ', port ' + servicePort); 47 | proxySocket.end(); 48 | }); 49 | proxySocket.on("data", function (data) { 50 | if (proxyRoute.operational) { 51 | if (connected) { 52 | serviceSocket.write(data); 53 | } else { 54 | buffers.push(data); 55 | } 56 | } 57 | }); 58 | serviceSocket.on("data", function(data) { 59 | if (proxyRoute.operational) { 60 | proxySocket.write(data); 61 | } 62 | }); 63 | proxySocket.on("close", function(had_error) { 64 | serviceSocket.end(); 65 | }); 66 | serviceSocket.on("close", function(had_error) { 67 | proxySocket.end(); 68 | }); 69 | }); 70 | proxyRoute.listen(); 71 | }; 72 | module.exports.route.prototype.listen = function () { 73 | var proxyRoute = this; 74 | debug('listening for proxy connection...'); 75 | proxyRoute.operational = true; 76 | proxyRoute.server.listen(proxyRoute.proxyPort); 77 | }; 78 | module.exports.route.prototype.close = function () { 79 | var proxyRoute = this; 80 | debug('closing proxy connection...'); 81 | proxyRoute.operational = false; 82 | for (var index in proxyRoute.serviceSockets) { 83 | proxyRoute.serviceSockets[index].destroy(); 84 | } 85 | proxyRoute.serviceSockets = []; 86 | for (var index in proxyRoute.proxySockets) { 87 | proxyRoute.proxySockets[index].destroy(); 88 | } 89 | proxyRoute.proxySockets = []; 90 | proxyRoute.server.close(); 91 | }; 92 | module.exports.route.prototype.interrupt = function (howLong) { 93 | var proxyRoute = this; 94 | debug('interrupting proxy connection...'); 95 | proxyRoute.close(); 96 | setTimeout(function () { 97 | proxyRoute.listen(); 98 | }, howLong || 50); 99 | }; 100 | 101 | if (!module.parent) { 102 | var proxyPort = process.argv[2]; 103 | var servicePort = process.argv[3]; 104 | var serviceHost = process.argv[4]; 105 | var proxyRoute = new module.exports.route(proxyPort, servicePort, serviceHost); 106 | // Don't exit until parent kills us. 107 | setInterval(function () { 108 | if (process.argv[5]) { 109 | proxyRoute.interrupt(); 110 | } 111 | }, parseInt(process.argv[5]) || 1000); 112 | } 113 | -------------------------------------------------------------------------------- /test/heartbeat.test.coffee: -------------------------------------------------------------------------------- 1 | should = require('should') 2 | async = require('async') 3 | _ = require('underscore') 4 | Proxy = require('./proxy') 5 | 6 | AMQP = require('src/amqp') 7 | 8 | describe 'Connection Heartbeats', () -> 9 | it 'we can get a heartbeat 541', (done)-> 10 | this.timeout(5000) 11 | amqp = null 12 | 13 | async.series [ 14 | (next)-> 15 | amqp = new AMQP {host:'localhost', port: 5672, heartbeat: 1000}, (e, r)-> 16 | should.not.exist e 17 | next() 18 | 19 | (next)-> 20 | amqp.parser.once 'heartbeat', ()-> 21 | next() 22 | 23 | (next)-> 24 | amqp.close() 25 | next() 26 | 27 | ], done 28 | 29 | 30 | it 'we reset the heartbeat timer while the connection is doing other things', (done)-> 31 | this.timeout(60000) 32 | amqp = null 33 | stage = null 34 | 35 | async.series [ 36 | (next)-> 37 | amqp = new AMQP {host:'localhost', port: 5672, heartbeat: 1000}, (e, r)-> 38 | should.not.exist e 39 | next() 40 | 41 | (next)-> 42 | queuename = "queuename" 43 | heartbeat = false 44 | stage = 2 45 | amqp.on 'close', ()-> 46 | if stage is 2 47 | throw new Error("connection closed") 48 | 49 | doThings = ()-> 50 | 51 | amqp.queue {queue:queuename}, (e, q)-> 52 | should.not.exist e 53 | should.exist q 54 | 55 | q.declare {passive:false}, (e,r)-> 56 | should.not.exist e 57 | doThings() if !heartbeat 58 | 59 | doThings() 60 | 61 | _.delay next, 3000 62 | 63 | (next)-> 64 | stage = 3 65 | amqp.close() 66 | next() 67 | 68 | ], done 69 | 70 | 71 | it 'we disconnect and we dont reconnect because of the heartbeat 540', (done)-> 72 | this.timeout(60000) 73 | amqp = null 74 | 75 | async.series [ 76 | (next)-> 77 | amqp = new AMQP {host:'localhost', port: 5672, heartbeat: 1000}, (e, r)-> 78 | should.not.exist e 79 | next() 80 | 81 | (next)-> 82 | amqp.close() 83 | _.delay next, 3000 84 | 85 | (next)-> 86 | amqp.state.should.eql 'destroyed' 87 | amqp.close() 88 | next() 89 | 90 | ], done 91 | 92 | 93 | it 'hearthbeat missing reconnects 574', (done)-> 94 | 95 | this.timeout(60000) 96 | proxy = new Proxy.route(7070, 5672, "localhost") 97 | amqp = null 98 | 99 | async.series [ 100 | (next)-> 101 | amqp = new AMQP {host:'localhost', port: 7070}, (e, r)-> 102 | should.not.exist e 103 | next() 104 | 105 | (next)-> 106 | _.delay ()-> 107 | amqp._missedHeartbeat() 108 | , 100 109 | 110 | amqp.once 'close', next 111 | 112 | (next)-> 113 | amqp.once 'ready', next 114 | 115 | (next)-> 116 | amqp.close() 117 | proxy.close() 118 | next() 119 | 120 | ], done 121 | 122 | 123 | 124 | it 'we send heartbeats 575', (done)-> 125 | this.timeout(7000) 126 | amqp = null 127 | consumer = null 128 | queueName = null 129 | 130 | async.series [ 131 | (next)-> 132 | amqp = new AMQP {host:'localhost'}, (e, r)-> 133 | should.not.exist e 134 | 135 | amqp.once 'ready', next 136 | 137 | (next)-> 138 | consumer = new AMQP {host:'localhost', heartbeat: 1000}, (e, r)-> 139 | should.not.exist e 140 | 141 | consumer.once 'ready', next 142 | 143 | (next)-> 144 | amqp.queue {queue: ''}, (err, queueInfo)-> 145 | should.not.exist err 146 | 147 | queueInfo.declare (err, queueInfo)-> 148 | should.not.exist err 149 | queueName = queueInfo.queue 150 | next() 151 | 152 | (next)-> 153 | 154 | consumer.consume queueName, {}, ()-> 155 | 156 | shouldStop = false 157 | 158 | setTimeout ()-> 159 | shouldStop = true 160 | , 4000 161 | 162 | async.until ()-> 163 | return shouldStop 164 | , (done)-> 165 | amqp.publish '', queueName, 'message', done 166 | , next 167 | 168 | (next)-> 169 | amqp.close() 170 | consumer.close() 171 | next() 172 | 173 | ], done 174 | 175 | 176 | -------------------------------------------------------------------------------- /test/connection.test.coffee: -------------------------------------------------------------------------------- 1 | should = require('should') 2 | async = require('async') 3 | _ = require('underscore') 4 | Proxy = require('./proxy') 5 | 6 | AMQP = require('src/amqp') 7 | 8 | describe 'Connection', () -> 9 | it 'tests it can connect to localhost', (done) -> 10 | amqp = new AMQP {host:'localhost'}, (e, r)-> 11 | should.not.exist e 12 | done() 13 | 14 | it 'tests it can connect to nested hosts array', (done) -> 15 | amqp = new AMQP {host:[['localhost']]}, (e, r)-> 16 | should.not.exist e 17 | done() 18 | 19 | it 'we fail connecting to an invalid host', (done) -> 20 | amqp = new AMQP {reconnect:false, host:'iamnotthequeueyourlookingfor'}, (e, r)-> 21 | should.exist e 22 | amqp.close() 23 | done() 24 | 25 | it 'we fail connecting to an invalid no callback', (done) -> 26 | amqp = new AMQP {reconnect:false, host:'iamnotthequeueyourlookingfor'} 27 | amqp.on 'error', ()-> 28 | done() 29 | 30 | it 'we can reconnect if the connection fails 532', (done)-> 31 | proxy = new Proxy.route(7001, 5672, "localhost") 32 | amqp = null 33 | 34 | async.series [ 35 | (next)-> 36 | amqp = new AMQP {host:'localhost', port: 7001}, (e, r)-> 37 | should.not.exist e 38 | next() 39 | 40 | (next)-> 41 | proxy.interrupt() 42 | next() 43 | 44 | (next)-> 45 | amqp.queue {queue:"test"}, (e, q)-> 46 | should.not.exist e 47 | should.exist q 48 | next() 49 | 50 | ], done 51 | 52 | 53 | it 'we disconnect', (done)-> 54 | # proxy = new proxy.route(9001, 5672, "localhost") 55 | amqp = null 56 | 57 | async.series [ 58 | (next)-> 59 | amqp = new AMQP {host:'localhost'}, (e, r)-> 60 | should.not.exist e 61 | next() 62 | 63 | (next)-> 64 | amqp.close() 65 | next() 66 | 67 | (next)-> 68 | setTimeout next, 100 69 | 70 | (next)-> 71 | amqp.state.should.eql 'destroyed' 72 | next() 73 | 74 | ], done 75 | 76 | 77 | it 'we can connect to an array of hosts', (done)-> 78 | # proxy = new proxy.route(9001, 5672, "localhost") 79 | amqp = null 80 | 81 | async.series [ 82 | (next)-> 83 | amqp = new AMQP {host:['localhost','127.0.0.1']}, (e, r)-> 84 | should.not.exist e 85 | next() 86 | 87 | (next)-> 88 | amqp.close() 89 | next() 90 | 91 | ], done 92 | 93 | 94 | 95 | it 'we emit only one close event', (done)-> 96 | proxy = new Proxy.route(9010, 5672, "localhost") 97 | amqp = null 98 | closes = 0 99 | 100 | async.series [ 101 | (next)-> 102 | amqp = new AMQP {host:['localhost','127.0.0.1'], port: 9010}, (e, r)-> 103 | should.not.exist e 104 | next() 105 | 106 | (next)-> 107 | amqp.on 'close', ()-> 108 | closes++ 109 | amqp.close() 110 | 111 | _.delay ()-> 112 | closes.should.eql 1 113 | amqp.close() 114 | done() 115 | , 300 116 | 117 | 118 | proxy.close() 119 | next() 120 | 121 | ], (e,r)-> 122 | should.not.exist e 123 | 124 | 125 | it 'we can reconnect to an array of hosts if the connection fails', (done)-> 126 | this.timeout(5000) 127 | proxy = new Proxy.route(9009, 5672, "localhost") 128 | amqp = null 129 | 130 | async.series [ 131 | (next)-> 132 | amqp = new AMQP {host:['localhost','127.0.0.1'], port: 9009}, (e, r)-> 133 | should.not.exist e 134 | next() 135 | 136 | (next)-> 137 | proxy.interrupt() 138 | next() 139 | 140 | (next)-> 141 | amqp.queue {queue:"test"}, (e, q)-> 142 | should.not.exist e 143 | should.exist q 144 | next() 145 | 146 | (next)-> 147 | amqp.close() 148 | proxy.close() 149 | next() 150 | 151 | ], done 152 | 153 | 154 | it 'we can connect to an array of hosts randomly', (done)-> 155 | 156 | amqp = null 157 | 158 | async.series [ 159 | (next)-> 160 | amqp = new AMQP {hostRandom: true, host:['localhost','127.0.0.1']}, (e, r)-> 161 | should.not.exist e 162 | next() 163 | 164 | ], done 165 | 166 | 167 | it 'we can timeout connecting to a host', (done)-> 168 | amqp = null 169 | 170 | async.series [ 171 | (next)-> 172 | amqp = new AMQP {reconnect:false, connectTimeout: 100, host:'test.com'}, (e, r)-> 173 | should.exist e 174 | next() 175 | 176 | ], done 177 | 178 | 179 | -------------------------------------------------------------------------------- /src/lib/defaults.coffee: -------------------------------------------------------------------------------- 1 | try 2 | clientVersion = JSON.parse(require('fs').readFileSync("#{__dirname}/../../../package.json")).version 3 | catch e 4 | clientVersion = '0.0.1' 5 | 6 | { MaxFrameSize } = require('./constants') 7 | 8 | os = require('os') 9 | 10 | module.exports = 11 | defaults : 12 | defaultExchangeName: '' 13 | amqp : 5672 14 | amqps : 5671 15 | 16 | connection: 17 | host: "localhost" 18 | login: "guest" 19 | password: "guest" 20 | vhost: '/' 21 | port : 5672 22 | ssl: false 23 | sslPort: 5671 24 | heartbeat: 10000 # in ms 25 | reconnect: true 26 | reconnectDelayTime: 1000 # in ms 27 | hostRandom: false 28 | connectTimeout: 30000 # in ms 29 | channelMax: 0 # unlimited 30 | frameMax: MaxFrameSize 31 | noDelay: true # disable Nagle's algorithm by default 32 | 33 | temporaryChannelTimeout: 2000 # in ms 34 | temporaryChannelTimeoutCheck: 1000 # in ms 35 | 36 | clientProperties: 37 | version: clientVersion 38 | platform: os.hostname() + '-node-' + process.version 39 | product: 'node-amqp-coffee' 40 | 41 | 42 | basicPublish: 43 | mandatory: false 44 | immediate: false 45 | contentType: 'application/octet-stream' 46 | 47 | basicConsume: 48 | ### 49 | If the no­local field is set the server will not send messages to the 50 | connection that published them. 51 | ### 52 | noLocal: false 53 | 54 | ### 55 | If this field is set the server does not expect acknowledgements for 56 | messages. That is, when a message is delivered to the client the server 57 | assumes the delivery will succeed and immediately dequeues it. This 58 | functionality may increase performance but at the cost of reliability. 59 | Messages can get lost if a client dies before they are delivered to 60 | the application. 61 | ### 62 | noAck: true 63 | exclusive: false 64 | noWait: false 65 | arguments: {} 66 | 67 | basicQos: 68 | prefetchSize: 0 69 | ### 70 | RabbitMQ has reinterpreted this field. The original specification said: 71 | "By default the QoS settings apply to the current channel only. If 72 | this field is set, they are applied to the entire connection." Instead, 73 | RabbitMQ takes global=false to mean that the QoS settings should apply 74 | per-consumer (for new consumers on the channel; existing ones being 75 | unaffected) and global=true to mean that the QoS settings should apply 76 | per-channel. 77 | 78 | THIS IS CHANGED TO TRUE FOR RABBITMQ VERSION 3.3.0 AND UP IN CONSUMER 79 | ### 80 | global: false 81 | 82 | 83 | exchange: 84 | type: "direct" 85 | passive: false 86 | durable: false 87 | noWait: false 88 | autoDelete: true 89 | arguments: {} 90 | ### 91 | If set, the exchange may not be used directly by publishers, but only 92 | when bound to other exchanges. 93 | 94 | Internal exchanges are used to construct wiring that is not visible to applications. 95 | ### 96 | internal: false 97 | 98 | exchangeDelete: 99 | ifUnused: false 100 | noWait: false 101 | 102 | queueDelete: 103 | ### 104 | If set, the server will only delete the queue if it has no consumers. If the queue has consumers the server does does not delete it but raises a channel exception instead. 105 | ### 106 | ifUnused: false 107 | 108 | # If set, the server will only delete the queue if it has no messages. 109 | ifEmpty: true 110 | noWait: false 111 | arguments: {} 112 | 113 | queue: 114 | # Queue declare defaults 115 | autoDelete: true 116 | arguments: {} 117 | noWait: false 118 | 119 | ### 120 | Exclusive queues may only be accessed by the current connection, and are deleted when that connection 121 | closes. Passive declaration of an exclusive queue by other connections are not allowed. 122 | 123 | * The server MUST support both exclusive (private) and non-exclusive (shared) queues. 124 | * The client MAY NOT attempt to use a queue that was declared as exclusive by another still-open 125 | connection. Error code: resource-locked 126 | ### 127 | exclusive: false 128 | 129 | ### 130 | If set when creating a new queue, the queue will be marked as durable. Durable queues remain active when a 131 | server restarts. Non-durable queues (transient queues) are purged if/when a server restarts. Note that 132 | durable queues do not necessarily hold persistent messages, although it does not make sense to send 133 | persistent messages to a transient queue. 134 | ### 135 | durable: false 136 | 137 | ### 138 | If set, the server will reply with Declare-Ok if the queue already exists with the same name, and raise an 139 | error if not. The client can use this to check whether a queue exists without modifying the server state. 140 | When set, all other method fields except name and no-wait are ignored. A declare with both passive and 141 | no-wait has no effect. Arguments are compared for semantic equivalence. 142 | ### 143 | passive: false 144 | -------------------------------------------------------------------------------- /src/lib/parseHelpers.coffee: -------------------------------------------------------------------------------- 1 | { AMQPTypes } = require('./constants') 2 | 3 | module.exports = 4 | parseIntFromBuffer : (buffer, size) -> 5 | switch size 6 | when 1 7 | return buffer[buffer.read++] 8 | 9 | when 2 10 | return (buffer[buffer.read++] << 8) + buffer[buffer.read++] 11 | 12 | when 4 13 | return (buffer[buffer.read++] << 24) + (buffer[buffer.read++] << 16) + 14 | (buffer[buffer.read++] << 8) + buffer[buffer.read++] 15 | 16 | when 8 17 | return (buffer[buffer.read++] << 56) + (buffer[buffer.read++] << 48) + 18 | (buffer[buffer.read++] << 40) + (buffer[buffer.read++] << 32) + 19 | (buffer[buffer.read++] << 24) + (buffer[buffer.read++] << 16) + 20 | (buffer[buffer.read++] << 8) + buffer[buffer.read++] 21 | 22 | else 23 | throw new Error("cannot parse ints of that size") 24 | 25 | parseTable : (buffer)-> 26 | length = buffer.read + module.exports.parseIntFromBuffer(buffer, 4) 27 | table = {} 28 | 29 | while (buffer.read < length) 30 | table[module.exports.parseShortString(buffer)] = module.exports.parseValue(buffer) 31 | 32 | return table 33 | 34 | 35 | parseFields: (buffer, fields)-> 36 | args = {}; 37 | 38 | bitIndex = 0; 39 | 40 | for field, i in fields 41 | #debug("parsing field " + field.name + " of type " + field.domain); 42 | switch field.domain 43 | when 'bit' 44 | # 8 bits can be packed into one octet. 45 | # XXX check if bitIndex greater than 7? 46 | 47 | value = (buffer[buffer.read] & (1 << bitIndex)) ? true : false; 48 | 49 | if (fields[i+1] && fields[i+1].domain == 'bit') 50 | bitIndex++; 51 | 52 | else 53 | bitIndex = 0; 54 | buffer.read++; 55 | 56 | when 'octet' 57 | value = buffer[buffer.read++]; 58 | 59 | when 'short' 60 | value = module.exports.parseIntFromBuffer(buffer, 2) 61 | 62 | when 'long' 63 | value = module.exports.parseIntFromBuffer(buffer, 4) 64 | 65 | when 'timestamp', 'longlong' 66 | value = module.exports.parseIntFromBuffer(buffer, 8) 67 | 68 | when 'shortstr' 69 | value = module.exports.parseShortString(buffer) 70 | 71 | 72 | when 'longstr' 73 | value = module.exports.parseLongString(buffer) 74 | 75 | 76 | when 'table' 77 | value = module.exports.parseTable(buffer) 78 | 79 | 80 | else 81 | throw new Error("Unhandled parameter type " + field.domain); 82 | 83 | #debug("got " + value); 84 | args[field.name] = value; 85 | 86 | return args; 87 | 88 | parseShortString: (buffer)-> 89 | length = buffer[buffer.read++] 90 | s = buffer.toString('utf8', buffer.read, buffer.read + length) 91 | buffer.read += length 92 | return s 93 | 94 | parseLongString: (buffer)-> 95 | length = module.exports.parseIntFromBuffer(buffer, 4) 96 | s = buffer.slice(buffer.read, buffer.read + length) 97 | buffer.read += length 98 | return s.toString() 99 | 100 | parseSignedInteger: (buffer)-> 101 | int = module.exports.parseIntFromBuffer(buffer, 4) 102 | if (int & 0x80000000) 103 | int |= 0xEFFFFFFF 104 | int = -int 105 | return int 106 | 107 | 108 | parseValue: (buffer)-> 109 | switch (buffer[buffer.read++]) 110 | when AMQPTypes.STRING 111 | return module.exports.parseLongString(buffer); 112 | 113 | when AMQPTypes.INTEGER 114 | return module.exports.parseIntFromBuffer(buffer, 4); 115 | 116 | when AMQPTypes.DECIMAL 117 | dec = module.exports.parseIntFromBuffer(buffer, 1); 118 | num = module.exports.parseIntFromBuffer(buffer, 4); 119 | return num / (dec * 10); 120 | 121 | when AMQPTypes._64BIT_FLOAT 122 | b = []; 123 | for i in [0...8] 124 | b[i] = buffer[buffer.read++]; 125 | 126 | return (new jspack(true)).Unpack('d', b); 127 | 128 | when AMQPTypes._32BIT_FLOAT 129 | b = []; 130 | for i in [0...4] 131 | b[i] = buffer[buffer.read++]; 132 | 133 | return (new jspack(true)).Unpack('f', b); 134 | 135 | when AMQPTypes.TIME 136 | int = module.exports.parseIntFromBuffer(buffer, 8); 137 | return (new Date()).setTime(int * 1000); 138 | 139 | when AMQPTypes.HASH 140 | return module.exports.parseTable(buffer); 141 | 142 | when AMQPTypes.SIGNED_64BIT 143 | return module.exports.parseIntFromBuffer(buffer, 8); 144 | 145 | when AMQPTypes.BOOLEAN 146 | return (module.exports.parseIntFromBuffer(buffer, 1) > 0); 147 | 148 | when AMQPTypes.BYTE_ARRAY 149 | len = module.exports.parseIntFromBuffer(buffer, 4); 150 | buf = new Buffer(len); 151 | buffer.copy(buf, 0, buffer.read, buffer.read + len); 152 | buffer.read += len; 153 | return buf; 154 | 155 | when AMQPTypes.ARRAY 156 | len = module.exports.parseIntFromBuffer(buffer, 4); 157 | end = buffer.read + len; 158 | arr = new Array(); 159 | 160 | while (buffer.read < end) 161 | arr.push(module.exports.parseValue(buffer)); 162 | 163 | return arr; 164 | 165 | else 166 | throw new Error("Unknown field value type " + buffer[buffer.read-1]); 167 | 168 | 169 | -------------------------------------------------------------------------------- /src/lib/Publisher.coffee: -------------------------------------------------------------------------------- 1 | # Publisher 2 | debug = require('./config').debug('amqp:Publisher') 3 | Channel = require('./Channel') 4 | defaults = require('./defaults') 5 | 6 | _ = require('underscore') 7 | 8 | bson = require('bson') 9 | BSON = new bson.BSONPure.BSON() 10 | 11 | { methodTable, classes, methods } = require('./config').protocol 12 | 13 | class Publisher extends Channel 14 | 15 | constructor: (connection, channel, confirm)-> 16 | super(connection, channel) 17 | 18 | @seqCallbacks = {} # publisher confirms 19 | @confirm = confirm ? false 20 | 21 | @currentMethod = null 22 | @currentArgs = null 23 | 24 | if @confirm then @confirmMode() 25 | return @ 26 | 27 | confirmMode: (cb)=> 28 | @confirmState = 'opening' 29 | @taskPush methods.confirmSelect, {noWait:false}, methods.confirmSelectOk, ()=> 30 | @confirmState = 'open' 31 | @confirm = true 32 | @seq = 1 33 | cb() if cb? 34 | @emit 'confirm' 35 | 36 | _channelClosed: (message)=> 37 | @confirmState = 'closed' 38 | if !message? then message = "Channel closed, try again" 39 | 40 | for key, cb of @seqCallbacks 41 | if typeof cb is 'function' 42 | cb(message) 43 | 44 | @seqCallbacks = {} 45 | 46 | if @confirm then @confirmMode() 47 | 48 | publish: (exchange, routingKey, data, options, cb)-> 49 | if typeof options is 'function' 50 | cb = options 51 | options = {} 52 | 53 | # Because we add modify options, we want to make sure we only modify our internal version 54 | # this is why we clone it. 55 | if !options? then options = {} else options = _.clone options 56 | 57 | if @state isnt "open" or (@confirm and @confirmState isnt "open") 58 | if @state is "opening" or @state is "closed" or (@confirm and @confirmState is 'opening') 59 | 60 | if @confirm then waitFor = 'confirm' else waitFor = 'open' 61 | return @once waitFor, ()=> 62 | @publish(exchange, routingKey, data, options, cb) 63 | 64 | else 65 | return cb("Channel is closed and will not re-open? #{@state} #{@confirm} #{@confirmState}") if cb 66 | 67 | # data must be a buffer 68 | if typeof data is 'string' 69 | options.contentType = 'string/utf8' 70 | data = new Buffer(data, 'utf8') 71 | 72 | else if typeof data is 'object' and !(data instanceof Buffer) 73 | if options.contentType? 74 | debug 1, ()=> return "contentType specified but data isn't a buffer, #{JSON.stringify options}" 75 | if cb? 76 | cb("contentType specified but data isn't a buffer") 77 | return 78 | 79 | # default use JSON 80 | data = new Buffer(JSON.stringify(data), 'utf8') 81 | options.contentType = 'application/json' 82 | 83 | # data = BSON.serialize data 84 | # options.contentType = 'application/bson' 85 | 86 | else if data is undefined 87 | data = new Buffer(0) 88 | options.contentType = 'application/undefined' 89 | 90 | 91 | # increment this as the final step before publishing, to make sure we're in sync with the server 92 | thisSequenceNumber = @seq++ if @confirm 93 | 94 | # Apply default options after we deal with potentially converting the data 95 | options = _.defaults options, defaults.basicPublish 96 | options.exchange = exchange 97 | options.routingKey = routingKey 98 | 99 | # This is to tie back this message as failed if it failed in confirm mode with a mandatory or immediate publish 100 | if @confirm and cb? and (options.mandatory || options.immediate ) 101 | options.headers ?= {} 102 | options.headers['x-seq'] = thisSequenceNumber 103 | 104 | @queuePublish methods.basicPublish, data, options 105 | 106 | if @confirm and cb? 107 | debug 4, ()=> return JSON.stringify {exchange, routingKey, data, options, thisSequenceNumber} 108 | @_waitForSeq thisSequenceNumber, cb 109 | else 110 | debug 4, ()=> return JSON.stringify {exchange, routingKey, data, options, noConfirm: true} 111 | _.defer(cb) if cb? 112 | 113 | 114 | _onMethod: (channel, method, args)-> 115 | @currentMethod = method 116 | @currentArgs = args 117 | 118 | switch method 119 | when methods.basicAck 120 | if @confirm 121 | # debug 4, ()=> return JSON.stringify args 122 | @_gotSeq args.deliveryTag, args.multiple 123 | 124 | _onContentHeader: (channel, classInfo, weight, properties, size)-> 125 | switch @currentMethod 126 | when methods.basicReturn 127 | if properties.headers?['x-seq']? 128 | @_gotSeq properties.headers['x-seq'], false, @currentArgs 129 | 130 | _onContent: (channel, data)-> 131 | # Content is not needed on a basicReturn 132 | 133 | _waitForSeq: (seq, cb)-> 134 | if typeof cb is 'function' 135 | @seqCallbacks[seq] = cb 136 | else 137 | debug "callback requested for publish that isn't a function" 138 | console.error cb 139 | 140 | _gotSeq:(seq, multi, err = null)-> 141 | if multi 142 | keys = _.keys @seqCallbacks 143 | for key in keys 144 | if key <= seq 145 | @seqCallbacks[key](err) 146 | delete @seqCallbacks[key] 147 | else 148 | if @seqCallbacks[seq]? 149 | @seqCallbacks[seq](err) 150 | else 151 | debug 3, ()-> return "got a seq for #{seq} but that callback either doesn't exist or was already called or was returned" 152 | 153 | delete @seqCallbacks[seq] 154 | 155 | module.exports = Publisher 156 | -------------------------------------------------------------------------------- /src/lib/AMQPParser.coffee: -------------------------------------------------------------------------------- 1 | # debug = require('debug')('amqp:AMQPParser') 2 | {EventEmitter} = require('events') 3 | 4 | { Indicators, FrameType } = require('./config').constants 5 | { methodTable, classes, methods } = require('./config').protocol 6 | debug = require('./config').debug('amqp:AMQPParser') 7 | 8 | 9 | {parseIntFromBuffer, parseFields} = require('./parseHelpers') 10 | 11 | 12 | class AMQPParser extends EventEmitter 13 | constructor: (version, type, connection) -> 14 | @connection = connection 15 | 16 | # send the start of the handshake.... 17 | @connection.connection.write("AMQP" + String.fromCharCode(0,0,9,1)); 18 | 19 | # set up some defaults, for reuse 20 | @frameHeader = new Buffer(7) 21 | @frameHeader.used = 0 22 | @frameHeader.length = @frameHeader.length 23 | 24 | # set the first step in out parser 25 | @parser = @header 26 | 27 | execute: (data)-> 28 | # each parser will return the next parser for us to use. 29 | @parser = @parser(data) 30 | 31 | 32 | # Data Handlers #################################################################### 33 | header: (data)-> 34 | dataLength = data.length 35 | neededForCompleteHeader = @frameHeader.length - @frameHeader.used 36 | 37 | # copy all of our data to our frame header 38 | data.copy(@frameHeader, @frameHeader.used, 0, dataLength) 39 | 40 | # update where we are in the header 41 | @frameHeader.used += dataLength 42 | 43 | # if we have all the header data we need we're done here 44 | if @frameHeader.used >= @frameHeader.length 45 | 46 | @frameHeader.read = 0 # this is used to keep track of where we are with parseIntFromBuffer 47 | 48 | # What do we know from the header packet. 49 | @frameType = @frameHeader[@frameHeader.read++] 50 | @frameChannel = parseIntFromBuffer(@frameHeader,2) 51 | @frameSize = parseIntFromBuffer(@frameHeader,4) 52 | 53 | if @frameSize > @connection.frameMax 54 | @connection?.connection?.destroy?() 55 | return @error "#{@frameChannel} Oversize frame size #{@frameSize} of max #{@connection.frameMax}" 56 | 57 | 58 | # setup our frameBuffer 59 | @frameBuffer = new Buffer(@frameSize) 60 | @frameBuffer.used = 0 61 | 62 | # reset out frameHeader 63 | @frameHeader.used = 0 64 | return @frame(data.slice(neededForCompleteHeader)) 65 | else 66 | return @header 67 | 68 | frame: (data)-> 69 | dataLength = data.length 70 | 71 | neededForCompleteFrame = @frameBuffer.length - @frameBuffer.used 72 | 73 | data.copy(@frameBuffer, @frameBuffer.used, 0, dataLength) 74 | @frameBuffer.used += dataLength 75 | 76 | # we have everything we need AND more so lets make sure we pass that through 77 | if dataLength > neededForCompleteFrame 78 | return @frameEnd(data.slice(neededForCompleteFrame)) 79 | 80 | # we have exactly what we need for this frame 81 | else if dataLength == neededForCompleteFrame 82 | return @frameEnd 83 | 84 | # we dont have enough info to continue so lets wait for more frame data 85 | else 86 | return @frame 87 | 88 | frameEnd: (data)-> 89 | if !(data.length > 0) then return @frameEnd 90 | if data[0] != Indicators.FRAME_END 91 | return @error "Missing frame end marker" 92 | 93 | switch @frameType 94 | when FrameType.METHOD then @parseMethodFrame(@frameChannel, @frameBuffer) 95 | when FrameType.HEADER then @parseHeaderFrame(@frameChannel, @frameBuffer) 96 | when FrameType.BODY then @parseContent(@frameChannel, @frameBuffer) 97 | 98 | when FrameType.HEARTBEAT 99 | @emit 'heartbeat' 100 | else 101 | @error "Unknown frametype #{@frameType}" 102 | 103 | return @header(data.slice(1)) 104 | 105 | 106 | # Frame Parsers ################################################################ 107 | parseMethodFrame: (channel, buffer)-> 108 | buffer.read = 0 109 | classId = parseIntFromBuffer(buffer, 2) 110 | methodId = parseIntFromBuffer(buffer, 2) 111 | 112 | if !methodTable[classId]? or !methodTable[classId][methodId]? 113 | return @error "bad classId, methodId pair: #{classId}, #{methodId}" 114 | 115 | method = methodTable[classId][methodId] 116 | args = parseFields(buffer, method.fields) 117 | 118 | debug 3, ()->return "#{channel} > method #{method.name} #{JSON.stringify args}" 119 | @emit 'method', channel, method, args 120 | 121 | parseHeaderFrame: (channel, buffer)-> 122 | buffer.read = 0 123 | 124 | classIndex = parseIntFromBuffer(buffer, 2) 125 | weight = parseIntFromBuffer(buffer, 2) 126 | size = parseIntFromBuffer(buffer, 8) 127 | 128 | classInfo = classes[classIndex] 129 | propertyFlags = parseIntFromBuffer(buffer, 2) 130 | fields = [] 131 | for field, i in classInfo.fields 132 | if (i + 1) % 15 is 0 133 | parseIntFromBuffer(buffer, 2) 134 | 135 | if propertyFlags & (1 << (15-(i%15))) 136 | fields.push field 137 | 138 | properties = parseFields(buffer, fields) 139 | 140 | debug 3, ()->return "#{channel} > contentHeader #{JSON.stringify properties} #{size}" 141 | @emit 'contentHeader', channel, classInfo, weight, properties, size 142 | 143 | parseContent: (channel, data)-> 144 | debug 3, ()->return "#{channel} > content #{data.length}" 145 | @emit 'content', channel, data 146 | 147 | error: (error)-> 148 | debug "Parser error #{error}" 149 | 150 | parserError = new Error(error) 151 | parserError.code = 'parser' 152 | 153 | @connection.emit 'error', parserError 154 | @frameHeader.used = 0 155 | return @header 156 | 157 | module.exports = AMQPParser 158 | 159 | 160 | 161 | -------------------------------------------------------------------------------- /src/amqp-definitions-0-9-1.js: -------------------------------------------------------------------------------- 1 | exports.constants = [[1,"frameMethod"],[2,"frameHeader"],[3,"frameBody"],[8,"frameHeartbeat"],[4096,"frameMinSize"],[206,"frameEnd"],[200,"replySuccess"],[311,"contentTooLarge"],[313,"noConsumers"],[320,"connectionForced"],[402,"invalidPath"],[403,"accessRefused"],[404,"notFound"],[405,"resourceLocked"],[406,"preconditionFailed"],[501,"frameError"],[502,"syntaxError"],[503,"commandInvalid"],[504,"channelError"],[505,"unexpectedFrame"],[506,"resourceError"],[530,"notAllowed"],[540,"notImplemented"],[541,"internalError"]] 2 | exports.classes = [{"name":"connection","index":10,"fields":[],"methods":[{"name":"start","index":10,"fields":[{"name":"versionMajor","domain":"octet"},{"name":"versionMinor","domain":"octet"},{"name":"serverProperties","domain":"table"},{"name":"mechanisms","domain":"longstr"},{"name":"locales","domain":"longstr"}]},{"name":"startOk","index":11,"fields":[{"name":"clientProperties","domain":"table"},{"name":"mechanism","domain":"shortstr"},{"name":"response","domain":"longstr"},{"name":"locale","domain":"shortstr"}]},{"name":"secure","index":20,"fields":[{"name":"challenge","domain":"longstr"}]},{"name":"secureOk","index":21,"fields":[{"name":"response","domain":"longstr"}]},{"name":"tune","index":30,"fields":[{"name":"channelMax","domain":"short"},{"name":"frameMax","domain":"long"},{"name":"heartbeat","domain":"short"}]},{"name":"tuneOk","index":31,"fields":[{"name":"channelMax","domain":"short"},{"name":"frameMax","domain":"long"},{"name":"heartbeat","domain":"short"}]},{"name":"open","index":40,"fields":[{"name":"virtualHost","domain":"shortstr"},{"name":"reserved1","domain":"shortstr"},{"name":"reserved2","domain":"bit"}]},{"name":"openOk","index":41,"fields":[{"name":"reserved1","domain":"shortstr"}]},{"name":"close","index":50,"fields":[{"name":"replyCode","domain":"short"},{"name":"replyText","domain":"shortstr"},{"name":"classId","domain":"short"},{"name":"methodId","domain":"short"}]},{"name":"closeOk","index":51,"fields":[]},{"name":"blocked","index":60,"fields":[{"name":"reason","domain":"shortstr"}]},{"name":"unblocked","index":61,"fields":[]}]},{"name":"channel","index":20,"fields":[],"methods":[{"name":"open","index":10,"fields":[{"name":"reserved1","domain":"shortstr"}]},{"name":"openOk","index":11,"fields":[{"name":"reserved1","domain":"longstr"}]},{"name":"flow","index":20,"fields":[{"name":"active","domain":"bit"}]},{"name":"flowOk","index":21,"fields":[{"name":"active","domain":"bit"}]},{"name":"close","index":40,"fields":[{"name":"replyCode","domain":"short"},{"name":"replyText","domain":"shortstr"},{"name":"classId","domain":"short"},{"name":"methodId","domain":"short"}]},{"name":"closeOk","index":41,"fields":[]}]},{"name":"exchange","index":40,"fields":[],"methods":[{"name":"declare","index":10,"fields":[{"name":"reserved1","domain":"short"},{"name":"exchange","domain":"shortstr"},{"name":"type","domain":"shortstr"},{"name":"passive","domain":"bit"},{"name":"durable","domain":"bit"},{"name":"autoDelete","domain":"bit"},{"name":"internal","domain":"bit"},{"name":"noWait","domain":"bit"},{"name":"arguments","domain":"table"}]},{"name":"declareOk","index":11,"fields":[]},{"name":"delete","index":20,"fields":[{"name":"reserved1","domain":"short"},{"name":"exchange","domain":"shortstr"},{"name":"ifUnused","domain":"bit"},{"name":"noWait","domain":"bit"}]},{"name":"deleteOk","index":21,"fields":[]},{"name":"bind","index":30,"fields":[{"name":"reserved1","domain":"short"},{"name":"destination","domain":"shortstr"},{"name":"source","domain":"shortstr"},{"name":"routingKey","domain":"shortstr"},{"name":"noWait","domain":"bit"},{"name":"arguments","domain":"table"}]},{"name":"bindOk","index":31,"fields":[]},{"name":"unbind","index":40,"fields":[{"name":"reserved1","domain":"short"},{"name":"destination","domain":"shortstr"},{"name":"source","domain":"shortstr"},{"name":"routingKey","domain":"shortstr"},{"name":"noWait","domain":"bit"},{"name":"arguments","domain":"table"}]},{"name":"unbindOk","index":51,"fields":[]}]},{"name":"queue","index":50,"fields":[],"methods":[{"name":"declare","index":10,"fields":[{"name":"reserved1","domain":"short"},{"name":"queue","domain":"shortstr"},{"name":"passive","domain":"bit"},{"name":"durable","domain":"bit"},{"name":"exclusive","domain":"bit"},{"name":"autoDelete","domain":"bit"},{"name":"noWait","domain":"bit"},{"name":"arguments","domain":"table"}]},{"name":"declareOk","index":11,"fields":[{"name":"queue","domain":"shortstr"},{"name":"messageCount","domain":"long"},{"name":"consumerCount","domain":"long"}]},{"name":"bind","index":20,"fields":[{"name":"reserved1","domain":"short"},{"name":"queue","domain":"shortstr"},{"name":"exchange","domain":"shortstr"},{"name":"routingKey","domain":"shortstr"},{"name":"noWait","domain":"bit"},{"name":"arguments","domain":"table"}]},{"name":"bindOk","index":21,"fields":[]},{"name":"unbind","index":50,"fields":[{"name":"reserved1","domain":"short"},{"name":"queue","domain":"shortstr"},{"name":"exchange","domain":"shortstr"},{"name":"routingKey","domain":"shortstr"},{"name":"arguments","domain":"table"}]},{"name":"unbindOk","index":51,"fields":[]},{"name":"purge","index":30,"fields":[{"name":"reserved1","domain":"short"},{"name":"queue","domain":"shortstr"},{"name":"noWait","domain":"bit"}]},{"name":"purgeOk","index":31,"fields":[{"name":"messageCount","domain":"long"}]},{"name":"delete","index":40,"fields":[{"name":"reserved1","domain":"short"},{"name":"queue","domain":"shortstr"},{"name":"ifUnused","domain":"bit"},{"name":"ifEmpty","domain":"bit"},{"name":"noWait","domain":"bit"}]},{"name":"deleteOk","index":41,"fields":[{"name":"messageCount","domain":"long"}]}]},{"name":"basic","index":60,"fields":[{"name":"contentType","domain":"shortstr"},{"name":"contentEncoding","domain":"shortstr"},{"name":"headers","domain":"table"},{"name":"deliveryMode","domain":"octet"},{"name":"priority","domain":"octet"},{"name":"correlationId","domain":"shortstr"},{"name":"replyTo","domain":"shortstr"},{"name":"expiration","domain":"shortstr"},{"name":"messageId","domain":"shortstr"},{"name":"timestamp","domain":"timestamp"},{"name":"type","domain":"shortstr"},{"name":"userId","domain":"shortstr"},{"name":"appId","domain":"shortstr"},{"name":"reserved","domain":"shortstr"}],"methods":[{"name":"qos","index":10,"fields":[{"name":"prefetchSize","domain":"long"},{"name":"prefetchCount","domain":"short"},{"name":"global","domain":"bit"}]},{"name":"qosOk","index":11,"fields":[]},{"name":"consume","index":20,"fields":[{"name":"reserved1","domain":"short"},{"name":"queue","domain":"shortstr"},{"name":"consumerTag","domain":"shortstr"},{"name":"noLocal","domain":"bit"},{"name":"noAck","domain":"bit"},{"name":"exclusive","domain":"bit"},{"name":"noWait","domain":"bit"},{"name":"arguments","domain":"table"}]},{"name":"consumeOk","index":21,"fields":[{"name":"consumerTag","domain":"shortstr"}]},{"name":"cancel","index":30,"fields":[{"name":"consumerTag","domain":"shortstr"},{"name":"noWait","domain":"bit"}]},{"name":"cancelOk","index":31,"fields":[{"name":"consumerTag","domain":"shortstr"}]},{"name":"publish","index":40,"fields":[{"name":"reserved1","domain":"short"},{"name":"exchange","domain":"shortstr"},{"name":"routingKey","domain":"shortstr"},{"name":"mandatory","domain":"bit"},{"name":"immediate","domain":"bit"}]},{"name":"return","index":50,"fields":[{"name":"replyCode","domain":"short"},{"name":"replyText","domain":"shortstr"},{"name":"exchange","domain":"shortstr"},{"name":"routingKey","domain":"shortstr"}]},{"name":"deliver","index":60,"fields":[{"name":"consumerTag","domain":"shortstr"},{"name":"deliveryTag","domain":"longlong"},{"name":"redelivered","domain":"bit"},{"name":"exchange","domain":"shortstr"},{"name":"routingKey","domain":"shortstr"}]},{"name":"get","index":70,"fields":[{"name":"reserved1","domain":"short"},{"name":"queue","domain":"shortstr"},{"name":"noAck","domain":"bit"}]},{"name":"getOk","index":71,"fields":[{"name":"deliveryTag","domain":"longlong"},{"name":"redelivered","domain":"bit"},{"name":"exchange","domain":"shortstr"},{"name":"routingKey","domain":"shortstr"},{"name":"messageCount","domain":"long"}]},{"name":"getEmpty","index":72,"fields":[{"name":"reserved1","domain":"shortstr"}]},{"name":"ack","index":80,"fields":[{"name":"deliveryTag","domain":"longlong"},{"name":"multiple","domain":"bit"}]},{"name":"reject","index":90,"fields":[{"name":"deliveryTag","domain":"longlong"},{"name":"requeue","domain":"bit"}]},{"name":"recoverAsync","index":100,"fields":[{"name":"requeue","domain":"bit"}]},{"name":"recover","index":110,"fields":[{"name":"requeue","domain":"bit"}]},{"name":"recoverOk","index":111,"fields":[]},{"name":"nack","index":120,"fields":[{"name":"deliveryTag","domain":"longlong"},{"name":"multiple","domain":"bit"},{"name":"requeue","domain":"bit"}]}]},{"name":"tx","index":90,"fields":[],"methods":[{"name":"select","index":10,"fields":[]},{"name":"selectOk","index":11,"fields":[]},{"name":"commit","index":20,"fields":[]},{"name":"commitOk","index":21,"fields":[]},{"name":"rollback","index":30,"fields":[]},{"name":"rollbackOk","index":31,"fields":[]}]},{"name":"confirm","index":85,"fields":[],"methods":[{"name":"select","index":10,"fields":[{"name":"noWait","domain":"bit"}]},{"name":"selectOk","index":11,"fields":[]}]}] 3 | -------------------------------------------------------------------------------- /test/exchange.test.coffee: -------------------------------------------------------------------------------- 1 | should = require('should') 2 | async = require('async') 3 | _ = require('underscore') 4 | proxy = require('./proxy') 5 | uuid = require('uuid').v4 6 | 7 | AMQP = require('src/amqp') 8 | 9 | describe 'Exchange', () -> 10 | 11 | it 'test it can declare a exchange', (done)-> 12 | this.timeout(5000); 13 | amqp = null 14 | exchange = null 15 | async.series [ 16 | (next)-> 17 | amqp = new AMQP {host:'localhost'}, (e, r)-> 18 | should.not.exist e 19 | next() 20 | 21 | (next)-> 22 | amqp.exchange {exchange:"testsing"}, (e, exc)-> 23 | should.not.exist e 24 | should.exist exc 25 | exchange = exc 26 | next() 27 | 28 | (next)-> 29 | exchange.declare {}, (e,r)-> 30 | should.not.exist e 31 | next() 32 | 33 | (next)-> 34 | amqp.close() 35 | next() 36 | ], done 37 | 38 | 39 | it 'test it can declare a exchange using name', (done)-> 40 | this.timeout(5000); 41 | amqp = null 42 | exchange = null 43 | async.series [ 44 | (next)-> 45 | amqp = new AMQP {host:'localhost'}, (e, r)-> 46 | should.not.exist e 47 | next() 48 | 49 | (next)-> 50 | amqp.exchange {name:"testsing"}, (e, exc)-> 51 | should.not.exist e 52 | should.exist exc 53 | exchange = exc 54 | next() 55 | 56 | (next)-> 57 | exchange.declare {}, (e,r)-> 58 | should.not.exist e 59 | next() 60 | 61 | (next)-> 62 | amqp.close() 63 | next() 64 | ], done 65 | 66 | 67 | 68 | it 'test it can declare a exchange with no options', (done)-> 69 | this.timeout(5000); 70 | amqp = null 71 | exchange = null 72 | async.series [ 73 | (next)-> 74 | amqp = new AMQP {host:'localhost'}, (e, r)-> 75 | should.not.exist e 76 | next() 77 | 78 | (next)-> 79 | amqp.exchange {name:"testsing"}, (e, exc)-> 80 | should.not.exist e 81 | should.exist exc 82 | exchange = exc 83 | next() 84 | 85 | (next)-> 86 | exchange.declare (e,r)-> 87 | should.not.exist e 88 | next() 89 | 90 | (next)-> 91 | amqp.close() 92 | next() 93 | ], done 94 | 95 | 96 | it 'test it can declare a exchange with no callback', (done)-> 97 | this.timeout(5000); 98 | amqp = null 99 | exchange = null 100 | async.series [ 101 | (next)-> 102 | amqp = new AMQP {host:'localhost'}, (e, r)-> 103 | should.not.exist e 104 | next() 105 | 106 | (next)-> 107 | amqp.exchange({name:"nocallbacktesting"}).declare (e,r)-> 108 | should.not.exist e 109 | next() 110 | 111 | (next)-> 112 | amqp.close() 113 | next() 114 | ], done 115 | 116 | 117 | 118 | it 'test it can fail declaring an exchange', (done)-> 119 | this.timeout(5000); 120 | amqp = null 121 | exchange = null 122 | async.series [ 123 | (next)-> 124 | amqp = new AMQP {host:'localhost'}, (e, r)-> 125 | should.not.exist e 126 | next() 127 | 128 | (next)-> 129 | amqp.exchange {idontbelong:"testsing"}, (e, exc)-> 130 | should.exist e 131 | next() 132 | 133 | 134 | (next)-> 135 | amqp.close() 136 | next() 137 | 138 | ], done 139 | 140 | 141 | it 'test it can delete a exchange', (done)-> 142 | this.timeout(5000); 143 | amqp = null 144 | exchange = null 145 | async.series [ 146 | (next)-> 147 | amqp = new AMQP {host:'localhost'}, (e, r)-> 148 | should.not.exist e 149 | next() 150 | 151 | (next)-> 152 | amqp.exchange {exchange:"testsing"}, (e, exc)-> 153 | should.not.exist e 154 | should.exist exc 155 | exchange = exc 156 | next() 157 | 158 | (next)-> 159 | exchange.declare {}, (e,r)-> 160 | should.not.exist e 161 | next() 162 | 163 | (next)-> 164 | exchange.delete {}, (e,r)-> 165 | should.not.exist e 166 | next() 167 | 168 | (next)-> 169 | amqp.close() 170 | next() 171 | 172 | ], done 173 | 174 | 175 | it 'test it can declare a exchange exchange binding 5541', (done)-> 176 | this.timeout(5000); 177 | amqp = null 178 | exchange = null 179 | async.series [ 180 | (next)-> 181 | amqp = new AMQP {host:'localhost'}, (e, r)-> 182 | should.not.exist e 183 | next() 184 | 185 | (next)-> 186 | amqp.exchange {exchange:"exchone", autoDelete: false}, (e, exc)-> 187 | should.not.exist e 188 | should.exist exc 189 | exchange = exc 190 | next() 191 | 192 | (next)-> 193 | exchange.declare {}, (e,r)-> 194 | should.not.exist e 195 | next() 196 | 197 | (next)-> 198 | amqp.exchange {exchange:"exchtwo", autoDelete: false}, (e, exc)-> 199 | should.not.exist e 200 | should.exist exc 201 | exchange = exc 202 | next() 203 | 204 | (next)-> 205 | exchange.declare {}, (e,r)-> 206 | should.not.exist e 207 | next() 208 | 209 | 210 | (next)-> 211 | exchange.bind "exchone", "ee-routingkey", (e, r)-> 212 | should.not.exist e 213 | next() 214 | 215 | (next)-> 216 | exchange.unbind "exchone", "ee-routingkey", (e, r)-> 217 | should.not.exist e 218 | next() 219 | 220 | (next)-> 221 | amqp.exchange({exchange:"exchone"}).delete(next) 222 | 223 | (next)-> 224 | amqp.exchange({exchange:"exchtwo"}).delete(next) 225 | 226 | (next)-> 227 | amqp.close() 228 | next() 229 | ], done 230 | 231 | 232 | it 'test it can declare a exchange exchange binding chained 5542', (done)-> 233 | this.timeout(5000); 234 | amqp = null 235 | exchange = null 236 | async.series [ 237 | (next)-> 238 | amqp = new AMQP {host:'localhost'}, (e, r)-> 239 | should.not.exist e 240 | next() 241 | 242 | (next)-> 243 | amqp.exchange({exchange:"exchone", autoDelete: false}).declare {}, (e,r)-> 244 | should.not.exist e 245 | next() 246 | 247 | (next)-> 248 | exchange = amqp.exchange({exchange:"exchtwo", autoDelete: false}).declare().bind "exchone", "ee-routingkey", (e, r)-> 249 | should.not.exist e 250 | next() 251 | 252 | (next)-> 253 | exchange.unbind "exchone", "ee-routingkey", (e, r)-> 254 | should.not.exist e 255 | next() 256 | 257 | (next)-> 258 | amqp.exchange({exchange:"exchone"}).delete(next) 259 | 260 | (next)-> 261 | amqp.exchange({exchange:"exchtwo"}).delete(next) 262 | 263 | (next)-> 264 | amqp.close() 265 | next() 266 | ], done 267 | 268 | 269 | 270 | it 'test it can delete a exchange with no options', (done)-> 271 | this.timeout(5000); 272 | amqp = null 273 | exchange = null 274 | async.series [ 275 | (next)-> 276 | amqp = new AMQP {host:'localhost'}, (e, r)-> 277 | should.not.exist e 278 | next() 279 | 280 | (next)-> 281 | amqp.exchange {exchange:"testsing"}, (e, exc)-> 282 | should.not.exist e 283 | should.exist exc 284 | exchange = exc 285 | next() 286 | 287 | (next)-> 288 | exchange.declare {}, (e,r)-> 289 | should.not.exist e 290 | next() 291 | 292 | (next)-> 293 | exchange.delete (e,r)-> 294 | should.not.exist e 295 | next() 296 | 297 | (next)-> 298 | amqp.close() 299 | next() 300 | 301 | ], done 302 | 303 | 304 | 305 | it 'test we can timeout a exchange channel and reopen it', (done)-> 306 | this.timeout(2000) 307 | amqp = null 308 | exchange = null 309 | async.series [ 310 | (next)-> 311 | amqp = new AMQP {host:'localhost'}, (e, r)-> 312 | should.not.exist e 313 | next() 314 | 315 | (next)-> 316 | amqp.exchange {exchange:"testsing"}, (e, exc)-> 317 | should.not.exist e 318 | should.exist exc 319 | exchange = exc 320 | next() 321 | 322 | (next)-> 323 | exchange.declare {}, (e,r)-> 324 | should.not.exist e 325 | next() 326 | 327 | (next)-> 328 | _.keys(amqp.channels).length.should.eql 2 329 | _.delay next, 500 330 | 331 | (next)-> 332 | _.keys(amqp.channels).length.should.eql 1 333 | next() 334 | 335 | (next)-> 336 | exchange.declare {}, (e,r)-> 337 | should.not.exist e 338 | _.keys(amqp.channels).length.should.eql 2 339 | next() 340 | 341 | (next)-> 342 | amqp.close() 343 | next() 344 | 345 | ], done 346 | -------------------------------------------------------------------------------- /src/lib/Channel.coffee: -------------------------------------------------------------------------------- 1 | # Channel 2 | {EventEmitter} = require('events') 3 | 4 | debug = require('./config').debug('amqp:Channel') 5 | async = require('async') 6 | _ = require('underscore') 7 | 8 | defaults = require('./defaults') 9 | { methodTable, classes, methods } = require('./config').protocol 10 | 11 | 12 | # we track this to avoid node's max stack size with a saturated async queue 13 | OVERFLOW_PROTECTION = 0 14 | 15 | class Channel extends EventEmitter 16 | constructor: (connection, channel)-> 17 | 18 | @channel = channel 19 | @connection = connection 20 | 21 | @state = 'closed' 22 | @waitingCallbacks = {} # channel operations 23 | 24 | @queue = async.queue(@_taskWorker, 1) 25 | 26 | @open() 27 | @transactional = false 28 | 29 | 30 | temporaryChannel: ()-> 31 | @transactional = true # THIS IS NOT AMQP TRANSACTIONS 32 | @lastChannelAccess = Date.now() 33 | 34 | if process.env.AMQP_TEST? 35 | @connection.connectionOptions.temporaryChannelTimeout = 200 36 | @connection.connectionOptions.temporaryChannelTimeoutCheck = 100 37 | 38 | if !@channelTracker? 39 | @channelTracker = setInterval ()=> 40 | if @lastChannelAccess < (Date.now() - @connection.connectionOptions.temporaryChannelTimeout) 41 | debug 4, ()->return "Closing channel due to inactivity" 42 | @close(true) 43 | , @connection.connectionOptions.temporaryChannelTimeoutCheck 44 | 45 | open: (cb)-> 46 | if @state is "closed" 47 | @state = 'opening' 48 | 49 | @waitForMethod(methods.channelOpenOk, cb) if cb? 50 | @connection._sendMethod(@channel, methods.channelOpen, {}) 51 | @connection.channelCount++ 52 | 53 | if @transactional then @temporaryChannel() 54 | else 55 | cb("state isn't closed. not opening channel") if cb? 56 | 57 | reset: (cb)=> 58 | 59 | @_callOutstandingCallbacks("Channel Opening or Reseting") if @state isnt 'open' 60 | # if our state is closed and either we arn't a transactional channel (queue, exchange declare etc..) 61 | # or we're within our acceptable time window for this queue 62 | if @state is 'closed' and (!@transactional or @listeners('open').length > 0 or (@transactional and @lastChannelAccess > (Date.now() - @connection.connectionOptions.temporaryChannelTimeout))) 63 | debug 1, ()->return "State is closed... reconnecting" 64 | 65 | async.series [ 66 | (next)=> 67 | @open(next) 68 | 69 | (next)=> 70 | @_onChannelReconnect(next) 71 | ], cb 72 | 73 | else 74 | cb() if cb? 75 | 76 | crash: (cb)=> 77 | if !process.env.AMQP_TEST? 78 | cb?() 79 | return true 80 | 81 | # this will crash a channel forcing a channelOpen from the server 82 | # this is really only for testing 83 | debug "Trying to crash channel" 84 | @connection._sendMethod @channel, methods.queuePurge, {queue:"idontexist"} 85 | @waitForMethod(methods.channelClose, cb) if cb? 86 | 87 | close: (auto)=> 88 | if !auto? or !auto then debug 1, ()->return "User requested channel close" 89 | 90 | clearInterval(@channelTracker) 91 | @channelTracker = null 92 | 93 | if @state is 'open' 94 | @connection.channelCount-- 95 | @state = 'closed' 96 | @connection._sendMethod @channel, methods.channelClose, { 97 | replyText : 'Goodbye' 98 | replyCode : 200 99 | classId : 0 100 | methodId : 0 101 | } 102 | 103 | waitForMethod: (method, cb)-> 104 | @waitingCallbacks[method.name] = [] if !@waitingCallbacks[method]? 105 | @waitingCallbacks[method.name].push cb 106 | 107 | callbackForMethod: (method)-> 108 | if !method? or !@waitingCallbacks[method.name]? 109 | return ()-> return true 110 | 111 | cb = @waitingCallbacks[method.name].shift() 112 | if @waitingCallbacks[method.name].length is 0 113 | delete @waitingCallbacks[method.name] 114 | 115 | return cb 116 | 117 | 118 | # Functions to overwrite 119 | _channelOpen: ()-> 120 | debug 4, ()->return "channel open called and should be overwritten" 121 | 122 | _channelClosed: ()-> 123 | debug 4, ()->return "channel closed called and should be overwritten" 124 | 125 | _onChannelReconnect: (cb)-> 126 | debug 4, ()->return "channel reconnect called and should be overwritten" 127 | cb() 128 | 129 | _onMethod: (method, args)-> 130 | debug 3, ()->return "_onMethod MUST be overwritten by whoever extends Channel" 131 | 132 | 133 | # TASK QUEUEING --------------------------------------------------------- 134 | taskPush: ( method, args, okMethod, cb)=> # same as queueSendMethod 135 | @queue.push {type: 'method', method, args, okMethod, cb} 136 | 137 | taskPushPreflight: ( method, args, okMethod, preflight, cb)=> 138 | @queue.push {type: 'method', method, args, okMethod, preflight, cb} 139 | 140 | taskQueuePushRaw: (task, cb)=> 141 | task.cb = cb if cb? and task? 142 | @queue.push task 143 | 144 | queueSendMethod: (method, args, okMethod, cb)=> 145 | @queue.push {type: 'method', method, args, okMethod, cb} 146 | 147 | queuePublish: (method, data, options)=> 148 | @queue.push {type: 'publish', method, data, options} 149 | 150 | _taskWorker: (task, done)=> 151 | if @transactional then @lastChannelAccess = Date.now() 152 | {type, method, okMethod, args, cb, data, options, preflight} = task 153 | 154 | doneFn = (err, res)-> 155 | cb(err, res) if cb? 156 | if OVERFLOW_PROTECTION > 100 157 | OVERFLOW_PROTECTION = 0 158 | _.defer done 159 | else 160 | OVERFLOW_PROTECTION++ 161 | done() 162 | 163 | # if preflight is false do not proceed 164 | if preflight? and !preflight() 165 | return doneFn('preflight check failed') 166 | 167 | if @state is 'closed' and @connection.state is 'open' 168 | debug 1, ()->return "Channel reassign" 169 | @connection.channelManager.channelReassign(@) 170 | @open (e, r)=> 171 | @_taskWorker(task, done) 172 | 173 | else if @state isnt 'open' 174 | # if our connection is closed that ok, but if its destroyed it will not reopen 175 | if @connection.state is 'destroyed' 176 | doneFn("Connection is destroyed") 177 | 178 | else 179 | if @connection.channelManager.isChannelClosed(@channel) 180 | @connection.channelManager.channelReassign(@) 181 | @once 'open', ()=> 182 | @_taskWorker(task, done) 183 | 184 | else 185 | @waitForMethod(okMethod, doneFn) if okMethod? 186 | 187 | if type is 'method' 188 | @connection._sendMethod(@channel, method, args) 189 | doneFn() if !okMethod? 190 | 191 | else if type is 'publish' 192 | @connection._sendMethod(@channel, method, options) 193 | @connection._sendBody @channel, data, options, (err, res)-> 194 | doneFn() if !okMethod? 195 | 196 | else 197 | throw new Error("a task was queue with an unknown type of #{type}") 198 | 199 | 200 | _callOutstandingCallbacks: (message)=> 201 | outStandingCallbacks = @waitingCallbacks 202 | @waitingCallbacks = {} 203 | 204 | if !message? then message = "Channel Unavaliable" 205 | for key, cbs of outStandingCallbacks 206 | for cb in cbs 207 | cb?(message) 208 | 209 | 210 | # incomming channel messages for us 211 | _onChannelMethod: (channel, method, args )-> 212 | if @transactional then @lastChannelAccess = Date.now() 213 | 214 | if channel isnt @channel 215 | return debug 1, ()->return ["channel was sent to the wrong channel object", channel, @channel] 216 | 217 | @callbackForMethod(method)(null, args) 218 | 219 | switch method 220 | when methods.channelCloseOk 221 | @connection.channelManager.channelClosed(@channel) 222 | 223 | @state = 'closed' 224 | 225 | @_channelClosed("Channel closed") 226 | @_callOutstandingCallbacks({msg: "Channel closed"}) 227 | 228 | when methods.channelClose 229 | @connection.channelManager.channelClosed(channel) 230 | 231 | debug 1, ()->return "Channel closed by server #{JSON.stringify args}" 232 | @state = 'closed' 233 | 234 | if args.classId? and args.methodId? 235 | closingMethod = methodTable[args.classId][args.methodId].name 236 | @callbackForMethod(methods["#{closingMethod}Ok"])(args) #this would be the error 237 | 238 | @_channelClosed({msg: "Server closed channel", error: args}) 239 | @_callOutstandingCallbacks("Channel closed by server #{JSON.stringify args}") 240 | 241 | when methods.channelOpenOk 242 | @state = 'open' 243 | @_channelOpen() 244 | @emit 'open' 245 | 246 | 247 | else 248 | @_onMethod( channel, method, args ) 249 | 250 | _connectionClosed: ()-> 251 | # if the connection closes, make sure we reflect that because that channel is also closed 252 | if @state isnt 'closed' 253 | @state = 'closed' 254 | @_channelClosed() 255 | if @channelTracker? 256 | clearInterval(@channelTracker) 257 | @channelTracker = null 258 | 259 | module.exports = Channel 260 | -------------------------------------------------------------------------------- /src/lib/serializationHelpers.coffee: -------------------------------------------------------------------------------- 1 | jspack = require('../jspack') 2 | 3 | exports.serializeFloat = serializeFloat = (b, size, value, bigEndian)-> 4 | jp = new jspack(bigEndian) 5 | 6 | switch size 7 | when 4 8 | x = jp.Pack('f', [value]) 9 | for i in x 10 | b[b.used++] = i 11 | 12 | when 8 13 | x = jp.Pack('d', [value]) 14 | for i in x 15 | b[b.used++] = i 16 | 17 | else 18 | throw new Error("Unknown floating point size") 19 | 20 | 21 | exports.serializeInt = serializeInt = (b, size, int)-> 22 | if (b.used + size > b.length) 23 | throw new Error("write out of bounds") 24 | 25 | # Only 4 cases - just going to be explicit instead of looping. 26 | switch size 27 | # octet 28 | when 1 29 | b[b.used++] = int 30 | 31 | # short 32 | when 2 33 | b[b.used++] = (int & 0xFF00) >> 8 34 | b[b.used++] = (int & 0x00FF) >> 0 35 | 36 | # long 37 | when 4 38 | b[b.used++] = (int & 0xFF000000) >> 24 39 | b[b.used++] = (int & 0x00FF0000) >> 16 40 | b[b.used++] = (int & 0x0000FF00) >> 8 41 | b[b.used++] = (int & 0x000000FF) >> 0 42 | 43 | 44 | # long long 45 | when 8 46 | b[b.used++] = (int & 0xFF00000000000000) >> 56 47 | b[b.used++] = (int & 0x00FF000000000000) >> 48 48 | b[b.used++] = (int & 0x0000FF0000000000) >> 40 49 | b[b.used++] = (int & 0x000000FF00000000) >> 32 50 | b[b.used++] = (int & 0x00000000FF000000) >> 24 51 | b[b.used++] = (int & 0x0000000000FF0000) >> 16 52 | b[b.used++] = (int & 0x000000000000FF00) >> 8 53 | b[b.used++] = (int & 0x00000000000000FF) >> 0 54 | 55 | else 56 | throw new Error("Bad size") 57 | 58 | 59 | exports.serializeShortString = serializeShortString = (b, string)-> 60 | if (typeof(string) != "string") 61 | throw new Error("param must be a string") 62 | 63 | byteLength = Buffer.byteLength(string, 'utf8') 64 | if (byteLength > 0xFF) 65 | throw new Error("String too long for 'shortstr' parameter") 66 | 67 | if (1 + byteLength + b.used >= b.length) 68 | throw new Error("Not enough space in buffer for 'shortstr'") 69 | 70 | b[b.used++] = byteLength 71 | b.write(string, b.used, 'utf8') 72 | b.used += byteLength 73 | 74 | 75 | exports.serializeLongString = serializeLongString = (b, string)-> 76 | # we accept string, object, or buffer for this parameter. 77 | # in the when of string we serialize it to utf8. 78 | if (typeof(string) == 'string') 79 | byteLength = Buffer.byteLength(string, 'utf8') 80 | serializeInt(b, 4, byteLength) 81 | b.write(string, b.used, 'utf8') 82 | b.used += byteLength 83 | else if (typeof(string) == 'object') 84 | serializeTable(b, string) 85 | else 86 | # data is Buffer 87 | byteLength = string.length 88 | serializeInt(b, 4, byteLength) 89 | b.write(string, b.used) # memcpy 90 | b.used += byteLength 91 | 92 | exports.serializeDate = serializeDate = (b, date)-> 93 | serializeInt(b, 8, date.valueOf() / 1000) 94 | 95 | exports.serializeBuffer = serializeBuffer = (b, buffer)-> 96 | serializeInt(b, 4, buffer.length) 97 | buffer.copy(b, b.used, 0) 98 | b.used += buffer.length 99 | 100 | exports.serializeBase64 = serializeBase64 = (b, buffer)-> 101 | serializeLongString(b, buffer.toString('base64')) 102 | 103 | 104 | exports.serializeValue = serializeValue = (b, value)-> 105 | switch typeof(value) 106 | when 'string' 107 | b[b.used++] = 'S'.charCodeAt(0) 108 | serializeLongString(b, value) 109 | 110 | when 'number' 111 | if !isFloat(value) 112 | if isBigInt(value) 113 | # 64-bit uint 114 | b[b.used++] = 'l'.charCodeAt(0) 115 | serializeInt(b, 8, value) 116 | else 117 | #32-bit uint 118 | b[b.used++] = 'I'.charCodeAt(0) 119 | serializeInt(b, 4, value) 120 | 121 | else 122 | #64-bit float 123 | b[b.used++] = 'd'.charCodeAt(0) 124 | serializeFloat(b, 8, value) 125 | 126 | 127 | when 'boolean' 128 | b[b.used++] = 't'.charCodeAt(0) 129 | b[b.used++] = value 130 | 131 | else 132 | if value instanceof Date 133 | b[b.used++] = 'T'.charCodeAt(0) 134 | serializeDate(b, value) 135 | else if value instanceof Buffer 136 | b[b.used++] = 'x'.charCodeAt(0) 137 | serializeBuffer(b, value) 138 | else if Array.isArray(value) 139 | b[b.used++] = 'A'.charCodeAt(0) 140 | serializeArray(b, value) 141 | else if typeof(value) == 'object' 142 | b[b.used++] = 'F'.charCodeAt(0) 143 | serializeTable(b, value) 144 | else 145 | this.throwError("unsupported type in amqp table = " + typeof(value)) 146 | 147 | 148 | 149 | exports.serializeTable = serializeTable = (b, object)-> 150 | if (typeof(object) != "object") 151 | throw new Error("param must be an object"); 152 | 153 | 154 | # Save our position so that we can go back and write the length of this table 155 | # at the beginning of the packet (once we know how many entries there are). 156 | lengthIndex = b.used 157 | b.used += 4 # sizeof long 158 | startIndex = b.used 159 | 160 | for key, value of object 161 | serializeShortString(b, key) 162 | serializeValue(b, value) 163 | 164 | endIndex = b.used 165 | b.used = lengthIndex 166 | serializeInt(b, 4, endIndex - startIndex) 167 | b.used = endIndex 168 | 169 | 170 | exports.serializeArray = serializeArray = (b, arr)-> 171 | # Save our position so that we can go back and write the byte length of this array 172 | # at the beginning of the packet (once we have serialized all elements). 173 | lengthIndex = b.used 174 | b.used += 4 # sizeof long 175 | startIndex = b.used 176 | 177 | for i in arr 178 | serializeValue(b, i); 179 | 180 | endIndex = b.used 181 | b.used = lengthIndex 182 | serializeInt(b, 4, endIndex - startIndex) 183 | b.used = endIndex 184 | 185 | 186 | exports.serializeFields = serializeFields = (buffer, fields, args, strict)-> 187 | bitField = 0 188 | bitIndex = 0 189 | for i in [0...fields.length] 190 | field = fields[i] 191 | domain = field.domain 192 | if !(args.hasOwnProperty(field.name)) 193 | if strict 194 | if field.name.indexOf("reserved") is 0 195 | 196 | # populate default reserved values, this is to keep the code cleaner, but may be wrong 197 | switch domain 198 | when 'short' then args[field.name] = 0 199 | when 'bit' then args[field.name] = false 200 | when 'shortstr' then args[field.name] = "" 201 | else args[field.name] = true 202 | else if field.name is "noWait" 203 | # defaults noWait to false 204 | args[field.name] = false 205 | else 206 | throw new Error("Missing field '" + field.name + "' of type '" + domain + "' while executing AMQP method '" + arguments.callee.caller.arguments[1].name + "'"); 207 | else 208 | continue 209 | 210 | param = args[field.name] 211 | switch domain 212 | when 'bit' 213 | if (typeof(param) != "boolean") 214 | throw new Error("Unmatched field " + JSON.stringify(field)); 215 | 216 | 217 | if param then bitField |= (1 << bitIndex) 218 | bitIndex++ 219 | 220 | if (!fields[i+1] || fields[i+1].domain != 'bit') 221 | # debug('SET bit field ' + field.name + ' 0x' + bitField.toString(16)); 222 | buffer[buffer.used++] = bitField; 223 | bitField = 0; 224 | bitIndex = 0; 225 | 226 | 227 | when 'octet' 228 | if (typeof(param) != "number" || param > 0xFF) 229 | throw new Error("Unmatched field " + JSON.stringify(field)) 230 | 231 | buffer[buffer.used++] = param; 232 | 233 | when 'short' 234 | if (typeof(param) != "number" || param > 0xFFFF) 235 | throw new Error("Unmatched field " + JSON.stringify(field)) 236 | 237 | serializeInt(buffer, 2, param) 238 | break; 239 | 240 | when 'long' 241 | if (typeof(param) != "number" || param > 0xFFFFFFFF) 242 | throw new Error("Unmatched field " + JSON.stringify(field)) 243 | 244 | serializeInt(buffer, 4, param) 245 | 246 | when 'timestamp', 'longlong' 247 | serializeInt(buffer, 8, param); 248 | 249 | when 'shortstr' 250 | if (typeof(param) != "string" || param.length > 0xFF) 251 | throw new Error("Unmatched field " + JSON.stringify(field)) 252 | 253 | serializeShortString(buffer, param) 254 | 255 | when 'longstr' 256 | serializeLongString(buffer, param) 257 | 258 | when 'table' 259 | if (typeof(param) != "object") 260 | throw new Error("Unmatched field " + JSON.stringify(field)) 261 | 262 | serializeTable(buffer, param) 263 | 264 | else 265 | throw new Error("Unknown domain value type " + domain) 266 | 267 | 268 | exports.isBigInt = isBigInt = (value)-> 269 | return value > 0xffffffff; 270 | 271 | 272 | exports.getCode = getCode = (dev)-> 273 | hexArray = "0123456789ABCDEF".split('') 274 | 275 | code1 = Math.floor(dec / 16) 276 | code2 = dec - code1 * 16 277 | return hexArray[code2] 278 | 279 | 280 | exports.isFloat = isFloat = (value)-> 281 | return value is +value and value isnt value|0 282 | 283 | -------------------------------------------------------------------------------- /src/lib/Consumer.coffee: -------------------------------------------------------------------------------- 1 | # Exchange 2 | os = require('os') 3 | 4 | debug = require('./config').debug('amqp:Consumer') 5 | Channel = require('./Channel') 6 | _ = require('underscore') 7 | async = require('async') 8 | defaults = require('./defaults') 9 | 10 | bson = require('bson') 11 | BSON = new bson.BSONPure.BSON() 12 | 13 | { methodTable, classes, methods } = require('./config').protocol 14 | { MaxEmptyFrameSize } = require('./config').constants 15 | 16 | CONSUMER_STATE_OPEN = 'open' 17 | CONSUMER_STATE_OPENING = 'opening' 18 | 19 | CONSUMER_STATE_CLOSED = 'closed' 20 | CONSUMER_STATE_USER_CLOSED = 'user_closed' 21 | CONSUMER_STATE_CHANNEL_CLOSED = 'channel_closed' 22 | CONSUMER_STATE_CONNECTION_CLOSED = 'connection_closed' 23 | 24 | CONSUMER_STATES_CLOSED = [CONSUMER_STATE_CLOSED, CONSUMER_STATE_USER_CLOSED, CONSUMER_STATE_CONNECTION_CLOSED, CONSUMER_STATE_CHANNEL_CLOSED] 25 | 26 | class Consumer extends Channel 27 | 28 | constructor: (connection, channel)-> 29 | debug 2, ()=>return "channel open for consumer #{channel}" 30 | super(connection, channel) 31 | @consumerState = CONSUMER_STATE_CLOSED 32 | @messageHandler = null 33 | 34 | @incomingMessage = null 35 | @outstandingDeliveryTags = {} 36 | return @ 37 | 38 | consume: (queueName, options, messageHandler, cb)-> 39 | if typeof options == 'function' 40 | if typeof messageHandler == 'function' 41 | cb = messageHandler 42 | 43 | messageHandler = options 44 | options = {} 45 | 46 | @consumerTag = options.consumerTag ? "#{os.hostname()}-#{process.pid}-#{Date.now()}" 47 | 48 | debug 2, ()=>return "Consuming to #{queueName} on channel #{@channel} #{@consumerTag}" 49 | 50 | @consumerState = CONSUMER_STATE_OPENING 51 | 52 | if options.prefetchCount? 53 | # this should be a qos channel and we should expect ack's on messages 54 | @qos = true 55 | 56 | providedOptions = {prefetchCount: options.prefetchCount} 57 | providedOptions['global'] = options.global if options.global? 58 | 59 | qosOptions = _.defaults providedOptions, defaults.basicQos 60 | options.noAck = false 61 | delete options.prefetchCount 62 | else 63 | @qos = false 64 | options.noAck = true 65 | 66 | consumeOptions = _.defaults options, defaults.basicConsume 67 | consumeOptions.queue = queueName 68 | consumeOptions.consumerTag = @consumerTag 69 | 70 | @messageHandler = messageHandler if messageHandler? 71 | if !@messageHandler? then return cb?("No message handler") 72 | 73 | @consumeOptions = consumeOptions 74 | @qosOptions = qosOptions 75 | 76 | @_consume(cb) 77 | 78 | return @ 79 | 80 | close: (cb)=> 81 | @cancel ()=> 82 | @consumerState = CONSUMER_STATE_USER_CLOSED 83 | super() 84 | cb?() 85 | 86 | cancel: (cb)=> 87 | if !(@consumerState in CONSUMER_STATES_CLOSED) 88 | @taskPushPreflight methods.basicCancel, {consumerTag: @consumerTag, noWait:false}, methods.basicCancelOk, @_consumerStateOpenPreflight, cb 89 | else 90 | cb?() 91 | 92 | pause: (cb)-> 93 | if !(@consumerState in CONSUMER_STATES_CLOSED) 94 | @cancel (err, res)=> 95 | # should pause be a different state? 96 | @consumerState = CONSUMER_STATE_USER_CLOSED 97 | cb?(err, res) 98 | else 99 | cb?() 100 | 101 | resume: (cb)-> 102 | if @consumerState in CONSUMER_STATES_CLOSED 103 | @_consume(cb) 104 | else 105 | cb?() 106 | 107 | flow: (active, cb)-> 108 | if active then @resume(cb) else @pause(cb) 109 | 110 | setQos: (prefetchCount, cb)-> 111 | if typeof prefetchCount is 'function' 112 | cb = prefetchCount 113 | qosOptions = @qosOptions 114 | else 115 | # if our prefetch count has changed and we're rabbit version > 3.3.* 116 | # Rabbitmq 3.3.0 changes the behavior of qos. we default to gloabl true in this case. 117 | if prefetchCount isnt @qosOptions.prefetchCount and \ 118 | @connection.serverProperties?.product == 'RabbitMQ' and\ 119 | ( @connection.serverProperties?.capabilities?.per_consumer_qos == true or \ 120 | @connection.serverProperties?.version == "3.3.0" ) 121 | 122 | global = true 123 | 124 | 125 | qosOptions = _.defaults({prefetchCount, global}, @qosOptions) 126 | 127 | @taskPush methods.basicQos, qosOptions, methods.basicQosOk, cb 128 | 129 | # Private 130 | 131 | _consume: (cb)=> 132 | async.series [ 133 | (next)=> 134 | if @qos 135 | @setQos next 136 | else 137 | next() 138 | 139 | (next)=> 140 | @taskQueuePushRaw {type: 'method', method: methods.basicConsume, args: @consumeOptions, okMethod: methods.basicConsumeOk, preflight: @_basicConsumePreflight}, next 141 | 142 | (next)=> 143 | @consumerState = CONSUMER_STATE_OPEN 144 | next() 145 | ], cb 146 | 147 | _basicConsumePreflight: ()=> 148 | return @consumerState != CONSUMER_STATE_OPEN 149 | 150 | _consumerStateOpenPreflight: ()=> 151 | return @consumerState == CONSUMER_STATE_OPEN 152 | 153 | _channelOpen: ()=> 154 | if @consumeOptions? and @consumerState is CONSUMER_STATE_CONNECTION_CLOSED then @_consume() 155 | 156 | _channelClosed: (reason)=> 157 | # if we're reconnecting it is approiate to emit the error on reconnect, this is specifically useful 158 | # for auto delete queues 159 | if @consumerState is CONSUMER_STATE_CHANNEL_CLOSED 160 | if !reason? then reason = {} 161 | @emit 'error', reason 162 | 163 | @outstandingDeliveryTags = {} 164 | if @connection.state is 'open' and @consumerState is CONSUMER_STATE_OPEN 165 | @consumerState = CONSUMER_STATE_CHANNEL_CLOSED 166 | @_consume() 167 | else 168 | @consumerState = CONSUMER_STATE_CONNECTION_CLOSED 169 | 170 | 171 | # QOS RELATED Callbacks 172 | ack: ()-> 173 | if @subscription.qos and @subscription.outstandingDeliveryTags[@deliveryTag]? 174 | delete @subscription.outstandingDeliveryTags[@deliveryTag] 175 | 176 | if @subscription.state is 'open' 177 | basicAckOptions = { deliveryTag: @deliveryTag, multiple: false } 178 | @subscription.connection._sendMethod @subscription.channel, methods.basicAck, basicAckOptions 179 | 180 | reject: ()-> 181 | if @subscription.qos and @subscription.outstandingDeliveryTags[@deliveryTag]? 182 | delete @subscription.outstandingDeliveryTags[@deliveryTag] 183 | 184 | if @subscription.state is 'open' 185 | basicAckOptions = { deliveryTag: @deliveryTag, requeue: false } 186 | @subscription.connection._sendMethod @subscription.channel, methods.basicReject, basicAckOptions 187 | 188 | retry: ()-> 189 | if @subscription.qos and @subscription.outstandingDeliveryTags[@deliveryTag]? 190 | delete @subscription.outstandingDeliveryTags[@deliveryTag] 191 | 192 | if @subscription.state is 'open' 193 | basicAckOptions = { deliveryTag: @deliveryTag, requeue: true } 194 | @subscription.connection._sendMethod @subscription.channel, methods.basicReject, basicAckOptions 195 | 196 | # CONTENT HANDLING 197 | _onMethod: (channel, method, args)-> 198 | debug 3, ()->return "onMethod #{method.name}, #{JSON.stringify args}" 199 | switch method 200 | when methods.basicDeliver 201 | delete args['consumerTag'] # TODO evaluate if this is a good idea 202 | if @qos 203 | @incomingMessage = args 204 | else 205 | @incomingMessage = args 206 | 207 | when methods.basicCancel 208 | debug 1, ()->return "basicCancel" 209 | @consumerState = CONSUMER_STATE_CLOSED 210 | 211 | if @listeners('cancel').length > 0 212 | @emit 'cancel', "Server initiated basicCancel" 213 | else 214 | cancelError = new Error("Server initiated basicCancel") 215 | cancelError.code = 'basicCancel' 216 | @emit 'error', cancelError 217 | 218 | 219 | _onContentHeader: (channel, classInfo, weight, properties, size)-> 220 | debug 3, ()->return "_onContentHeader #{JSON.stringify properties} #{size}" 221 | @incomingMessage = _.extend @incomingMessage, {weight, properties, size} 222 | 223 | # if we're only expecting one packet lets just copy the buffer when we get it 224 | # otherwise lets create a new incoming data buffer and pre alloc the space 225 | if size > @connection.frameMax - MaxEmptyFrameSize 226 | @incomingMessage.data = new Buffer(size) 227 | @incomingMessage.data.used = 0 228 | 229 | if size == 0 230 | @_onContent(channel, new Buffer(0)) 231 | 232 | _onContent: (channel, data)=> 233 | if !@incomingMessage.data? and @incomingMessage.size is data.length 234 | # if our size is equal to the data we have, just replace the data object 235 | @incomingMessage.data = data 236 | 237 | else 238 | # if there are multiple packets just copy the data starting from the last used bit. 239 | data.copy(@incomingMessage.data, @incomingMessage.data.used) 240 | @incomingMessage.data.used += data.length 241 | 242 | if @incomingMessage.data.used >= @incomingMessage.size || @incomingMessage.size == 0 243 | message = _.clone @incomingMessage 244 | message.raw = @incomingMessage.data 245 | 246 | # DEFINE GETTERS ON THE DATA FIELD WHICH RETURN A COPY OF THE RAW DATA 247 | if @incomingMessage.properties?.contentType is "application/json" 248 | 249 | # we use defineProperty here because we want to keep our original message intact and dont want to pass around a special message 250 | Object.defineProperty message, "data", { 251 | get: ()=> 252 | try 253 | return JSON.parse message.raw.toString() 254 | catch e 255 | console.error e 256 | return message.raw 257 | } 258 | 259 | else if @incomingMessage.properties?.contentType is "application/bson" 260 | # we use defineProperty here because we want to keep our original message intact and dont want to pass around a special message 261 | Object.defineProperty message, "data", { 262 | get: ()=> 263 | try 264 | return BSON.deserialize message.raw 265 | catch e 266 | console.error e 267 | return message.raw 268 | } 269 | 270 | else if @incomingMessage.properties?.contentType is "string/utf8" 271 | # we use defineProperty here because we want to keep our original message intact and dont want to pass around a special message 272 | Object.defineProperty message, "data", { 273 | get: ()=> 274 | try 275 | return message.raw.toString('utf8') 276 | catch e 277 | console.error e 278 | return message.raw 279 | } 280 | 281 | 282 | else if @incomingMessage.size == 0 and @incomingMessage.properties?.contentType is "application/undefined" 283 | # we use defineProperty here because we want to keep our original message intact and dont want to pass around a special message 284 | Object.defineProperty message, "data", { 285 | get: ()=> 286 | return undefined 287 | } 288 | 289 | 290 | else 291 | Object.defineProperty message, "data", { 292 | get: ()=> 293 | return message.raw 294 | } 295 | 296 | if @qos 297 | message.ack = @ack 298 | message.reject = @reject 299 | message.retry = @retry 300 | message.subscription = @ 301 | 302 | @outstandingDeliveryTags[@incomingMessage.deliveryTag] = true 303 | @messageHandler message 304 | 305 | module.exports = Consumer 306 | -------------------------------------------------------------------------------- /src/jspack.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2008, Fair Oaks Labs, Inc. 2 | // All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without modification, are 5 | // permitted provided that the following conditions are met: 6 | // 7 | // * Redistributions of source code must retain the above copyright notice, this list 8 | // of conditions and the following disclaimer. 9 | // * Redistributions in binary form must reproduce the above copyright notice, this 10 | // list of conditions and the following disclaimer in the documentation and/or other 11 | // materials provided with the distribution. 12 | // * Neither the name of Fair Oaks Labs, Inc. nor the names of its contributors may be 13 | // used to endorse or promote products derived from this software without specific 14 | // prior written permission. 15 | // 16 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 17 | // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 18 | // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 19 | // THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 21 | // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 22 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 23 | // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 24 | // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | // Modified from original JSPack 26 | exports.jspack = function (bigEndian) { 27 | this.bigEndian = bigEndian; 28 | } 29 | 30 | exports.jspack.prototype._DeArray = function (a, p, l) { 31 | return [a.slice(p, p + l)]; 32 | }; 33 | 34 | exports.jspack.prototype._EnArray = function (a, p, l, v) { 35 | for (var i = 0; i < l; ++i) { 36 | a[p + i] = v[i] ? v[i] : 0; 37 | } 38 | }; 39 | 40 | exports.jspack.prototype._DeChar = function (a, p) { 41 | return String.fromCharCode(a[p]); 42 | }; 43 | 44 | exports.jspack.prototype._EnChar = function (a, p, v) { 45 | a[p] = v.charCodeAt(0); 46 | }; 47 | 48 | exports.jspack.prototype._DeInt = function (a, p) { 49 | var lsb = bigEndian ? format.len - 1 : 0; 50 | var nsb = bigEndian ? -1 : 1; 51 | var stp = lsb + nsb * format.len, 52 | rv; 53 | var ret = 0; 54 | 55 | var i = lsb; 56 | var f = 1; 57 | while (i != stp) { 58 | rv += a[p + i] * f; 59 | i += nsb; 60 | f *= 256; 61 | } 62 | 63 | if (format.signed) { 64 | if ((rv & Math.pow(2, format.len * 8 - 1)) != 0) { 65 | rv -= Math.pow(2, format.len * 8); 66 | } 67 | } 68 | 69 | return rv; 70 | }; 71 | 72 | exports.jspack.prototype._EnInt = function (a, p, v) { 73 | var lsb = bigEndian ? format.len - 1 : 0; 74 | var nsb = bigEndian ? -1 : 1; 75 | var stp = lsb + nsb * format.len; 76 | 77 | v = v < format.min ? format.min : ((v > format.max) ? format.max : v); 78 | 79 | var i = lsb; 80 | while (i != stp) { 81 | a[p + i] = v & 0xff; 82 | i += nsb; 83 | v >>= 8; 84 | } 85 | }; 86 | 87 | exports.jspack.prototype._DeString = function (a, p, l) { 88 | var rv = new Array(1); 89 | 90 | for (i = 0; i < l; i++) { 91 | rv[i] = String.fromCharCode(a[p + i]); 92 | } 93 | 94 | return rv.join(''); 95 | }; 96 | 97 | exports.jspack.prototype._EnString = function (a, p, l, v) { 98 | for (var t, i = 0; i < l; ++i) { 99 | t = v.charCodeAt(i); 100 | if (!t) t = 0; 101 | 102 | a[p + i] = t; 103 | } 104 | }; 105 | 106 | exports.jspack.prototype._De754 = function (a, p) { 107 | var s, e, m, i, d, bits, bit, len, bias, max; 108 | 109 | bit = format.bit; 110 | len = format.len * 8 - format.bit - 1; 111 | max = (1 << len) - 1; 112 | bias = max >> 1; 113 | 114 | i = bigEndian ? 0 : format.len - 1; 115 | d = bigEndian ? 1 : -1;; 116 | s = a[p + i]; 117 | i = i + d; 118 | 119 | bits = -7; 120 | 121 | e = s & ((1 << -bits) - 1); 122 | s >>= -bits; 123 | 124 | for (bits += len; bits > 0; bits -= 8) { 125 | e = e * 256 + a[p + i]; 126 | i += d; 127 | } 128 | 129 | m = e & ((1 << -bits) - 1); 130 | e >>= -bits; 131 | 132 | for (bits += bit; bits > 0; bits -= 8) { 133 | m = m * 256 + a[p + i]; 134 | i += d; 135 | } 136 | 137 | switch (e) { 138 | case 0: 139 | // Zero, or denormalized number 140 | e = 1 - bias; 141 | break; 142 | 143 | case max: 144 | // NaN, or +/-Infinity 145 | return m ? NaN : ((s ? -1 : 1) * Infinity); 146 | 147 | default: 148 | // Normalized number 149 | m = m + Math.pow(2, bit); 150 | e = e - bias; 151 | break; 152 | } 153 | 154 | return (s ? -1 : 1) * m * Math.pow(2, e - bit); 155 | }; 156 | 157 | exports.jspack.prototype._En754 = function (a, p, v) { 158 | var s, e, m, i, d, c, bit, len, bias, max; 159 | 160 | bit = format.bit; 161 | len = format.len * 8 - format.bit - 1; 162 | max = (1 << len) - 1; 163 | bias = max >> 1; 164 | 165 | s = v < 0 ? 1 : 0; 166 | v = Math.abs(v); 167 | 168 | if (isNaN(v) || (v == Infinity)) { 169 | m = isNaN(v) ? 1 : 0; 170 | e = max; 171 | } else { 172 | e = Math.floor(Math.log(v) / Math.LN2); // Calculate log2 of the value 173 | c = Math.pow(2, -e); 174 | if (v * c < 1) { 175 | e--; 176 | c = c * 2; 177 | } 178 | 179 | // Round by adding 1/2 the significand's LSD 180 | if (e + bias >= 1) { 181 | v += format.rt / c; // Normalized: bit significand digits 182 | } else { 183 | v += format.rt * Math.pow(2, 1 - bias); // Denormalized: <= bit significand digits 184 | } 185 | if (v * c >= 2) { 186 | e++; 187 | c = c / 2; // Rounding can increment the exponent 188 | } 189 | 190 | if (e + bias >= max) { // overflow 191 | m = 0; 192 | e = max; 193 | } else if (e + bias >= 1) { // normalized 194 | m = (v * c - 1) * Math.pow(2, bit); // do not reorder this expression 195 | e = e + bias; 196 | } else { 197 | // Denormalized - also catches the '0' case, somewhat by chance 198 | m = v * Math.pow(2, bias - 1) * Math.pow(2, bit); 199 | e = 0; 200 | } 201 | } 202 | 203 | i = bigEndian ? format.len - 1 : 0; 204 | d = bigEndian ? -1 : 1;; 205 | 206 | while (bit >= 8) { 207 | a[p + i] = m & 0xff; 208 | i += d; 209 | m /= 256; 210 | bit -= 8; 211 | } 212 | 213 | e = (e << bit) | m; 214 | for (len += bit; len > 0; len -= 8) { 215 | a[p + i] = e & 0xff; 216 | i += d; 217 | e /= 256; 218 | } 219 | 220 | a[p + i - d] |= s * 128; 221 | }; 222 | 223 | // Unpack a series of n formatements of size s from array a at offset p with fxn 224 | exports.jspack.prototype._UnpackSeries = function (n, s, a, p) { 225 | var fxn = format.de; 226 | 227 | var ret = []; 228 | for (var i = 0; i < n; i++) { 229 | ret.push(fxn(a, p + i * s)); 230 | } 231 | 232 | return ret; 233 | }; 234 | 235 | // Pack a series of n formatements of size s from array v at offset i to array a at offset p with fxn 236 | exports.jspack.prototype._PackSeries = function (n, s, a, p, v, i) { 237 | var fxn = format.en; 238 | 239 | for (o = 0; o < n; o++) { 240 | fxn(a, p + o * s, v[i + o]); 241 | } 242 | }; 243 | 244 | // Unpack the octet array a, beginning at offset p, according to the fmt string 245 | exports.jspack.prototype.Unpack = function (fmt, a, p) { 246 | bigEndian = fmt.charAt(0) != '<'; 247 | 248 | if (p == undefined || p == null) p = 0; 249 | 250 | var re = new RegExp(this._sPattern, 'g'); 251 | 252 | var ret = []; 253 | 254 | for (var m; m = re.exec(fmt); /* */ ) { 255 | var n; 256 | if (m[1] == undefined || m[1] == '') n = 1; 257 | else n = parseInt(m[1]); 258 | 259 | var s = this._lenLut[m[2]]; 260 | 261 | if ((p + n * s) > a.length) return undefined; 262 | 263 | switch (m[2]) { 264 | case 'A': 265 | case 's': 266 | rv.push(this._formatLut[m[2]].de(a, p, n)); 267 | break; 268 | case 'c': 269 | case 'b': 270 | case 'B': 271 | case 'h': 272 | case 'H': 273 | case 'i': 274 | case 'I': 275 | case 'l': 276 | case 'L': 277 | case 'f': 278 | case 'd': 279 | format = this._formatLut[m[2]]; 280 | ret.push(this._UnpackSeries(n, s, a, p)); 281 | break; 282 | } 283 | 284 | p += n * s; 285 | } 286 | 287 | return Array.prototype.concat.apply([], ret); 288 | }; 289 | 290 | // Pack the supplied values into the octet array a, beginning at offset p, according to the fmt string 291 | exports.jspack.prototype.PackTo = function (fmt, a, p, values) { 292 | bigEndian = (fmt.charAt(0) != '<'); 293 | 294 | var re = new RegExp(this._sPattern, 'g'); 295 | 296 | for (var m, i = 0; m = re.exec(fmt); /* */ ) { 297 | var n; 298 | if (m[1] == undefined || m[1] == '') n = 1; 299 | else n = parseInt(m[1]); 300 | 301 | var s = this._lenLut[m[2]]; 302 | 303 | if ((p + n * s) > a.length) return false; 304 | 305 | switch (m[2]) { 306 | case 'A': 307 | case 's': 308 | if ((i + 1) > values.length) return false; 309 | 310 | this._formatLut[m[2]].en(a, p, n, values[i]); 311 | 312 | i += 1; 313 | break; 314 | 315 | case 'c': 316 | case 'b': 317 | case 'B': 318 | case 'h': 319 | case 'H': 320 | case 'i': 321 | case 'I': 322 | case 'l': 323 | case 'L': 324 | case 'f': 325 | case 'd': 326 | format = this._formatLut[m[2]]; 327 | 328 | if (i + n > values.length) return false; 329 | 330 | this._PackSeries(n, s, a, p, values, i); 331 | 332 | i += n; 333 | break; 334 | 335 | case 'x': 336 | for (var j = 0; j < n; j++) { 337 | a[p + j] = 0; 338 | } 339 | break; 340 | } 341 | 342 | p += n * s; 343 | } 344 | 345 | return a; 346 | }; 347 | 348 | // Pack the supplied values into a new octet array, according to the fmt string 349 | exports.jspack.prototype.Pack = function (fmt, values) { 350 | return this.PackTo(fmt, new Array(this.CalcLength(fmt)), 0, values); 351 | }; 352 | 353 | // Determine the number of bytes represented by the format string 354 | exports.jspack.prototype.CalcLength = function (fmt) { 355 | var re = new RegExp(this._sPattern, 'g'); 356 | var sz = 0; 357 | 358 | while (match = re.exec(fmt)) { 359 | var n; 360 | if (match[1] == undefined || match[1] == '') n = 1; 361 | else n = parseInt(match[1]); 362 | 363 | sz += n * this._lenLut[match[2]]; 364 | } 365 | 366 | return sz; 367 | }; 368 | 369 | // Regular expression for counting digits 370 | exports.jspack.prototype._sPattern = '(\\d+)?([AxcbBhHsfdiIlL])'; 371 | 372 | // Byte widths for associated formats 373 | exports.jspack.prototype._lenLut = { 374 | 'A': 1, 375 | 'x': 1, 376 | 'c': 1, 377 | 'b': 1, 378 | 'B': 1, 379 | 'h': 2, 380 | 'H': 2, 381 | 's': 1, 382 | 'f': 4, 383 | 'd': 8, 384 | 'i': 4, 385 | 'I': 4, 386 | 'l': 4, 387 | 'L': 4 388 | }; 389 | 390 | exports.jspack.prototype._formatLut = { 391 | 'A': { 392 | en: exports.jspack.prototype._EnArray, 393 | de: exports.jspack.prototype._DeArray 394 | }, 395 | 's': { 396 | en: exports.jspack.prototype._EnString, 397 | de: exports.jspack.prototype._DeString 398 | }, 399 | 'c': { 400 | en: exports.jspack.prototype._EnChar, 401 | de: exports.jspack.prototype._DeChar 402 | }, 403 | 'b': { 404 | en: exports.jspack.prototype._EnInt, 405 | de: exports.jspack.prototype._DeInt, 406 | len: 1, 407 | signed: true, 408 | min: -Math.pow(2, 7), 409 | max: Math.pow(2, 7) - 1 410 | }, 411 | 'B': { 412 | en: exports.jspack.prototype._EnInt, 413 | de: exports.jspack.prototype._DeInt, 414 | len: 1, 415 | signed: false, 416 | min: 0, 417 | max: Math.pow(2, 8) - 1 418 | }, 419 | 'h': { 420 | en: exports.jspack.prototype._EnInt, 421 | de: exports.jspack.prototype._DeInt, 422 | len: 2, 423 | signed: true, 424 | min: -Math.pow(2, 15), 425 | max: Math.pow(2, 15) - 1 426 | }, 427 | 'H': { 428 | en: exports.jspack.prototype._EnInt, 429 | de: exports.jspack.prototype._DeInt, 430 | len: 2, 431 | signed: false, 432 | min: 0, 433 | max: Math.pow(2, 16) - 1 434 | }, 435 | 'i': { 436 | en: exports.jspack.prototype._EnInt, 437 | de: exports.jspack.prototype._DeInt, 438 | len: 4, 439 | signed: true, 440 | min: -Math.pow(2, 31), 441 | max: Math.pow(2, 31) - 1 442 | }, 443 | 'I': { 444 | en: exports.jspack.prototype._EnInt, 445 | de: exports.jspack.prototype._DeInt, 446 | len: 4, 447 | signed: false, 448 | min: 0, 449 | max: Math.pow(2, 32) - 1 450 | }, 451 | 'l': { 452 | en: exports.jspack.prototype._EnInt, 453 | de: exports.jspack.prototype._DeInt, 454 | len: 4, 455 | signed: true, 456 | min: -Math.pow(2, 31), 457 | max: Math.pow(2, 31) - 1 458 | }, 459 | 'L': { 460 | en: exports.jspack.prototype._EnInt, 461 | de: exports.jspack.prototype._DeInt, 462 | len: 4, 463 | signed: false, 464 | min: 0, 465 | max: Math.pow(2, 32) - 1 466 | }, 467 | 'f': { 468 | en: exports.jspack.prototype._En754, 469 | de: exports.jspack.prototype._De754, 470 | len: 4, 471 | bit: 23, 472 | rt: Math.pow(2, -24) - Math.pow(2, -77) 473 | }, 474 | 'd': { 475 | en: exports.jspack.prototype._En754, 476 | de: exports.jspack.prototype._De754, 477 | len: 8, 478 | bit: 52, 479 | rt: 0 480 | } 481 | }; 482 | -------------------------------------------------------------------------------- /test/publish.test.coffee: -------------------------------------------------------------------------------- 1 | should = require('should') 2 | async = require('async') 3 | _ = require('underscore') 4 | proxy = require('./proxy') 5 | uuid = require('uuid').v4 6 | 7 | AMQP = require('src/amqp') 8 | 9 | { MaxFrameSize, FrameType, HeartbeatFrame } = require('../src/lib/config').constants 10 | 11 | describe 'Publisher', () -> 12 | this.timeout(15000) 13 | 14 | it 'test we can publish a message in confirm mode', (done)-> 15 | amqp = null 16 | queue = uuid() 17 | async.series [ 18 | (next)-> 19 | amqp = new AMQP {host:'localhost'}, (e, r)-> 20 | should.not.exist e 21 | next() 22 | 23 | (next)-> 24 | amqp.publish "amq.direct", queue, "test message", {confirm:true}, (e,r)-> 25 | should.not.exist e 26 | next() 27 | 28 | ], done 29 | 30 | 31 | it 'we can publish a series of messages in confirm mode', (done)-> 32 | amqp = null 33 | queue = uuid() 34 | async.series [ 35 | (next)-> 36 | amqp = new AMQP {host:'localhost'}, (e, r)-> 37 | should.not.exist e 38 | next() 39 | 40 | (next)-> 41 | async.forEach [0...100], (i, done)-> 42 | amqp.publish "amq.direct", queue, "test message", {confirm:true}, (e,r)-> 43 | should.not.exist e 44 | done() 45 | , next 46 | 47 | ], done 48 | 49 | 50 | it 'we can agressivly publish a series of messages in confirm mode 214', (done)-> 51 | amqp = null 52 | queue = uuid() 53 | done = _.once done 54 | 55 | amqp = new AMQP {host:'localhost'}, (e, r)-> 56 | should.not.exist e 57 | 58 | amqp.queue {queue}, (e,q)-> 59 | q.declare ()-> 60 | q.bind "amq.direct", queue, ()-> 61 | i = 0 62 | j = 0 63 | while i <= 100 64 | amqp.publish "amq.direct", queue, {b:new Buffer(500)}, {deliveryMode:2, confirm:true}, (e,r)-> 65 | should.not.exist e 66 | j++ 67 | if j >=100 68 | done() 69 | 70 | i++ 71 | 72 | 73 | it 'test we can publish a message a string', (done)-> 74 | amqp = null 75 | queue = uuid() 76 | async.series [ 77 | (next)-> 78 | amqp = new AMQP {host:'localhost'}, (e, r)-> 79 | should.not.exist e 80 | next() 81 | 82 | (next)-> 83 | amqp.publish "amq.direct", queue, "test message", {}, (e,r)-> 84 | should.not.exist e 85 | next() 86 | 87 | ], done 88 | 89 | 90 | it 'test we can publish without waiting for a connection', (done)-> 91 | amqp = null 92 | queue = uuid() 93 | 94 | amqp = new AMQP {host:'localhost'}, (e, r)-> 95 | should.not.exist e 96 | 97 | amqp.publish "amq.direct", queue, "test message", {}, (e,r)-> 98 | should.not.exist e 99 | done() 100 | 101 | 102 | # it 'test we can publish a big string message', (done)-> 103 | # amqp = null 104 | # queue = uuid() 105 | # async.series [ 106 | # (next)-> 107 | # amqp = new AMQP {host:'localhost'}, (e, r)-> 108 | # should.not.exist e 109 | # next() 110 | 111 | # (next)-> 112 | # amqp.publish "amq.direct", queue, "test message #{new Buffer(10240000).toString()}", {confirm: true}, (e,r)-> 113 | # should.not.exist e 114 | # next() 115 | 116 | # ], done 117 | 118 | 119 | it 'test we can publish a JSON message', (done)-> 120 | amqp = null 121 | queue = uuid() 122 | async.series [ 123 | (next)-> 124 | amqp = new AMQP {host:'localhost'}, (e, r)-> 125 | should.not.exist e 126 | next() 127 | 128 | (next)-> 129 | amqp.publish "amq.direct", queue, {look:"im jason", jason:"nope"}, {}, (e,r)-> 130 | should.not.exist e 131 | next() 132 | 133 | ], done 134 | 135 | 136 | it 'test we can publish a string message 413', (done)-> 137 | amqp = null 138 | queueName = uuid() 139 | content = "ima string" 140 | amqp = null 141 | 142 | async.series [ 143 | (next)-> 144 | amqp = new AMQP {host:'localhost'}, (e, r)-> 145 | should.not.exist e 146 | next() 147 | 148 | (next)-> 149 | amqp.queue {queue:queueName}, (err, queue)-> 150 | queue.declare() 151 | queue.bind 'amq.direct', queueName, next 152 | 153 | (next)-> 154 | amqp.publish "amq.direct", queueName, content, {}, (e,r)-> 155 | should.not.exist e 156 | next() 157 | 158 | (next)-> 159 | amqp.consume queueName, {}, (message)-> 160 | message.data.should.eql content 161 | next() 162 | 163 | ], done 164 | 165 | 166 | 167 | it 'test we can publish a buffer message', (done)-> 168 | amqp = null 169 | queue = uuid() 170 | async.series [ 171 | (next)-> 172 | amqp = new AMQP {host:'localhost'}, (e, r)-> 173 | should.not.exist e 174 | next() 175 | 176 | (next)-> 177 | amqp.publish "amq.direct", queue, new Buffer(15), {}, (e,r)-> 178 | should.not.exist e 179 | next() 180 | 181 | ], done 182 | 183 | 184 | it 'test we can publish a buffer message that need to be multiple data packets', (done)-> 185 | amqp = null 186 | queue = uuid() 187 | packetSize = MaxFrameSize * 2.5 188 | async.series [ 189 | (next)-> 190 | amqp = new AMQP {host:'localhost'}, (e, r)-> 191 | should.not.exist e 192 | next() 193 | 194 | (next)-> 195 | amqp.publish "amq.direct", uuid(), new Buffer(packetSize), {}, (e,r)-> 196 | should.not.exist e 197 | next() 198 | 199 | ], done 200 | 201 | 202 | it 'test we can publish a message size 344', (done)-> 203 | amqp = null 204 | queue = uuid() 205 | packetSize = 344 206 | async.series [ 207 | (next)-> 208 | amqp = new AMQP {host:'localhost'}, (e, r)-> 209 | should.not.exist e 210 | next() 211 | 212 | (next)-> 213 | amqp.publish "amq.direct", uuid(), new Buffer(packetSize), {confirm: true}, (e,r)-> 214 | should.not.exist e 215 | next() 216 | 217 | ], done 218 | 219 | 220 | it 'test we can publish a lots of messages in confirm mode 553', (done)-> 221 | this.timeout(5000) 222 | amqp = null 223 | queue = uuid() 224 | packetSize = 344 225 | async.series [ 226 | (next)-> 227 | amqp = new AMQP {host:'localhost'}, (e, r)-> 228 | should.not.exist e 229 | next() 230 | 231 | (next)-> 232 | async.forEach [0...1000], (i, next)-> 233 | amqp.publish "amq.direct", "queue-#{i}", new Buffer(packetSize), {confirm: true}, (e,r)-> 234 | should.not.exist e 235 | next() 236 | , next 237 | 238 | ], done 239 | 240 | 241 | it 'test we can publish a lots of messages in confirm mode quickly 187', (done)-> 242 | this.timeout(5000) 243 | amqp = null 244 | queue = uuid() 245 | packetSize = 256837 246 | 247 | amqp = new AMQP {host:'localhost'}, (e, r)-> 248 | should.not.exist e 249 | 250 | async.forEach [0...10], (i, next)-> 251 | amqp.publish "amq.direct", "queue-#{i}", new Buffer(packetSize), {confirm: true}, (e,r)-> 252 | should.not.exist e 253 | next() 254 | , done 255 | 256 | 257 | 258 | it 'test we can publish a mandatory message to a invalid route and not crash 188', (done)-> 259 | amqp = null 260 | queue = null 261 | async.series [ 262 | (next)-> 263 | amqp = new AMQP {host:'localhost'}, (e, r)-> 264 | should.not.exist e 265 | next() 266 | 267 | (next)-> 268 | amqp.publish "amq.direct", "idontExist", new Buffer(50), {confirm:true, mandatory: true}, (e,r)-> 269 | should.exist e 270 | e.replyCode.should.eql 312 271 | next() 272 | 273 | ], done 274 | 275 | 276 | it 'test we can publish many mandatory messages to a some invalid routes 189', (done)-> 277 | amqp = null 278 | queue = null 279 | queueName = uuid() 280 | 281 | async.series [ 282 | (next)-> 283 | amqp = new AMQP {host:'localhost'}, (e, r)-> 284 | should.not.exist e 285 | next() 286 | 287 | (next)-> 288 | amqp.queue {queue:queueName}, (err, queue)-> 289 | queue.declare() 290 | queue.bind 'amq.direct', queueName, next 291 | 292 | (next)-> 293 | async.parallel [ 294 | 295 | (next)-> 296 | async.forEachSeries [0...100], (i, next)-> 297 | amqp.publish "amq.direct", "idontExist", new Buffer(50), {confirm:true, mandatory: true}, (e,r)-> 298 | should.exist e 299 | e.replyCode.should.eql 312 300 | next() 301 | ,next 302 | 303 | (next)-> 304 | async.forEachSeries [0...100], (i, next)-> 305 | amqp.publish "amq.direct", queueName, new Buffer(50), {confirm:true, mandatory: true}, (e,r)-> 306 | should.not.exist e 307 | next() 308 | ,next 309 | 310 | ], next 311 | 312 | 313 | ], done 314 | 315 | 316 | it 'test we can publish quickly to multiple queues shared options 1891', (done)-> 317 | amqp = null 318 | queue = null 319 | queueName1 = "testpublish1" 320 | queueName2 = "testpublish2" 321 | 322 | async.series [ 323 | (next)-> 324 | amqp = new AMQP {host:'localhost'}, (e, r)-> 325 | should.not.exist e 326 | next() 327 | 328 | (next)-> 329 | amqp.queue {queue:queueName1}, (err, queue)-> 330 | queue.declare() 331 | queue.bind 'amq.direct', queueName1, next 332 | 333 | (next)-> 334 | amqp.queue {queue:queueName2}, (err, queue)-> 335 | queue.declare() 336 | queue.bind 'amq.direct', queueName2, next 337 | 338 | (next)-> 339 | options = {confirm:true, mandatory: false} 340 | async.forEach [0...10], (i, next)-> 341 | amqp.publish "amq.direct", ["testpublish",((i%2)+1)].join(''), new Buffer(50), options, (e,r)-> 342 | should.not.exist e 343 | next() 344 | ,next 345 | 346 | (next)-> 347 | 348 | q1count = 0 349 | q2count = 0 350 | messageProcessor = (message)-> 351 | if message.routingKey == queueName1 then q1count++ 352 | if message.routingKey == queueName2 then q2count++ 353 | 354 | if q1count == 5 and q2count == 5 then next() 355 | 356 | consumer = amqp.consume queueName1, {}, messageProcessor, (e,r)-> 357 | should.not.exist e 358 | consumer2 = amqp.consume queueName2, {}, messageProcessor, (e,r)-> 359 | should.not.exist e 360 | 361 | 362 | ], (e, res)-> 363 | amqp.close() 364 | done() 365 | 366 | 367 | 368 | it 'test when be publishing and an out of order op happens we recover', (done)-> 369 | this.timeout(10000) 370 | amqp = null 371 | 372 | testData = {test:"message"} 373 | amqp = null 374 | queue = uuid() 375 | messagesRecieved = 0 376 | consumer = null 377 | q= null 378 | messageProcessor = (m)-> 379 | m.data.should.eql testData 380 | messagesRecieved++ 381 | 382 | if messagesRecieved is 3 383 | q.connection.crashOOO() 384 | 385 | if messagesRecieved is 55 386 | done() 387 | 388 | m.ack() 389 | 390 | async.series [ 391 | (next)-> 392 | amqp = new AMQP {host:'localhost'}, (e, r)-> 393 | should.not.exist e 394 | next() 395 | 396 | (next)-> 397 | q = amqp.queue {queue, autoDelete:false}, (e,q)-> 398 | q.declare ()-> 399 | q.bind "amq.direct", queue, next 400 | 401 | (next)-> 402 | async.forEach [0...5], (i, done)-> 403 | amqp.publish "amq.direct", queue, testData, {confirm: true}, (err, res)-> 404 | # console.error "#{i}", err, res 405 | if !err? then return done() else 406 | setTimeout ()-> 407 | amqp.publish "amq.direct", queue, testData, {confirm: true}, (err, res)-> 408 | # console.error "*#{i}", err, res 409 | done(err,res) 410 | , 200 411 | , next 412 | 413 | (next)-> 414 | consumer = amqp.consume queue, {prefetchCount: 1}, messageProcessor, (e,r)-> 415 | should.not.exist e 416 | next() 417 | 418 | (next)-> 419 | async.forEach [0...50], (i, done)-> 420 | amqp.publish "amq.direct", queue, testData, {confirm: true}, (err, res)-> 421 | # console.error "#{i}", err, res 422 | if !err? then return done() else 423 | setTimeout ()-> 424 | amqp.publish "amq.direct", queue, testData, {confirm: true}, (err, res)-> 425 | # console.error "*#{i}", err, res 426 | done(err,res) 427 | , 200 428 | , next 429 | 430 | ], (err,res)-> 431 | # console.error "DONE AT THE END HERE", err, res 432 | should.not.exist err 433 | 434 | 435 | it 'test when an out of order op happens while publishing large messages we recover 915', (done)-> 436 | this.timeout(10000) 437 | amqp = null 438 | 439 | testData = {test:"message", size: new Buffer(1000)} 440 | amqp = null 441 | queue = uuid() 442 | messagesRecieved = 0 443 | consumer = null 444 | q= null 445 | 446 | messageProcessor = (m)-> 447 | # m.data.should.eql testData 448 | messagesRecieved++ 449 | 450 | if messagesRecieved is 100 451 | q.connection.crashOOO() 452 | 453 | if messagesRecieved is 500 454 | done() 455 | 456 | m.ack() 457 | 458 | async.series [ 459 | (next)-> 460 | amqp = new AMQP {host:'localhost'}, (e, r)-> 461 | should.not.exist e 462 | next() 463 | 464 | (next)-> 465 | q = amqp.queue {queue, autoDelete:false}, (e,q)-> 466 | q.declare ()-> 467 | q.bind "amq.direct", queue, next 468 | 469 | (next)-> 470 | consumer = amqp.consume queue, {prefetchCount: 1}, messageProcessor, (e,r)-> 471 | should.not.exist e 472 | next() 473 | 474 | (next)-> 475 | async.forEach [0...500], (i, done)-> 476 | amqp.publish "amq.direct", queue, testData, {confirm: true}, (err, res)-> 477 | if !err? then return done() else 478 | setTimeout ()-> 479 | amqp.publish "amq.direct", queue, testData, {confirm: true}, (err, res)-> 480 | done(err,res) 481 | , 200 482 | , next 483 | 484 | ], (err,res)-> 485 | # console.error "DONE AT THE END HERE", err, res 486 | should.not.exist err 487 | 488 | 489 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | amqp-coffee 2 | =========== 3 | 4 | [![Build Status](https://travis-ci.org/dropbox/amqp-coffee.svg?branch=master)](https://travis-ci.org/dropbox/amqp-coffee) Node.JS AMQP 0.9.1 Client 5 | 6 | ## Sample 7 | 8 | ```coffeescript 9 | AMQP = require('amqp-coffee') # path to this 10 | 11 | testData = "the data to be published.. I am a string but could be anything" 12 | 13 | amqpConnection = new AMQP {host:'localhost'}, (e, r)-> 14 | if e? 15 | console.error "Error", e 16 | 17 | amqpConnection.queue {queue: "queueName"}, (e,q)-> 18 | q.declare ()-> 19 | q.bind "amq.direct", "queueName", ()-> 20 | 21 | amqpConnection.publish "amq.direct", "queueName", testData, {confirm: true}, (err, res)-> 22 | console.log "Message published" 23 | 24 | consumer = amqpConnection.consume "queueName", {prefetchCount: 2}, (message)-> 25 | console.log message.data.toString() 26 | message.ack() 27 | 28 | , (e,r)-> 29 | console.log "Consumer setup" 30 | amqpConnection.publish "amqp.direct", "queueName", "message contents", {deliveryMode:2, confirm:true}, (e, r)-> 31 | if !e? then console.log "Message Sent" 32 | ``` 33 | 34 | ## Methods 35 | 36 | * Class: amqp-coffee 37 | * [new amqp-coffee([connectionOptions],[callback])](#new-amqp-coffeeconnectionoptionscallback) 38 | * [connection.queue([queueOptions],[callback])](#connectionqueuequeueoptionscallback) 39 | * [queue.declare([queueOptions],[callback])](#queuedeclarequeueoptionscallback) 40 | * [queue.delete([queueDeleteOptions],[callback])](#queuedeletequeuedeleteoptionscallback) 41 | * [queue.bind(exchange, routingkey, [queueName], [callback])](#queuebindexchange-routingkey-queuename-callback) 42 | * [queue.unbind(exchange, routingKey, [queueName], [callback])](#queueunbindexchange-routingkey-queuename-callback) 43 | * [queue.messageCount(queueOptions, callback)](#queuemessagecountqueueoptions-callback) 44 | * [queue.consumerCount(queueOptions, callback)](#queuconsumercountqueueoptions-callback) 45 | * [connection.exchange([exchangeArgs],[callback])](#connectionexchangeexchangeargscallback) 46 | * [exchange.declare([exchangeArgs],[callback])](#exchangedeclareexchangeargscallback) 47 | * [exchange.delete([exchangeDeleteOptions], [callback])](#exchangedeleteexchangedeleteoptions-callback) 48 | * [exchange.bind(destinationExchange, routingKey, [sourceExchange], [callback])](#exchangedeleteexchangedeleteoptions-callback) 49 | * [exchange.unbind(destinationExchange, routingKey, [sourceExchange], [callback])](#exchangedeleteexchangedeleteoptions-callback) 50 | * [connection.publish(exchange, routingKey, data, [publishOptions], [callback])](#connectionpublishexchange-routingkey-data-publishoptions-callback) 51 | * [connection.consume(queueName, options, messageListener, [callback])](#connectionconsumequeuename-options-messagelistener-callback) 52 | * [consumer.setQos(prefetchCount, [callback])](#consumersetqosprefetchcount-callback) 53 | * [consumer.cancel([callback])](#consumercancelcallback) 54 | * [consumer.resume([callback])](#consumerresumecallback) 55 | * [consumer.pause([callback])](#consumerpausecallback) 56 | * [consumer.close([callback])](#consumerclosecallback) 57 | * [connection.close()](#connectionclose) 58 | 59 | 60 | ## new amqp-coffee([connectionOptions],[callback]) 61 | Creates a new amqp Connection. The connection is returned directly and in the callback. The connection extends EventEmitter. 62 | 63 | The callback is called if there is a sucessful connection OR a unsucessful connection and connectionOptions.reconnect is false. If connectionOptions.reconnect is false, you will get a error back in the callback. If no callback is specified it will be emitted. 64 | 65 | The `connectionOptions` argument should be an object which specifies: 66 | * `host`: a string of the hostname OR an array of hostname strings OR an array of hostname objects {host, port} 67 | * `port`: a integer of the port to connect to. Not used if host is an object. 68 | * `login`: "guest" 69 | * `password`: "guest" 70 | * `vhost`: '/' 71 | * `port`: 5672 72 | * `heartbeat`: 10000 # in ms 73 | * `reconnect`: true 74 | * `reconnectDelayTime`: 1000 # in ms 75 | * `hostRandom`: false 76 | * `connectTimeout: 30000 # in ms, this is only used if reconnect is false 77 | * `clientProperties` : {version: clientVersion, platform, product} 78 | * `ssl`: false 79 | * `sslOptions` : {} # tls options like cert, key, ca, secureProtocol, passphrase 80 | * `temporaryChannelTimeout`: 2000 # in ms, temporary channels are used to setup queues, bindings, and exchanges. If you are frequently tearing down and setting up new queues it could make sense to make this longer. 81 | * `noDelay`: true # disable Nagle's algorithm by default 82 | 83 | Host Examples 84 | ```coffeescript 85 | host: 'localhost' 86 | host: {host: 'localhost', port: 15672} 87 | host: ['localhost','yourhost'] 88 | host: [{host: 'localhost', port:15672}, {host: 'localhost', port:15673}] 89 | ``` 90 | Sample Connection 91 | ```coffeescript 92 | amqp-coffee = require('amqp-coffee') 93 | 94 | amqp = new amqp-coffee {host: 'localhost'}, (error, amqpConnection)-> 95 | assert(amqp == amqpConnection) 96 | ``` 97 | 98 | #### Reconnect Flow 99 | On a connection close, we start the reconnect process if `reconnect` is true. 100 | After the `reconnectDelayTime` the hosts are rotated if more than one `host` is specified. 101 | A new connection is atempted, if the connection is not sucessful this process repeats. 102 | After a connection is re-establed, all of the channels are reset, this atempts to reopen that channel. Different channel types re-establish there channels differently. 103 | * Publisher channels, will only reconnect when a publish is atempted. 104 | * Consumer channels will reconnect and resume consuming. If it was a autoDelete queue, this could fail. Make sure you listen to the ready even on the connection to re-set up and consume any autoDelete queues. 105 | * Queue / Exchange channels are recreated on demand. 106 | 107 | ### Event: 'ready' 108 | Emitted when the connection is open successfully. This will be called after each successful reconnect. 109 | 110 | ### Event: 'close' 111 | Emitted when a open connection leaves the ready state and is closed. 112 | 113 | ### Event: 'error' 114 | Very rare, only emitted when there's a server version mismatch 115 | 116 | 117 | ### connection.queue([queueOptions],[callback]) 118 | 119 | This returns a channel that can be used to declare, bind, unbind, or delete queus. This on its own does NOT declare a queue. 120 | When creating a queue class using connection.queue, you can specify options that will be used in all the child methods. 121 | 122 | The `queueOptions` argument should be an object which specifies: 123 | * `queue`: a string repensenting the queue name, can also be empty to use a autogenerated queue name 124 | * `autoDelete`: default: true 125 | * `noWait`: default: false 126 | * `exclusive`: default: false. The queue can only be used by the current connection. 127 | * `durable`: default: false 128 | * `passive`: default: false. The queue creation will not fail if the queue already exists. 129 | * `arguments`: default: {}. Pass queue configuartion arguments, e.g. `'x-dead-letter-exchange'`. 130 | 131 | Both queues and exchanges use "temporary" channels, which are channels amqp-coffee manages specifically for declaring, binding, unbinding, and deleting queues and exchanges. After 2 seconds of inactivity these channels are closed, and reopened on demand. 132 | 133 | #### queue.declare([queueOptions],[callback]) 134 | 135 | Will take a new set of queueOptions, or use the default. 136 | Issues a queueDeclare and waits on queueDeclareOk if a callback is specified. 137 | 138 | ```coffeescript 139 | amqp = new AMQP, ()-> 140 | amqp.queue({queue:'queueToCreate'}, (err, Queue)-> 141 | Queue.declare (err, res)-> 142 | # the queue is now declared 143 | ``` 144 | 145 | To use a auto-generated queue name 146 | 147 | ```coffeescript 148 | amqp = new AMQP, ()-> 149 | amqp.queue({queue:''}, (err, Queue)-> 150 | Queue.declare (err, res)-> 151 | queueName = res.queue 152 | ``` 153 | 154 | #### queue.delete([queueDeleteOptions],[callback]) 155 | 156 | The `queueDeleteOptions` argument should be an object which specifies: 157 | * `queue`: name of the queue 158 | * `ifUnused`: default: false 159 | * `ifEmpty`: default: true 160 | * `noWait`: default: false 161 | 162 | #### queue.bind(exchange, routingkey, [queueName], [callback]) 163 | Sets up bindings from an already existing exchange to an already existing queue 164 | 165 | #### queue.unbind(exchange, routingKey, [queueName], [callback]) 166 | Tears down an already existing binding 167 | 168 | #### queue.messageCount(queueOptions, callback) 169 | Rabbitmq specific, re-declares the queue and returns the messageCount from the response 170 | 171 | #### queue.consumerCount(queueOptions, callback) 172 | Rabbitmq specific, re-declares the queue and returns the consumerCount from the response 173 | 174 | ### connection.exchange([exchangeArgs],[callback]) 175 | This returns a channel that can be used to declare, bind, unbind, or delete exchanges. This on its own does NOT declare a exchange. 176 | When creating an exchange class using connection.exchange, you can specify options that will be used in all the child methods. 177 | 178 | The `exchangeArgs` argument should be an object which specifies: 179 | * `exchange`: a string representing the exchange name 180 | * `type`: "direct" 181 | * `passive`: false 182 | * `durable`: false 183 | * `noWait`: false 184 | * `autoDelete`: true 185 | * `internal`: false 186 | 187 | Both queues and exchanges use "temporary" channels, which are channels amqp-coffee manages specifically for declaring, binding, unbinding, and deleting queues and exchanges. After 2 seconds of inactivity these channels are closed, and reopened on demand. 188 | 189 | #### exchange.declare([exchangeArgs],[callback]) 190 | #### exchange.delete([exchangeDeleteOptions], [callback]) 191 | 192 | The `exchangeDeleteOptions` argument should be an object which specifies: 193 | * `exchange`: the name of the exchange 194 | * `ifUnused`: false 195 | * `noWait`: false 196 | 197 | #### exchange.bind(destinationExchange, routingKey, [sourceExchange], [callback]) 198 | Rabbitmq Extension, to bind between exchanges, `sourceExchange` if omitted will be defaulted to the exchange it's being called on. 199 | 200 | #### exchange.unbind(exchange.unbind(destinationExchange, routingKey, [sourceExchange], [callback]) 201 | Rabbitmq Extension, to bind between exchanges, `sourceExchange` if omitted will be defaulted to the exchange it's being called on. 202 | 203 | ### connection.publish(exchange, routingKey, data, [publishOptions], [callback]) 204 | 205 | amqp-coffee manages publisher channels and sets them up on the first publish. Confirming is a state a channel must be put in, so a channel is needed for confimed publishes and one for non confimed publishes. They are only created on demand. So you should have a maximum of 2 channels publishing for a single connection. 206 | 207 | New in 0.1.20 if you set the mandatory or immediate flag with the confirm flag we add a tracking header on that message headers.x-seq which is a numeric representation of that message just like the sequence number. That flag is used to re-connect a messages that has failed publishing and come back as a "basicReturn" to a already existing callback. This allows you to publish to a queue that may not exist and get a bounce if it doesnt. Or if a queue is in a bad state the message will fail routing and come back. 208 | 209 | * `exchange`: string of the exchange to publish to 210 | * `routingKey`: string to use to route the message 211 | * `data`: any type of data, if it is an object it will be converted into json automatically and unconverted on consume. Strings are converted into buffers. 212 | * `publishOptions`: All parameters are passed through as arguments to the publisher. 213 | * `confirm`: false 214 | * `mandatory`: false 215 | * `immediate`: false 216 | * `contentType`: 'application/octet-stream' 217 | 218 | 219 | 220 | ### connection.consume(queueName, options, messageListener, [callback]) 221 | 222 | consumers use their own channels and are re-subscribed to on reconnect. Returns a consumer object. 223 | 224 | * `queueName`: string of the queue to subscribe to 225 | * `options`: 226 | * `noLocal`: false 227 | * `noAck`: true 228 | * `exclusive`: false 229 | * `noWait`: false 230 | * `prefetchCount` : integer. If specified the consumer will enter qos mode and you will have to ack messages. If specified `noAck` will be set to false 231 | * `consumerTag`: optional string. If not specified one will be generated for you. 232 | * `messageListener`: a function (message) 233 | * `callback`: a function that is called once the consume is setup 234 | 235 | messageListener is a function that gets a message object which has the following attributes: 236 | * `data`: a getter that returns the data in its parsed form, eg a parsed json object, a string, or the raw buffer 237 | * `raw`: the raw buffer that was returned 238 | * `properties`: headers specified for the message 239 | * `size`: message body size 240 | * `ack()`: function : only used when prefetchCount is specified 241 | * `reject()`: function: only used when prefetchCount is specified 242 | * `retry()`: function: only used when prefetchCount is specified 243 | 244 | ```coffeescript 245 | listener = (message)-> 246 | # we will only get 1 message at a time because prefetchCount is set to 1 247 | console.log "Message Data", message.data 248 | message.ack() 249 | 250 | amqp = new AMQP ()-> 251 | amqp.queue {queue: 'testing'}, (e, queue)-> 252 | queue.declare ()-> 253 | queue.bind 'amq.direct', 'testing', ()-> 254 | amqp.publish 'amq.direct', 'testing', 'here is one message 1' 255 | amqp.publish 'amq.direct', 'testing', 'here is one message 2' 256 | 257 | amqp.consume 'testing', {prefetchCount: 1}, listener, ()-> 258 | console.log "Consumer Ready" 259 | ``` 260 | #### consumer Event: error 261 | Errors will be emitted from the consumer if we can not consumer from that queue anymore. For example if you're consuming a autoDelete queue and you reconnect that queue will be gone. It will return the raw error message with code as the message. 262 | 263 | #### consumer Event: cancel 264 | The cancel event will be emitted from the consumer if we receive a server initiated "basic.cancel". For this to happen you must 265 | let the server know you are expecting a cancel, you do this by specifying clientProperties on connect. `clientProperties: { capabilities: { consumer_cancel_notify: true }}` https://www.rabbitmq.com/consumer-cancel.html 266 | 267 | #### consumer.setQos(prefetchCount, [callback]) 268 | 269 | Will update the prefetch count of an already existing consumer; can be used to dynamically tune a consumer. 270 | 271 | #### consumer.cancel([callback]) 272 | Sends basicCancel and waits on basicCancelOk 273 | 274 | #### consumer.pause([callback]) 275 | consumer.cancel 276 | 277 | #### consumer.close([callback]) 278 | Calls consumer.cancel, if we're currently consuming. Then calls channel.close and calls the callback as soon as the channel close is sent, NOT when channelCloseOk is returned. 279 | 280 | #### consumer.resume([callback]) 281 | consumer.consume, sets up the consumer with a new consumer tag 282 | 283 | #### consumer.flow(active, [callback]) 284 | An alias for consumer.pause (active == false) and consome.resume (active == true) 285 | 286 | ### connection.close() 287 | 288 | 289 | More documentation to come. The tests are a good place to reference. 290 | 291 | ## Differences between amqp-coffee and node-amqp 292 | 293 | First of all this was heavily inspired by https://github.com/postwait/node-amqp 294 | 295 | Changes from node-amqp 296 | - the ability to share channels intelligently. ( if you are declaring multiple queues and exchanges there is no need to use multiple channels ) 297 | - auto channel closing for transient channels ( channels used for declaring and binding if they are inactive ) 298 | - consumer reconnecting 299 | - fixed out-of-order channel operations by ensuring things are writing in order and not overwriting buffers that may not have been pushed to the network. 300 | - switch away from event emitters for consumer acks 301 | - everything that can be async is async 302 | - native bson support for messages with contentType application/bson 303 | - ability to delete, bind, and unbind a queue without having to know everything about the queue like auto delete etc... 304 | - can get the message and consumer count of a queue 305 | - can turn flow control on and off for a consumer (pause, resume) receiving messages. 306 | - rabbitmq master queue connection preference. When you connect to an array of hosts that have queues that are highly available (HA) it can talk to the rabbit api and make sure it talks to the master node for that queue. You can get way better performance with consumer acks. 307 | -------------------------------------------------------------------------------- /src/lib/Connection.coffee: -------------------------------------------------------------------------------- 1 | debug = require('./config').debug('amqp:Connection') 2 | 3 | {EventEmitter} = require('events') 4 | net = require('net') 5 | tls = require('tls') 6 | _ = require('underscore') 7 | async = require('async') 8 | 9 | defaults = require('./defaults') 10 | { methodTable, classes, methods } = require('./config').protocol 11 | { FrameType, HeartbeatFrame, EndFrame } = require('./config').constants 12 | { serializeInt, serializeFields } = require('./serializationHelpers') 13 | 14 | Queue = require('./Queue') 15 | Exchange = require('./Exchange') 16 | AMQPParser = require('./AMQPParser') 17 | ChannelManager = require('./ChannelManager') 18 | 19 | 20 | if process.env.AMQP_TEST? 21 | defaults.connection.reconnectDelayTime = 100 22 | 23 | 24 | class Connection extends EventEmitter 25 | 26 | ### 27 | 28 | host: localhost | [localhost, localhost] | [{host: localhost, port: 5672}, {host: localhost, port: 5673}] 29 | port: int 30 | vhost: %2F 31 | hostRandom: default false, if host is an array 32 | 33 | @state : opening | open | closed | reconnecting | destroyed 34 | 35 | ### 36 | constructor: (args, cb)-> 37 | @id = Math.round(Math.random() * 1000) 38 | 39 | if typeof args is 'function' 40 | cb = args 41 | args = {} 42 | 43 | # this is the main connect event 44 | cb = _.once cb if cb? 45 | @cb = cb 46 | 47 | @state = 'opening' 48 | 49 | @connectionOptions = _.defaults args, defaults.connection 50 | 51 | # setup our defaults 52 | @channelCount = 0 53 | 54 | @channels = {0:this} 55 | @queues = {} 56 | @exchanges = {} 57 | 58 | # connection tuning paramaters 59 | @channelMax = @connectionOptions.channelMax 60 | @frameMax = @connectionOptions.frameMax 61 | 62 | @sendBuffer = new Buffer(@frameMax) 63 | 64 | @channelManager = new ChannelManager(@) 65 | 66 | async.series [ 67 | 68 | (next)=> 69 | # determine to host to connect to if we have an array of hosts 70 | @connectionOptions.hosts = _.flatten([@connectionOptions.host]).map (uri)=> 71 | if uri.port? and uri.host? 72 | return {host: uri.host.toLowerCase(), port: parseInt(uri.port)} 73 | 74 | # our host name has a : and theat implies a uri with host and port 75 | else if typeof(uri) is 'string' and uri.indexOf(":") isnt -1 76 | [host, port] = uri.split(":") 77 | return {host: host.toLowerCase(), port: parseInt(port)} 78 | 79 | else if typeof(uri) is 'string' 80 | return {host: uri.toLowerCase(), port: if !@connectionOptions.ssl then @connectionOptions.port else @connectionOptions.sslPort} 81 | 82 | else 83 | throw new Error("we dont know what do do with the host #{uri}") 84 | 85 | @connectionOptions.hosti = 0 86 | 87 | if @connectionOptions.hostRandom 88 | @connectionOptions.hosti = Math.floor(Math.random() * @connectionOptions.hosts.length) 89 | 90 | @updateConnectionOptionsHostInformation() 91 | next() 92 | 93 | 94 | (next)=> 95 | if @connectionOptions.rabbitMasterNode?.queue? 96 | require('./plugins/rabbit').masterNode @, @connectionOptions.rabbitMasterNode.queue, next 97 | else 98 | next() 99 | 100 | (next)=> 101 | 102 | setupConnectionListeners = ()=> 103 | if @connectionOptions.ssl 104 | connectionEvent = 'secureConnect' 105 | else 106 | connectionEvent = 'connect' 107 | 108 | @connection.once connectionEvent, ()=> @_connectedFirst() 109 | @connection.on connectionEvent, ()=> @_connected() 110 | @connection.on 'error', @_connectionErrorEvent 111 | @connection.on 'close', @_connectionClosedEvent 112 | 113 | if @connectionOptions.ssl 114 | tlsOptions = @connectionOptions.sslOptions ? {} 115 | 116 | setupTlsConnection = ()=> 117 | if @connection? 118 | @connection.removeAllListeners() 119 | 120 | if @connection?.socket? 121 | @connection.socket.end() 122 | 123 | @connection = tls.connect @connectionOptions.port, @connectionOptions.host, tlsOptions, ()=> 124 | @connection.on 'error', ()=> 125 | @connection.emit 'close' 126 | 127 | @connection.connect = setupTlsConnection 128 | setupConnectionListeners() 129 | 130 | setupTlsConnection() 131 | 132 | else 133 | @connection = net.connect @connectionOptions.port, @connectionOptions.host 134 | setupConnectionListeners() 135 | 136 | if @connectionOptions.noDelay 137 | @connection.setNoDelay() 138 | 139 | # start listening for timeouts 140 | 141 | if @connectionOptions.connectTimeout? and !@connectionOptions.reconnect 142 | clearTimeout(@_connectTimeout) 143 | 144 | @_connectTimeout = setTimeout ()=> 145 | debug 1, ()-> return "Connection timeout triggered" 146 | @close() 147 | cb?({code:'T', message:'Connection Timeout', host:@connectionOptions.host, port:@connectionOptions.port}) 148 | , @connectionOptions.connectTimeout 149 | 150 | next() 151 | ], (e, r)-> 152 | if e? and cb? 153 | cb(e) 154 | 155 | if cb? then @once 'ready', cb 156 | @on 'close', @_closed 157 | 158 | super() 159 | return @ 160 | 161 | updateConnectionOptionsHostInformation: ()=> 162 | @connectionOptions.host = @connectionOptions.hosts[@connectionOptions.hosti].host 163 | @connectionOptions.port = @connectionOptions.hosts[@connectionOptions.hosti].port 164 | 165 | # User called functions 166 | queue: (args, cb)-> 167 | if !cb? or typeof(cb) isnt 'function' 168 | return new Queue( @channelManager.temporaryChannel() , args) 169 | 170 | else 171 | @channelManager.temporaryChannel (err, channel)-> 172 | if err? then return cb err 173 | q = new Queue(channel, args, cb) 174 | 175 | 176 | exchange: (args, cb)-> 177 | if !cb? or typeof(cb) isnt 'function' 178 | return new Exchange(@channelManager.temporaryChannel(), args) 179 | 180 | else 181 | @channelManager.temporaryChannel (err, channel)-> 182 | if err? then return cb err 183 | e = new Exchange(channel, args, cb) 184 | 185 | consume: (queueName, options, messageParser, cb)-> 186 | @channelManager.consumerChannel (err, channel)=> 187 | consumerChannel = @channels[channel] if !err? 188 | 189 | if err? or !consumerChannel? then return cb({err, channel}) 190 | consumerChannel.consume(queueName, options, messageParser, cb) 191 | 192 | # channel is optional! 193 | publish: (exchange, routingKey, data, options, cb)=> 194 | if cb? and options.confirm # there is no point to confirm without a callback 195 | confirm = true 196 | else 197 | confirm = false 198 | 199 | @channelManager.publisherChannel confirm, (err, channel)=> 200 | publishChannel = @channels[channel] if !err? 201 | #TODO figure out error messages 202 | if err? or !publishChannel? then return cb({err, channel}) 203 | 204 | publishChannel.publish exchange, routingKey, data, options, cb 205 | 206 | 207 | close: (cb)=> 208 | # should close all the things and reset for a new clean guy 209 | # @connection.removeAllListeners() TODO evaluate this 210 | @_clearHeartbeatTimer() 211 | 212 | _.defer ()=> 213 | @state = 'destroyed' 214 | 215 | if cb? then cb = _.once cb 216 | 217 | # only atempt to cleanly close the connection if our current connection is writable 218 | if @connection.writable 219 | @_sendMethod 0, methods.connectionClose, {classId:0, methodId: 0, replyCode:200, replyText:'closed'} 220 | else 221 | return cb?() 222 | 223 | state = {write: @connection.writable, read: @connection.readable} 224 | 225 | forceConnectionClose = setTimeout ()=> 226 | @connection.destroy() 227 | cb?() 228 | , 1000 229 | 230 | @connection.once 'close', ()=> 231 | clearTimeout forceConnectionClose 232 | cb?() 233 | 234 | 235 | # TESTING OUT OF ORDER OPERATION 236 | crashOOO: ()=> 237 | if !process.env.AMQP_TEST? then return true 238 | # this will crash a channel forcing an out of order operation 239 | debug "Trying to crash connection by an oow op" 240 | @_sendBody @channel, new Buffer(100), {} 241 | 242 | # Service Called Functions 243 | _connectedFirst: ()=> 244 | debug 1, ()=> return "Connected to #{@connectionOptions.host}:#{@connectionOptions.port}" 245 | 246 | _connected: ()-> 247 | clearTimeout(@_connectTimeout) 248 | @_resetAllHeartbeatTimers() 249 | @_setupParser(@_reestablishChannels) 250 | 251 | _connectionErrorEvent: (e, r)=> 252 | if @state isnt 'destroyed' 253 | debug 1, ()=> return ["Connection Error ", e, r, @connectionOptions.host] 254 | 255 | # if we are to keep trying we wont callback until we're successful, or we've hit a timeout. 256 | if !@connectionOptions.reconnect 257 | if @cb? 258 | @cb(e,r) 259 | else 260 | @emit 'error', e 261 | 262 | _connectionClosedEvent: (had_error)=> 263 | # go through all of our channels and close them 264 | for channelNumber, channel of @channels 265 | channel._connectionClosed?() 266 | 267 | clearTimeout(@_connectTimeout) 268 | @emit 'close' if @state is "open" 269 | 270 | if @state isnt 'destroyed' 271 | if !@connectionOptions.reconnect 272 | debug 1, ()-> return "Connection closed not reconnecting..." 273 | return 274 | 275 | @state = 'reconnecting' 276 | debug 1, ()-> return "Connection closed reconnecting..." 277 | 278 | _.delay ()=> 279 | # rotate hosts if we have multiple hosts 280 | if @connectionOptions.hosts.length > 1 281 | @connectionOptions.hosti = (@connectionOptions.hosti + 1) % @connectionOptions.hosts.length 282 | @updateConnectionOptionsHostInformation() 283 | 284 | @connection.connect @connectionOptions.port, @connectionOptions.host 285 | , @connectionOptions.reconnectDelayTime 286 | 287 | 288 | _reestablishChannels: ()=> 289 | async.forEachSeries _.keys(@channels), (channel, done)=> 290 | if channel is "0" then done() else 291 | # check to make sure the channel is still around before attempting to reset it 292 | # the channel could have been temporary 293 | if @channelManager.isChannelClosed(channel) then done() else 294 | @channels[channel].reset?(done) 295 | 296 | _closed: ()=> 297 | @_clearHeartbeatTimer() 298 | 299 | # we should expect a heartbeat at least once every heartbeat interval x 2 300 | # we should reset this timer every time we get a heartbeat 301 | 302 | # on initial connection we should start expecting heart beats 303 | # on disconnect or close we should stop expecting these. 304 | # on heartbeat received we should expect another 305 | _receivedHeartbeat: ()=> 306 | debug 4, ()=> return "♥ heartbeat" 307 | @_resetHeartbeatTimer() 308 | 309 | _resetAllHeartbeatTimers: ()=> 310 | @_resetSendHeartbeatTimer() 311 | @_resetHeartbeatTimer() 312 | 313 | _resetHeartbeatTimer: ()=> 314 | debug 6, ()=> return "_resetHeartbeatTimer" 315 | clearInterval @heartbeatTimer 316 | @heartbeatTimer = setInterval @_missedHeartbeat, @connectionOptions.heartbeat * 2 317 | 318 | _clearHeartbeatTimer: ()=> 319 | debug 6, ()=> return "_clearHeartbeatTimer" 320 | clearInterval @heartbeatTimer 321 | clearInterval @sendHeartbeatTimer 322 | @heartbeatTimer = null 323 | @sendHeartbeatTimer = null 324 | 325 | _resetSendHeartbeatTimer: ()=> 326 | debug 6, ()=> return "_resetSendHeartbeatTimer" 327 | clearInterval @sendHeartbeatTimer 328 | @sendHeartbeatTimer = setInterval(@_sendHeartbeat, @connectionOptions.heartbeat) 329 | 330 | _sendHeartbeat: ()=> 331 | @connection.write HeartbeatFrame 332 | 333 | # called directly in tests to simulate missed heartbeat 334 | _missedHeartbeat: ()=> 335 | if @state is 'open' 336 | debug 1, ()-> return "We missed a heartbeat, destroying the connection." 337 | @connection.destroy() 338 | 339 | @_clearHeartbeatTimer() 340 | 341 | _setupParser: (cb)-> 342 | if @parser? then @parser.removeAllListeners() 343 | 344 | # setup the parser 345 | @parser = new AMQPParser('0-9-1', 'client', @) 346 | 347 | @parser.on 'method', @_onMethod 348 | @parser.on 'contentHeader', @_onContentHeader 349 | @parser.on 'content', @_onContent 350 | @parser.on 'heartbeat', @_receivedHeartbeat 351 | 352 | # network --> parser 353 | # send any connection data events to our parser 354 | @connection.removeAllListeners('data') # cleanup reconnections 355 | @connection.on 'data', (data)=> @parser.execute data 356 | 357 | if cb? 358 | @removeListener('ready', cb) 359 | @once 'ready', cb 360 | 361 | _sendMethod: (channel, method, args)=> 362 | if channel isnt 0 and @state in ['opening', 'reconnecting'] 363 | return @once 'ready', ()=> 364 | @_sendMethod(channel, method, args) 365 | 366 | 367 | debug 3, ()-> return "#{channel} < #{method.name}"# #{util.inspect args}" 368 | b = @sendBuffer 369 | 370 | b.used = 0 371 | 372 | b[b.used++] = 1 # constants. FrameType.METHOD 373 | serializeInt(b, 2, channel) 374 | 375 | # will replace with actuall length later 376 | lengthIndex = b.used 377 | serializeInt(b, 4, 0) 378 | startIndex = b.used 379 | 380 | serializeInt(b, 2, method.classIndex); # short, classId 381 | serializeInt(b, 2, method.methodIndex); # short, methodId 382 | 383 | serializeFields(b, method.fields, args, true); 384 | 385 | endIndex = b.used 386 | 387 | # write in the frame length now that we know it. 388 | b.used = lengthIndex 389 | serializeInt(b, 4, endIndex - startIndex); 390 | b.used = endIndex 391 | 392 | b[b.used++] = 206; # constants Indicators.frameEnd; 393 | 394 | # we create this new buffer to make sure it doesn't get overwritten in a situation where we're backed up flushing to the network 395 | methodBuffer = new Buffer(b.used) 396 | b.copy(methodBuffer,0 ,0 ,b.used) 397 | @connection.write(methodBuffer) 398 | @_resetSendHeartbeatTimer() 399 | 400 | # Only used in sendBody 401 | _sendHeader: (channel, size, args)=> 402 | debug 3, ()=> return "#{@id} #{channel} < header #{size}"# #{util.inspect args}" 403 | b = @sendBuffer 404 | 405 | classInfo = classes[60] 406 | 407 | b.used = 0 408 | b[b.used++] = 2 # constants. FrameType.HEADER 409 | 410 | serializeInt(b, 2, channel) 411 | 412 | lengthStart = b.used 413 | serializeInt(b, 4, 0) # temporary length 414 | bodyStart = b.used 415 | 416 | serializeInt(b, 2, classInfo.index) # class 60 for Basic 417 | serializeInt(b, 2, 0) # weight, always 0 for rabbitmq 418 | serializeInt(b, 8, size) # byte size of body 419 | 420 | #properties - first propertyFlags 421 | propertyFlags = [0] 422 | propertyFields = [] 423 | 424 | ### 425 | The property flags are an array of bits that indicate the presence or absence of each 426 | property value in sequence. The bits are ordered from most high to low - bit 15 indicates 427 | the first property. 428 | 429 | The property flags can specify more than 16 properties. If the last bit (0) is set, this indicates that a 430 | further property flags field follows. There are many property flags fields as needed. 431 | ### 432 | for field, i in classInfo.fields 433 | if (i + 1) % 16 is 0 434 | # we have more than 15 properties, set bit 0 to 1 of the previous bit set 435 | propertyFlags[Math.floor((i-1)/15)] |= 1 << 0 436 | propertyFlags.push 0 437 | 438 | if args[field.name]? 439 | propertyFlags[Math.floor(i/15)] |= 1 <<(15-i) 440 | 441 | for propertyFlag in propertyFlags 442 | serializeInt(b, 2, propertyFlag) 443 | 444 | #now the actual properties. 445 | serializeFields(b, classInfo.fields, args, false) 446 | 447 | #serializeTable(b, props); 448 | bodyEnd = b.used; 449 | 450 | # Go back to the header and write in the length now that we know it. 451 | b.used = lengthStart; 452 | serializeInt(b, 4, bodyEnd - bodyStart) 453 | b.used = bodyEnd; 454 | 455 | # 1 OCTET END 456 | b[b.used++] = 206 # constants.frameEnd; 457 | 458 | # we create this new buffer to make sure it doesn't get overwritten in a situation where we're backed up flushing to the network 459 | headerBuffer = new Buffer(b.used) 460 | b.copy(headerBuffer,0 ,0 ,b.used) 461 | @connection.write(headerBuffer) 462 | @_resetSendHeartbeatTimer() 463 | 464 | _sendBody: (channel, body, args, cb)=> 465 | if body instanceof Buffer 466 | @_sendHeader channel, body.length, args 467 | 468 | offset = 0 469 | while offset < body.length 470 | 471 | length = Math.min((body.length - offset), @frameMax) 472 | h = new Buffer(7) 473 | h.used = 0 474 | 475 | h[h.used++] = 3 # constants.frameBody 476 | serializeInt(h, 2, channel) 477 | serializeInt(h, 4, length) 478 | 479 | debug 3, ()=> return "#{@id} #{channel} < body #{offset}, #{offset+length} of #{body.length}" 480 | @connection.write(h) 481 | @connection.write(body.slice(offset,offset+length)) 482 | @connection.write(EndFrame) 483 | @_resetSendHeartbeatTimer() 484 | 485 | offset += @frameMax 486 | 487 | cb?() 488 | return true 489 | else 490 | debug 1, ()-> return "invalid body type" 491 | cb?("Invalid body type for publish, expecting a buffer") 492 | return false 493 | 494 | _onContentHeader: (channel, classInfo, weight, properties, size)=> 495 | @_resetHeartbeatTimer() 496 | channel = @channels[channel] 497 | if channel?._onContentHeader? 498 | channel._onContentHeader(channel, classInfo, weight, properties, size) 499 | else 500 | debug 1, ()-> return ["unhandled -- _onContentHeader #{channel} > ", {classInfo, properties, size}] 501 | 502 | _onContent: (channel, data)=> 503 | @_resetHeartbeatTimer() 504 | channel = @channels[channel] 505 | if channel?._onContent? 506 | channel._onContent(channel, data) 507 | else 508 | debug 1, ()-> return "unhandled -- _onContent #{channel} > #{data.length}" 509 | 510 | 511 | _onMethod: (channel, method, args)=> 512 | @_resetHeartbeatTimer() 513 | if channel > 0 514 | # delegate to correct channel 515 | if !@channels[channel]? 516 | return debug 1, ()-> return "Received a message on untracked channel #{channel}, #{method.name} #{JSON.stringify args}" 517 | if !@channels[channel]._onChannelMethod? 518 | return debug 1, ()-> return "Channel #{channel} has no _onChannelMethod" 519 | @channels[channel]._onChannelMethod(channel, method, args) 520 | 521 | 522 | else 523 | # connection methods for channel 0 524 | switch method 525 | when methods.connectionStart 526 | if args.versionMajor != 0 and args.versionMinor!=9 527 | serverVersionError = new Error('Bad server version') 528 | serverVersionError.code = 'badServerVersion' 529 | 530 | return @emit 'error', serverVersionError 531 | 532 | # set our server properties up 533 | @serverProperties = args.serverProperties 534 | @_sendMethod 0, methods.connectionStartOk, { 535 | clientProperties: @connectionOptions.clientProperties 536 | mechanism: 'AMQPLAIN' 537 | response:{ 538 | LOGIN: @connectionOptions.login 539 | PASSWORD: @connectionOptions.password 540 | } 541 | locale: 'en_US' 542 | } 543 | when methods.connectionTune 544 | if args.channelMax? and args.channelMax isnt 0 and args.channelMax < @channelMax or @channelMax is 0 545 | @channelMax = args.channelMax 546 | 547 | if args.frameMax? and args.frameMax < @frameMax 548 | @frameMax = args.frameMax 549 | @sendBuffer = new Buffer(@frameMax) 550 | 551 | @_sendMethod 0, methods.connectionTuneOk, { 552 | channelMax: @channelMax 553 | frameMax: @frameMax 554 | heartbeat: @connectionOptions.heartbeat / 1000 555 | } 556 | 557 | @_sendMethod 0, methods.connectionOpen, { 558 | virtualHost: @connectionOptions.vhost 559 | } 560 | 561 | when methods.connectionOpenOk 562 | @state = 'open' 563 | @emit 'ready' 564 | 565 | when methods.connectionClose 566 | @state = 'closed' 567 | @_sendMethod 0, methods.connectionCloseOk, {} 568 | 569 | e = new Error(args.replyText) 570 | e.code = args.replyCode 571 | @emit 'close', e 572 | 573 | when methods.connectionCloseOk 574 | @emit 'close' 575 | @connection.destroy() 576 | 577 | else 578 | debug 1, ()-> return "0 < no matched method on connection for #{method.name}" 579 | 580 | module.exports = Connection 581 | -------------------------------------------------------------------------------- /test/queue.test.coffee: -------------------------------------------------------------------------------- 1 | should = require('should') 2 | async = require('async') 3 | _ = require('underscore') 4 | proxy = require('./proxy') 5 | uuid = require('uuid').v4 6 | 7 | AMQP = require('src/amqp') 8 | 9 | describe 'Queue', () -> 10 | 11 | it 'test it can declare a queue 500', (done)-> 12 | amqp = null 13 | queue = null 14 | async.series [ 15 | (next)-> 16 | amqp = new AMQP {host:'localhost'}, (e, r)-> 17 | should.not.exist e 18 | next() 19 | 20 | (next)-> 21 | amqp.queue {queue:"testing"}, (e, q)-> 22 | should.not.exist e 23 | should.exist q 24 | queue = q 25 | next() 26 | 27 | (next)-> 28 | queue.declare {passive:false}, (e,r)-> 29 | should.not.exist e 30 | next() 31 | 32 | (next)-> 33 | queue.delete(next) 34 | 35 | ], done 36 | 37 | it 'test it can declare a queue with no name 5001', (done)-> 38 | amqp = null 39 | queue = null 40 | async.series [ 41 | (next)-> 42 | amqp = new AMQP {host:'localhost'}, (e, r)-> 43 | should.not.exist e 44 | next() 45 | 46 | (next)-> 47 | amqp.queue {queue:''}, (e, q)-> 48 | should.not.exist e 49 | should.exist q 50 | queue = q 51 | next() 52 | 53 | (next)-> 54 | queue.declare {passive:false}, (e,r)-> 55 | should.not.exist e 56 | should.exist r.queue 57 | next() 58 | 59 | (next)-> 60 | queue.bind "amq.direct", uuid(), next 61 | 62 | (next)-> 63 | queue.queueOptions.queue.should.not.eql '' 64 | next() 65 | 66 | ], (err, res)-> 67 | should.not.exist err 68 | done() 69 | 70 | 71 | it 'test it can get a queues message count 501', (done)-> 72 | amqp = null 73 | queue = null 74 | queuename = uuid() 75 | async.series [ 76 | (next)-> 77 | amqp = new AMQP {host:'localhost'}, (e, r)-> 78 | should.not.exist e 79 | next() 80 | 81 | (next)-> 82 | amqp.queue {queue:queuename}, (e, q)-> 83 | should.not.exist e 84 | should.exist q 85 | queue = q 86 | next() 87 | 88 | (next)-> 89 | queue.declare {passive:false}, (e,r)-> 90 | should.not.exist e 91 | next() 92 | 93 | (next)-> 94 | queue.bind "amq.direct", queuename, (e,r)-> 95 | should.not.exist e 96 | next() 97 | 98 | (next)-> 99 | queue.messageCount (err, res)-> 100 | res.should.eql 0 101 | next() 102 | 103 | (next)-> 104 | amqp.publish "amq.direct", queuename, "test message", {confirm:true}, (e,r)-> 105 | next() 106 | 107 | (next)-> 108 | queue.messageCount (err, res)-> 109 | res.should.eql 1 110 | next() 111 | 112 | (next)-> 113 | queue.delete({ifEmpty:false}, next) 114 | 115 | (next)-> 116 | amqp.close() 117 | next() 118 | 119 | ], done 120 | 121 | 122 | it 'test it can get a queues consumer count 502', (done)-> 123 | amqp = null 124 | queue = null 125 | async.series [ 126 | (next)-> 127 | amqp = new AMQP {host:'localhost'}, (e, r)-> 128 | should.not.exist e 129 | next() 130 | 131 | (next)-> 132 | amqp.queue {queue:"testing"}, (e, q)-> 133 | should.not.exist e 134 | should.exist q 135 | queue = q 136 | next() 137 | 138 | (next)-> 139 | queue.declare {passive:false}, (e,r)-> 140 | should.not.exist e 141 | next() 142 | 143 | (next)-> 144 | queue.bind "amq.direct", "testing", (e,r)-> 145 | should.not.exist e 146 | next() 147 | 148 | (next)-> 149 | queue.consumerCount (err, res)-> 150 | res.should.eql 0 151 | next() 152 | 153 | (next)-> 154 | processor = ()-> 155 | # i do nothing :) 156 | amqp.consume "testing", {} , processor, next 157 | 158 | (next)-> 159 | queue.consumerCount (err, res)-> 160 | res.should.eql 1 161 | next() 162 | 163 | (next)-> 164 | queue.delete(next) 165 | 166 | (next)-> 167 | amqp.close() 168 | next() 169 | 170 | ], done 171 | 172 | 173 | 174 | it 'test it can get a queues consumer count with connection trouble 503', (done)-> 175 | 176 | thisproxy = new proxy.route(7008, 5672, "localhost") 177 | 178 | amqp = null 179 | queue = null 180 | async.series [ 181 | (next)-> 182 | amqp = new AMQP {host:'localhost', port:7008, heartbeat: 1000}, (e, r)-> 183 | should.not.exist e 184 | next() 185 | 186 | (next)-> 187 | amqp.queue {queue:"testing"}, (e, q)-> 188 | should.not.exist e 189 | should.exist q 190 | queue = q 191 | next() 192 | 193 | (next)-> 194 | queue.declare {passive:false}, (e,r)-> 195 | should.not.exist e 196 | next() 197 | 198 | (next)-> 199 | queue.bind "amq.direct", "testing", (e,r)-> 200 | should.not.exist e 201 | next() 202 | 203 | (next)-> 204 | queue.consumerCount (err, res)-> 205 | res.should.eql 0 206 | thisproxy.interrupt() 207 | next() 208 | 209 | (next)-> 210 | processor = ()-> 211 | # i do nothing :) 212 | amqp.consume "testing", {} , processor, next 213 | 214 | (next)-> 215 | queue.consumerCount (err, res)-> 216 | res.should.eql 1 217 | next() 218 | 219 | (next)-> 220 | queue.delete(next) 221 | 222 | (next)-> 223 | amqp.close() 224 | thisproxy.close() 225 | next() 226 | 227 | ], done 228 | 229 | 230 | 231 | it 'test it can get a queues consumer count with connection trouble 504', (done)-> 232 | this.timeout(5000) 233 | thisproxy = new proxy.route(7008, 5672, "localhost") 234 | 235 | amqp = null 236 | queue = null 237 | async.series [ 238 | (next)-> 239 | amqp = new AMQP {host:'localhost', port:7008, heartbeat: 30000}, (e, r)-> 240 | should.not.exist e 241 | next() 242 | 243 | (next)-> 244 | amqp.queue {queue:"testing", autoDelete: false, durable: true}, (e, q)-> 245 | should.not.exist e 246 | should.exist q 247 | queue = q 248 | next() 249 | 250 | (next)-> 251 | queue.declare {passive:false}, (e,r)-> 252 | should.not.exist e 253 | next() 254 | 255 | (next)-> 256 | queue.bind "amq.direct", "testing", (e,r)-> 257 | should.not.exist e 258 | 259 | thisproxy.close() 260 | next() 261 | 262 | (next)-> 263 | queue.consumerCount (err, res)-> 264 | should.exist err 265 | thisproxy.interrupt() 266 | next() 267 | 268 | _.delay ()-> 269 | thisproxy.listen() 270 | , 1000 271 | 272 | (next)-> 273 | queue.consumerCount (err, res)-> 274 | res.should.eql 0 275 | next() 276 | 277 | (next)-> 278 | queue.delete(next) 279 | 280 | (next)-> 281 | amqp.close() 282 | thisproxy.close() 283 | next() 284 | 285 | ], done 286 | 287 | 288 | it 'test it can declare a queue while its trying to close a temp channel 632', (done)-> 289 | amqp = null 290 | queue = null 291 | channel = null 292 | async.series [ 293 | (next)-> 294 | amqp = new AMQP {host:'localhost'}, (e, r)-> 295 | should.not.exist e 296 | next() 297 | 298 | (next)-> 299 | channel = amqp.queue {queue:"testing"}, (e, q)-> 300 | should.not.exist e 301 | should.exist q 302 | queue = q 303 | next() 304 | 305 | (next)-> 306 | channel.close() 307 | 308 | queue.declare {passive:false}, (e,r)-> 309 | should.not.exist e 310 | next() 311 | 312 | (next)-> 313 | queue.delete(next) 314 | 315 | ], done 316 | 317 | 318 | it 'test it can declare a queue while its trying to close a temp channel deferred 633', (done)-> 319 | amqp = null 320 | queue = null 321 | channel = null 322 | async.series [ 323 | (next)-> 324 | amqp = new AMQP {host:'localhost'}, (e, r)-> 325 | should.not.exist e 326 | next() 327 | 328 | (next)-> 329 | channel = amqp.queue {queue:"testing"}, (e, q)-> 330 | should.not.exist e 331 | should.exist q 332 | queue = q 333 | next() 334 | 335 | (next)-> 336 | 337 | queue.declare {passive:false}, (e,r)-> 338 | should.not.exist e 339 | next() 340 | 341 | setTimeout channel.close, 1 342 | 343 | (next)-> 344 | queue.delete(next) 345 | 346 | 347 | ], done 348 | 349 | 350 | 351 | it 'test it can delete a queue', (done)-> 352 | amqp = null 353 | queue = null 354 | async.series [ 355 | (next)-> 356 | amqp = new AMQP {host:'localhost'}, (e, r)-> 357 | should.not.exist e 358 | next() 359 | 360 | (next)-> 361 | amqp.queue {queue:"testing"}, (e, q)-> 362 | should.not.exist e 363 | should.exist q 364 | queue = q 365 | next() 366 | 367 | (next)-> 368 | queue.declare {passive:false}, (e,r)-> 369 | should.not.exist e 370 | next() 371 | 372 | (next)-> 373 | queue.delete {}, (e,r)-> 374 | should.not.exist e 375 | next() 376 | ], done 377 | 378 | 379 | it 'test we can bind a queue', (done)-> 380 | amqp = null 381 | queue = null 382 | queueName = uuid() 383 | async.series [ 384 | (next)-> 385 | amqp = new AMQP {host:'localhost'}, (e, r)-> 386 | should.not.exist e 387 | next() 388 | 389 | (next)-> 390 | amqp.queue {queue:queueName}, (e, q)-> 391 | should.not.exist e 392 | should.exist q 393 | queue = q 394 | next() 395 | 396 | (next)-> 397 | queue.declare {passive:false}, (e,r)-> 398 | should.not.exist e 399 | next() 400 | 401 | (next)-> 402 | queue.bind "amq.direct", queueName, (e,r)-> 403 | should.not.exist e 404 | next() 405 | 406 | ], done 407 | 408 | it 'test we do not error on a double bind', (done)-> 409 | amqp = null 410 | queue = null 411 | queueName = uuid() 412 | async.series [ 413 | (next)-> 414 | amqp = new AMQP {host:'localhost'}, (e, r)-> 415 | should.not.exist e 416 | next() 417 | 418 | (next)-> 419 | amqp.queue {queue:queueName}, (e, q)-> 420 | should.not.exist e 421 | should.exist q 422 | queue = q 423 | next() 424 | 425 | (next)-> 426 | queue.declare {passive:false}, (e,r)-> 427 | should.not.exist e 428 | next() 429 | 430 | (next)-> 431 | queue.bind "amq.direct", "testing", (e,r)-> 432 | should.not.exist e 433 | next() 434 | 435 | (next)-> 436 | queue.bind "amq.direct", "testing", (e,r)-> 437 | should.not.exist e 438 | next() 439 | 440 | (next)-> 441 | queue.delete(next) 442 | 443 | ], done 444 | 445 | 446 | it 'test we can unbind a queue 2885', (done)-> 447 | this.timeout(1000000) 448 | amqp = null 449 | queue = null 450 | queueName = uuid() 451 | async.series [ 452 | (next)-> 453 | amqp = new AMQP {host:'localhost'}, (e, r)-> 454 | should.not.exist e 455 | next() 456 | 457 | (next)-> 458 | amqp.queue {queue:queueName}, (e, q)-> 459 | should.not.exist e 460 | should.exist q 461 | queue = q 462 | next() 463 | 464 | (next)-> 465 | queue.declare {passive:false}, (e,r)-> 466 | should.not.exist e 467 | next() 468 | 469 | (next)-> 470 | queue.bind "amq.direct", "testing", (e,r)-> 471 | should.not.exist e 472 | next() 473 | 474 | (next)-> 475 | queue.bind "amq.direct", "testing2", (e,r)-> 476 | should.not.exist e 477 | next() 478 | 479 | (next)-> 480 | queue.unbind "amq.direct", "testing", (e,r)-> 481 | should.not.exist e 482 | next() 483 | 484 | (next)-> 485 | queue.unbind "amq.direct", "testing2", (e,r)-> 486 | should.not.exist e 487 | next() 488 | (next)-> 489 | _.delay -> 490 | openChannels = 0 491 | for channelNumber,channel of amqp.channels 492 | openChannels++ if channel.state is 'open' 493 | openChannels.should.eql 2 494 | next() 495 | , 10 496 | 497 | (next)-> 498 | _.delay -> 499 | openChannels = 0 500 | for channelNumber,channel of amqp.channels 501 | openChannels++ if channel.state is 'open' 502 | openChannels.should.eql 1 503 | next() 504 | , 500 505 | 506 | (next)-> 507 | queue.delete(next) 508 | 509 | ], done 510 | 511 | 512 | it 'test we can unbind a queue with no callbacks 2886', (done)-> 513 | amqp = null 514 | queue = null 515 | queueName = uuid() 516 | async.series [ 517 | (next)-> 518 | amqp = new AMQP {host:'localhost'}, (e, r)-> 519 | should.not.exist e 520 | next() 521 | 522 | (next)-> 523 | amqp.queue {queue:queueName}, (e, q)-> 524 | should.not.exist e 525 | should.exist q 526 | queue = q 527 | next() 528 | 529 | (next)-> 530 | queue.declare {passive:false}, (e,r)-> 531 | should.not.exist e 532 | next() 533 | 534 | (next)-> 535 | queue.bind "amq.direct", "test1" 536 | queue.bind "amq.direct", "test2" 537 | queue.bind "amq.direct", "test3" 538 | _.delay next, 30 539 | 540 | (next)-> 541 | queue.unbind "amq.direct", "test1" 542 | queue.unbind "amq.direct", "test2" 543 | queue.unbind "amq.direct", "test3" 544 | _.delay next, 500 545 | 546 | (next)-> 547 | openChannels = 0 548 | for channelNumber,channel of amqp.channels 549 | openChannels++ if channel.state is 'open' 550 | openChannels.should.eql 1 551 | next() 552 | ], done 553 | 554 | 555 | 556 | it 'test we can unbind a queue with no callbacks on bad binds 2887', (done)-> 557 | amqp = null 558 | queue = null 559 | queueName = uuid() 560 | consumer = null 561 | 562 | async.series [ 563 | (next)-> 564 | amqp = new AMQP {host:'localhost'}, (e, r)-> 565 | should.not.exist e 566 | next() 567 | 568 | (next)-> 569 | amqp.queue {queue:queueName, passive:false, exclusive: true, autodelete: true}, (e, q)-> 570 | should.not.exist e 571 | should.exist q 572 | queue = q 573 | next() 574 | 575 | (next)-> 576 | queue.declare (e,r)-> 577 | should.not.exist e 578 | next() 579 | 580 | (next)-> 581 | consumer = amqp.consume queueName, {}, (message)-> 582 | console.error messge 583 | , (cb)-> 584 | next() 585 | 586 | (next)-> 587 | queue.bind "amq.direct", "test1" 588 | queue.bind "amq.direct", "test2" 589 | queue.bind "amq.direct", "test3" 590 | queue.bind "amq.direct2", "test5" 591 | _.delay next, 30 592 | 593 | (next)-> 594 | queue.unbind "amq.direct", "test1" 595 | queue.unbind "amq.direct", "test2" 596 | queue.unbind "amq.direct", "test3" 597 | queue.unbind "amq.direct", "test4" 598 | _.delay next, 500 599 | 600 | (next)-> 601 | queue.unbind "amq.direct", "test1" 602 | queue.unbind "amq.direct", "test2" 603 | queue.unbind "amq.direct", "test3" 604 | queue.unbind "amq.direct", "test4" 605 | queue.unbind "amq.direct", "test4" 606 | _.delay next, 500 607 | 608 | (next)-> 609 | consumer.close() 610 | _.delay next, 50 611 | 612 | (next)-> 613 | openChannels = 0 614 | for channelNumber,channel of amqp.channels 615 | openChannels++ if channel.state is 'open' 616 | openChannels.should.eql 1 617 | next() 618 | ], done 619 | 620 | 621 | 622 | it 'test we can bind to a non-existing exchange and not leave channels open 2889', (done)-> 623 | amqp = null 624 | queue = null 625 | queueName = uuid() 626 | 627 | async.series [ 628 | (next)-> 629 | amqp = new AMQP {host:'localhost'}, (e, r)-> 630 | should.not.exist e 631 | next() 632 | 633 | (next)-> 634 | amqp.queue {queue:queueName, passive:false, exclusive: true, autodelete: true}, (e, q)-> 635 | should.not.exist e 636 | should.exist q 637 | queue = q 638 | next() 639 | 640 | (next)-> 641 | queue.declare (e,r)-> 642 | should.not.exist e 643 | next() 644 | 645 | (next)-> 646 | queue.bind "amq.direct2", "test1", ()-> next() 647 | (next)-> 648 | queue.bind "amq.direct2", "test1", ()-> next() 649 | (next)-> 650 | _.delay next, 100 651 | (next)-> 652 | queue.bind "amq.direct2", "test1", ()-> next() 653 | (next)-> 654 | queue.bind "amq.direct2", "test1", ()-> next() 655 | 656 | (next)-> 657 | _.delay next, 500 658 | 659 | 660 | (next)-> 661 | openChannels = 0 662 | for channelNumber,channel of amqp.channels 663 | openChannels++ if channel.state is 'open' 664 | openChannels.should.eql 1 665 | next() 666 | ], done 667 | 668 | 669 | 670 | it 'test we can timeout a queue channel and reopen it', (done)-> 671 | this.timeout(2000) 672 | amqp = null 673 | queue = null 674 | queueName = uuid() 675 | async.series [ 676 | (next)-> 677 | amqp = new AMQP {host:'localhost'}, (e, r)-> 678 | should.not.exist e 679 | next() 680 | 681 | (next)-> 682 | amqp.queue {queue:queueName}, (e, q)-> 683 | should.not.exist e 684 | should.exist q 685 | queue = q 686 | next() 687 | 688 | (next)-> 689 | queue.declare {passive:false}, (e,r)-> 690 | should.not.exist e 691 | next() 692 | 693 | (next)-> 694 | _.keys(amqp.channels).length.should.eql 2 695 | _.delay next, 500 696 | 697 | (next)-> 698 | _.keys(amqp.channels).length.should.eql 1 699 | next() 700 | 701 | (next)-> 702 | queue.declare {passive:false}, (e,r)-> 703 | should.not.exist e 704 | _.keys(amqp.channels).length.should.eql 2 705 | next() 706 | 707 | ], done 708 | 709 | 710 | it 'test after a unbind error we could rebind, on a different channel', (done)-> 711 | amqp = null 712 | queue = null 713 | queueName = uuid() 714 | channel = null 715 | async.series [ 716 | (next)-> 717 | amqp = new AMQP {host:'localhost'}, (e, r)-> 718 | should.not.exist e 719 | next() 720 | 721 | (next)-> 722 | channel = amqp.queue {queue:queueName}, (e, q)-> 723 | should.not.exist e 724 | should.exist q 725 | queue = q 726 | next() 727 | 728 | (next)-> 729 | queue.declare {passive:false}, (e,r)-> 730 | should.not.exist e 731 | next() 732 | 733 | (next)-> 734 | queue.bind "amq.direct", "testing", (e,r)-> 735 | should.not.exist e 736 | next() 737 | 738 | (next)-> 739 | queue.unbind "amq.direct", "testing", (e,r)-> 740 | should.not.exist e 741 | next() 742 | 743 | (next)-> 744 | channel.crash next 745 | 746 | (next)-> 747 | queue.bind "amq.direct", "testing", (e,r)-> 748 | should.not.exist e 749 | next() 750 | 751 | ], done 752 | 753 | 754 | 755 | it 'test we get a error on a bad bind', (done)-> 756 | amqp = null 757 | queue = null 758 | queueName = uuid() 759 | channel = null 760 | async.series [ 761 | (next)-> 762 | amqp = new AMQP {host:'localhost'}, (e, r)-> 763 | should.not.exist e 764 | next() 765 | 766 | (next)-> 767 | channel = amqp.queue {queue:queueName}, (e, q)-> 768 | should.not.exist e 769 | should.exist q 770 | queue = q 771 | next() 772 | 773 | 774 | (next)-> 775 | queue.bind "amq.direct", "testing", (e,r)-> 776 | should.exist e 777 | e.replyCode.should.eql 404 778 | next() 779 | 780 | ], done 781 | 782 | 783 | it 'test it can declare a AD queue twice 5897', (done)-> 784 | amqp = null 785 | amqp2 = null 786 | eventFired = 0 787 | 788 | async.series [ 789 | (next)-> 790 | amqp = new AMQP {host:'localhost'}, (e, r)-> 791 | should.not.exist e 792 | next() 793 | 794 | amqp.on 'error', (e)-> 795 | should.not.exist e 796 | eventFired++ 797 | 798 | (next)-> 799 | queue = amqp.queue {queue:'testQueueHA', durable: true, autoDelete: true}, (e,q)-> 800 | should.not.exist e 801 | eventFired++ 802 | 803 | q.declare {}, (e)-> 804 | should.not.exist e 805 | eventFired++ 806 | next() 807 | 808 | queue.on 'error', (e)-> 809 | should.not.exist e 810 | eventFired++ 811 | (next)-> 812 | amqp2 = new AMQP {host:'localhost'}, (e, r)-> 813 | should.not.exist e 814 | next() 815 | 816 | (next)-> 817 | amqp2.on 'error', (e)-> 818 | should.not.exist e 819 | eventFired++ 820 | 821 | queue = amqp2.queue {queue:'testQueueHA', durable: true, autoDelete: false}, (e, q)-> 822 | should.not.exist e 823 | eventFired++ 824 | q.declare {}, (e)-> 825 | should.exist e 826 | eventFired++ 827 | next() 828 | 829 | queue.on 'error', (e)-> 830 | should.not.exist e 831 | eventFired++ 832 | 833 | ], (err, res)-> 834 | should.not.exist err 835 | eventFired.should.eql 4 836 | _.delay done, 1000 837 | --------------------------------------------------------------------------------