I run a chat application similar to chatroulette built using Meteor. the algorithms behind matching users is all done on the server by utilizing js arrays and objects as such data is not very sensitive to me and don’t feel like bloating the MongoDB with unnecessary transactions.
I use arrays for queuing the users, normally users are matched by popping them from the queue. however in some cases users are matched based on certain properties that are separated in different queues for which I implemented a seek search algorithm for faster search by constructing an index array for each type of queue.
Each match generates a pair ID which is used to identify each conversation/chat room.
I rely on connection ID to identify each connected user because I want to allow each user to possibly have multiple conversation using the same user ID.
when users match in a chat room, they are removed from their designated queue and are added to an associative object called “noQueue” which helps me track users that are in conversations, another records is also added to “currentChats” object which helps me also keep track of the current conversations and their duration so far.
when users are disconnected the connection.onClose() handler function looks for the connection ID and removes it from it’s current position whether it’s in a conversation or still in one of the queues.
The problem is: on an average of twice a day, records in the “currentChats” associative object are not removed on user disconnection using the connection.onClose() handler function. which leaves me with faulty records of 1 or 2 conversations with durations for more than 18 hours. To validate the information I check to find out that those users are not even online.
the following code snippet contains the connection.onClose() handler function:
Meteor.onConnection(function(connection){
connection.onClose(function(){//fires on user disconnection
const uniQConnIndex = uniQConn.indexOf(connection.id);
const QConnIndex = QConn.indexOf(connection.id);
if(uniQConnIndex > -1){//removes user from the university queue
console.log('User disconnected: '+ uniQueue[uniQConnIndex].email);
uniQueue.splice(uniQConnIndex,1);
indexUniQueue.splice(uniQConnIndex,1);
uniQConn.splice(uniQConnIndex,1);
}else if(QConnIndex > -1){ //removes user from the all queue
console.log('User disconnected: '+ queue[QConnIndex].email);
queue.splice(QConnIndex,1);
indexQueue.splice(QConnIndex,1);
QConn.splice(QConnIndex,1);
}else if(noQueue[connection.id]){
const targetNoQueue = noQueue[connection.id]; /*saves object*/
//emits to all chat room users forcing them to discconect
Meteor.setTimeout(function () {
streamer.emit('matchDisconnected', targetNoQueue.pairId);
},500);
delete noQueue[targetNoQueue.matchedUser.connectionId]; /*deletes matched user fron no Queue*/
delete noQueue[connection.id]; /*deletes user from no queue*/
//THIS IS THE SECTION THAT FAILS TO RUN SOMETIMES
if(currentChats[targetNoQueue.pairId]){
delete currentChats[targetNoQueue.pairId];/*removes the current chat from the array*/
console.log("Chat ended between: "
+targetNoQueue.email
+" & "
+targetNoQueue.matchedUser.email);
}
}
});
This snippet shows how the users are matched into conversations:
Meteor.methods({
'match': function (settings) { //finding a matchgit
user = {};
/*user.connection = this.connection;*/
user.queueTime = undefined;
user.queueTime = moment().toDate();
user.connectionId = this.connection.id;
/*user.ip = this.connection.clientAddress;*/
user.id = Meteor.userId();
user.uniName = Meteor.user().profile.uniName;
user.gender = Meteor.user().profile.gender;
user.email = Meteor.user().emails[0].address;
user.rate = Meteor.user().profile.rate;
user.matchedUser = null;
user.pairId = null;
console.log("User connected: " + Meteor.user().emails[0].address);
// console.log("ConnectionId: " + user.connectionId);
// console.log("Id: " + user.id);
if (settings == 'all') {
if (queue.length > 0) {
if (queue[0].id !== user.id) {
matchedUser = queue.pop();
QConn.pop();
indexQueue.pop();
} else {
queue.push(user);
indexQueue.push(user.uniName);
QConn.push(user.connectionId);
// console.log("Queued user");
// Meteor.call('count');
return;
}
} else if (uniQueue.length > 0) {
const matchedUserUniQueueIndex = indexUniQueue.indexOf(user.uniName);
if (matchedUserUniQueueIndex > -1) {
if (uniQueue[matchedUserUniQueueIndex].id !== user.id) {
//match the user from the uni queue
matchedUser = uniQueue[matchedUserUniQueueIndex];
uniQueue.splice(matchedUserUniQueueIndex, 1);
indexUniQueue.splice(matchedUserUniQueueIndex, 1);
uniQConn.splice(matchedUserUniQueueIndex, 1);
} else {
queue.push(user);
indexQueue.push(user.uniName);
QConn.push(user.connectionId);
// console.log("Queued user");
// Meteor.call('count');
return;
}
} else {
queue.push(user);
indexQueue.push(user.uniName);
QConn.push(user.connectionId);
// console.log("Queued user");
// Meteor.call('count');
return;
}
}
else {
queue.push(user);
indexQueue.push(user.uniName);
QConn.push(user.connectionId);
// console.log("Queued user");
// Meteor.call('count');
return;
}
} else if (settings == 'uni') {
if (uniQueue.length > 0) {
//implementing seek search to find a match of same uni
const matchedUserUniQueueIndex = indexUniQueue.indexOf(user.uniName);
//if the user of the same uni is found
if (matchedUserUniQueueIndex > -1) {
if (uniQueue[matchedUserUniQueueIndex].id !== user.id) {
//match the user from the uni queue
matchedUser = uniQueue[matchedUserUniQueueIndex];
uniQueue.splice(matchedUserUniQueueIndex, 1);
indexUniQueue.splice(matchedUserUniQueueIndex, 1);
uniQConn.splice(matchedUserUniQueueIndex, 1);
} else {
//queue the user if no one from the same university is found
uniQueue.push(user);
indexUniQueue.push(user.uniName);
uniQConn.push(user.connectionId);
// console.log("Queued user");
// Meteor.call('count');
return; //exit when no university match in both
}
} else {
//queue the user if no one from the same university is found
uniQueue.push(user);
indexUniQueue.push(user.uniName);
uniQConn.push(user.connectionId);
// console.log("Queued user");
// Meteor.call('count');
return; //exit when no university match in both
}
} else if (queue.length > 0) {
const matchedUserQueueIndex = indexQueue.indexOf(user.uniName);
if (matchedUserQueueIndex > -1) {
if (queue[matchedUserQueueIndex].id !== user.id) {
//match the user from the regular queue
matchedUser = queue[matchedUserQueueIndex];
queue.splice(matchedUserQueueIndex, 1);
indexQueue.splice(matchedUserQueueIndex, 1);
QConn.splice(matchedUserQueueIndex, 1);
} else {
//queue the user if no one from the same university is found
uniQueue.push(user);
indexUniQueue.push(user.uniName);
uniQConn.push(user.connectionId);
// console.log("Queued user");
// Meteor.call('count');
return; //exit when no university match in both
}
}else {
//queue the user if no one from the same university is found
uniQueue.push(user);
indexUniQueue.push(user.uniName);
uniQConn.push(user.connectionId);
// console.log("Queued user");
// Meteor.call('count');
return; //exit when no university match in both
}
} else {
//queue the user if there is no one in the queues
uniQueue.push(user);
indexUniQueue.push(user.uniName);
uniQConn.push(user.connectionId);
// console.log("Queued user");
// Meteor.call('count');
return;
}
}
//pairing the user after successful match finding
pairId = user.connectionId + "#" + matchedUser.connectionId;
matchedUser.pairId = pairId;
user.pairId = pairId;
streamer.emit('match',{
to: user.connectionId,
pairId: pairId,
match: matchedUser
});
streamer.emit('match',{
to: matchedUser.connectionId,
pairId: pairId,
match: user
});
console.log('Matched between: '+ user.email + " & " + matchedUser.email);
user.matchedUser = matchedUser;
matchedUser.matchedUser = user;
noQueue[user.connectionId] = user;
noQueue[matchedUser.connectionId] = matchedUser;
currentChats[pairId] = {user1: user.email, user2: matchedUser.email, start: moment()};
Meteor.call('currentChats');
Meteor.call('count');
},
the problem leaves me with server logs like this one having conversations that lasted for 2 days which is faulty info of course
app.web.1: ===Current Chats===
app.web.1: Users: giovanni.louis@student.guc.edu.eg & rehan.khattab@student.guc.edu.eg
app.web.1: Started: 2 days ago
app.web.1: Users: ibrahim.mohamad@student.guc.edu.eg & amr148209@bue.edu.eg
app.web.1: Started: 2 days ago
app.web.1: Users: abdel-rahman.ali@student.guc.edu.eg & rawan.abdo@student.guc.edu.eg
app.web.1: Started: 2 days ago
app.web.1: Users: hesham154970@bue.edu.eg & youssef.ahmed7@msa.edu.eg
app.web.1: Started: 17 hours ago
app.web.1: Users: mahmoud.nasser@student.guc.edu.eg & amr.mostafa4@msa.edu.eg
app.web.1: Started: a few seconds ago
I have had some suspicions in the fact that the users may disconnect at the same time which will call the function at the same time deleting the records at the same time which might lead to call failing which I believe is highly unlikely, although I precariously added a delay between match disconnections to make sure this doesn’t happen. but this doesn’t solve it if both users disconnect at the same time, is this even possible?