Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

node.js + mongo + atomic update of multiple entities = head ache

My setup:

  1. Node.js
  2. Mongojs
  3. A simple database containing two collections - inventory and invoices.
  4. Users may concurrently create invoices.
  5. An invoice may refer to several inventory items.

My problem:

Keeping the inventory integrity. Imagine a scenario were two users submit two invoices with overlapping item sets.

A naive (and wrong) implementation would do the following:

  1. For each item in the invoice read the respective item from the inventory collection.
  2. Fix the quantity of the inventory items.
  3. If any item quantity goes below zero - abandon the request with the relevant message to the user.
  4. Save the inventory items.
  5. Save the invoice.

Obviously, this implementation is bad, because the actions of the two users are going to interleave and affect each other. In a typical blocking server + relational database this is solved with complex locking/transaction schemes.

What is the nodish + mongoish way to solve this? Are there any tools that the node.js platform provides for these kind of things?

like image 200
mark Avatar asked Sep 05 '12 02:09

mark


1 Answers

You can look at a two phase commit approach with MongoDB, or you can forget about transactions entirely and decouple your processes via a service bus approach. Use Amazon as an example - they will allow you to submit your order, but they will not confirm it until they have been able to secure your inventory item, charged your card, etc. None of this occurs in a single transaction - it is a series of steps that can occur in isolation and can have compensating steps applied where necessary.

A naive bus implementation would do the following (keep in mind that this is just a generic suggestion for you to work from and the exact implementation would depend on your specific needs for concurrency, etc.):

  1. place the order on the queue. At this point, you can continue to have your client wait, or you can thank them for their order and let them know they will receive an email when its been processed.
  2. an "inventory worker" will grab the order and lock the inventory items that it needs to reserve. This can be done in many different ways. With Mongo you could create a collection that has a document per orderid. This document would have as its ID the inventory item ID and a TTL that is reasonable (say 30 seconds). As long as the worker has the lock, then it can manage the inventory levels of the items it has locks for. Once its made its changes, it could delete the "lock" document.
  3. If another worker comes along that wants to manage the same item while its locked, you could put the blocked worker into sleep mode for X seconds and then retry or, better yet, you could put the request back onto the message bus to be picked up later by another worker.
  4. Once the worker has resolved all the inventory items, it then can place another message on the service bus that indicates a card should be charged, or processing should receive a notification to pull the inventory, or an email can be sent to the person who made the order, etc., etc.

Sounds complex, but once you have a message bus setup, its actually relatively simple. A list of Node Message Bus Implementations can be found here.

Some developers will even skip the formal message bus completely and use a database as their message passing engine which can work in simple implementations. Google Mongo and Queues.

If you don't expect more than 1 server and the message bus implementation is too bulky, node could handle the locking and message passing for you. For example, if you really wanted to lock with node, you could create an array that stored the inventory item IDs. Although, to be frank, I think the message bus is the best way to go. Anyway, here's some code I have used in the past to handle simple external resource locking with Node.

// attempt to take out a lock, if the lock exists, then place the callback into the array.
this.getLock = function( id, cb ) {

        if(locks[id] ) {
            locks[id].push( cb );
            return false;
        }
        else {
            locks[id] = [];
            return true;
        }
    };

// call freelock when done
 this.freeLock = function( that, id ) {
            async.forEach(locks[id], function(item, callback) {
                item.apply( that,[id]);
                callback();
            }, function(err){
                if(err) {
                  // do something on error
                }

                locks[id] = null;

            });
        };
like image 192
AlexGad Avatar answered Sep 26 '22 00:09

AlexGad