[Mongoose] Transactions vs Fawn (Two Phase Commits)

In the tutorials, Mosh teaches us how to do MongoDB Two Phase Commits using Fawn and Mongoose. As of now, MongoDb and Mongoose support transactions. Fawn hasn’t been updated in 3 years on Npm. Is there anyone that knows how to send a transaction with Mongoose instead of Fawn? Does Fawn even support transactions or is it just Two Phase Commits?

2 Likes

mongoose doc looks very detailed… Mongoose v5.12.0: Transactions

1 Like

bee,

you may be well past this lesson, but…

IMHO, I think the suggestion that 2PC is not a transaction is a misnomer. AFAIK, all major TP monitors use 2PC. So I would frame this a little differently - 2PC is one option for implementing transactions.

Also, fawn has not been updated in 4 years, so I think native support in MongoDB/Mongoose is the preferred option for implementing transactions, but it seems a bit more complex than fawn.

Has anyone found an alternative for fawn, fawnd, fawna?

FWIW, RDBMS’s all have robust transaction processing with rollbacks, etc. that, IMHO, are vastly superior to MongoDB.

Does anyone have any realworld experience with MongoDB’s native transactions compared to RDBMS transaction processing?

Understanding Transaction Processing

Also looking for more information. I installed fawn and saw the listing of deprecations and vulnerabilities from NPM and was like “um… No!” I haven’t found a updated alternative so I started looking into the new “supported” transactions in mongodb and mongoose. I’m running mongoose v6.0.12 and mongodb 5.3 and still at this point it seems transactions are not supported on a standalone server like what we are using in Mosh’s course. Although I don’t see it documented clearly on either mongoose or mongodb documentation. Mongodb does state several times that sharded clusters and replica sets (I don’t even know what those are at this point in my learning) are supported but doesn’t explicitly state that a stand alone server is not supported. I picked that up from a stackoverflow post but still spent a couple of days trying to decide if it really wasn’t supported or I just didn’t know what I was doing.

I did decide that in this particular use case transactions are kinda overkill and figured out my own way to handle the situation. I save the rental and if there is an error I stop and respond, then if rental passes I save the movie. If movie fails to save I remove the previously saved rental document and then respond with a movie validation message followed by “rental canceled” message. Here is the code I have.

routes/rental.js

// import the helper function at top of file
const { removeRental } = require('../models/helpers/rental');

// after creating the new Rental object
// attempt to save rental document

    await rental.save((err) => {

        if(err) return res.status(400).send(`Rental validation failed...\n${err}...`);

    });

   

    // rental passed, attempt to modify and save movie

    movie.numberInStock--;

    await movie.save((err) => {

        // if movie validation fails, remove the new rental document

        if(err) {

            removeRental(rental);

            const message = `Movie validation failed...\n${err}...\n`;    

            return res.status(400).send(`${message}Rental cancled...`);

        }

        return res.status(200).send(rental);

    });

models/helpers/rental.js

const { Rental } = require('../rental');

async function removeRental(rental) {

    await Rental.findByIdAndDelete(rental._id);

}

module.exports.removeRental = removeRental;

I set min for movie numberInStock to 1 and then manually set the number to 1 in mongodb compass for one of my test movies so I could make move.save() have an error. I also tried submitting incorrect movieID and customerID. So far everything works great and I don’t worry about unnecessarily creating and then deleting the rental document because we check the numberInStock earlier in the course code so if it’s out of stock none of this code will ever be reached anyway.

I haven’t figured out what to do if deleting the rental document fails for some reason on the database side. I assume some kind of exception will be thrown but I don’t know how to force an exception to test it.

I found out a way:

In models/rentals.js

step 1: const mongoose = require(‘mongoose’);

step 2: after const rental = new Rental({}), add the following:

    try {
      const session = await mongoose.startSession();
      await session.withTransaction(async () => {
        const result = await rental.save();
        movie.numberInStock--;
        movie.save();
        res.send(result);
      });

      session.endSession();
      console.log('success');
    } catch (error) {
      console.log('error111', error.message);
    }
  }
6 Likes

For me, await the session didn’t worked. So I removed it.

