Papooch / nestjs-cls

A continuation-local storage (async context) module compatible with NestJS's dependency injection.

Home Page:https://papooch.github.io/nestjs-cls/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

How to store DB transaction object that is available only within a callback?

sam-artuso opened this issue · comments

Some DB libraries manage a DB transaction within a callback.

Here's an example of what Sequelize calls a "managed transaction":

await sequelize.transaction(async (t) => {
  // queries in here can use `t` 
});

Heres' another (very similar) example with pg-promise:

await db.tx(t => { /* ... */ })

The advantage is that the commit or rollback are automatic based on whether an error is thrown within the callback.

I'm having a hard time figuring out how to store t in AsyncLocalStorage globally. My sense is that I should use an interceptor, but I'm not sure how?

In short, basically the same way you did here. But you also need to make sure to remove the transaction object from the context once the transaction finishes.

This can be done in two ways:

  1. Re-use the same CLS-context
// if we're in a nested transaction, remember the previous one
const prevTx = cls.get('transaction')
// wrap the call in managed transaction
await db.tx(async t => {
  try {
     cls.set('transaction', t)
     return await someService.doStuff()
  } finally {
     // make sure to replace the transaction with the original value
     // otherwise it will be accessible even after the transaction is complete
     // which will cause errors in case you attempt to re-use it
     cls.set('transaction', prevTx)
  }
})
  1. Create a nested CLS-context around the call
// make sure to inherit the existing context, otherwise it will be empty
cls.run({ ifNested: 'inherit' }, async () => {
   return db.tx(async t => {
      // this sets the transaction on the nested context only
      cls.set('transaction', t)
      return await someService.doStuff()
   })
})

The latter one has the nice property that CLS-enabled transactions will work even if no context was initialized prior.

Note: Sequelize supports CLS-enabled transactions out of the box

I'm not sure I made myself clear. My question is about how to link an HTTP request to a fresh db transaction:

    ClsModule.forRoot({
      global: true,
      middleware: {
        mount: true,
        setup: (cls, req) => ???,
      },
    }),

P.S.: this is for an HTTP context, not a "CLI" context like in this other question of mine.

I see, well, if you want to wrap the entire request with a transaction, which I would not recommend for the reasons listed below (*), you cannot do it via the ClsMiddleware setup. That function is there only to populate the context, but you can't further wrap the execution of the flow. For that, you'd need to create a custom middleware or interceptor that wraps the next() (or handle.next()) call with what I shared earlier (replacing someService.doStuff() with the next call).

(*) Since wrapping code with transaction effectively executes the transaction at the database level, it is advised to only start it when you absolutely need it as close to the actual database calls as possible. If you implicitly wrap each request, then you'll end up with an empty transaction a lot of times, in case the request flow never makes it to the database call, or you'll be unnecessarily locking the database for simple SELECT queries.

Yes, it's true, my approach would create a lot of empty transactions and has overhead. Still, some people do it because it's simple.

Do you have any suggestions on how to create a decorator that I can just drop at the point closest to where I need the transaction to start?

As a matter of fact, I do. And I am currently in process of releasing a first draft of the @nestjs-cls/transactional plugin (#96). There is a PR ready (#102) so you can look through the code (although it's a bit complicated, given it should be generic).

There are two important parts there: The TransactionalHost provider, with the universal withTransaction method used to wrap pieces of code in a transaction, together with the TransactionalAdapter which defines how to wrap the piece of code with a transaction based on a specific database connection object, and the @Transactional decorator which makes Nest inject TransactionHost into the class instance and wraps the actual method with the TransactionHost#withTransaction call. The transactional client itself is available on TransactionHost#client.
You can also look at the unit test suite about how it is used.

In the following days, I plan on releasing version 4 of nestjs-cls together with the transactional plugin and a Prisma adapter for starters and then continue implementing other adapters as well.