WebSocket and multiple Dyno's on Heroku Node.js app Using Redis - node.js

I'm building an App deployed to Heroku which uses WebSocket and Redis.
The WebSocket connection is working properly when I use only 1 dyno, but when I scale to 2, then I send event my application do it twice.
const ws = require('ws')
const jwt = require('jsonwebtoken')
const redis = require('redis')
const User = require('../models/user')
function verifyClient (info, callback) {
let token = info.req.headers['sec-websocket-protocol']
if (!token) { callback(false, 401, 'Unauthorized') } else {
jwt.verify(token, Config.APP_SECRET, (err, decoded) => {
if (err) { callback(false, 401, 'Unauthorized') } else {
if (info.req.headers.gameId) { info.req.gameId = info.req.headers.gameId }
info.req.userId = decoded.aud
callback(true)
}
})
}
};
let websocketServer, pub, sub
let clients = {}
let namespaces = {}
exports.initialize = function (httpServer) {
websocketServer = new ws.Server({
server: httpServer,
verifyClient: verifyClient
})
pub = redis.createClient(Config.REDIS_URL, { no_ready_check: true, detect_buffers: true })
pub.auth(Config.REDIS_PASSWORD, function (err) {
if (err) throw err
})
sub = redis.createClient(Config.REDIS_URL, { no_ready_check: true, detect_buffers: true })
sub.auth(Config.REDIS_PASSWORD, function (err) {
if (err) throw err
})
function handleConnection (socket) {
// socket.send(socket.upgradeReq.userId);
socket.userId = socket.upgradeReq.userId // get the user id parsed from the decoded JWT in the middleware
socket.isAlive = true
socket.scope = socket.upgradeReq.url.split('/')[1] // url = "/scope/whatever" => ["", "scope", "whatever"]
console.log('New connection: ' + socket.userId + ', scope: ' + socket.scope)
socket.on('message', (data, flags) => { handleIncomingMessage(socket, data, flags) })
socket.once('close', (code, reason) => { handleClosedConnection(socket, code, reason) })
socket.on('pong', heartbeat)
if (socket.scope === 'gameplay') {
try {
User.findByIdAndUpdate(socket.userId, { $set: { isOnLine: 2, lastSeen: Date.now() } }).select('id').lean()
let key = [socket.userId, socket.scope].join(':')
clients[key] = socket
sub.psubscribe(['dispatch', '*', socket.userId, socket.scope].join(':'))
} catch (e) { console.log(e) }
} else {
console.log('Scope : ' + socket.scope)
}
console.log('Connected Users : ' + Object.keys(clients))
}
function handleIncomingMessage (socket, message, flags) {
let scope = socket.scope
let userId = socket.userId
let channel = ['dispatch', 'in', userId, scope].join(':')
pub.publish(channel, message)
}
function handleClosedConnection (socket, code, reason) {
console.log('Connection with ' + socket.userId + ' closed. Code: ' + code)
if (socket.scope === 'gameplay') {
try {
User.findByIdAndUpdate(socket.userId, { $set: { isOnLine: 1 } }).select('id').lean()
let key = [socket.userId, socket.scope].join(':')
delete clients[key]
} catch (e) {
console.log(e)
}
} else {
console.log('Scope : ' + socket.scope)
}
}
function heartbeat (socket) {
socket.isAlive = true
}
sub.on('pmessage', (pattern, channel, message) => {
let channelComponents = channel.split(':')
let dir = channelComponents[1]
let userId = channelComponents[2]
let scope = channelComponents[3]
if (dir === 'in') {
try {
let handlers = namespaces[scope] || []
if (handlers.length) {
handlers.forEach(h => {
h(userId, message)
})
}
} catch (e) {
console.log(e)
}
} else if (dir === 'out') {
try {
let key = [userId, scope].join(':')
if (clients[key]) { clients[key].send(message) }
} catch (e) {
console.log(e)
}
}
// otherwise ignore
})
websocketServer.on('connection', handleConnection)
}
exports.on = function (scope, callback) {
if (!namespaces[scope]) { namespaces[scope] = [callback] } else { namespaces[scope].push(callback) }
}
exports.send = function (userId, scope, data) {
let channel = ['dispatch', 'out', userId, scope].join(':')
if (typeof (data) === 'object') { data = JSON.stringify(data) } else if (typeof (data) !== 'string') { throw new Error('DispatcherError: Cannot send this type of message ' + typeof (data)) }
pub.publish(channel, data)
}
exports.clients = clients
This is working on localhost.
Please let me know if I need to provide more info or code.Any help on this would be greatly appreciated, thanks in advance!

