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();
}
}
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()
The following works fine, but I have noticed that it is really slow login in a client. How can I make it faster?
import NextAuth from "next-auth"
import CredentialsProvider from "next-auth/providers/credentials"
import { ObjectId } from 'mongodb'
import { MongoDBAdapter } from "#next-auth/mongodb-adapter"
import clientPromise from "../../../lib/mongodb";
import { v4 as uuidv4 } from 'uuid';
var CryptoJS = require("crypto-js");
const sFinder = async (task, token) => {
try{
const client = await clientPromise;
const database = client.db('DRN1');
const ses = await database.collection('sessions');
switch (task) {
case 1:
const result = await ses.find({
"userId": ObjectId(token.uuid)
}).sort({"_id":-1}).limit(1).toArray();
if (!result) {
return 202;
}
else{
return result[0].sessionToken
}
break;
case 2:
const insertResult = await ses.insertOne({"userId":token.uuid, "sessionToken":token.accessToken});
if (!insertResult) {
return 203;
}
else{
return insertResult
}
break;
case 3:
var expdate = new Date(token.exp * 1000);
const UpdateResult = await ses.updateOne({"userId":ObjectId(token.uuid), "sessionToken":token.accessToken},
{ $set: {"expires": expdate}}, { upsert: true });
if (!UpdateResult) {
return 203;
}
else{
return UpdateResult
}
break;
default:
break;
}
} catch(e){
console.error(e);
}
}
export default NextAuth({
adapter: MongoDBAdapter(clientPromise),
session: {
strategy: 'jwt',
jwt: true,
},
providers: [
CredentialsProvider({
name: 'DRN1',
credentials: {
username: { label: "Username", type: "text"},
password: { label: "Password", type: "password" }
},
async authorize(credentials, req) {
try{
const client = await clientPromise;
const database = client.db('DRN1');
const users = await database.collection('users');
const result = await users.findOne({
username: credentials.username,
});
if (!result) {
throw new Error('No user found with the username');
}
var bytes = CryptoJS.AES.decrypt(result.password, process.env.PASS_ENC);
var decryptedData = bytes.toString(CryptoJS.enc.Utf8);
//Check hased password with DB password
if(decryptedData != credentials.password){
throw new Error('Password doesnt match');
}
return {uuid:result._id, username: result.username, email: result.email, type:result.type, "sessionID":uuidv4()};
} catch(e){
console.error(e)
}
}
})
],
callbacks: {
signIn: async ({ user, account, profile, email, credentials }) => {
account.accessToken = user.sessionID
account.uuid = user.uuid
const test = await sFinder(2,account)
return true
},
jwt: async ({ token, account }) => {
if (account) {
token.uuid = account.uuid
token.accessToken = account.accessToken
}
const lastUsedToken = await sFinder(1,token)
const updateTokenExpire = await sFinder(3,token)
if(lastUsedToken != token.accessToken){
// console.log("I have made it an error")
token.error = 555;
}
return token
},
session: async ({ session, token, user }) => {
session.uuid = token.uuid
if(!token.accessToken){
//OAUTH Accounts
session.accessToken = uuidv4()
}else{
session.accessToken = token.accessToken
}
if(token.error == 555){
session.error = 555
}
return session
}
},
pages:{
error: 'signin'
},
theme: {
colorScheme: "dark", // "auto" | "dark" | "light"
brandColor: "", // Hex color code
logo: "https://storage.googleapis.com/radiomedia-images/station_logos/v2/DRN1_small.png" // Absolute URL to image
}
});
I believe what is slowing it down is the following
callbacks: {
signIn: async ({ user, account, profile, email, credentials }) => {
account.accessToken = user.sessionID
account.uuid = user.uuid
const test = await sFinder(2,account)
return true
},
jwt: async ({ token, account }) => {
if (account) {
token.uuid = account.uuid
token.accessToken = account.accessToken
}
const lastUsedToken = await sFinder(1,token)
const updateTokenExpire = await sFinder(3,token)
if(lastUsedToken != token.accessToken){
// console.log("I have made it an error")
token.error = 555;
}
return token
},
session: async ({ session, token, user }) => {
session.uuid = token.uuid
if(!token.accessToken){
//OAUTH Accounts
session.accessToken = uuidv4()
}else{
session.accessToken = token.accessToken
}
if(token.error == 555){
session.error = 555
}
return session
}
},
Mainly all the awaits, but the await functions are to make sure the user is not login on another device. As we log the old devices out automatically.
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 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?