te_nsqjs
Version:
NodeJS client for NSQ
680 lines (597 loc) • 20.4 kB
JavaScript
// Generated by CoffeeScript 1.12.3
var ConnectionConfig, ConnectionState, Debug, EventEmitter, FrameBuffer, Message, NSQDConnection, NodeState, SnappyStream, UnsnappyStream, WriterConnectionState, WriterNSQDConnection, _, fs, net, os, ref, tls, version, wire, zlib,
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty,
slice = [].slice;
Debug = require('debug');
net = require('net');
os = require('os');
tls = require('tls');
zlib = require('zlib');
fs = require('fs');
EventEmitter = require('events').EventEmitter;
ref = require('snappystream'), SnappyStream = ref.SnappyStream, UnsnappyStream = ref.UnsnappyStream;
_ = require('underscore');
NodeState = require('node-state');
ConnectionConfig = require('./config').ConnectionConfig;
FrameBuffer = require('./framebuffer');
Message = require('./message');
wire = require('./wire');
version = require('./version');
/*
NSQDConnection is a reader connection to a nsqd instance. It manages all
aspects of the nsqd connection with the exception of the RDY count which
needs to be managed across all nsqd connections for a given topic / channel
pair.
This shouldn't be used directly. Use a Reader instead.
Usage:
c = new NSQDConnection '127.0.0.1', 4150, 'test', 'default', 60, 30
c.on NSQDConnection.MESSAGE, (msg) ->
console.log "Callback [message]: #{msg.attempts}, #{msg.body.toString()}"
console.log "Timeout of message is #{msg.timeUntilTimeout()}"
setTimeout (-> console.log "timeout = #{msg.timeUntilTimeout()}"), 5000
msg.finish()
c.on NSQDConnection.FINISHED, ->
c.setRdy 1
c.on NSQDConnection.READY, ->
console.log "Callback [ready]: Set RDY to 100"
c.setRdy 10
c.on NSQDConnection.CLOSED, ->
console.log "Callback [closed]: Lost connection to nsqd"
c.on NSQDConnection.ERROR, (err) ->
console.log "Callback [error]: #{err}"
c.on NSQDConnection.BACKOFF, ->
console.log "Callback [backoff]: RDY 0"
c.setRdy 0
setTimeout (-> c.setRdy 100; console.log 'RDY 100'), 10 * 1000
c.connect()
*/
NSQDConnection = (function(superClass) {
extend(NSQDConnection, superClass);
NSQDConnection.BACKOFF = 'backoff';
NSQDConnection.CONNECTED = 'connected';
NSQDConnection.CLOSED = 'closed';
NSQDConnection.CONNECTION_ERROR = 'connection_error';
NSQDConnection.ERROR = 'error';
NSQDConnection.FINISHED = 'finished';
NSQDConnection.MESSAGE = 'message';
NSQDConnection.REQUEUED = 'requeued';
NSQDConnection.READY = 'ready';
function NSQDConnection(nsqdHost1, nsqdPort1, topic1, channel, options) {
var connId;
this.nsqdHost = nsqdHost1;
this.nsqdPort = nsqdPort1;
this.topic = topic1;
this.channel = channel;
if (options == null) {
options = {};
}
NSQDConnection.__super__.constructor.apply(this, arguments);
connId = this.id().replace(':', '/');
this.debug = Debug("nsqjs:reader:" + this.topic + "/" + this.channel + ":conn:" + connId);
this.config = new ConnectionConfig(options);
this.config.validate();
this.frameBuffer = new FrameBuffer();
this.statemachine = this.connectionState();
this.maxRdyCount = 0;
this.msgTimeout = 0;
this.maxMsgTimeout = 0;
this.nsqdVersion = null;
this.lastMessageTimestamp = null;
this.lastReceivedTimestamp = null;
this.conn = null;
this.identifyTimeoutId = null;
this.messageCallbacks = [];
}
NSQDConnection.prototype.id = function() {
return this.nsqdHost + ":" + this.nsqdPort;
};
NSQDConnection.prototype.connectionState = function() {
return this.statemachine || new ConnectionState(this);
};
NSQDConnection.prototype.connect = function() {
this.statemachine.raise('connecting');
return process.nextTick((function(_this) {
return function() {
_this.conn = net.connect(_this.nsqdPort, _this.nsqdHost, function() {
_this.statemachine.raise('connected');
_this.emit(NSQDConnection.CONNECTED);
return _this.identifyTimeoutId = setTimeout(_this.identifyTimeout.bind(_this), 5000);
});
return _this.registerStreamListeners(_this.conn);
};
})(this));
};
NSQDConnection.prototype.registerStreamListeners = function(conn) {
conn.on('data', (function(_this) {
return function(data) {
return _this.receiveRawData(data);
};
})(this));
conn.on('error', (function(_this) {
return function(err) {
_this.statemachine.goto('CLOSED');
return _this.emit('connection_error', err);
};
})(this));
return conn.on('close', (function(_this) {
return function(err) {
return _this.statemachine.raise('close');
};
})(this));
};
NSQDConnection.prototype.startTLS = function(callback) {
var event, i, len, options, ref1, tlsConn;
ref1 = ['data', 'error', 'close'];
for (i = 0, len = ref1.length; i < len; i++) {
event = ref1[i];
this.conn.removeAllListeners(event);
}
options = {
socket: this.conn,
rejectUnauthorized: this.config.tlsVerification
};
tlsConn = tls.connect(options, (function(_this) {
return function() {
_this.conn = tlsConn;
return typeof callback === "function" ? callback() : void 0;
};
})(this));
return this.registerStreamListeners(tlsConn);
};
NSQDConnection.prototype.startDeflate = function(level) {
this.inflater = zlib.createInflateRaw({
flush: zlib.Z_SYNC_FLUSH
});
this.deflater = zlib.createDeflateRaw({
level: level,
flush: zlib.Z_SYNC_FLUSH
});
return this.reconsumeFrameBuffer();
};
NSQDConnection.prototype.startSnappy = function() {
this.inflater = new UnsnappyStream();
this.deflater = new SnappyStream();
return this.reconsumeFrameBuffer();
};
NSQDConnection.prototype.reconsumeFrameBuffer = function() {
var data;
if (this.frameBuffer.buffer && this.frameBuffer.buffer.length) {
data = this.frameBuffer.buffer;
delete this.frameBuffer.buffer;
return this.receiveRawData(data);
}
};
NSQDConnection.prototype.setRdy = function(rdyCount) {
return this.statemachine.raise('ready', rdyCount);
};
NSQDConnection.prototype.receiveRawData = function(data) {
if (!this.inflater) {
return this.receiveData(data);
} else {
return this.inflater.write(data, (function(_this) {
return function() {
var uncompressedData;
uncompressedData = _this.inflater.read();
if (uncompressedData) {
return _this.receiveData(uncompressedData);
}
};
})(this));
}
};
NSQDConnection.prototype.receiveData = function(data) {
var frame, frameId, payload, results;
this.lastReceivedTimestamp = Date.now();
this.frameBuffer.consume(data);
results = [];
while (frame = this.frameBuffer.nextFrame()) {
frameId = frame[0], payload = frame[1];
switch (frameId) {
case wire.FRAME_TYPE_RESPONSE:
results.push(this.statemachine.raise('response', payload));
break;
case wire.FRAME_TYPE_ERROR:
results.push(this.statemachine.goto('ERROR', new Error(payload.toString())));
break;
case wire.FRAME_TYPE_MESSAGE:
this.lastMessageTimestamp = this.lastReceivedTimestamp;
results.push(this.statemachine.raise('consumeMessage', this.createMessage(payload)));
break;
default:
results.push(void 0);
}
}
return results;
};
NSQDConnection.prototype.identify = function() {
var i, identify, key, len, longName, removableKeys, shortName;
longName = os.hostname();
shortName = longName.split('.')[0];
identify = {
client_id: this.config.clientId || shortName,
deflate: this.config.deflate,
deflate_level: this.config.deflateLevel,
feature_negotiation: true,
heartbeat_interval: this.config.heartbeatInterval * 1000,
long_id: longName,
msg_timeout: this.config.messageTimeout,
output_buffer_size: this.config.outputBufferSize,
output_buffer_timeout: this.config.outputBufferTimeout,
sample_rate: this.config.sampleRate,
short_id: shortName,
snappy: this.config.snappy,
tls_v1: this.config.tls,
user_agent: "nsqjs/" + version
};
removableKeys = ['msg_timeout', 'output_buffer_size', 'output_buffer_timeout', 'sample_rate'];
for (i = 0, len = removableKeys.length; i < len; i++) {
key = removableKeys[i];
if (identify[key] === null) {
delete identify[key];
}
}
return identify;
};
NSQDConnection.prototype.identifyTimeout = function() {
return this.statemachine.goto('ERROR', new Error('Timed out identifying with nsqd'));
};
NSQDConnection.prototype.clearIdentifyTimeout = function() {
clearTimeout(this.identifyTimeoutId);
return this.identifyTimeoutId = null;
};
NSQDConnection.prototype.createMessage = function(msgPayload) {
var msg, msgComponents;
msgComponents = wire.unpackMessage(msgPayload);
msg = (function(func, args, ctor) {
ctor.prototype = func.prototype;
var child = new ctor, result = func.apply(child, args);
return Object(result) === result ? result : child;
})(Message, slice.call(msgComponents).concat([this.config.requeueDelay], [this.msgTimeout], [this.maxMsgTimeout]), function(){});
this.debug("Received message [" + msg.id + "] [attempts: " + msg.attempts + "]");
msg.on(Message.RESPOND, (function(_this) {
return function(responseType, wireData) {
_this.write(wireData);
if (responseType === Message.FINISH) {
_this.debug("Finished message [" + msg.id + "] [timedout=" + (msg.timedout === true) + ", elapsed=" + (Date.now() - msg.receivedOn) + "ms, touch_count=" + msg.touchCount + "]");
return _this.emit(NSQDConnection.FINISHED);
} else if (responseType === Message.REQUEUE) {
_this.debug("Requeued message [" + msg.id + "]");
return _this.emit(NSQDConnection.REQUEUED);
}
};
})(this));
msg.on(Message.BACKOFF, (function(_this) {
return function() {
return _this.emit(NSQDConnection.BACKOFF);
};
})(this));
return msg;
};
NSQDConnection.prototype.write = function(data) {
if (this.deflater) {
return this.deflater.write(data, (function(_this) {
return function() {
return _this.conn.write(_this.deflater.read());
};
})(this));
} else {
return this.conn.write(data);
}
};
NSQDConnection.prototype.destroy = function() {
return this.conn.destroy();
};
return NSQDConnection;
})(EventEmitter);
ConnectionState = (function(superClass) {
extend(ConnectionState, superClass);
function ConnectionState(conn1) {
this.conn = conn1;
ConnectionState.__super__.constructor.call(this, {
autostart: true,
initial_state: 'INIT',
sync_goto: true
});
this.identifyResponse = null;
}
ConnectionState.prototype.log = function(message) {
if (this.current_state_name !== 'INIT') {
this.conn.debug("" + this.current_state_name);
}
if (message) {
return this.conn.debug(message);
}
};
ConnectionState.prototype.afterIdentify = function() {
return 'SUBSCRIBE';
};
ConnectionState.prototype.states = {
INIT: {
connecting: function() {
return this.goto('CONNECTING');
}
},
CONNECTING: {
connected: function() {
return this.goto('CONNECTED');
}
},
CONNECTED: {
Enter: function() {
return this.goto('SEND_MAGIC_IDENTIFIER');
}
},
SEND_MAGIC_IDENTIFIER: {
Enter: function() {
this.conn.write(wire.MAGIC_V2);
return this.goto('IDENTIFY');
}
},
IDENTIFY: {
Enter: function() {
var identify;
identify = this.conn.identify();
this.conn.debug(identify);
this.conn.write(wire.identify(identify));
return this.goto('IDENTIFY_RESPONSE');
}
},
IDENTIFY_RESPONSE: {
response: function(data) {
if (data.toString() === 'OK') {
data = JSON.stringify({
max_rdy_count: 2500,
max_msg_timeout: 15 * 60 * 1000,
msg_timeout: 60 * 1000
});
}
this.identifyResponse = JSON.parse(data);
this.conn.debug(this.identifyResponse);
this.conn.maxRdyCount = this.identifyResponse.max_rdy_count;
this.conn.maxMsgTimeout = this.identifyResponse.max_msg_timeout;
this.conn.msgTimeout = this.identifyResponse.msg_timeout;
this.conn.nsqdVersion = this.identifyResponse.version;
this.conn.clearIdentifyTimeout();
if (this.identifyResponse.tls_v1) {
return this.goto('TLS_START');
}
return this.goto('IDENTIFY_COMPRESSION_CHECK');
}
},
IDENTIFY_COMPRESSION_CHECK: {
Enter: function() {
var deflate, ref1, snappy;
ref1 = this.identifyResponse, deflate = ref1.deflate, snappy = ref1.snappy;
if (deflate) {
return this.goto('DEFLATE_START', this.identifyResponse.deflate_level);
}
if (snappy) {
return this.goto('SNAPPY_START');
}
return this.goto('AUTH');
}
},
TLS_START: {
Enter: function() {
this.conn.startTLS();
return this.goto('TLS_RESPONSE');
}
},
TLS_RESPONSE: {
response: function(data) {
if (data.toString() === 'OK') {
return this.goto('IDENTIFY_COMPRESSION_CHECK');
} else {
return this.goto('ERROR', new Error('TLS negotiate error with nsqd'));
}
}
},
DEFLATE_START: {
Enter: function(level) {
this.conn.startDeflate(level);
return this.goto('COMPRESSION_RESPONSE');
}
},
SNAPPY_START: {
Enter: function() {
this.conn.startSnappy();
return this.goto('COMPRESSION_RESPONSE');
}
},
COMPRESSION_RESPONSE: {
response: function(data) {
if (data.toString() === 'OK') {
return this.goto('AUTH');
} else {
return this.goto('ERROR', new Error('Bad response when enabling compression'));
}
}
},
AUTH: {
Enter: function() {
if (!this.conn.config.authSecret) {
return this.goto(this.afterIdentify());
}
this.conn.write(wire.auth(this.conn.config.authSecret));
return this.goto('AUTH_RESPONSE');
}
},
AUTH_RESPONSE: {
response: function(data) {
this.conn.auth = JSON.parse(data);
return this.goto(this.afterIdentify());
}
},
SUBSCRIBE: {
Enter: function() {
this.conn.write(wire.subscribe(this.conn.topic, this.conn.channel));
return this.goto('SUBSCRIBE_RESPONSE');
}
},
SUBSCRIBE_RESPONSE: {
response: function(data) {
if (data.toString() === 'OK') {
this.goto('READY_RECV');
return this.conn.emit(NSQDConnection.READY);
}
}
},
READY_RECV: {
consumeMessage: function(msg) {
return this.conn.emit(NSQDConnection.MESSAGE, msg);
},
response: function(data) {
if (data.toString() === '_heartbeat_') {
return this.conn.write(wire.nop());
}
},
ready: function(rdyCount) {
if (rdyCount > this.conn.maxRdyCount) {
rdyCount = this.conn.maxRdyCount;
}
return this.conn.write(wire.ready(rdyCount));
},
close: function() {
return this.goto('CLOSED');
}
},
READY_SEND: {
Enter: function() {
return this.conn.emit(NSQDConnection.READY);
},
produceMessages: function(data) {
var callback, msgs, topic;
topic = data[0], msgs = data[1], callback = data[2];
this.conn.messageCallbacks.push(callback);
if (!_.isArray(msgs)) {
throw new Error('Expect an array of messages to produceMessages');
}
if (msgs.length === 1) {
return this.conn.write(wire.pub(topic, msgs[0]));
} else {
return this.conn.write(wire.mpub(topic, msgs));
}
},
response: function(data) {
var cb;
switch (data.toString()) {
case 'OK':
cb = this.conn.messageCallbacks.shift();
return typeof cb === "function" ? cb(null) : void 0;
case '_heartbeat_':
return this.conn.write(wire.nop());
}
},
close: function() {
return this.goto('CLOSED');
}
},
ERROR: {
Enter: function(err) {
var cb, errorCode, ref1;
cb = this.conn.messageCallbacks.shift();
if (typeof cb === "function") {
cb(err);
}
this.conn.emit(NSQDConnection.ERROR, err);
if (!_.isString(err)) {
err = err.toString();
}
errorCode = (ref1 = err.split(/\s+/)) != null ? ref1[1] : void 0;
if (errorCode === 'E_REQ_FAILED' || errorCode === 'E_FIN_FAILED' || errorCode === 'E_TOUCH_FAILED') {
return this.goto('READY_RECV');
} else {
return this.goto('CLOSED');
}
},
close: function() {
return this.goto('CLOSED');
}
},
CLOSED: {
Enter: function() {
var cb, err, i, len, ref1;
if (!this.conn) {
return;
}
err = new Error('nsqd connection closed');
ref1 = this.conn.messageCallbacks;
for (i = 0, len = ref1.length; i < len; i++) {
cb = ref1[i];
if (typeof cb === "function") {
cb(err);
}
}
this.conn.messageCallbacks = [];
this.disable();
this.conn.destroy();
this.conn.emit(NSQDConnection.CLOSED);
return delete this.conn;
},
close: function() {}
}
};
ConnectionState.prototype.transitions = {
'*': {
'*': function(data, callback) {
this.log();
return callback(data);
},
CONNECTED: function(data, callback) {
this.log();
return callback(data);
},
ERROR: function(err, callback) {
this.log("" + err);
return callback(err);
}
}
};
return ConnectionState;
})(NodeState);
/*
c = new NSQDConnectionWriter '127.0.0.1', 4150, 30
c.connect()
c.on NSQDConnectionWriter.CLOSED, ->
console.log "Callback [closed]: Lost connection to nsqd"
c.on NSQDConnectionWriter.ERROR, (err) ->
console.log "Callback [error]: #{err}"
c.on NSQDConnectionWriter.READY, ->
c.produceMessages 'sample_topic', ['first message']
c.produceMessages 'sample_topic', ['second message', 'third message']
c.destroy()
*/
WriterNSQDConnection = (function(superClass) {
extend(WriterNSQDConnection, superClass);
function WriterNSQDConnection(nsqdHost, nsqdPort, options) {
if (options == null) {
options = {};
}
WriterNSQDConnection.__super__.constructor.call(this, nsqdHost, nsqdPort, null, null, options);
this.debug = Debug("nsqjs:writer:conn:" + nsqdHost + "/" + nsqdPort);
}
WriterNSQDConnection.prototype.connectionState = function() {
return this.statemachine || new WriterConnectionState(this);
};
WriterNSQDConnection.prototype.produceMessages = function(topic, msgs, callback) {
return this.statemachine.raise('produceMessages', [topic, msgs, callback]);
};
return WriterNSQDConnection;
})(NSQDConnection);
WriterConnectionState = (function(superClass) {
extend(WriterConnectionState, superClass);
function WriterConnectionState() {
return WriterConnectionState.__super__.constructor.apply(this, arguments);
}
WriterConnectionState.prototype.afterIdentify = function() {
return 'READY_SEND';
};
return WriterConnectionState;
})(ConnectionState);
module.exports = {
NSQDConnection: NSQDConnection,
ConnectionState: ConnectionState,
WriterNSQDConnection: WriterNSQDConnection,
WriterConnectionState: WriterConnectionState
};