MongoDB race conditions or concurency issues - node.js

I have the following code in my chat application based on NodeJS and MongoDB to change admin for room:
export function setAdmin(room, user) {
const userGuid = getGuid(user);
if (room.users && room.users.length) {
// filter current and new
room.users = room.users.filter(guid =>
guid !== room.adminGuid && guid !== userGuid
);
} else {
room.users = [];
}
room.users.push(userGuid);
room.adminGuid = userGuid;
return roomsCollection.save(room, { w: 1 });
}
Each room have only 2 users: admin and customer.
To update several rooms:
const rooms = await roomsCollection.find({ adminGuid: currentAdminGuid }).toArray();
for (const room of rooms) {
await setAdmin(room, newAdminUser);
}
I had some problems under highload with my application. They were resolved with help of indexes.
But after, working with MongoDB dump I found out that I have rooms with 3 users guids in room.users array. I think that save works as update for exist document, but how it updates array? If $set, why I have such 3 users rooms. Any thoughts how it would be possible?

save() behaves as insert if document does not exist. You have to use update() with {upsert: false}
some resources to update array: Array Query, array Operator
simple tutorial to nest array inside document, here
advanced example of dealing with multiple nested array, here

Related

How to Update All Documents in a Collection With Thousands of Documents in FireStore with Firebase Admin?

Updated to reflect Firebase-Admin, rather than v9
New Update: solution at the bottom
How to update a single field (in a map) for all documents in a collection with thousands of documents -- in firebase-admin/firestore (10.0.2)? I attempted to use this:
import { getFirestore } from 'firebase-admin/firestore'
const db = getFirestore()
db.collection("users").get().then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
doc.ref.update({
"words.subscription": 0
})
})
})
I use it within a Node.js (v12) cloud function. The function runs, I see no errors, but none of the documents are updated. Previously, I attempted to use set() because some documents may not have the field, but Frank let me know update() can update fields that don't exist too.
However, this updated code also does not update all the documents in the 'users' collection.
Thank you in advance.
Update/Solution
#Dharmara - thank you for the gist. It did not work as-is, so I decided to change to async/await and wrap it in try/catch (to hopefully find out why it wasn't working), like so:
try {
let querySnapshot = await db.collection('users').get()
if (querySnapshot.size === 0) {
console.log('No documents to update')
return 'No documents to update'
}
const batches: any[] = [] // hold batches to update at once
querySnapshot.docs.forEach((doc, i) => {
if (i % 500 === 0) {
batches.push(db.batch())
}
const batch = batches[batches.length - 1]
batch.update(doc.ref, { "words.subscription": 0 })
})
await Promise.all(batches.map(batch => batch.commit()))
console.log(`${querySnapshot.size} documents updated`)
return `${querySnapshot.size} documents updated`
}
catch (error) {
console.log(`***ERROR: ${error}`)
return error
}
When I tested this however, it worked. From what I can tell it's the same as the then() way you had in the gist (and from what I had found elsewhere in SO).
If you wanted to put this into an answer I'd gladly mark it as the answer. Your code helped tremendously. Thank you.
The dot notation you use to write a nested field only works when you call update(), not for set(). Luckily update() works for non-existing fields, it only requires that the document already exists.
So for the namespaced API of v8 and before that'd be:
doc.ref.update({
"words.subscription": 0
})
For the modular API for v9 and beyond, it's:
update(doc.ref, {
"words.subscription": 0
})

Correct approach in Mongoose - validating my code

I have 3 collections in my application.
Member - has a field called MemberMatches which is referencing the Matches collection.
Tournament - has a field called TournamentMatches which is referencing the Matches collection
Matches.
On the REACT front end on the click of a button, I am creating a bunch of matches using a for loop and making entries in the matches collection via node. At the time I want to enter the Object ID into the Member and Tournament collections.
Here is the code:-
for(i=0; i<playersList.length;){
player1Name = playersList[i]
for(j=0; j<playersList.length;){
if(j<=i){
j=i
j++
continue
}
player2Name = playersList[j]
var newMatch = new MatchRegister({
Player1Name: player1Name,
Player1GroupNumber: groupNumber,
Player2Name: player2Name,
Player2GroupNumber: groupNumber,
});newMatch.save(async function(err,data){
if(err){
console.log(err)
} else {
await Tournament.findOneAndUpdate({_id: tid}, {$push: {TournamentMatches: data._id}})
await Member.findOneAndUpdate({MemberName: data.Player1Name}, {$push: {MemberMatches: data._id}})
await Member.findOneAndUpdate({MemberName: data.Player2Name}, {$push: {MemberMatches: data._id}})
}
})
j++
}
i++
}
}
return
I am calling this function from the route.push. Saving the matches first and then 3 FindOneAndUpdate with the await keyword.
This entire code and functionality works. Upon execution I can clearly see that the data is being populated in all 3 collections correctly.
Question: Is this the right approach? Is there another Mongoose way to do this.

CoreMongooseArray to Normal Array

