I have implemented transaction in MongoDB as described in this article:
https://forums.meteor.com/t/solved-transactions-with-mongodb-meteor-methods/48677
Utils.js
import { MongoInternals } from 'meteor/mongo';
// utility async function to wrap async raw mongo operations with a transaction
export const runTransactionAsync = async function (asyncRawMongoOperations, errorCode) {
// setup a transaction
const { client } = MongoInternals.defaultRemoteCollectionDriver().mongo;
const session = await client.startSession();
await session.startTransaction();
try {
// running the async operations
let result = await asyncRawMongoOperations(session);
await session.commitTransaction();
// transaction committed - return value to the client
return result;
} catch (err) {
await session.abortTransaction();
console.error(err.message);
// transaction aborted - report error to the client
throw new Meteor.Error(errorCode, err.message);
} finally {
session.endSession();
}
};
Example of using the transactions:
Meteor.methods({
'changeLanguage': async function(poemId, newLanguageId) {
// define the operations we want to run in transaction
const asyncRawMongoOperations = async session => {
const poem = Poems.findOne({ _id: poemId });
const prevLanguage = poem.languageId;
const profile = Profiles.findOne({ userId: this.userId });
await Profiles.rawCollection().update(
{ userId: this.userId },
{
$inc: {
['countLanguages.' + prevLanguage]: -1,
['countLanguages.' + newLanguageId]: 1
},
$addToSet: { languages: newLanguageId }
},
{ session: session }
);
await Poems.rawCollection().update({
'_id': poemId,
'userId': this.userId
},
{
$set: {
languageId: newLanguageId
}
},
{ session: session }
);
return true; // will be the result in the client
};
let result = await runTransactionAsync(asyncRawMongoOperations, 'Error-01');
return result;
}
});
Transactions work correct, but too slow. It can take up to 7 seconds.
MongoDB hosting is MongoDB.Atlas. MongoDB version is 4.0.12.
Where can be the bottleneck in this case?
Related
const client = new MongoClient(uri);
await client.connect();
await client
.db('mydb1')
.collection('foo');
const session = client.startSession();
const transactionOptions = {
readPreference: 'primary',
readConcern: { level: 'local' },
writeConcern: { w: 'majority' }
};
// Step 3: Use withTransaction to start a transaction, execute the callback, and commit (or abort on error)
// Note: The callback for withTransaction MUST be async and/or return a Promise.
try {
await session.withTransaction(async () => {
const coll1 = client.db('mydb1').collection('foo');
await coll1.insertOne({user_id: 12344, paid: true }, { session });
await calls_third_party_payment_vendor_api_to_process_payment();
}, transactionOptions);
} finally {
await session.endSession();
await client.close();
}
Suppose that calls_third_party_payment_vendo_apir_to_process_payment throws an error or any system fails after await coll1.insertOne({user_id: 12344, paid: true }, { session }); was successfully written such that to cause the payment to never actually process, will the document that was inserted be guaranteed to be removed?
I have a cluster with 1 primary and 2 secondary nodes. Also, I have an atlas function witch uses a multi-document transaction. I faced a problem when I do an update and then read this document using .find() I always get not updated version of this document. Here is the function:
exports = async function({ query, headers, body}, response) {
const collection = context.services.get("mongodb-atlas").db("zakhar").collection("w");
// clean up and insert init data
await collection.deleteMany({});
await collection.insertOne({"name": 1, "category": "toy"});
await collection.insertOne({"name": 2, "category": "toy"});
await collection.insertOne({"name": 3, "category": "game"});
await collection.insertOne({"name": 4, "category": "game"});
const session = context.services.get("mongodb-atlas").startSession();
const transactionOptions = {
readPreference: "primary",
readConcern: { level: "local" },
writeConcern: { w: "majority" }
};
try {
await session.withTransaction(async () => {
await updateStatus(session);
}, transactionOptions);
return 'ok';
} catch (err) {
await session.abortTransaction();
throw err;
} finally {
await session.endSession();
}
};
async function updateStatus(session, ){
const collection = context.services.get("mongodb-atlas").db("zakhar").collection("w");
const countBeforeUpdate = await collection.find({"category": "toy"}, {}, { session }).toArray();
console.log("countBeforeUpdate: " + countBeforeUpdate.length);
const query = {"name": 1};
const update = {"$set": {"category": "game"}};
await collection.updateOne(query, update, { session })
.then(result => {
const { matchedCount, modifiedCount } = result;
console.log("matchedCount: " + matchedCount);
console.log("modifiedCount: " + modifiedCount);
});
const countAfterUpdate = await collection.find({"category": "toy"}, {}, { session }).toArray();
console.log("countAfterUpdate: " + countAfterUpdate.length);
}
And it is an output, but I expect countAfterUpdate: 1
"countBeforeUpdate: 2",
"matchedCount: 1",
"modifiedCount: 1",
"countAfterUpdate: 2"
I tried to use all other transaction options(readPreference, readConcern, writeConcern), but nothing changed. Also tried to use session.startTransaction(...) instead of withTransaction()
below is a code snippet from https://www.mongodb.com/blog/post/quick-start-nodejs--mongodb--how-to-implement-transactions
It works perfectly but here is what I don't understand:
This function didn't call session.commitTransaction(), how does it commit the transaction?
it aborts the transaction by determining if isListingReservedResults is null or not null, but my implementation is to throw an error in the if block and catches the error then calling the session.abortTransaction() (which result in a "MongoError: Cannot call abortTransaction twice"), I wonder Why would this happen since i only call it onece.
async function createReservation(client, userEmail, nameOfListing, reservationDates, reservationDetails) {
const usersCollection = client.db("sample_airbnb").collection("users");
const listingsAndReviewsCollection = client.db("sample_airbnb").collection("listingsAndReviews");
const reservation = createReservationDocument(nameOfListing, reservationDates, reservationDetails);
const session = client.startSession();
try {
const transactionResults = await session.withTransaction(async () => {
const usersUpdateResults = await usersCollection.updateOne(
{ email: userEmail },
{ $addToSet: { reservations: reservation } },
{ session });
const isListingReservedResults = await listingsAndReviewsCollection.findOne(
{ name: nameOfListing, datesReserved: { $in: reservationDates } },
{ session });
if (isListingReservedResults) {
await session.abortTransaction();
return;
// throw new Error('message'); myi mplementaion, throw error here then abort the transaction in catch block
}
const listingsAndReviewsUpdateResults = await listingsAndReviewsCollection.updateOne(
{ name: nameOfListing },
{ $addToSet: { datesReserved: { $each: reservationDates } } },
{ session });
});
if (transactionResults) {
console.log("The reservation was successfully created.");
} else {
console.log("The transaction was intentionally aborted.");
}
} catch(e){
console.log("The transaction was aborted due to an unexpected error: " + e);
// await session.abortTransaction(); result in a "MongoError: Cannot call abortTransaction twice"
} finally {
await session.endSession();
}
}
I have the code block below for a MongoDB transaction:
return new Promise(async (resolve, reject) => {
let client = await this.mongoClient();
const transactionSession = client.startSession();
const transactionOptions = {
readPreference: 'primary',
readConcern: { level: 'snapshot' },
writeConcern: { w: 'majority' }
};
const db = client.db("XXX");
try {
const transactionResults = await transactionSession.withTransaction(async () => {
const A = await db.collection("users").updateOne({query}, {$set: {set}}, {transactionSession}).catch(console.error);
const B = await db.collection(this.sessionCollection).updateMany({query}, {$set: {set}}}, {transactionSession}).catch(console.error);
console.log(A, B)
await transactionSession.abortTransaction();
}, transactionOptions);
if (transactionResults) {
console.log("The reservation was successfully created.");
} else {
console.log("The transaction was intentionally aborted.");
return reject({code: 5000});
}
} catch(e){
console.log("The transaction was aborted due to an unexpected error: " + e);
} finally {
await transactionSession.endSession();
}
});
This block code logs "The transaction was intentionally aborted.", but both A and B execute on database!
I am trying to update a field to the document with findByIdAndUpdate. The field I am trying to update is defined in the Bar Model. And I can also assure that req.body.bookId has a valid id.
Here's how my request looks,
app.patch("/foo", async (req, res) => {
try {
await validateId(req.body.bookId);
let doc = await Bar.findByIdAndUpdate(
req.body.bookId,
{ DateT: Date.now() },
{ new: true }
);
res.send(doc);
} catch (err) {
console.log(err);
}
});
Bar schema,
const mongoose = require("mongoose");
const barSchema = mongoose.Schema({
bookId: {
type: String,
unique: true,
},
DateT: {
type: Date,
default: null,
},
});
module.exports = mongoose.model("Bar", barSchema);
use updateOne, when you use async don't use .then() use try/catch
test it:
app.patch("/foo", async (req, res) => {
try {
let doc = await Bar.updateOne(
{ bookId : req.body.bookId },
{ DateT: Date.now() },
{ new: true }
);
res.send(doc);
} catch (error) {
console.log(error);
}
});
app.patch("/foo", async (req, res) => {
await Bar.findByIdAndUpdate(
req.body.bookId,
{ DateT: Date.now()},
(err, docs) => {
if (err) {
console.log(err);
} else {
res.send(docs);
}
}
);
});