Mongoose/Mongodb, update each doc query, very slow - mongodb

I have this update query in mongoose. It's 1600 posts and takes like 5 min to run.
What's the bottleneck? Am I using the wrong approach?
export const getAndStoreLatestKPI = async () => {
console.log("start kpi");
try {
const marketCaps = await getKPI();
const stocks = await mongoose.model("stock").find().exec();
for (const stock of stocks) {
const marketCap = marketCaps.find(
(marketCap) => marketCap.i === stock.insId
);
if (marketCap != null) {
const marketCapAdjustedVal =
stock.country === "Finland" ? marketCap.n * 10 : marketCap.n;
const update = {
marketCap: marketCapAdjustedVal,
};
console.log(marketCapAdjustedVal);
await mongoose
.model("stock")
.findOneAndUpdate({ insId: stock.insId }, { update });
}
}
console.log("done");
return Promise.resolve();
} catch (err) {
return Promise.reject(err);
}
};
export const getKPI = async (kpiId: number) => {
try {
const kpiFetch = await Axios.get(someurl);
return Promise.resolve(kpiFetch.data.values);
} catch (err) {
return Promise.reject(err);
}
};

So the main bottle neck is your for loop. for each stock item you perform several "expensive" actions such as data fetching from external API + a single update, and you're doing them 1 by 1.
What I would recommend you doing is looping on several items at once. similar to the idea multithreading.
There are several different solutions on how to do it in nodejs for example nodejs worker threads
However I personally use and recommend using bluebird which gives you this ability and many others straight out of the box.
Some sample code:
import Bluebird = require('bluebird');
const stocks = await mongoose.model("stock").find().exec();
await Bluebird.map(stocks, async (stock) => {
const marketCap = marketCaps.find(
(marketCap) => marketCap.i === stock.insId
);
if (marketCap != null) {
const marketCapAdjustedVal =
stock.country === "Finland" ? marketCap.n * 10 : marketCap.n;
const update = {
marketCap: marketCapAdjustedVal,
};
console.log(marketCapAdjustedVal);
await mongoose
.model("stock")
.findOneAndUpdate({ insId: stock.insId }, { update });
}
}, {concurrency: 25})
// concurrency details how many concurrent process run parallel. the heavier they are the less you want concurrent for obvious reasons.

Related

Mongoose - MongoDB | "Query was already executed" error on query in loop

I'm trying to collection queries in the while loop. But I'm getting a "Query was already executed" error. How can I do that? I have a custom field and I need to make sure it is unique. I also need to use this function elsewhere.
async function createRandomID() {
let id = Math.random().toString(36).slice(2, 6).toUpperCase();
let unique = false;
while (!unique) {
await Deed.findOne({ deedId: id }, async (err, doc) => {
if (!doc) {
unique = true;
}
else {
id = Math.random().toString(36).slice(2, 6).toUpperCase();
}
})
}
return id;
}
const addNewDeed = asyncErrorWrapper(async (req, res, next) => {
let information = req.body;
const deedId = await createRandomID();
/*Other Operations...*/
});

Order of save() and find() in NodeJS with MongoDB

I'm trying to create a new record in my MongoDB ("thisPlayer") and save it to my database, then find all records in my database (including the new one) and render them.
I am having trouble understanding why my save() function actually occurs after my find() function. When this code executes, the find() function does not include my new thisPlayer record. However, after the find() runs, the save occurs -- the record is saved to the database AFTER the find() ran.
Thanks in advance!
const playerNumber = async function countPlayers() {
return new Promise((resolve, reject) => {
Player.count(function(err, numOfDocs) {
err ? reject(err) : resolve(numOfDocs);
console.log('I have '+numOfDocs+' documents in my collection');
});
});
}
async function playerProfile() {
var count = await playerNumber();
console.log("count already in db: "+ count);
if (count===0) {
teamCaptain=1;
} else {teamCaptain=0};
count++;
const thisPlayer = new Player({
playerNum: count,
playerName: Name,
});
thisPlayer.save();
Player.find({}, function(err, playaz){
var playerOne;
if (playaz.length > 0) {
playerOne = playaz[0].playerName;
} else {
playerOne = "";
}
res.renderPjax("leavetakings",
{player1: "1: " + playerOne}
);
});
}
playerProfile();
You need to use await
for example.
await Player.find({})

