Permalink
Browse files

[api test doc] Added reconnection logic to instances of `NsSocket` wi…

…th tests and documentation
  • Loading branch information...
1 parent 309b6b5 commit 1ecaf40ed57f6962b0e259c304301999e2c34c1e @indexzero indexzero committed Oct 15, 2011
Showing with 296 additions and 52 deletions.
  1. +37 −1 README.md
  2. +29 −0 examples/reconnect.js
  3. +150 −51 lib/nssocket.js
  4. +80 −0 test/tcp-reconnect-test.js
View
38 README.md
@@ -20,7 +20,8 @@ With `nssocket` this tedious bookkeeping work is done automatically for you in t
1. Leverages wildcard and namespaced events from [EventEmitter2][0]
2. Automatically serializes messages passed to `.send()` and deserializes messages from `data` events.
-3. Automatically wraps TCP connections with TLS using [a known workaround][1]
+3. Implements default reconnect logic for potentially faulty connections.
+4. Automatically wraps TCP connections with TLS using [a known workaround][1]
## Messages
Messages in `nssocket` are serialized JSON arrays of the following form:
@@ -71,6 +72,41 @@ So get on with it right? _SHOW ME SOME CODE!_
outbound.connect(6785);
```
+### Reconnect
+`nssocket` exposes simple options for enabling reconnection of the underlying socket. By default, these options are disabled. Lets look at a simple example:
+
+``` js
+ var net = require('net'),
+ nssocket = require('nssocket');
+
+ net.createServer(function (socket) {
+ //
+ // Close the underlying socket after `1000ms`
+ //
+ setTimeout(function () {
+ socket.destroy();
+ }, 1000);
+ }).listen(8345);
+
+ //
+ // Create an NsSocket instance with reconnect enabled
+ //
+ var socket = new nssocket.NsSocket({
+ reconnect: true,
+ type: 'tcp4',
+ });
+
+ socket.on('start', function () {
+ //
+ // The socket will emit this event periodically
+ // as it attempts to reconnect
+ //
+ console.dir('start');
+ });
+
+ socket.connect(8345);
+```
+
### Methods
#### socket.send(event, data)
View
29 examples/reconnect.js
@@ -0,0 +1,29 @@
+var net = require('net'),
+ nssocket = require('../lib/nssocket');
+
+net.createServer(function (socket) {
+ //
+ // Close the underlying socket after `1000ms`
+ //
+ setTimeout(function () {
+ socket.destroy();
+ }, 1000);
+}).listen(8345);
+
+//
+// Create an NsSocket instance with reconnect enabled
+//
+var socket = new nssocket.NsSocket({
+ reconnect: true,
+ type: 'tcp4',
+});
+
+socket.on('start', function () {
+ //
+ // The socket will emit this event periodically
+ // as it attempts to reconnect
+ //
+ console.dir('start');
+});
+
+socket.connect(8345);
View
201 lib/nssocket.js
@@ -46,12 +46,27 @@ var NsSocket = exports.NsSocket = function (socket, options) {
var self = this,
startName;
+ //
+ // Setup underlying socket state.
+ //
this.socket = socket;
this.connected = options.connected || socket.writable && socket.readable || false;
//
+ // Setup reconnect options.
+ //
+ this._reconnect = options.reconnect || false;
+ this.retry = {
+ retries: 0,
+ max: options.maxRetries || 10,
+ interval: options.retryInterval || 5000,
+ wait: options.retryInterval || 5000
+ };
+
+ //
// Setup default instance variables.
//
+ this._options = options;
this._type = options.type || 'tcp4',
this._delimiter = options.delimiter || '::';
this._buffer = '';
@@ -62,51 +77,7 @@ var NsSocket = exports.NsSocket = function (socket, options) {
maxListeners: options.maxListeners || 10
});
- //
- // Because of how the code node.js `tls` module works, we have
- // to separate some bindings. The main difference is on
- // connection, some socket activities.
- //
- if (this._type === 'tcp4') {
- startName = 'connect';
- socket.on('data', this._onData.bind(this));
-
- // create a stub for the setKeepAlive functionality
- self.setKeepAlive = function () {
- socket.setKeepAlive.apply(socket, arguments);
- };
- }
- else if (this._type === 'tls') {
- startName = 'secureConnection';
- socket.once('connect', function () {
- socket.cleartext.on('data', self._onData.bind(self));
- });
-
- // create a stub for the setKeepAlive functionality
- self.setKeepAlive = function () {
- socket.socket.setKeepAlive.apply(socket.socket, arguments);
- };
- }
- else {
- // bad arguments, so throw an error
- this.emit('error', new Error('Bad Option Argument [type]'));
- return null;
- }
-
- // make sure we listen to the underlying socket
- socket.on(startName, this._onStart.bind(this));
- socket.on('close', this._onClose.bind(this));
-
- if (socket.socket) {
- //
- // otherwise we get a error passed from net.js
- // they need to backport the fix from v5 to v4
- //
- socket.socket.on('error', this._onError.bind(this));
- }
-
- socket.on('error', this._onError.bind(this));
- socket.on('timeout', this._onIdle.bind(this));
+ this._setup();
};
//
@@ -117,7 +88,8 @@ util.inherits(NsSocket, events2.EventEmitter2);
//
// ### function createServer (options, connectionListener)
// #### @options {Object} **Optional**
-//
+// Creates a new TCP/TLS server which wraps every incoming connection
+// in an instance of `NsSocket`.
//
exports.createServer = function createServer(options, connectionListener) {
if (!connectionListener && typeof options === 'function') {
@@ -174,6 +146,12 @@ NsSocket.prototype.send = function send(event, data, callback) {
}
};
+//
+// ### function data (event, callback)
+// #### @event {Array|string} Namespaced `data` event to listen to
+// #### @callback {function} Continuation to call when the event is raised.
+// Shorthand function for listening to `['data', '*']` events.
+//
NsSocket.prototype.data = function (event, callback) {
if (typeof event === 'string') {
event = event.split(this._delimiter);
@@ -240,7 +218,7 @@ NsSocket.prototype.end = function end() {
};
//
-// ### function connect (/*port, host, callback*/)
+// ### function connect (port[, host, callback])
// A passthrough to the underlying socket's connect function
//
NsSocket.prototype.connect = function connect(/*port, host, callback*/) {
@@ -269,16 +247,129 @@ NsSocket.prototype.connect = function connect(/*port, host, callback*/) {
});
host = host || '127.0.0.1';
- this._connectOpts = [port, host, callback];
-
+ this.port = port || this.port;
+ this.host = host || this.host;
+ args = [this.port, this.host];
+
+ if (callback) {
+ args.push(callback);
+ }
+
if (['tcp4', 'tls'].indexOf(this._type) === -1) {
return this.emit('error', new Error('Unknown Socket Type'));
}
- this.socket.connect.apply(this.socket, arguments);
+ this.socket.connect.apply(this.socket, args);
this.connected = true;
};
+//
+// ### function reconnect ()
+// Attempts to reconnect the current socket on `close` or `error`.
+// This instance will attempt to reconnect until `this.retry.max` is reached,
+// with an interval increasing by powers of 10.
+//
+NsSocket.prototype.reconnect = function reconnect() {
+ var self = this;
+
+ //
+ // Helper function containing the core reconnect logic
+ //
+ function doReconnect() {
+ //
+ // Cleanup and recreate the socket associated
+ // with this instance.
+ //
+ self.retry.waiting = true;
+ self.socket.removeAllListeners();
+ self.socket = common.createSocket(self._options);
+
+ //
+ // Cleanup reconnect logic once the socket connects
+ //
+ self.socket.once('connect', function () {
+ self.retry.waiting = false;
+ self.retry.retries = 0;
+ });
+
+ //
+ // Attempt to reconnect the socket
+ //
+ self._setup();
+ self.connect();
+ }
+
+ //
+ // Helper function which attempts to retry if
+ // it is less than the maximum
+ //
+ function tryReconnect() {
+ self.retry.retries++;
+ if (self.retry.retries >= self.retry.max) {
+ return self.emit('error', new Error('Did not reconnect after maximum retries: ' + self.retry.max));
+ }
+
+ doReconnect();
+ }
+
+ this.retry.wait = this.retry.interval * Math.pow(10, this.retry.retries);
+ setTimeout(tryReconnect, this.retry.wait);
+};
+
+//
+// ### @private function _setup ()
+// Sets up the underlying socket associate with this instance.
+//
+NsSocket.prototype._setup = function () {
+ var self = this,
+ startName;
+
+ //
+ // Because of how the code node.js `tls` module works, we have
+ // to separate some bindings. The main difference is on
+ // connection, some socket activities.
+ //
+ if (this._type === 'tcp4') {
+ startName = 'connect';
+ this.socket.on('data', this._onData.bind(this));
+
+ // create a stub for the setKeepAlive functionality
+ this.setKeepAlive = function () {
+ self.socket.setKeepAlive.apply(self.socket, arguments);
+ };
+ }
+ else if (this._type === 'tls') {
+ startName = 'secureConnection';
+ this.socket.once('connect', function () {
+ self.socket.cleartext.on('data', self._onData.bind(self));
+ });
+
+ // create a stub for the setKeepAlive functionality
+ this.setKeepAlive = function () {
+ self.socket.socket.setKeepAlive.apply(self.socket.socket, arguments);
+ };
+ }
+ else {
+ // bad arguments, so throw an error
+ this.emit('error', new Error('Bad Option Argument [type]'));
+ return null;
+ }
+
+ // make sure we listen to the underlying socket
+ this.socket.on(startName, this._onStart.bind(this));
+ this.socket.on('close', this._onClose.bind(this));
+
+ if (this.socket.socket) {
+ //
+ // otherwise we get a error passed from net.js
+ // they need to backport the fix from v5 to v4
+ //
+ this.socket.socket.on('error', this._onError.bind(this));
+ }
+
+ this.socket.on('error', this._onError.bind(this));
+ this.socket.on('timeout', this._onIdle.bind(this));
+};
//
// ### @private function _onStart ()
@@ -330,6 +421,9 @@ NsSocket.prototype._onClose = function _onClose(hadError) {
}
this.connected = false;
+ if (this._reconnect) {
+ this.reconnect();
+ }
};
//
@@ -339,7 +433,12 @@ NsSocket.prototype._onClose = function _onClose(hadError) {
//
NsSocket.prototype._onError = function _onError(error) {
this.connected = false;
- this.emit('error', error || new Error('An Unknown Error occured'));
+
+ if (!this._reconnect) {
+ return this.emit('error', error || new Error('An Unknown Error occured'));
+ }
+
+ this.reconnect();
};
//
View
80 test/tcp-reconnect-test.js
@@ -0,0 +1,80 @@
+/*
+ * nssocket-test.js : namespace socket unit test for TCP
+ *
+ * (C) 2011, Nodejitsu Inc.
+ *
+ */
+
+var assert = require('assert'),
+ fs = require('fs'),
+ net = require('net'),
+ path = require('path'),
+ vows = require('vows'),
+ NsSocket = require('../lib/nssocket').NsSocket;
+
+var TCP_PORT = 30105;
+
+var tcpServer = net.createServer(),
+ tcpOpt;
+
+tcpOpt = {
+ type : 'tcp4',
+ delimiter: '.}',
+ reconnect: true,
+ retryInterval: 1000
+};
+
+tcpServer.listen(TCP_PORT);
+
+vows.describe('nssocket/tcp/reconnect').addBatch({
+ "When using NsSocket with TCP": {
+ topic: new NsSocket(tcpOpt),
+ "the connect() method": {
+ topic: function (outbound) {
+ var that = this;
+ tcpServer.on('connection', this.callback.bind(null, null, outbound));
+ outbound.connect(TCP_PORT);
+ },
+ "should actually connect": function (_, outbound, inbound) {
+ assert.instanceOf(outbound, NsSocket);
+ assert.instanceOf(inbound, net.Socket);
+ },
+ "when the server closes": {
+ topic: function (outbound, inbound) {
+ outbound.once('close', this.callback.bind(this, null, outbound));
+ tcpServer.close();
+ inbound.destroy();
+ },
+ "and then restarts": {
+ topic: function (outbound) {
+ tcpServer = net.createServer();
+ tcpServer.listen(TCP_PORT);
+ tcpServer.on('connection', this.callback.bind(null, null, outbound));
+ },
+ "the socket should reconnect correctly": function (_, outbound, inbound) {
+ assert.instanceOf(outbound, NsSocket);
+ assert.instanceOf(inbound, net.Socket);
+ },
+ "the on() method": {
+ topic: function (outbound, inbound) {
+ outbound.on('data.}here.}is', this.callback.bind(outbound, null));
+ inbound.write(JSON.stringify(['here', 'is', 'something.']));
+ },
+ "should handle namespaced events": function (_, data) {
+ assert.isArray(this.event);
+ assert.length(this.event, 3);
+ assert.isString(this.event[0]);
+ assert.isString(this.event[1]);
+ assert.isString(this.event[2]);
+ assert.isString(data);
+ assert.equal(this.event[0], 'data');
+ assert.equal(this.event[1], 'here');
+ assert.equal(this.event[2], 'is');
+ assert.equal(data, 'something.');
+ }
+ }
+ }
+ }
+ }
+ }
+}).export(module);

0 comments on commit 1ecaf40

Please sign in to comment.