session.withTransaction(async () => {..}

2 Likes

This took a while to understand. Here is what I found:

Transactions cannot be done on a standalone mongod instance (this is exactly what we have if you are following along with the course). We must convert our standalone instance of our database into a replica set. In my opinion, the easiest way to do this is using the mongod command with the --replSet arg option. First, you must edit your mongod config file. This file is found in your bin folder within your MongoDB directory (Generally file is located under this path: C:\Program Files\MongoDB\Server\6.0\bin\mongod.cfg). To edit this file, I recommend notepad++.

mongod.cfg edit:

Within the mongod.cfg file, look for #replication. Add the following lines to end up with:

#replication:
oplogSizeMB: 2000
replSetName: vidlyRep

The two lines following #replication may require indentation (not sure on this).
Note: you can use any name you want for your replica set. I used vidlyRep (feel free to change this). Save the file and you are ready to go.

Now, from the CLI, use the command: mongod --replSet vidlyRep. That’s it. We can now use transactions.

One of the posts above uses the withTransaction() method. This is a cleaner and more maintainable approach to what I ended up with (I was messing around with this for a while and finally landed on code that seems to function appropriately). However, the code above does not include the { session } parameter. This is a critically important parameter to pass to your methods which are updating, creating, etc. a document in your database. My understanding is that the { session } parameter will tie the executed line of code back to the session. Then, if at any point something goes wrong (i.e. exception, error, etc.), the session will abort and rollback all activities tied to it (i.e. those where the { session } parameter was passed). My code does not use this withTransaction() helper method, but rather the traditional approach (i.e. start/abort/end session manually). Here is the code:

4 Likes

Hi all. Like many who take Mosha’s wonderful course, I wondered about transactions and spent a lot of time solving the issue. Unfortunately, the course has become too old and some of the solutions in it have lost their relevance. I spent a couple of days on the problem and searching for relevant solutions and realized that sometimes inaction is a better solution than vigorous activity. After talking with friends who have been in the profession for a long time, I found out that transactions are relevant for distributed systems, where the server application and database are physically located on different computers. Thanks to Mosh for emphasizing this point in the course, it’s important for learning, but in our case, for those who just want to do an educational project, such troubles are unnecessary

Hey guys if you are still looking for soln. As we all know mongoDB now supports transactions. So below is my code.

const express = require("express");
const router = express.Router();
const { Rental, validateRental } = require("../models/rental");
const { validateObjectId } = require("../utils/validateObjectId");
const { Customer } = require("../models/customer");
const { Movie } = require("../models/movie");

router.get("/", async (req, res) => {
  res.send(await Rental.find().sort("-dateOut"));
});

router.get("/:id", validateObjectId, async (req, res) => {
  const rental = await Rental.findById(req.params.id);

  if (!rental) {
    return res.status(404).send("The rental with the given ID was not found.");
  } else res.send(rental);
});

router.post("/", async (req, res) => {
  const { error, value } = validateRental(req.body);
  if (error) return res.status(400).send(error.details[0].message);

  const customer = await Customer.findById(value.customerId);
  if (!customer) return res.status(404).send("Invalid Customer.");

  const movie = await Movie.findById(value.movieId);
  if (!movie) return res.status(404).send("Invalid Movie.");

  if (movie.numberInStock === 0)
    return res.status(400).send("Movie not in stock.");

  // TODO: test this code for ATOMICITY
  const session = await Rental.startSession();
  if (!session)
    return res
      .status(500)
      .send("Internal Server Error: Unable to start a database session.");

  session.startTransaction();

  try {
    const rental = new Rental({
      customer: {
        _id: customer._id,
        name: customer.name,
        phone: customer.phone,
      },
      movie: {
        _id: movie._id,
        title: movie.title,
        dailyRentalRate: movie.dailyRentalRate,
      },
    });

    await rental.save({ session });

    movie.numberInStock--;
    await movie.save({ session });

    await session.commitTransaction();

    res.status(201).send(rental);
  } catch (ex) {
    await session.abortTransaction();
    res.status(500).send(ex.message);
  } finally {
    session.endSession();
  }
});

module.exports = router;

2 Likes