I'm shortlisting the 2 elements from one schema and want to update in another schema. for that i used slice method to shortlist first 2 elements from an array. but am Getting
CoreMongooseArray ['element1','element2']
instead of ["element1", "element2"]
How do i remove "CoreMongooseArray" ?
connection.connectedusers.find({}, async (err, docs) => {
if(err) throw err;
var users = docs[0].connectArray;
if (docs[0] != null && users.length >= 2) {
var shortListed = users.slice(0, 2);
try {
await connection.chatschema.updateMany({}, { $push: { usersConnected: [shortListed] } }, { upsert: true });
} catch (err) {
res.status(201).json(err);
}
}
You need to add lean() to your query.
From the docs:
Documents returned from queries with the lean option enabled are plain javascript objects, not Mongoose Documents. They have no save method, getters/setters, virtuals, or other Mongoose features.
If you already got the mongoose array and like to convert to simple js array
const jsArray = mongooseArray.toObject();
https://mongoosejs.com/docs/api/array.html#mongoosearray_MongooseArray-toObject
For some reason .toObject() didn't work for me. lean() option works, but it's not suitable when you already have an object with mongoose array in it. So in case if you already have mongoose array and you want just to convert it to plain js array you can use following code:
function mongooseArrayToArray(mongooseArray) {
const array = [];
for (let i = 0; i < mongooseArray.length; i += 1) {
array.push(mongooseArray[0]);
}
return array;
};
usage:
const array = mongooseArrayToArray(mongooseArray);
If you just want to convert the CoreMongooseArray to a normal array without changing anything else:
const jsArray = [...mongooseArray];

Update mongodb document, if the document exists, else create

So I am trying to make a discord bot for me and my friends for tracking stats in CS GO 10 mans, and I am using cheerio for webscraping from the site that provides us the stats, and then pass them into mongodb. The scraping functionality works fine, but im trying to figure out how to avoid creating duplicate documents for each user. If I enter *userid 857575 it pulls the stats for that user, and puts in the DB, but if i call that multiple times, its making multiple documents in the DB. My question is, how would I get mongodb to update the document based on if the message author in discord matches the username in the db? So if username bob sends *userid3939 and bob already exists in the db, update the document. If bob doesnt exist, create document. code below, appreciate any tips.
module.exports.run = async (bot, message, args) => {
console.log(args);
var userUrl = 'https://popflash.site/user/' +args;
console.log(userUrl);
console.log(message.member.user.tag);
rp(userUrl)
.then(function (html) {
const arr = [];
var i = 0;
$('.stat-container', html).each(function (key, value) {
arr[i++] = $(this).find(".stat").text();
});
const stats = new Stats({
_id: mongoose.Types.ObjectId(),
userName: message.member.user.tag,
userId: args,
HLTV: arr[0],
ADR: arr[1],
HS: arr[2],
W: arr[3],
L: arr[4],
T: arr[5],
win_percent: arr[6]
});
stats.save()
.then(function (result) {
let botembed = new Discord.RichEmbed()
.setDescription(message.member.user + "'s 10 Man stats")
.setColor("#15f153")
.addField("stats", result)
return message.channel.send(botembed);
})
})
}
module.exports.help = {
name: "userid"
}
Through db.collection.update, you can specify the upsert: true option to get the behavior I think you're desiring. It will update an existing record if matched, otherwise it will create a new record.

How do I increment a value for an existing object in Firebase?

I'm building a step counter app.
I got an iOS app that pushes the sum of each day to /users/{mobile}/steps/{date}/
When a new steps child is updated or added, I want to sum the value of all the steps for that particular user and update his stepsTotal.
To achieve that I need to
Find the original user and sum all the steps.
Save the new value to stepsTotal.
I would be most grateful if someone could give some help here. :-)
database
{
"users": {
"92291000": {
"firstName": "Tore",
"stepsTotal": "1500",
"steps": {
"02-09-2017": "500",
"03-09-2017": "1000"
},
import.js
var db = admin.database();
var dbRoot = db.ref("/");
var usersRef = dbRoot.child("users");
// This works
function saveUser(attributes) {
let mobile = attributes.mobile;
delete attributes['mobile']
let user = usersRef.child(mobile);
user.update(attributes);
}
function increaseSteps( { mobile=null, steps=null } = {}) {
// Find the User
console.log("looking for mobile", mobile); // OK
let userRef = usersRef.child(mobile);
// Here I'm not able to read the old data from the user.
userRef.transaction(function(user) {
console.log("user: ", user); // null
// ^ User is null.
});
/*
If I mangage to find user above, I expect to do something like this.
Or it is possible to only update *stepsTotal*?
*/
let attributes = {
firstName: user.firstName,
lastName: user.lastName,
stepsTotal: user.stepsTotal + steps,
}
user.update( attributes );
}
If I understand correctly, you have a problem in this snippet of the code:
let userRef = usersRef.child(mobile);
// Here I'm not able to read the old data from the user.
userRef.transaction(function(user) {
console.log("user: ", user); // null
// ^ User is null.
});
In Firebase Database transactions the initial value is often null. From the Firebase documentation on transactions:
Transaction Function is Called Multiple Times
Your transaction handler is called multiple times and must be able to handle null data. Even if there is existing data in your database it may not be locally cached when the transaction function is run.
This is due to how Firebase transactions work behind the scenes. To learn more about that, see my answers here Transcation updateFunction parameter is null and Firebase runTransaction not working.
The solution is to handle both cases: if the user node doesn't exist yet count the initial number of steps, otherwise update the number of steps:
let userRef = usersRef.child(mobile);
userRef.transaction(function(user) {
return (user || 0) + new_steps_for_user;
});

Resources