I have an Express based CRUD application which uses MongoDB as its DB. I have noticed that some concurrent writes fail if they use bulkWrite and session.
A simplified example looks like this:
import express from 'express';
import { v4 } from 'uuid';
import mongoose from 'mongoose';
const router = express.Router();
const mongoString = 'mongodb://127.0.0.1:27017/testMongo?directConnection=true';
const port = 3288;
const testId = mongoose.Types.ObjectId();
const Model = mongoose.model('Data', new mongoose.Schema({
whateverString: {
required: true,
type: String,
},
}));
mongoose.connect(mongoString);
const database = mongoose.connection;
database.on('error', (error) => {
console.log(error);
});
database.once('connected', async () => {
console.log('Database connected');
// Add test data if not exists
if (!await Model.exists({ _id: testId })) {
const data = new Model({
_id: testId,
whateverString: v4(),
});
await data.save();
}
});
const app = express();
app.use(express.json());
router.post('/', async (req, res) => {
const session = await mongoose.startSession();
session.startTransaction();
try {
await Model.bulkWrite([
{
updateOne: {
filter: {
_id: testId,
},
update: {
whateverString: v4(),
},
},
},
], { session });
await session.commitTransaction();
res.status(200).json({ allRight: true });
} catch (error) {
await session.abortTransaction();
console.log(error.message);
res.status(400).json({ message: error.message });
} finally {
session.endSession();
}
});
app.use('/', router);
app.listen(port, async () => {
console.log(`Server started at ${port}`);
});
What this does is:
connecting to Mongo
creating a test document
creating a web server and one post route
if the post route is called, the test document is updated with a random string in a bulkWrite and a session
Now take a simple client script which does three requests in parallel:
import fetch from 'node-fetch';
function doFetch() {
return fetch('http://localhost:3288', { method: 'post' });
}
async function myMain() {
try {
const promises = [doFetch(), doFetch(), doFetch()];
const response = await Promise.all(promises);
console.log('response', response.map(resp => ({ status: resp.status, text: resp.statusText })));
} catch (error) {
console.log(error);
}
}
myMain();
The result is: Only one DB query works, whereas the others fail with the error WriteConflict error: this operation conflicted with another operation. Please retry your operation or multi-document transaction.
I am rather new to MongoDB and Mongoose but in my understanding of databases, such a use-case should be fine. (In this example, the requests would overwrite each other and create chaos, but in the real-life use case that should not be a problem at all.)
Some observations I've made:
Without passing session to bulkWrite, everything works fine.
It is obviously some kind of race condition: sometimes two queries go through, sometimes only one.
Setting maxTransactionLockRequestTimeoutMillis to 20000 did not help.
If I include the fetching in the server process itself (after console.log('Server started ...), then everything works fine. I cannot explain that to be honest.
What am I doing wrong? How can I solve that problem?
Appendix: The package.json file of that example looks like this:
{
"name": "rest-api-express-mongo",
"dependencies": {
"express": "^4.17.3",
"mongoose": "^6.2.2",
"node-fetch": "^3.2.10",
"uuid": "^9.0.0"
},
"type": "module"
}
``
Thanks to the comment provided by Marco Luzzara I was able to refactor and solve the issue via callbacks.
The code being now:
let retry = 0;
await database.getClient().withSession(async (session) => {
try {
await session.withTransaction(async () => {
await Model.bulkWrite([
{
updateOne: {
filter: {
_id: testId,
},
update: {
whateverString: v4(),
},
},
},
], { session });
await session.commitTransaction();
res.status(200).json({ allRight: true });
});
} catch (error) {
console.log(error.message);
res.status(400).json({ message: error.message });
retry += 1;
if (retry > 5) {
session.endSession();
}
}
});
Just for reference - the whole file looks now like:
import express from 'express';
import { v4 } from 'uuid';
import mongoose from 'mongoose';
const router = express.Router();
const mongoString = 'mongodb://127.0.0.1:27017/testMongo?directConnection=true';
const port = 3288;
const testId = mongoose.Types.ObjectId();
const Model = mongoose.model('Data', new mongoose.Schema({
whateverString: {
required: true,
type: String,
},
}));
mongoose.connect(mongoString);
const database = mongoose.connection;
database.on('error', (error) => {
console.log(error);
});
database.once('connected', async () => {
console.log('Database connected');
// Add test data if not exists
if (!await Model.exists({ _id: testId })) {
const data = new Model({
_id: testId,
whateverString: v4(),
});
await data.save();
}
});
const app = express();
app.use(express.json());
router.post('/', async (req, res) => {
let retry = 0;
await database.getClient().withSession(async (session) => {
try {
await session.withTransaction(async () => {
await Model.bulkWrite([
{
updateOne: {
filter: {
_id: testId,
},
update: {
whateverString: v4(),
},
},
},
], { session });
await session.commitTransaction();
res.status(200).json({ allRight: true });
});
} catch (error) {
console.log(error.message);
res.status(400).json({ message: error.message });
retry += 1;
if (retry > 5) {
session.endSession();
}
}
});
});
app.use('/', router);
app.listen(port, async () => {
console.log(`Server started at ${port}`);
});
Related
I've read through some of the answers similar to this question and have struggled to see how they would apply to mine. The issue I am having is that when I try to retrieve any data from my MongoDB database, it returns an empty array. I've already set my network access to 0.0.0.0/0, and it says it successfully connects to my DB when I run my backend. I was hoping someone might be able to lend a hand. The GitHub for this file is https://github.com/dessygil/des-personal-site-backend
Here is my index.js file
const express = require("express");
const app = express();
const dotenv = require('dotenv').config();
const mongoose = require("mongoose")
const jobsRoute = require("./routes/jobs");
mongoose.set('strictQuery', false);
app.use(express.json());
mongoose.connect(process.env.MONGO_URL, {
useNewUrlParser: true,
useUnifiedTopology: true,
}).then(console.log("Connected to MongoDB")).catch((err) => {
console.log(err)
});
app.use("/", jobsRoute);
app.listen(process.env.PORT || 5000, () => {
console.log("Connected to port 5000");
});
My Jobs model
const mongoose = require("mongoose");
const JobSchema = new mongoose.Schema(
{
startDate: {
type: Date,
required: true,
},
endDate: {
type: Date,
required: false,
},
company: {
type: String,
required: true,
unique: false,
},
title: {
type: String,
required: true,
unique: true,
},
url: {
type: String,
required: true,
},
duties: {
type: [String],
required: true,
},
},
{ timestamps: true },
);
module.exports = mongoose.model("Job", JobSchema)
My route file
const router = require("express").Router();
const Job = require("../models/Job");
//Create new post
router.post("/", async (req, res) => {
const newJob = new Job(req.body);
try {
const savedJob = await newJob.save();
res.status(200).json(savedJob);
} catch (err) {
res.status(500).json(err);
};
});
//Update post
router.put("/:id", async (req, res) => {
try {
const updatedJob = await Job.findByIdAndUpdate(
req.params.id,
{
$set: req.body,
},
{ new: true }
);
res.status(200).json(updatedJob);
} catch (err) {
res.status(500).json(err);
}
});
//Delete post
router.delete("/:id", async (req, res) => {
try {
const job = await Job.findById(req.params.id);
await job.delete()
res.status(200).json("Post had been deleted");
} catch (err) {
res.status(500).json(err);
}
});
//Get post
router.get("/:id", async (req, res) => {
try {
const job = await Job.findById(req.params.id);
res.status(200).json(job);
} catch (err) {
res.status(500).json(err);
}
});
//Get all posts
router.get("/", async (req, res) => {
try {
allJobs = await Job.find();
console.log(allJobs,"you are here");
res.status(200).json(allJobs);
} catch (err) {
res.status(500).json(err);
}
});
module.exports = router;
The structure of my db
What is returned on postman (hosted on heroku but also doesn't work when I run locally and returns the same results
The type of data stored in jobs there is 3 documents that should show up
I am using socket.io with express and typescript, when want to emit to particular logged in user it is not working, the rest is working fine, when new user join and other things, in App.ts in backend it looks like:
httpServer.listen(8000, () => {
console.log(`app is running on: http://localhost:8000`);
});
//SocketIO
const io = new Server(httpServer, {
cors: {
credentials: true,
},
});
app.set("socketio", io);
io.use((socket: SocketWithUser, next: any) => {
const token: any = socket.handshake.query.token;
if (token) {
try {
const payload = jwt.verify(
token,
<string>process.env.JWT_TOKEN
) as DataStoredInToken;
socket.userId = payload._id;
return next();
} catch (err) {
next(err);
}
} else {
return next(new Error("Access Denied"));
}
});
io.on("connection", (socket: SocketWithUser) => {
if (socket.userId) {
socket.join(socket.userId);
socket.emit("joined", `user ${socket.userId} joined`);
}
socket.on("disconnect", () => {
console.log("disconnect");
});
});
and in another route sockethandler
import { Request } from "express";
import { NotificationProps } from "types/notification";
export const sendNotification = (
req: Request,
notification: NotificationProps
) => {
const io = req.app.get("socketio");
io.sockets
.in(String(`${notification.receiver}`))
.emit("newNotification", notification);
};
the like post route looks like
export const likePost = async (req: RequestWithUser, res: Response) => {
const { postId } = req.body;
const post = await PostModel.findById(postId);
if (!post) return res.status(400).send({ msg: `post does not exist` });
const checkIfLiked = post.likes.find(
(item: any) => String(item.user._id) === String(req.user_id)
);
if (!checkIfLiked) {
await post.updateOne(
{
$push: { likes: { user: req.user_id } },
},
{ new: true }
);
const notification = new Notification({
sender: req.user_id,
receiver: post.user,
notificaitonType: "like",
});
await notification.save();
sendNotification(req, notification);
return res.status(200).send({ success: true });
}
const postWithOutLike = await post.updateOne(
{
$pull: { likes: { user: req.user_id } },
},
{ new: true }
);
return res.status(200).send({ postWithOutLike });
};
in the frontend react app just calling it like:
socketIo().on("newNotification", (data) => {
console.log({ data });
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
any help please?
I believe you need want io.in
// to all clients in room
io.in(notification.receiver).emit('newNotification', notification);
Ref
I'm new in Express.js,MongoDb and mongoose, I have created HTTP request methods, but when running the Post method, nothing done (nothing saved in the database), and postman continues loading and it stops only when I cancel.
I want to know what's wrong in my code, thank you .
this is my routes file 'department.js':
const express = require("express");
const router = express.Router();
const Department = require("../models/department")
router.get("/v1/departments", async (req, res) => {
try {
const departments = await Department.find({ isDeleted: false })
if (!departments) {
return res.status(404).send()
}
res.status(200).send()
} catch (error) {
res.status(500).send(error)
}
});
router.get("/v1/department/:id", async (req, res) => {
//test if the department exist => by id
const _id = req.params._id
try {
const depatment = await Department.findByid(_id, { isDeleted: false })
if (!depatment) {
return res.status(404).send()
}
res.status(200).send(depatment)
} catch (error) {
res.status(500).send(error)
}
});
router.post("/department", async (req, res) => {
const department = new Department(req.body) //this param is for testing the post methode by sending request from postman
try {
await department.save()
// res.status(201).send(department)
} catch (error) {
res.status(500).send(error)
}
});
router.put("/v1/department/:id", async (req, res) => {
//updates , allowedUpdates ,isValideOperations : those constants are used for checking if the updated fields exists or not !
//especially useful when testing the put method using postman
const updates = Object.keys(req.body)
const allowedUpdates = ['name', 'email']
const isValideOperations = updates.every((update) => allowedUpdates.includes(update)) //isValideOperations return true if all keys exists
if (!isValideOperations) {
return res.status(400).send({ error: 'Invalid updates' })
}
try {
const _id = req.params.id
const department = await Department.findByIdAndUpdate(_id)
if (!department) {
return res.status(404).send()
}
res.send(department)
} catch (error) {
res.status(500).send(error)
}
})
//Safe delete by updating the field isDeleted to true
router.delete('/v1/department/:id', async (req, res) => {
try {
const _id = req.params.id
const department = await Department.findByIdAndUpdate(_id, { isDeleted: true })
if (!department) {
return res.status(400).send()
}
res.status(200).send(department)
} catch (error) {
res.status(500).send(error)
}
})
module.exports = router
And this is the Model
const mongoose = require("mongoose");
const validator = require('validator')
const Department = mongoose.model('Department', {
name: {
type: String,
required: true,
}
,
email: {
type: String,
required: true,
trim: true,
lowercase: true,
validate(value) {
if (!validator.isEmail(value)) {
throw new Error('Invalid email!')
}
}
}
,
createdBy: {
type: String,
default: 'SYS_ADMIN'
}
,
updatedBy: {
type: String,
default: 'SYS_ADMIN'
}
,
createdAt: {
type: Date
// ,
// default: Date.getDate()
}
,
updatedAt: {
type: Date
// ,
// default: Date.getDate()
},
isDeleted: {
type: Boolean,
default: false
}
})
module.exports = Department
this is Index.js (the main file)
const express = require("express");
const app = express()
const departmentRouter = require("../src/routes/department")
app.use(express.json())
app.use(departmentRouter)
//app.use('/', require('./routes/department'))
const port = process.env.PORT || 3000;//local machine port 3000
app.listen(port, () => (`Server running on local machine port ${port} 🔥`));
In order for you to get a response in Postman (or in an API call in general), you must actually send back a response, whether it's with sendFile, render, json, send, etc. These methods all call the built-in res.end which sends back data. However you commented out res.send which is the reason
I think you forget to add '/v1' in this route. it should be
router.post("/v1/department", async (req, res) => {
const department = new Department(req.body) //this param is for testing the post methode by sending request from postman
try {
await department.save()
// res.status(201).send(department)
} catch (error) {
res.status(500).send(error)
}
});
I am developing a backend project using nodejs, express and mongoose. But when I try to test my endpoints on Postman I keep getting this error. I am new to nodejs so I am not quite sure what this means.
This is my main js file index.js
const app = express();
const mongoose = require('mongoose');
const bodyParser = require('body-parser');
require('dotenv/config');
const apiRoute = require('./routes/api');
const adminRoute = require('./routes/admin');
mongoose.Promise = global.Promise;
const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`listening to port ${port}`));
app.use(bodyParser.json());
app.use('/api', apiRoute);
app.use('/admin', adminRoute);
// As per Mongoose Documentation
const options = {
useNewUrlParser: true,
useCreateIndex: true,
useFindAndModify: false,
useUnifiedTopology: true,
autoIndex: false, // Don't build indexes
//reconnectTries: Number.MAX_VALUE, // Never stop trying to reconnect
//reconnectInterval: 500, // Reconnect every 500ms
poolSize: 10, // Maintain up to 10 socket connections
// If not connected, return errors immediately rather than waiting for reconnect
bufferMaxEntries: 0,
connectTimeoutMS: 10000, // Give up initial connection after 10 seconds
socketTimeoutMS: 45000, // Close sockets after 45 seconds of inactivity
family: 4 // Use IPv4, skip trying IPv6
};
mongoose.connect('mongodb://localhost:27017/admin', options).then(
() => {
console.log('db connected');
},
(err) => {
console.log('connection error');
}
);
This is /routes/api.js
const router = express.Router();
const Ticket = require('../models/Ticket');
const Joi = require('joi');
const { seatValidation, typeValidation } = require('../validation');
//View all tickets
router.get('/tickets', async (req, res) => {
try {
let tickets = await Ticket.find();
res.json(tickets);
} catch (error) {
res.json({ message: error });
}
});
//View Ticket Status
router.get('/status/seat/:seat', async (req, res) => {
// input validation - seat should be a number and between 1-40
let { error } = seatValidation(req.params.seat);
if (error) return res.status(404).send(error);
try {
let status = await Ticket.findOne({ seat: parseInt(req.params.seat) }).select('status');
res.json(status);
} catch (error) {
res.json({ message: error });
}
});
//View Details of person owning the ticket
router.get('/details/seat/:seat', async (req, res) => {
// input validation - seat should be a number and between 1-40
let { error } = seatValidation(req.params.seat);
if (error) return res.status(404).send(error);
try {
let status = await Ticket.findOne({ seat: parseInt(req.params.seat) })
.select('status')
.select('first_name')
.select('last_name')
.select('gender')
.select('email')
.select('mobile');
res.json(status);
} catch (error) {
res.json({ message: error });
}
});
// adding empty seats
router.post('/add', async (req, res) => {
// input validation - seat should be a number and between 1-40
let { error } = seatValidation(req.body);
if (error) return res.status(404).send(error);
let ticket = new Ticket({
seat: req.body.seat,
status: 'open'
});
let ticketSaved = await ticket.save();
try {
res.json(ticketSaved);
} catch (error) {
res.json({ message: error });
}
});
//View all open tickets
router.get('/tickets/open', async (req, res) => {
try {
let tickets = await Ticket.find({ status: 'open' });
res.json(tickets);
} catch (error) {
res.json({ message: error });
}
});
//View all closed tickets
router.get('/tickets/closed', async (req, res) => {
try {
let tickets = await Ticket.find({ status: 'close' });
res.json(tickets);
} catch (error) {
res.json({ message: error });
}
});
//Update the ticket status (open/close + adding user details)
router.put('/status/:seat/:type', async (req, res) => {
// input validation - seat should be a number and between 1-40
let { seatError } = seatValidation(req.params.seat);
if (seatError) return res.status(404).send(seatError);
// input validation - type should be a string and should be 'open' or 'close'
let { typeError } = typeValidation(req.params.type);
if (typeError) return res.status(400).send(typeError);
try {
let type = req.params.type;
if (type === 'open') {
try {
let ticket = await Ticket.updateOne(
{ seat: req.params.seat },
{
$set: { status: 'open' },
$unset: { first_name: 1, last_name: 1, gender: 1, email: 1, mobile: 1 }
}
);
res.json(ticket);
} catch (error) {
res.json({ message: error });
}
} else if (type === 'close') {
// input validation - type should be a string and should be 'open' or 'close'
let ticketSchema = {
first_name: Joi.string().min(3).required(),
last_name: Joi.string().min(3).required(),
gender: Joi.string().valid('M').valid('F').valid('U').required(),
email: Joi.string().email().required(),
mobile: Joi.number().integer().min(1000000000).max(9999999999).required()
};
let validation = Joi.validate(req.body, ticketSchema);
if (validation.error) {
res.status(400).send(validation.error);
return;
}
try {
let ticket = await Ticket.updateOne(
{ seat: req.params.seat },
{
$set: {
status: 'close',
first_name: req.body.first_name,
last_name: req.body.last_name,
gender: req.body.gender,
email: req.body.email,
mobile: req.body.mobile
}
}
);
res.json(ticket);
} catch (error) {
res.json({ message: error });
}
}
} catch (error) {
res.json({ message: error });
}
});
module.exports = router;
This is schema /models/Ticket.js
const mongoose = require('mongoose');
const TicketSchema = mongoose.Schema({
seat: Number,
status: String,
first_name: String,
last_name: String,
gender: String,
email: String,
mobile: Number
});
module.exports = mongoose.model('Ticket', TicketSchema);
Can anyone help me out?
Here is the error from postman:
Postman screenshot
In your index.js file you have specified route for apiRoute as /api and in the screenshot, it appears that you have missed that.
Please use the URL as localhost:3000/api/tickets
instead of just localhost:3000/tickets and it should work correctly.
As you can see as you have missed the /api part the express does not have the path you requested and is returning the status of 404 not found.
I have wrote a simple Update function. Its working fine for some minutes and then again its not working. Where I am going wrong? Please help me. I use PUT as my method.
code
accept = (req, res) => {
this._model.update({
user: new mongoose.Types.ObjectId(req.params.uid)
}, {
$set: {
status: 'active'
}
}, (err, obj) => {
if (err || !obj) {
res.send(err);
} else {
res.send(obj);
}
});
}
Model
{
"_id":"5d3189a00789e24a23438a0d",
"status":"pending",
"user":ObjectId("5d3189a00789e24a23438a0d"),
"code":"CT-123-345-234-233-423344",
"created_Date":"2019-07-19T09:13:04.297Z",
"updated_Date":"2019-07-19T09:13:04.297Z",
"__v":0
}
Request
api.abc.com/api/accept/5d3189a00789e24a23438a0d
Sometime it is returing values and sometime null.
You can use the following code to ensure the model is tied to a connection. This could be an issue of connection to the database.
const config = require('./config');
console.log('config.database.url', config.database.url);
return mongoose.createConnection(config.database.url, {
useMongoClient: true
})
.then((connection) => {
// associate model with connection
User = connection.model('User', UserSchema);
const user = new User({
email: 'someuser#somedomain.com',
password: 'xxxxx'
});
const prom = user.update();
// Displays: 'promise: Promise { <pending> }'
console.log('promise:', prom);
return prom
.then((result) => {
// Don't see this output
console.log('result:', result);
})
.catch((error) => {
// Don't see this output either
console.log('error:', error);
});
})
.catch((error) => {
console.log(error);
});
I think you need to use promise or async/await, try this
accept = async (req, res) => {
try {
const result = await this._model.update({
user: new mongoose.Types.ObjectId(req.params.uid)
}, {
$set: {
status: 'active'
}
});
return res.send(result);
} catch (e) {
return res.send(e);
}
};