UNPKG

te_nsqjs

Version:
614 lines (465 loc) 19.8 kB
_ = require 'underscore' should = require 'should' sinon = require 'sinon' {EventEmitter} = require 'events' {NSQDConnection} = require '../src/nsqdconnection' Message = require '../src/message' {ReaderRdy, ConnectionRdy} = require '../src/readerrdy' class StubNSQDConnection extends EventEmitter constructor: (@nsqdHost, @nsqdPort, @topic, @channel, @requeueDelay, @heartbeatInterval) -> @conn = localPort: 1 @maxRdyCount = 2500 @msgTimeout = 60 * 1000 @maxMsgTimeout = 15 * 60 * 1000 @rdyCounts = [] id: -> "#{@nsqdHost}:#{@nsqdPort}" connect: -> # Empty destroy: -> # Empty setRdy: (rdyCount) -> @rdyCounts.push rdyCount createMessage: (msgId, msgTimestamp, attempts, msgBody) -> msgComponents = [msgId, msgTimestamp, attempts, msgBody] msgArgs = msgComponents.concat [@requeueDelay, @msgTimeout, @maxMsgTimeout] msg = new Message msgArgs... msg.on Message.RESPOND, (responseType, wireData) => if responseType is Message.FINISH @emit NSQDConnection.FINISHED else if responseType is Message.REQUEUE @emit NSQDConnection.REQUEUED msg.on Message.BACKOFF, => @emit NSQDConnection.BACKOFF @emit NSQDConnection.MESSAGE, msg msg createNSQDConnection = (id) -> conn = new StubNSQDConnection "host#{id}", '4150', 'test', 'default', 60, 30 conn.conn.localPort = id conn describe 'ConnectionRdy', -> [conn, spy, cRdy] = [null, null, null] beforeEach -> conn = createNSQDConnection 1 spy = sinon.spy conn, 'setRdy' cRdy = new ConnectionRdy conn cRdy.start() it 'should register listeners on a connection', -> conn = new NSQDConnection 'localhost', 1234, 'test', 'test' mock = sinon.mock conn mock.expects('on').withArgs NSQDConnection.ERROR mock.expects('on').withArgs NSQDConnection.FINISHED mock.expects('on').withArgs NSQDConnection.MESSAGE mock.expects('on').withArgs NSQDConnection.REQUEUED mock.expects('on').withArgs NSQDConnection.READY cRdy = new ConnectionRdy conn mock.verify() it 'should have a connection RDY max of zero', -> cRdy.maxConnRdy.should.eql 0 it 'should not increase RDY when connection RDY max has not been set', -> # This bump should be a no-op cRdy.bump() cRdy.maxConnRdy.should.eql 0 spy.called.should.not.be.ok it 'should not allow RDY counts to be negative', -> cRdy.setConnectionRdyMax 10 cRdy.setRdy -1 spy.notCalled.should.be.ok() it 'should not allow RDY counts to exceed the connection max', -> cRdy.setConnectionRdyMax 10 cRdy.setRdy 9 cRdy.setRdy 10 cRdy.setRdy 20 spy.calledTwice.should.be.ok() spy.firstCall.args[0].should.eql 9 spy.secondCall.args[0].should.eql 10 it 'should set RDY to max after initial bump', -> cRdy.setConnectionRdyMax 3 cRdy.bump() spy.firstCall.args[0].should.eql 3 it 'should keep RDY at max after 1+ bumps', -> cRdy.setConnectionRdyMax 3 for i in [1..3] cRdy.bump() cRdy.maxConnRdy.should.eql 3 for i in [0...spy.callCount] should.ok spy.getCall(i).args[0] <= 3 it 'should set RDY to zero from after first bump and then backoff', -> cRdy.setConnectionRdyMax 3 cRdy.bump() cRdy.backoff() spy.lastCall.args[0].should.eql 0 it 'should set RDY to zero after 1+ bumps and then a backoff', -> cRdy.setConnectionRdyMax 3 cRdy.bump() cRdy.backoff() spy.lastCall.args[0].should.eql 0 it 'should raise RDY when new connection RDY max is lower', -> cRdy.setConnectionRdyMax 3 cRdy.bump() cRdy.setConnectionRdyMax 5 cRdy.maxConnRdy.should.eql 5 spy.lastCall.args[0].should.eql 5 it 'should reduce RDY when new connection RDY max is higher', -> cRdy.setConnectionRdyMax 3 cRdy.bump() cRdy.setConnectionRdyMax 2 cRdy.maxConnRdy.should.eql 2 spy.lastCall.args[0].should.eql 2 it 'should update RDY when 75% of previous RDY is consumed', -> cRdy.setConnectionRdyMax 10 cRdy.bump() spy.firstCall.args[0].should.eql 10 for i in [1..7] msg = conn.createMessage "#{i}", Date.now(), 0, "Message #{i}" msg.finish() cRdy.bump() spy.callCount.should.eql 1 msg = conn.createMessage '8', Date.now(), 0, 'Message 8' msg.finish() cRdy.bump() spy.callCount.should.eql 2 spy.lastCall.args[0].should.eql 10 describe 'ReaderRdy', -> readerRdy = null beforeEach -> readerRdy = new ReaderRdy 1, 128, 'topic/channel' afterEach -> readerRdy.close() it 'should register listeners on a connection', -> # Stub out creation of ConnectionRdy to ignore the events registered by # ConnectionRdy. sinon.stub readerRdy, 'createConnectionRdy', -> on: -> # Empty conn = createNSQDConnection 1 mock = sinon.mock conn mock.expects('on').withArgs NSQDConnection.CLOSED mock.expects('on').withArgs NSQDConnection.FINISHED mock.expects('on').withArgs NSQDConnection.REQUEUED mock.expects('on').withArgs NSQDConnection.BACKOFF readerRdy.addConnection conn mock.verify() it 'should be in the zero state until a new connection is READY', -> conn = createNSQDConnection 1 readerRdy.current_state_name.should.eql 'ZERO' readerRdy.addConnection conn readerRdy.current_state_name.should.eql 'ZERO' conn.emit NSQDConnection.READY readerRdy.current_state_name.should.eql 'MAX' it 'should be in the zero state if it loses all connections', -> conn = createNSQDConnection 1 readerRdy.addConnection conn conn.emit NSQDConnection.READY conn.emit NSQDConnection.CLOSED readerRdy.current_state_name.should.eql 'ZERO' it 'should evenly distribute RDY count across connections', -> readerRdy = new ReaderRdy 100, 128, 'topic/channel' conn1 = createNSQDConnection 1 conn2 = createNSQDConnection 2 setRdyStub1 = sinon.spy conn1, 'setRdy' setRdyStub2 = sinon.spy conn2, 'setRdy' readerRdy.addConnection conn1 conn1.emit NSQDConnection.READY setRdyStub1.lastCall.args[0].should.eql 100 readerRdy.addConnection conn2 conn2.emit NSQDConnection.READY setRdyStub1.lastCall.args[0].should.eql 50 setRdyStub2.lastCall.args[0].should.eql 50 describe 'low RDY conditions', -> assertAlternatingRdyCounts = (conn1, conn2) -> minSize = Math.min conn1.rdyCounts.length, conn2.rdyCounts.length zippedCounts = _.zip conn1.rdyCounts[-minSize..], conn2.rdyCounts[-minSize..] # We expect the connection RDY counts to look like this: # conn 0: [1, 0, 1, 0] # conn 1: [0, 1, 0, 1] for [firstRdy, secondRdy] in zippedCounts should.ok (firstRdy + secondRdy) is 1 it 'should periodically redistribute RDY', (done) -> # Shortening the periodically `balance` calls to every 10ms. readerRdy = new ReaderRdy 1, 128, 'topic/channel', 0.01 connections = for i in [1..2] createNSQDConnection i # Add the connections and trigger the NSQDConnection event that tells # listeners that the connections are connected and ready for message flow. for conn in connections readerRdy.addConnection conn conn.emit NSQDConnection.READY # Given the number of connections and the maxInFlight, we should be in low # RDY conditions. readerRdy.isLowRdy().should.eql true checkRdyCounts = -> assertAlternatingRdyCounts connections... done() # We have to wait a small period of time for log events to occur since the # `balance` call is invoked perdiocally. setTimeout checkRdyCounts, 50 it 'should handle the transition from normal', (done) -> # Shortening the periodica `balance` calls to every 10ms. readerRdy = new ReaderRdy 1, 128, 'topic/channel', 0.01 conn1 = createNSQDConnection 1 conn2 = createNSQDConnection 2 # Add the connections and trigger the NSQDConnection event that tells # listeners that the connections are connected and ready for message flow. readerRdy.addConnection conn1 conn1.emit NSQDConnection.READY readerRdy.isLowRdy().should.eql false addConnection = -> readerRdy.addConnection conn2 conn2.emit NSQDConnection.READY # Given the number of connections and the maxInFlight, we should be in # low RDY conditions. readerRdy.isLowRdy().should.eql true # Add the 2nd connections after some duration to simulate a new nsqd being # discovered and connected. setTimeout addConnection, 20 checkRdyCounts = -> assertAlternatingRdyCounts conn1, conn2 done() # We have to wait a small period of time for log events to occur since the # `balance` call is invoked perdiocally. setTimeout checkRdyCounts, 40 it 'should handle the transition to normal conditions', (done) -> # Shortening the periodica `balance` calls to every 10ms. readerRdy = new ReaderRdy 1, 128, 'topic/channel', 0.01 connections = for i in [1..2] createNSQDConnection i # Add the connections and trigger the NSQDConnection event that tells # listeners that the connections are connected and ready for message flow. for conn in connections readerRdy.addConnection conn conn.emit NSQDConnection.READY readerRdy.isLowRdy().should.eql true removeConnection = -> connections[1].emit NSQDConnection.CLOSED setTimeout checkNormal, 20 checkNormal = -> readerRdy.isLowRdy().should.eql false should.ok readerRdy.balanceId is null readerRdy.connections[0].lastRdySent.should.eql 1 done() # Remove a connection after some period of time to get back to normal # conditions. setTimeout removeConnection, 20 it 'should move to normal conditions with connections in backoff', (done) -> ### 1. Create two nsqd connections 2. Close the 2nd connection when the first connection is in the BACKOFF state. 3. Check to see if the 1st connection does get it's RDY count. ### # Shortening the periodica `balance` calls to every 10ms. readerRdy = new ReaderRdy 1, 128, 'topic/channel', 0.01 connections = for i in [1..2] createNSQDConnection i for conn in connections readerRdy.addConnection conn conn.emit NSQDConnection.READY readerRdy.isLowRdy().should.eql true removeConnection = _.once -> connections[1].emit NSQDConnection.CLOSED setTimeout checkNormal, 30 removeOnBackoff = -> connRdy1 = readerRdy.connections[0] connRdy1.on ConnectionRdy.STATE_CHANGE, -> if connRdy1.statemachine.current_state_name is 'BACKOFF' # If we don't do the connection CLOSED in the next tick, we remove # the connection immediately which leaves `@connections` within # `balance` in an inconsistent state which isn't possible normally. setTimeout removeConnection, 0 checkNormal = -> readerRdy.isLowRdy().should.eql false should.ok readerRdy.balanceId is null readerRdy.connections[0].lastRdySent.should.eql 1 done() # Remove a connection after some period of time to get back to normal # conditions. setTimeout removeOnBackoff, 20 it 'should not exceed maxInFlight for long running message.', (done) -> # Shortening the periodica `balance` calls to every 10ms. readerRdy = new ReaderRdy 1, 128, 'topic/channel', 0.01 connections = for i in [1..2] createNSQDConnection i for conn in connections readerRdy.addConnection conn conn.emit NSQDConnection.READY # Handle the message but delay finishing the message so that several # balance calls happen and the check to ensure that RDY count is zero for # all connections. handleMessage = (msg) -> finish = -> msg.finish() done() setTimeout finish, 40 for conn in connections conn.on NSQDConnection.MESSAGE, handleMessage sendMessageOnce = _.once -> connections[1].createMessage '1', Date.now(), new Buffer('test') setTimeout checkRdyCount, 20 # Send a message on the 2nd connection when we can. Only send the message # once so that we don't violate the maxInFlight count. sendOnRdy = -> connRdy2 = readerRdy.connections[1] connRdy2.on ConnectionRdy.STATE_CHANGE, -> if connRdy2.statemachine.current_state_name in ['ONE', 'MAX'] sendMessageOnce() # When the message is in-flight, balance cannot give a RDY count out to # any of the connections. checkRdyCount = -> readerRdy.isLowRdy().should.eql true readerRdy.connections[0].lastRdySent.should.eql 0 readerRdy.connections[1].lastRdySent.should.eql 0 # We have to wait a small period of time for log events to occur since the # `balance` call is invoked perdiocally. setTimeout sendOnRdy, 20 it 'should recover losing a connection with a message in-flight', (done) -> ### Detailed description: 1. Connect to 5 nsqds and add them to the ReaderRdy 2. When the 1st connection has the shared RDY count, it receives a message. 3. On receipt of a message, the 1st connection will process the message for a long period of time. 4. While the message is being processed, the 1st connection will close. 5. Finally, check that the other connections are indeed now getting the RDY count. ### # Shortening the periodica `balance` calls to every 10ms. readerRdy = new ReaderRdy 1, 128, 'topic/channel', 0.01 connections = for i in [1..5] createNSQDConnection i # Add the connections and trigger the NSQDConnection event that tells # listeners that the connections are connected and ready for message flow. for conn in connections readerRdy.addConnection conn conn.emit NSQDConnection.READY handleMessage = (msg) -> delayFinish = -> msg.finish() done() setTimeout closeConnection, 10 setTimeout checkRdyCount, 30 setTimeout delayFinish, 50 for conn in connections conn.on NSQDConnection.MESSAGE, handleMessage closeConnection = _.once -> connections[0].emit NSQDConnection.CLOSED sendMessageOnce = _.once -> connections[0].createMessage '1', Date.now(), new Buffer('test') # Send a message on the 2nd connection when we can. Only send the message # once so that we don't violate the maxInFlight count. sendOnRdy = -> connRdy = readerRdy.connections[0] connRdy.on ConnectionRdy.STATE_CHANGE, -> if connRdy.statemachine.current_state_name in ['ONE', 'MAX'] sendMessageOnce() # When the message is in-flight, balance cannot give a RDY count out to # any of the connections. checkRdyCount = -> readerRdy.isLowRdy().should.eql true rdyCounts = for connRdy in readerRdy.connections connRdy.lastRdySent readerRdy.connections.length.should.eql 4 should.ok 1 in rdyCounts # We have to wait a small period of time for log events to occur since the # `balance` call is invoked perdiocally. setTimeout sendOnRdy, 10 describe 'try', -> it 'should on completion of backoff attempt a single connection', (done) -> ### Detailed description: 1. Create ReaderRdy with connections to 5 nsqds. 2. Generate a message from an nsqd that causes a backoff. 3. Verify that all the nsqds are in backoff mode. 4. At the end of the backoff period, verify that only one ConnectionRdy is in the try one state and the others are still in backoff. ### # Shortening the periodic `balance` calls to every 10ms. Changing the # max backoff duration to 10 sec. readerRdy = new ReaderRdy 100, 10, 'topic/channel', 0.01 connections = for i in [1..5] createNSQDConnection i for conn in connections readerRdy.addConnection conn conn.emit NSQDConnection.READY msg = connections[0].createMessage "1", Date.now(), 0, 'Message causing a backoff' msg.requeue() checkInBackoff = -> for connRdy in readerRdy.connections connRdy.statemachine.current_state_name.should.eql 'BACKOFF' setTimeout checkInBackoff, 0 afterBackoff = -> states = for connRdy in readerRdy.connections connRdy.statemachine.current_state_name ones = (s for s in states when s is 'ONE') backoffs = (s for s in states when s is 'BACKOFF') ones.length.should.eql 1 backoffs.length.should.eql 4 done() # Add 50ms to the delay so that we're confident that the event fired. delay = readerRdy.backoffTimer.getInterval().plus 0.05 setTimeout afterBackoff, new Number(delay.valueOf()) * 1000 it 'should after backoff with a successful message go to MAX', (done) -> ### Detailed description: 1. Create ReaderRdy with connections to 5 nsqds. 2. Generate a message from an nsqd that causes a backoff. 3. At the end of backoff, generate a message that will succeed. 4. Verify that ReaderRdy is in MAX and ConnectionRdy instances are in either ONE or MAX. At least on ConnectionRdy should be in MAX as well. ### # Shortening the periodica `balance` calls to every 10ms. Changing the # max backoff duration to 1 sec. readerRdy = new ReaderRdy 100, 1, 'topic/channel', 0.01 connections = for i in [1..5] createNSQDConnection i for conn in connections readerRdy.addConnection conn conn.emit NSQDConnection.READY msg = connections[0].createMessage "1", Date.now(), 0, 'Message causing a backoff' msg.requeue() afterBackoff = -> [connRdy] = for connRdy in readerRdy.connections if connRdy.statemachine.current_state_name is 'ONE' connRdy msg = connRdy.conn.createMessage "1", Date.now(), 0, 'Success' msg.finish() verifyMax = -> states = for connRdy in readerRdy.connections connRdy.statemachine.current_state_name max = (s for s in states when s in ['ONE', 'MAX']) max.length.should.eql 5 should.ok 'MAX' in states done() setTimeout verifyMax, 0 delay = readerRdy.backoffTimer.getInterval() + 100 setTimeout afterBackoff, delay * 1000 describe 'pause / unpause', -> beforeEach -> # Shortening the periodic `balance` calls to every 10ms. Changing the # max backoff duration to 1 sec. readerRdy = new ReaderRdy 100, 1, 'topic/channel', 0.01 connections = for i in [1..5] createNSQDConnection i for conn in connections readerRdy.addConnection conn conn.emit NSQDConnection.READY it 'should drop ready count to zero on all connections when paused', -> readerRdy.pause() readerRdy.current_state_name.should.eql 'PAUSE' for conn in readerRdy.connections conn.lastRdySent.should.eql 0 it 'should unpause by trying one', -> readerRdy.pause() readerRdy.unpause() readerRdy.current_state_name.should.eql 'TRY_ONE' it 'should update the value of @isPaused when paused', -> readerRdy.pause() readerRdy.isPaused().should.ok readerRdy.unpause() readerRdy.isPaused().should.eql false