Best Practices for reconnecting a disconnected SignalR client (JS) - signalr

I'd like to improve the resilience of my clientside implementation of a signalR client.
Currently, I do this:
hub.server.sendClientNotification(string, appSettings.username());
However occasionally, an exception related to connectivity is thrown because either the server is not responding or the client's internet connection has become unusable.
I'd like to solve this by queuing up requests and then doing something like this:
try {
// pop from queue
hub.server.sendClientNotification(string, appSettings.username());
// success - discard element
} catch (e) {
// requeue element
}
With this kind of implementation, would I need to re-initialize my signalR connection using $.connection.hub.start between disconnects, or can I just continue attempting hub transmits within an interval?
This is what I'm proposing:
var hub = null;
const internalQueue = [];
const states = {
connecting: 0, connected: 1, reconnecting: 2, disconnected: 4
}
const signalrModule = {};
var isInitialized = false;
const connectSignalR = function () {
return new Promise(function (resolve, reject) {
if ($.connection.hub.state == states.connected) {
resolve();
} else {
window.hubReady = $.connection.hub.start({ transport: ["serverSentEvents", "foreverFrame", "longPolling"] });
window.hubReady.done(function () {
isInitialized = true;
resolve();
});
window.onbeforeunload = function (e) {
$.connection.hub.stop();
};
}
})
}
signalrModule.init = function (handleNotification) {
hub = $.connection.appHub;
hub.client.clientNotification = handleNotification;
$.connection.hub.qs = {
"username": appSettings.username()
};
connectSignalR();
}
const tryEmptyQueue = function () {
connectSignalR().then(function() {
if (isInitialized) {
var continueTrying = true;
while (internalQueue.length && continueTrying) {
const nextMessage = internalQueue.shift();
try {
hub.server.sendClientNotification(nextMessage, appSettings.username());
} catch (e) {
internalQueue.push(nextMessage);
continueTrying = false;
}
}
}
})
}
signalrModule.sendClientNotification = function (message) {
if (typeof message != "string") {
message = JSON.stringify(message);
}
if (isInitialized) {
try {
hub.server.sendClientNotification(message, appSettings.username());
tryEmptyQueue();
} catch (e) {
logger.log("SignalR disconnected; queuing request", logger.logLevels.warning);
internalQueue.push(message);
}
} else {
internalQueue.push(message);
};
}
const internalQueueInterval = setInterval(function () {
tryEmptyQueue();
}, 5000);
return signalrModule;

Follow the documentation
In some applications you might want to automatically re-establish a connection after it has been lost and the attempt to reconnect has timed out. To do that, you can call the Start method from your Closed event handler (disconnected event handler on JavaScript clients). You might want to wait a period of time before calling Start in order to avoid doing this too frequently when the server or the physical connection are unavailable. The following code sample is for a JavaScript client using the generated proxy.
$.connection.hub.disconnected(function() {
setTimeout(function() {
$.connection.hub.start();
}, 5000); // Restart connection after 5 seconds.
});

Related

SignalR connection closed but still works after angularjs scope destroy

I have a SignalR hub proxy in angularjs factory like this.
var app = angular.module('app');
app.factory("signalRHubProxy", ['$rootScope', "$timeout", function ($rootScope, $timeout) {
function signalRHubProxyFactory(serverUrl, hubName) {
var connection = $.hubConnection(serverUrl);
var proxy = connection.createHubProxy(hubName);
connection.disconnected(function () {
$timeout(function () {
connection.start();
}, 5000)
});
return {
on: function (eventName, callback) {
proxy.on(eventName, function (result) {
$rootScope.$apply(function () {
if (callback) {
callback(result);
}
});
});
connection.start();
},
stop: function () {
connection.stop();
},
connection: connection
};
}
return signalRHubProxyFactory;
}]);
I used connection timeout because sometimes server does not listen, so I can retry.
I am using this factory in my controller:
app.controller("directiveController", function($scope, signalRHubProxy){
var signalRProxy = signalRHubProxy(
"url",
"hubname");
signalRHubProxy.on("datapush", function(data){
});
$scope.$destroy(function(){
signalRHubProxy.stop();
??? how to kill signalr hub
})
})
But when I remove my directive, signalR still works.
I believe when you call stop(), it calls disconnected event, which will just start the connection again. You might need to check if the disconnection was intentional (with a simple boolean variable), then just avoid restarting the connection:
app.factory("signalRHubProxy", ['$rootScope', "$timeout", function ($rootScope, $timeout) {
function signalRHubProxyFactory(serverUrl, hubName) {
var connection = $.hubConnection(serverUrl);
var proxy = connection.createHubProxy(hubName);
var manualStop = false;
connection.connected(function () {
manualStop = false; // reset variable if connected
});
connection.disconnected(function () {
if (manualStop)
return;
$timeout(function () {
connection.start();
}, 5000)
});
return {
on: function (eventName, callback) {
proxy.on(eventName, function (result) {
$rootScope.$apply(function () {
if (callback) {
callback(result);
}
});
});
connection.start();
},
stop: function () {
manualStop = true;
connection.stop();
},
connection: connection
};
}
return signalRHubProxyFactory;
}]);
Also, make sure $scope.$destroy is being called.
Some people recommend calling $.connection.hub.stop(); instead of connection.stop();, although I don't really know whether it has some significant difference.