You have alot of extraneous info in the code you posted, so it's difficult to understand exactly what you mean.
However, if I understand correctly, you currently have multiple worker dyno instances subscribing to the same channels in some kind of pub/sub network. If you don't want all dynos to subscribe to the same channels, you need to put some logic in to make sure that your channels get distributed across dynos.
One simple way to do that might be to use something like the logic described in this answer.
In your case you might be able to use socket.userId as the key to distribute your channels across dynos.

Related

NodeJS API connect SQL Server stopped to return any things when run at 11 times

I've a function like this in app.js, to connect SQL Server to return the recordset, but when I run this function at 11 times, it will be stopped
var config = require('./dbconfig');
const sql = require('mssql');
//create a get product function form the database
async function getProducts(skus, callback) {
let pool;
try {
let result = null;
let pool = await sql.connect(config);
let ps = new sql.PreparedStatement(pool);
// Construct an object of parameters, using arbitrary keys
let skuArray = skus.split(",");
console.log(skuArray);
let paramsObj = skuArray.reduce((obj, val, idx) => {
//trim val single quote
val = val.replace(/'/g, "");
obj[`id${idx}`] = val;
ps.input(`id${idx}`, sql.VarChar(200));
return obj;
}, {});
// Manually insert the params' arbitrary keys into the statement
let stmt = 'select a.area, a.article, a.total_qty, b.Avg_Qty ' +
'FROM [DB].[dbo].[Items] a join [brand].[dbo].[Sales] b on a.article = b.article ' +
'where a.Article in (' + Object.keys(paramsObj).map((o) => {return '#'+o}).join(',') + ')';
ps.prepare(stmt, function(err) {
if (err) {
let response = {"message": "failed","data": []};
return callback(response);
} else {
ps.execute(paramsObj, function(err, data) {
let response;
if (err) {
response = {"message": "failed","data": []};
} else {
let result = data.recordset;
let groupedResult = result.reduce((groupedData, product) => {
let article = product.article;
if (!groupedData[article]) {
groupedData[article] = [];
}
groupedData[article].push(product);
return groupedData;
}, {});
response = {"message": "success","data": groupedResult};
}
return callback(response);
ps.unprepare(function(err) {
if (err) {
console.log(err);
}
sql.close();
pool.close();
});
});
}
});
} catch (error) {
console.log(error);
} finally {
if (pool) {
sql.close();
pool.close();
}
}
}
dbconfig.js
var config = {
user: 'example',
password: 'support#example.com',
server: 'exampledb', // You can use 'localhost\\instance' to connect to named instance
//domain:"example.com",
database: 'example',
options: {
trustServerCertificate: true,
},
max: 20, min: 0, idleTimeoutMillis: 30000
}
//export config
module.exports = config;
Is this related to database connection issue? Anyone know what is the problem?
i've fixed
async function getProducts(skus, callback) {
let pool;
try {
let result = null;
pool = await sql.connect(config);
let ps = new sql.PreparedStatement(pool);
// Construct an object of parameters, using arbitrary keys
let skuArray = skus.split(",");
console.log(skuArray);
let paramsObj = skuArray.reduce((obj, val, idx) => {
//trim val single quote
val = val.replace(/'/g, "");
obj[`id${idx}`] = val;
ps.input(`id${idx}`, sql.VarChar(200));
return obj;
}, {});
// Manually insert the params' arbitrary keys into the statement
let stmt = 'select a.area, a.article, a.total_qty, b.Avg_Qty ' +
'FROM [DB].[dbo].[Items] a join [brand].[dbo].[Sales] b on a.article = b.article ' +
'where a.Article in (' + Object.keys(paramsObj).map((o) => {return '#'+o}).join(',') + ')';
await ps.prepare(stmt);
let data = await ps.execute(paramsObj);
let response;
if (data && data.recordset) {
let result = data.recordset;
let groupedResult = result.reduce((groupedData, product) => {
let article = product.article;
if (!groupedData[article]) {
groupedData[article] = [];
}
groupedData[article].push(product);
return groupedData;
}, {});
response = {"message": "success","data": groupedResult};
} else {
response = {"message": "failed","data": []};
}
ps.unprepare();
callback(response);
} catch (error) {
console.log(error);
let response = {"message": "failed","data": []};
callback(response);
} finally {
if (pool) {
pool.close();
}
sql.close();
}
}

Get and return value outside function - Nodejs

I'm working on some method that have a callback with value, that I need to return and response, but value is null outside callback funciton
How can I solve?
async function createChannel(req) {
let user_id = req.query.user_id;
let chat_name = req.query.chat_name;
var chimesdkmessaging = new AWS.ChimeSDKMessaging({region: region});
var channel = null;
try {
let dateNow = new Date();
const params = {
Name: chat_name,
AppInstanceArn: config.aws_chime_app_istance,
ClientRequestToken: dateNow.getHours().toString() + dateNow.getMinutes().toString(),
ChimeBearer: await AppInstanceUserArn(user_id),
AppInstanceUserArn of the user making the API call.
Mode: 'RESTRICTED',
Privacy: 'PRIVATE'
};
chimesdkmessaging.createChannel(params, function (err, data) {
if (err) {
console.log(err, err.stack);
} // an error occurred
else {
console.log(data); // successful response
console.log('Assegno il canale: ' + data.ChannelArn);
channel = data.ChannelArn; // <----- VALUE I WANT TO RETURN OUTSIDE
}
});
} catch (e) {
return response({error: e}, 500);
}
return response({data: channel},200); // <---- but this channel is null
}
wrap with Promise
let user_id = req.query.user_id;
let chat_name = req.query.chat_name;
var chimesdkmessaging = new AWS.ChimeSDKMessaging({region: region});
let channel = null;
try {
let dateNow = new Date();
const params = {
Name: chat_name,
AppInstanceArn: config.aws_chime_app_istance,
ClientRequestToken: dateNow.getHours().toString() + dateNow.getMinutes().toString(),
ChimeBearer: await AppInstanceUserArn(user_id),
AppInstanceUserArn of the user making the API call.
Mode: 'RESTRICTED',
Privacy: 'PRIVATE'
};
channel = await new Promise((resolve,reject)=>{
chimesdkmessaging.createChannel(params, function (err, data) {
if (err) {
console.log(err, err.stack);
reject(err)
} // an error occurred
else {
console.log(data); // successful response
console.log('Assegno il canale: ' + data.ChannelArn);
resolve(data.ChannelArn)// <----- VALUE I WANT TO RETURN OUTSIDE
}
});
})
} catch (e) {
return response({error: e}, 500);
}
return response({data: channel},200); // <---- but this channel is null
}

use async - await with socket.io nodejs

I'm developing a web application using nodejs socket.io and angular 9. In my backend code I have written sockets in socket.connect.service.
Follows is a socket I'm using
socket.on('request-to-sit-on-the-table', async function (data, callback) { //Previously Register
let table = persistence.getTable(tableToken);
if (typeof table === 'undefined') {
let errMsg = 'This table does not exists or already closed.'
callback(prepareResponse({}, errMsg, new Error(errMsg)));
return;
}
//TODO: Get the displayName from the token.
let guest = await guestUserService.getGuestUserByPlayerToken(JSON.parse(data.userToken));***//Here is the issue***
// let displayName = 'DisplayName-' + guest;
let displayName = 'DisplayName-' + Math.random();
//TODO: Check whether the seat is available
// If the new screen name is not an empty string
let isPlayerInCurrentTable = persistence.isPlayerInCurrentTable(tableToken, userToken);
if (displayName && !isPlayerInCurrentTable) {
var nameExists = false;
let currentPlayersTokenArr = persistence.getTableObjectPlayersToken(table)
for (var token in currentPlayersTokenArr) {
let gamePlayer = persistence.getPlayerPlayer(currentPlayersTokenArr[token])
if (typeof gamePlayer !== "undefined" && gamePlayer.public.name === displayName) {
nameExists = true;
break;
}
}
if (!nameExists) {
//Emit event to inform the admin for requesting to sit on the table.
let ownerToken = persistence.getTableObjectOwnerToken(table);
let ownerSocket = persistence.getPlayerSocket(ownerToken);
ownerSocket.emit('requested-to-sit', {
seat: data.seat,
secondaryUserToken: userToken,
displayName,
numberOfChips: envConfig.defaultNumberOfChips
});
callback(prepareResponse({userToken}, 'Player connected successfully.'));
} else {
callback(prepareResponse({}, 'This name is already taken'));
}
} else {
callback(prepareResponse({}, 'This user has already joined to a game. Try clear caching'));
}
});
In my code I'm getting data from another code in guest.user.service. But I get undefined to the value of "guest"
Follows are the methods I have used in guest.user.service
exports.findById = (id) => {
return new Promise(function(resolve, reject) {
guestUserModel.findById(id, (err, data) =>{
if(err){
reject(err);
} else {
resolve(data);
}
});
});
};
exports.getGuestUserByPlayerToken = (playerToken) => {
var player = playerService.findOne({ token: playerToken })
.then(function (data) {
return self.findById(data.guestUser._id.toString());
})
.then(function (guestUser) {
return guestUser.displayName;
})
.catch(function (err) {
throw new Error(err);
})
};
Although I get my displayName for the return value It is not passed to the "guest" in my socket.Is there any syntax issue to get data as I'm using promises.please help
exports.getGuestUserByPlayerToken = async playerToken => {
try {
let player = await playerService.findOne({token:playerToken});
return playerService.findById(player.guestUser._id)
} catch(error) {
console.log(error);
return null;
}
};
This is just handle error on awaited promise not returned one. You need to handle that in caller side.

Wrong metadata when using cls-hooked

I have an app that connects to the MQTT, I want to publish 1200 devices with the id of each device as a metadata. the following is the code
"use-strict"
const RSVP = require('rsvp');
const Mqtt = require('mqtt');
const cls = require('cls-hooked');
const namespace = "firstName";
let clsNamespace;
let client = Mqtt.connect("alis://test.mosquitto.org");
if (!client) {
logger.Error("Test", 'Init', 'No mqtt client provided');
throw new extError('No mqtt client created');
}
client.on('connect', async () => {
console.log("Connected");
try {
clsNamespace = cls.createNamespace(namespace);
main();
} catch (error) {
console.log(error);
}
});
function main() {
var devices = [];
for (var i = 0; i < 1200; i++) {
devices.push({ "id": i });
}
RSVP.all(devices.map(async (item) => await updateDevice(item)));
}
async function updateDevice(device) {
try {
return await wrapContext(clsNamespace, async () => {
setContext({ device: device.id });
console.log("update " + device.id + " metadata =" + JSON.stringify(__getMetadata()));
return publish("message", device.id);
});
} catch (error) {
console.log(error);
}
}
function setContext(context) {
try {
let ctxKeys = clsNamespace.get('contextKeys') ? clsNamespace.get('contextKeys') : [];
for (const key in context) {
clsNamespace.set(key, context[key]);
if (ctxKeys.indexOf(key) === -1) {
ctxKeys.push(key);
}
}
clsNamespace.set('contextKeys', ctxKeys);
} catch (error) {
console.error(error);
console.log('cannot set context', context);
throw error;
}
}
function publish(message, deviceId) {
return new RSVP.Promise((resolve, reject) => {
try {
client.publish(message,
deviceId,
(error) => {
if (error) {
console.log("error")
reject(error);
} else {
console.log("publish " + deviceId + " metadata" + JSON.stringify(__getMetadata()));
resolve();
}
});
} catch (error) {
console.log(error);
}
});
}
async function wrapContext(cls, callback) {
let defer = RSVP.defer();
let context = await cls.run(async (contextObj) => {
try {
let result = await callback(contextObj);
defer.resolve(result);
} catch (error) {
defer.reject(error);
}
});
return defer.promise;
};
function __getMetadata() {
const metadata = {};
let contextData = {};
for (const key of clsNamespace.get('contextKeys') || []) {
contextData[key] = clsNamespace.get(key);
}
for (const key in contextData) {
metadata[key] = contextData[key];
}
return metadata;
}
the output is the following:
update 0 metadata ={"device":0}
publish 0 metadata{"device":0}
update 1 metadata ={"device":1}
publish 1 metadata{"device":1}
... (same thing for 1165 devices)
update 1166 metadata ={"device":1166}
update 1167 metadata ={"device":1167}
update 1168 metadata ={"device":1168}
update 1169 metadata ={"device":1169}
... (same thing until 1199)
update 1199 metadata ={"device":1199}
publish 1166 metadata{"device":1199}
publish 1167 metadata{"device":1199}
publish 1168 metadata{"device":1199}
... (same thing until 1199)
As you can see, the metadata is correct for the 1165 first publish log but once there's an interruption in the iteration and the function become asychnronous, the metadata of the first publish will be missmatched.
Is there a way to fix this?

wit.ai : sessionid undefined in my runActions function

I am writing my first apps with wit.ai using a node.js backend. I found some posts here similar to my question, but not really the answer :
I use a socket.io to communicate with my node script.
The two relevant parts of my node are :
io.sockets.on('connection', function (socket) {
socket.on('message',
function(data) {
var json = JSON.parse(data);
var sid = json['sessionid'];
console.log("Received on sid " + sid);
if (_sessions[sid] == undefined) {
_sessions[sid] = {};
_sessions[sid].context = {};
}
_sessions[sid].socket = socket;
client.runActions(sid, json['text'], _sessions[sid].context, 30)
.then((context) => {
_sessions[sid].context = context;
}
)
.catch((err) =>
{
console.error('Oops! Got an error from Wit: ', err.stack || err);
}
);
}
);
}
);
========
const actions = {
send(request, response) {
const {sessionId, context, entities} = request;
const {text, quickreplies} = response;
return new Promise(function(resolve, reject) {
var session = _sessions[sessionId];
console.log("-------------------------------");
console.dir(context);
console.log("-------------------------------");
session.socket.emit("message", JSON.stringify(response));
return resolve();
});
},
gettaxi ({sessionid, context, text, entities}) {
return new Promise(function(resolve, reject) {
console.log(`Session ${sessionid} received ${text}`);
var quand = firstEntityValue(entities, "quand");
if (!quand && context['quand'] != undefined) quand = context['quand'];
var depart = firstEntityValue(entities, "depart");
var arrivee = firstEntityValue(entities, "arrivee");
if (depart) {
console.log("Found depart");
context.depart = depart;
delete context.missing_depart;
}
else {
context.missing_depart = true;
}
if (arrivee) {
console.log("Found arrivee");
context.arrivee = arrivee;
delete context.missing_arrivee;
}
else {
context.missing_arrivee = true;
}
console.dir(context);
if (quand) {
console.log("Found quand");
context.quand = quand;
delete context.missing_quand;
}
else {
context.missing_quand = true;
}
return resolve(context);
}
);
},
};
All is working rather good, except than my gettaxi receives a undefined sessionid.
It's not the case for the send function that receives the correct sessionid.
What am I doing wrong ?

Resources