DEV Community

Cover image for Handling Nested Db Transactions using CLS
AkankshaPriyadrshini
AkankshaPriyadrshini

Posted on • Edited on • Originally published at Medium

Handling Nested Db Transactions using CLS

Before we start, let me briefly tell you about one of the issues we faced @Casaone. Once an order gets placed, we do a couple of associated actions like inventory allocation. Inventory Allocation performs a set of operations in a nested transactions. At times we encountered situations where the inventory allocation failed but other warehouse transactions (inside the main transaction) was successful. This led to a lot of confusion while debugging what went wrong.

What we really wanted is for the entire transaction to roll back if anything goes wrong while allocating the inventory. A change should reflect everywhere or nowhere at all.

Nested Database Transactions

function Func1() {
    Transaction1(() => {
        Func2()
        // other set of actions.
    });
};



function Func2() {
    Transaction2(() => {
       // set of actions
    });
};

Let’s assume we have some function Func1 and it has some code to perform a set of actions inside a Db Transaction (Transaction1). One of the actions inside it calls another function named Func2 which has another set of actions to be performed inside a transaction (Transaction2).

Now, imagine a case where the transaction inside Func2 gets committed but for some reason transaction inside Func1 rolls back. This could lead to different kinds of problem like data inconsistency.

One of the ways we could solve this is by passing the Transaction Object from parent to the child. We can then use the same transaction object instead of creating a new one. But, imagine having to do this at all the places. It’s quite cumbersome. This is where CLS comes at our rescue.

Continuation-Local Storage or CLS

In multithreaded languages like JAVA, global data can be associated or attached with each thread using Thread Local Storage. This however is not helpful in a single-threaded and asynchronous behaviour of Node JS. This is where CLS or Continuation-Local Storage comes into picture.

CLS enables us to attach the data to the current asynchronous execution context. Values in CLS are grouped into namespaces. These values are available to us until all the functions, called synchronously or asynchronously from the original function have finished executing.

We can automatically pass transaction object to all queries using CLS. Let’s look at how to do this. I will use Sequelize as an ORM for the purpose of this article.

cls-hooked is the module we will be using. Internally it uses async_hooks which keeps an eye on the context changes. Accordingly, it loads and unloads the data attached with it.

async_hooks module provides an API to track asynchronous resources. It helps in registering callbacks which is used to track the entire life of an asynchronous resource.

Let’s import the required packages.

const cls = require('cls-hooked');
const Sequelize = require('sequelize');

Now, we will create a namespace for keeping values in CLS using createNamespace method and ask Sequelize to use it. The level of grouping or the namespace depends on the use case.

const namespace = cls.createNamespace('your-namespace');
Sequelize.useCLS(namespace);

Now, whenever a db transaction will start, the details of that transaction will be stored against a key transaction in the particular namespace.

The idea is to execute a set of actions (task) under the same transaction object (if exists) else create a new one.

const sequelize = new Sequelize();
const db = {};
db.sequelize = sequelize();
// check if transaction object exists in the namespace else create a new one
db.transaction = (task) => {
    return namespace.get('transaction')
        ? task()
        : sequelize.transaction(task);
};

Now we can use our db.transaction wherever required.

const db = require('../db/models')
function Func1() {
    db.transaction(() => {
        Func2()
        // other set of actions.
    });
};
function Func2() {
    db.transaction(() => {
       // set of actions
    });
};

Notes:

async_hooks used with CLS might have some impact on performance. You should evaluate whether to go with it or not according to the use case. You can check out the demo by Benedikt Meurer on performance comparison with and without async_hooks.

Resources

  1. https://nodejs.org/api/async_hooks.html
  2. https://github.com/Jeff-Lewis/cls-hooked
  3. https://github.com/bmeurer/async-hooks-performance-impact ...

Originally published on my website.

Top comments (0)