Meteor.call and server methods not working properly

I have this code on my meteor app:
// client side
Template.lead.events({
'submit .insertExternalAccountForm': function (event) {
event.preventDefault();
Session.set('mcsStatus', 'Creating external account ...');
var target = {
code: event.target.code.value,
leadId: event.target.leadId.value,
name: event.target.name.value,
username: event.target.username.value,
password: event.target.password.value,
searchSourceId: event.target.searchSourceId.value,
clientId: event.target.clientId.value,
clientUserId: event.target.clientUserId.value
};
var noFormError = true;
if (target.username.length === 0) {
Session.set("username_error", "Field must not be empty");
noFormError = false;
} else {
Session.set("username_error", null);
}
if (target.password.length === 0) {
Session.set("password_error", "password must not be empty");
noFormError = false;
} else {
Session.set("password_error", null);
}
if (!noFormError) {
return noFormError;
}
Meteor.call('createExternalAccount', target, function (err, res) {
if (err) {
console.error(err);
}
console.log('in meteor call');
Router.go('/' + res.domain + '/' + res.externalId);
});
}
});
//server side
var createExternalAccountSync = function (query, external) {
return models.SearchSources.findOne(query).exec()
.then(function (searchsource) {
external.domain = searchsource.source;
var emr = searchsource.source.split('-');
return models.Organization.findOne({externalId: emr[2]}).exec();
}).then(function (org) {
console.log('after org');
external.organizationId = org._id;
return models.AppUser.findOne({clientId: external.clientId, externalId: external.clientUserId }).exec();
}).then(function (user) {
console.log('after app user');
external.userId = user._id;
external.userIds = [user._id];
return new Promise(function (resolve,reject) {
console.log('saveOrUpdate');
models.ExternalAccount.saveOrUpdate(external, function (err, newE) {
if (err) {
console.error(err)
reject(err);
}
resolve(newE)
});
});
})
.catch(function (e) {
console.error(e);
throw new Meteor.Error(e);
});
};
Meteor.methods({'createExternalAccount': function (data) {
var query = {};
var newExternalAccount = new models.ExternalAccount();
newExternalAccount.username = data.username;
newExternalAccount.password = data.password;
newExternalAccount.externalId = data.username;
newExternalAccount.name = data.name;
newExternalAccount.clientId = data.clientId;
newExternalAccount.clientUserId = data.clientUserId;
newExternalAccount._metadata = { leadId: data.leadId };
if (data.code === 'f') {
query.searchSourceId = '5744f0925db77e3e42136924';
} else {
query.searchSourceId = data.searchSourceId;
}
newExternalAccount.searchSourceId = query.searchSourceId;
console.log('creating external account')
createExternalAccountSync(query, newExternalAccount)
.then(function (external) {
console.log('should return to meteor call');
return external;
})
.catch(function (e) {
console.error(e);
throw new Meteor.Error(e);
});
}
});
The problem that I'm having is that the code on the server side, while it's being called properly, is not triggering the client side meteor.call, there's no console.log output or anything. I believe that the Meteor.wrapAsync method is properly used, but still not showing anything on the client side, and not in fact redirecting where I want the user to go after form submission.
UPDATE
The code has being updated to the newest form, but now I'm getting a weird error on the client, and its actually because the meteor.call method on the template returns neither error or result
Exception in delivering result of invoking 'createExternalAccount': http://localhost:3000/app/app.js?hash=c61e16cef6474ef12f0289b3f8662d8a83a184ab:540:40
http://localhost:3000/packages/meteor.js?hash=ae8b8affa9680bf9720bd8f7fa112f13a62f71c3:1105:27
_maybeInvokeCallback#http://localhost:3000/packages/ddp-client.js?hash=27502404fad7fc072e57e8b0b6719f40d92709c7:3557:21
receiveResult#http://localhost:3000/packages/ddp-client.js?hash=27502404fad7fc072e57e8b0b6719f40d92709c7:3577:30
_livedata_result#http://localhost:3000/packages/ddp-client.js?hash=27502404fad7fc072e57e8b0b6719f40d92709c7:4742:22
onMessage#http://localhost:3000/packages/ddp-client.js?hash=27502404fad7fc072e57e8b0b6719f40d92709c7:3385:28
http://localhost:3000/packages/ddp-client.js?hash=27502404fad7fc072e57e8b0b6719f40d92709c7:2736:19
forEach#[native code]
forEach#http://localhost:3000/packages/underscore.js?hash=27b3d669b418de8577518760446467e6ff429b1e:149:18
onmessage#http://localhost:3000/packages/ddp-client.js?hash=27502404fad7fc072e57e8b0b6719f40d92709c7:2735:15
dispatchEvent#http://localhost:3000/packages/ddp-client.js?hash=27502404fad7fc072e57e8b0b6719f40d92709c7:175:27
_dispatchMessage#http://localhost:3000/packages/ddp-client.js?hash=27502404fad7fc072e57e8b0b6719f40d92709c7:1160:23
_didMessage#http://localhost:3000/packages/ddp-client.js?hash=27502404fad7fc072e57e8b0b6719f40d92709c7:1218:34
onmessage#http://localhost:3000/packages/ddp-client.js?hash=27502404fad7fc072e57e8b0b6719f40d92709c7:1365:28
By the code you provided,it could be because you are calling different method.
You defined 'createAccount' but on client side you are calling 'createExternalAccount'