Updating sub document using save() method in mongoose does not get saved in database and shows no error

I have a Mongoose model like this:
const centerSchema = mongoose.Schema({
centerName: {
type: String,
required: true,
},
candidates: [
{
candidateName: String,
voteReceived: {
type: Number,
default: 0,
},
candidateQR: {
type: String,
default: null,
},
},
],
totalVote: {
type: Number,
default: 0,
},
centerQR: String,
});
I have a Node.JS controller function like this:
exports.createCenter = async (req, res, next) => {
const newCenter = await Center.create(req.body);
newCenter.candidates.forEach(async (candidate, i) => {
const candidateQRGen = await promisify(qrCode.toDataURL)(
candidate._id.toString()
);
candidate.candidateQR = candidateQRGen;
// ** Tried these: **
// newCenter.markModified("candidates." + i);
// candidate.markModified("candidateQR");
});
// * Also tried this *
// newCenter.markModified("candidates");
const upDatedCenter = await newCenter.save();
res.status(201).json(upDatedCenter);
};
Simply, I want to modify the candidateQR field on the subdocument. The result should be like this:
{
"centerName": "Omuk Center",
"candidates": [
{
"candidateName": "A",
"voteReceived": 0,
"candidateQR": "some random qr code text",
"_id": "624433fc5bd40f70a4fda276"
},
{
"candidateName": "B",
"voteReceived": 0,
"candidateQR": "some random qr code text",
"_id": "624433fc5bd40f70a4fda277"
},
{
"candidateName": "C",
"voteReceived": 0,
"candidateQR": "some random qr code text",
"_id": "624433fc5bd40f70a4fda278"
}
],
"totalVote": 0,
"_id": "624433fc5bd40f70a4fda275",
"__v": 1,
}
But I am getting the candidateQR still as null in the Database. I tried markModified() method. But that didn't help (showed in the comment section in the code above). I didn't get any error message. In response I get the expected result. But that result is not being saved on the database. I just want candidateQR field to be changed. But couldn't figure out how.
forEach loop was the culprit here. After replacing the forEach with for...of it solved the issue. Basically, forEach takes a callback function which is marked as async in the codebase which returns a Promise initially and gets executed later.
As for...of doesn't take any callback function so the await inside of it falls under the controller function's scope and gets executed immediately. Thanks to Indraraj26 for pointing this out. So, the final working version of the controller would be like this:
exports.createCenter = async (req, res, next) => {
const newCenter = await Center.create(req.body);
for(const candidate of newCenter.candidates) {
const candidateQRGen = await promisify(qrCode.toDataURL)(
candidate._id.toString()
);
candidate.candidateQR = candidateQRGen;
};
newCenter.markModified("candidates");
const upDatedCenter = await newCenter.save();
res.status(201).json(upDatedCenter);
};
Also, shoutout to Moniruzzaman Dipto for showing a different approach to solve the issue using async.eachSeries() method.
You can use eachSeries instead of the forEach loop.
const async = require("async");
exports.createCenter = async (req, res, next) => {
const newCenter = await Center.create(req.body);
async.eachSeries(newCenter.candidates, async (candidate, done) => {
const candidateQRGen = await promisify(qrCode.toDataURL)(
candidate._id.toString(),
);
candidate.candidateQR = candidateQRGen;
newCenter.markModified("candidates");
await newCenter.save(done);
});
res.status(201).json(newCenter);
};
As far as I understand, you are just looping through the candidates array but you
are not storing the updated array. You need to store the updated data in a variable as well. Please give it a try with the solution below using map.
exports.createCenter = async (req, res, next) => {
const newCenter = await Center.create(req.body);
let candidates = newCenter.candidates;
candidates = candidates.map(candidate => {
const candidateQRGen = await promisify(qrCode.toDataURL)(
candidate._id.toString()
);
return {
...candidate,
candidateQR: candidateQRGen
}
});
newCenter.candidates = candidates;
const upDatedCenter = await newCenter.save();
res.status(201).json(upDatedCenter);
};
You can use this before save()
newCenter.markModified('candidates');