Meteor External API calls works but client gets Undefined

Client Side:
Template.storeInfo.events({
'click #btn-user-data': function(e) {
Meteor.call('callApi', function(err, data) {
$('#result').text(JSON.stringify(data, undefined, 4));
console.log(data); //Not working!!
});
}
});
Server Side:
storeApi.prototype.retrieve = function(endpoint) {
try {
var gotiye = HTTP.get(endpoint);
console.log(gotiye); //Works!
return gotiye;
} catch (err) {
throw new Error("Failed to fetch call GET retrieve from store. " + err.message);
}
};
storeApi.prototype.getStoreInfo = function() {
var url = this.rootApiUrl.replace(this.endpointToken,
this.endpoints.store);
this.retrieve(url);
};
Meteor.methods({
callApi: function() {
var stnvy = new storeApi(Meteor.user().services.store.accessToken);
var data = stnvy.getStoreInfo();
return data;
}
});
Why it works on server side but impossible to use in client side? Is collection the only way to use this?
Forgot to return it at getStoreInfo function return this.retrieve(url); and it works! :)

how to push data back to client in meteor?

I have to make a aggregate query to DB when the user click on a button, however I don't know how to return that result back to the client since I'm doing an asynchronous request, this is part of my code:
//Server side
Meteor.startup(function() {
Meteor.methods({
getAllTotals: function (query){
var db = MongoInternals.defaultRemoteCollectionDriver().mongo.db;
var error = result = match = pipeline = '';
var group = {
$group: {
_id: null,
wall_clock: {
"$sum": "$wall_clock"
},
mem:{
"$sum": "$mem"
},
cpu:{
"$sum": "$cpu"
},
io:{
"$sum": "$io"
},
vmem:{
"$sum": "$vmem"
},
maxvmem:{
"$sum": "maxvmem"
}
}
};
if(typeof query.submission_time !== "undefined"){
match = {"$match": {submission_time: query.submission_time}};
pipeline = [match, group];
}else{
pipeline = [group];
}
db.collection("GE_qstat_job_monitor").aggregate(
pipeline,
Meteor.bindEnvironment(
function (error, result){
console.log(result); // <<--- this is OK!
},
function(error) {
Meteor._debug( "Error doing aggregation: " + error);
}
)
);
return result; // <<--- this is empty
}
});
}
any suggestion? :-)
Short answer:
Solution you can find here:
How to get an async data in a function with Meteor
Detailed answer
using Meteor._wrapAsync
var aggregateTotal = function(callback){
var db = MongoInternals.defaultRemoteCollectionDriver().mongo.db;
// ...
db.collection("GE_qstat_job_monitor").aggregate(
pipeline,
function (error, result){
if(error){
callback(error);
}else{
callback(null, result);
}
}
);
}
var aggregateTotalsSync = Meteor._wrapAsync(aggregateTotal);
Meteor.methods({
'getAllTotals': function(){
var result;
try{
result = aggregateTotalsSync();
}catch(e){
console.log("getAllTotals method returned error : " + e);
}finally{
return result;
}
}
});
using Futures (meteorPad example)
//Server side
Meteor.startup(function() {
var Future = Npm.require('fibers/future');
Meteor.methods({
getAllTotals: function (query){
var fut = new Future();
var db = MongoInternals.defaultRemoteCollectionDriver().mongo.db;
// ...
db.collection("GE_qstat_job_monitor").aggregate(
pipeline,
Meteor.bindEnvironment(
function (error, result){
if(error){
fut.throw(error);
}else{
fut.return(result)
}
},
function (exception){
// caught exception is passed to this callback
fut.throw(exception);
}
)
);
return fut.wait();
}
});
}
Easy but a bit dirty way (but not so much if you think well about your architecture) -> send back the result trough Mongo.
You can even do it without Meteor.methods, with the request creation inserted in the database on the client, an observer on the server that check it and does the async task, and then write back the result in the database.

How to set a timeout on a http.request() in Node?

I'm trying to set a timeout on an HTTP client that uses http.request with no luck. So far what I did is this:
var options = { ... }
var req = http.request(options, function(res) {
// Usual stuff: on(data), on(end), chunks, etc...
}
/* This does not work TOO MUCH... sometimes the socket is not ready (undefined) expecially on rapid sequences of requests */
req.socket.setTimeout(myTimeout);
req.socket.on('timeout', function() {
req.abort();
});
req.write('something');
req.end();
Any hints?
2019 Update
There are various ways to handle this more elegantly now. Please see some other answers on this thread. Tech moves fast so answers can often become out of date fairly quickly. My answer will still work but it's worth looking at alternatives as well.
2012 Answer
Using your code, the issue is that you haven't waited for a socket to be assigned to the request before attempting to set stuff on the socket object. It's all async so:
var options = { ... }
var req = http.request(options, function(res) {
// Usual stuff: on(data), on(end), chunks, etc...
});
req.on('socket', function (socket) {
socket.setTimeout(myTimeout);
socket.on('timeout', function() {
req.abort();
});
});
req.on('error', function(err) {
if (err.code === "ECONNRESET") {
console.log("Timeout occurs");
//specific error treatment
}
//other error treatment
});
req.write('something');
req.end();
The 'socket' event is fired when the request is assigned a socket object.
Just to clarify the answer above:
Now it is possible to use timeout option and the corresponding request event:
// set the desired timeout in options
const options = {
//...
timeout: 3000,
};
// create a request
const request = http.request(options, response => {
// your callback here
});
// use its "timeout" event to abort the request
request.on('timeout', () => {
request.destroy();
});
See the docs:
At this moment there is a method to do this directly on the request object:
request.setTimeout(timeout, function() {
request.abort();
});
This is a shortcut method that binds to the socket event and then creates the timeout.
Reference: Node.js v0.8.8 Manual & Documentation
The Rob Evans anwser works correctly for me but when I use request.abort(), it occurs to throw a socket hang up error which stays unhandled.
I had to add an error handler for the request object :
var options = { ... }
var req = http.request(options, function(res) {
// Usual stuff: on(data), on(end), chunks, etc...
}
req.on('socket', function (socket) {
socket.setTimeout(myTimeout);
socket.on('timeout', function() {
req.abort();
});
}
req.on('error', function(err) {
if (err.code === "ECONNRESET") {
console.log("Timeout occurs");
//specific error treatment
}
//other error treatment
});
req.write('something');
req.end();
There is simpler method.
Instead of using setTimeout or working with socket directly,
We can use 'timeout' in the 'options' in client uses
Below is code of both server and client, in 3 parts.
Module and options part:
'use strict';
// Source: https://github.com/nodejs/node/blob/master/test/parallel/test-http-client-timeout-option.js
const assert = require('assert');
const http = require('http');
const options = {
host: '127.0.0.1', // server uses this
port: 3000, // server uses this
method: 'GET', // client uses this
path: '/', // client uses this
timeout: 2000 // client uses this, timesout in 2 seconds if server does not respond in time
};
Server part:
function startServer() {
console.log('startServer');
const server = http.createServer();
server
.listen(options.port, options.host, function () {
console.log('Server listening on http://' + options.host + ':' + options.port);
console.log('');
// server is listening now
// so, let's start the client
startClient();
});
}
Client part:
function startClient() {
console.log('startClient');
const req = http.request(options);
req.on('close', function () {
console.log("got closed!");
});
req.on('timeout', function () {
console.log("timeout! " + (options.timeout / 1000) + " seconds expired");
// Source: https://github.com/nodejs/node/blob/master/test/parallel/test-http-client-timeout-option.js#L27
req.destroy();
});
req.on('error', function (e) {
// Source: https://github.com/nodejs/node/blob/master/lib/_http_outgoing.js#L248
if (req.connection.destroyed) {
console.log("got error, req.destroy() was called!");
return;
}
console.log("got error! ", e);
});
// Finish sending the request
req.end();
}
startServer();
If you put all the above 3 parts in one file, "a.js", and then run:
node a.js
then, output will be:
startServer
Server listening on http://127.0.0.1:3000
startClient
timeout! 2 seconds expired
got closed!
got error, req.destroy() was called!
Hope that helps.
For me - here is a less confusing way of doing the socket.setTimeout
var request=require('https').get(
url
,function(response){
var r='';
response.on('data',function(chunk){
r+=chunk;
});
response.on('end',function(){
console.dir(r); //end up here if everything is good!
});
}).on('error',function(e){
console.dir(e.message); //end up here if the result returns an error
});
request.on('error',function(e){
console.dir(e); //end up here if a timeout
});
request.on('socket',function(socket){
socket.setTimeout(1000,function(){
request.abort(); //causes error event ↑
});
});
Elaborating on the answer #douwe here is where you would put a timeout on a http request.
// TYPICAL REQUEST
var req = https.get(http_options, function (res) {
var data = '';
res.on('data', function (chunk) { data += chunk; });
res.on('end', function () {
if (res.statusCode === 200) { /* do stuff with your data */}
else { /* Do other codes */}
});
});
req.on('error', function (err) { /* More serious connection problems. */ });
// TIMEOUT PART
req.setTimeout(1000, function() {
console.log("Server connection timeout (after 1 second)");
req.abort();
});
this.abort() is also fine.
You should pass the reference to request like below
var options = { ... }
var req = http.request(options, function(res) {
// Usual stuff: on(data), on(end), chunks, etc...
});
req.setTimeout(60000, function(){
this.abort();
});
req.write('something');
req.end();
Request error event will get triggered
req.on("error", function(e){
console.log("Request Error : "+JSON.stringify(e));
});
Curious, what happens if you use straight net.sockets instead? Here's some sample code I put together for testing purposes:
var net = require('net');
function HttpRequest(host, port, path, method) {
return {
headers: [],
port: 80,
path: "/",
method: "GET",
socket: null,
_setDefaultHeaders: function() {
this.headers.push(this.method + " " + this.path + " HTTP/1.1");
this.headers.push("Host: " + this.host);
},
SetHeaders: function(headers) {
for (var i = 0; i < headers.length; i++) {
this.headers.push(headers[i]);
}
},
WriteHeaders: function() {
if(this.socket) {
this.socket.write(this.headers.join("\r\n"));
this.socket.write("\r\n\r\n"); // to signal headers are complete
}
},
MakeRequest: function(data) {
if(data) {
this.socket.write(data);
}
this.socket.end();
},
SetupRequest: function() {
this.host = host;
if(path) {
this.path = path;
}
if(port) {
this.port = port;
}
if(method) {
this.method = method;
}
this._setDefaultHeaders();
this.socket = net.createConnection(this.port, this.host);
}
}
};
var request = HttpRequest("www.somesite.com");
request.SetupRequest();
request.socket.setTimeout(30000, function(){
console.error("Connection timed out.");
});
request.socket.on("data", function(data) {
console.log(data.toString('utf8'));
});
request.WriteHeaders();
request.MakeRequest();

Resources