Puppeteer and express can not load new data using REST API

I'm using puppeteer to scrape page that has contents that change periodically and use express to present data in rest api.
If I turn on headless chrome to see what is being shown in the browser, the new data is there, but the data is not showing up in get() and http://localhost:3005/api-weather. The normal browser only shows the original data.
const express = require('express');
const server = new express();
const cors = require('cors');
const morgan = require('morgan');
const puppeteer = require('puppeteer');
server.use(morgan('combined'));
server.use(
cors({
allowHeaders: ['sessionId', 'Content-Type'],
exposedHeaders: ['sessionId'],
origin: '*',
methods: 'GET, HEAD, PUT, PATCH, POST, DELETE',
preflightContinue: false
})
);
const WEATHER_URL = 'https://forecast.weather.gov/MapClick.php?lat=40.793588904953985&lon=-73.95738513173298';
const hazard_url2 = `file://C:/Users/xdevtran/Documents/vshome/wc_api/weather-forecast-nohazard.html`;
(async () => {
try {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.setRequestInterception(true);
page.on("request", request => {
console.log(request.url());
request.continue();
});
await page.goto(hazard_url2, { timeout: 0, waitUntil: 'networkidle0' });
hazard = {
"HazardTitle": "stub",
"Hazardhref": "stub"
}
let forecast = await page.evaluate(() => {
try {
let forecasts = document.querySelectorAll("#detailed-forecast-body.panel-body")[0].children;
let weather = [];
for (var i = 0, element; element = forecasts[i]; i++) {
period = element.querySelector("div.forecast-label").textContent;
forecast = element.querySelector("div.forecast-text").textContent;
weather.push(
{
period,
forecast
}
)
}
return weather;
} catch (err) {
console.log('error in evaluate: ', err);
res.end();
}
}).catch(err => {
console.log('err.message :', err.message);
});
weather = forecast;
server.get('/api-weather', (req, res) => {
try {
res.end(JSON.stringify(weather, null, ' '));
console.log(weather);
} catch (err) {
console.log('failure: ', err);
res.sendStatus(500);
res.end();
return;
}
});
} catch (err) {
console.log('caught error :', err);
}
browser.close();
})();
server.listen(3005, () => {
console.log('http://localhost:3005/api-weather');
});
I've tried several solutions WaitUntil, WaitFor, .then and sleep but nothing seems to work.
I wonder if it has something to do with express get()? I'm using res.end() instead of res.send() is because the json looks better when I use res.end(). I don't really know the distinction.
I'm also open to using this reload solution. But I received errors and didn't use it.
I also tried waitForNavigation(), but I don't know how it works, either.
Maybe I'm using the wrong search term to find the solution. Could anyone point me in the right direction? Thank you.

Waterline ORM assign the result of find to a variable

I want to combine the results of 2 queries and then return them as one, like this:
test: async (req, res) => {
const valOne = TableOne.find({ id: id })
.exec((err, result) => {
if (err) {
res.serverError(err);
}
return result;
});
const valTwo = TableTwo.find({ id: id })
.exec((err, result) => {
if (err) {
res.serverError(err);
}
return result;
});
const data = {
keyOne: valOne,
keyTwo: valTwo,
};
res.json(data);
}
I understand above code won't return because it's async. How can I achieve this?
There is not much info you supply: node version, sails version, etc.
There are several approaches here:
1. Using promises
2. Using callback chaining
3. Using await/async
If you use sails 1.0 and node >= 8, your best bet is to use await/async, so your code should work like that:
test: async (req, res) => {
let valOne, valTwo;
try {
valOne = await TableOne.find({ id: id });
valTwo = await TableTwo.find({ id: id });
} catch (err) {
return res.serverError(err); //or res.badRequest(err);
}
const data = {
keyOne: valOne,
keyTwo: valTwo,
};
res.json(data);
}