Search

    Transaction Management

    Transaction management in CAP deals with (ACID) database transactions, principle / context propagation on service-to-service calls and tenant isolation.

    In Essence

    As an application developer, this is what you need to know:

    • For common service implementations, you don’t have to care for transactions, principle propagation, or tenant isolation — cds.Service core automatically manages that for you.
    • For background threads, use cds.spawn() to ensure proper isolation.

    All the rest of the following explanations are mostly for the rare cases where you need to go beyond this and manage transactions manually.

    Note: srv.tx(req) is deprecated
    Prior to release 5, you always had to write the like of db.tx(req) in application code to ensure context propagation and correctly managed transactions. This is not required nor recommended anymore.

    Automatic Transactions

    Root Transactions

    Whenever an instance of cds.Service processes inbound messages or requests, for example in response to a call like that:

    await db.read('Books')
    

    … the core framework automatically cares for…

    • Principle propagation and tenant isolation
    • Starting database transactions if applicable
    • Finally calling commit or rollback
    • Acquiring and returning physical connections from/to connection pools

    Nested Transactions

    Service implementations commonly consist of event handlers, sending messages/requests to one or more secondary services, database services or other, like this simplistic BankingService:

    class BankingService extends cds.ApplicationService {init(){
      
      const db = cds.connect.to('db')
      const log = cds.connect.to('log')
    
      this.on('transfer', req => {
        let { from, to, amount } = req.data
        return Promise.all ([ // runs all in parallel
          db.update('BankAccount',from).set('balance -=', amount),
          db.update('BankAccount',to).set('balance +=', amount),
          log.insert ({ kind:'Transfer', from, to, amount })
        ])
      })
    	
    }}
    

    Still, all transaction handling is done by the CAP core framework, in this case by orchestrating three transactions:

    1. A root transaction for BankingService.transfer
    2. A nested transaction for the calls to the db service
    3. A nested transaction for the calls to the log service

    Nested transactions are automatically committed when their root transaction is committed upon successful processing of the request; or rolled back if not.

    No Distributed Transactions

    Note that in the previous example, the two nested transactions are synchronized with respect to a final commit / rollback, but not as a distributed atomic transaction.

    This means, it still can happen, that the commit of one nested transaction succeeds, while the other fails. There is no reasonable solution for that; in case of question, apply eventual consistency approaches.

    Manual Transactions

    You can use srv.tx() as documented below to start and commit transactions manually.

    In particular, this is necessary for CLI tools running which want to run multiple database operations atomically, for example:

    // The two inserts below shall happen in one ACID transaction:
    const [ Emily ] = await db.insert (Authors, {name:'Emily Brontë'})
    await db.insert(Books, [
      { title: 'Wuthering Heights', author_ID: Emily.ID }
    ])
    

    srv.tx (context?, fn?) → tx<srv>

    Use cds.tx() to start new app-controlled transactions manually, most commonly for database services as in this example:

    let db = await cds.connect.to('db')
    let tx = db.tx()
    try {
      await tx.run (SELECT.from(Foo))
      await tx.create (Foo, {...})
      await tx.read (Foo)
      await tx.commit()  
    } catch(e) {
      await tx.rollback(e)
    }
    

    Arguments:

    • context – an optional context object → see below
    • fn – an optional function to run → see below

    Returns: a transaction object, which is constructed as a derivate of srv like that:

    tx = Object.create (srv, Object.getOwnPropertyDescriptors({
      commit(){...},
      rollback(){...},
    }))
    

    In effect, tx objects …

    • are concrete context-specific — i.e. tenant-specific — incarnations of srves
    • support all the Service API methods like run, create and read
    • support methods tx.commit and tx.rollback as documented below.

    Important: The caller of srv.tx() is responsible to commit or rollback the transaction, otherwise the transaction would never be finalized and respective physical driver connections never be released / returned to pools.

    srv.tx ({ tenant?, user?, … }) → tx<srv>

    Optionally specify an object with event context properties as the first argument to execute subsequent operations with different tenant or user context:

    let tx = db.tx ({ tenant:'t1' user:'u2' })
    

    The argument is an object with these properties:

    • user — a unique user ID string or an instance of cds.User
    • tenant — a unique string identifying the tenant
    • locale — a locale string in format <language>_<region>

    The implementation constructs a new instance of cds.EventContext from the given properties, which is assigned to tx.context of the new transaction.

    Learn more in section Continuations & Contexts.

    srv.tx ((tx)=>{…}) → tx<srv>

    Optionally specify a function as the last argument to have commit and rollback called automatically. For example, the following snippets are equivalent:

    await db.tx (tx => {
      await tx.run (SELECT.from(Foo))
      await tx.create (Foo, {...})
      await tx.read (Foo)
    })
    
    let tx = db.tx()
    try {
      await tx.run (SELECT.from(Foo))
      await tx.create (Foo, {...})
      await tx.read (Foo)
      await tx.commit()  
    } catch(e) {
      await tx.rollback(e)
    }
    

    srv.tx (ctx) → tx<srv>

    If the argument is an instance of cds.EventContext the constructed transaction will use this context as it’s tx.context. If the specified context was constructed for a transaction started with cds.tx(), the new transaction will be constructed as a nested transaction. If not, the new transaction will be constructed as a root transaction.

    cds.context = { tenant:'t1', user:'u2' }
    const tx = cds.tx (cds.context)
    //> tx is a new root transaction
    
    const tx = cds.context = cds.tx ({ tenant:'t1', user:'u2' })
    const tx1 = cds.tx (cds.context)
    //> tx1 is a new nested transaction to tx
    

    tx.context cds.EventContext

    Each new transaction created by cds.tx() will get a new instance of cds.EventContext constructed and assigned to this property. If there is a cds.context set in the current continuation, the newly constructed context object will inherit properties from that.

    Learn more in section Continuations & Contexts.

    tx.commit (res?) ⇢ res

    In case of database services, this sends a COMMIT (or ROLLBACK) command to the database and releases the physical connection, that is returns it to the connection pool. In addition, the commit is propagated to all nested transactions.

    The methods are bound to the tx instance, and the passed-in argument is returned, or rethrown in case of rollback, which allows them to be used as follows:

    let tx = cds.tx()
    tx.run(...) .then (tx.commit, tx.rollback)
    

    Note: commit or rollback releases the physical connection. This means subsequent attempts to send queries via this tx will fail. This is crucial to protect against hard-to-detect dangling connections, drained connection pools, and in turn servers not responding anymore.

    tx.rollback (err?) ⇢ err

    See documentation for commit.

    DEPRECATED: srv.tx (req) → tx<srv>

    Prior to release 5, you always had to write application code like that to ensure context propagation and correctly managed transactions:

    this.on('READ','Books', req => {
      const tx = cds.tx(req)
      return tx.read ('Books')
    })
    

    This still works but is not required nor recommended anymore.

    Continuations & Contexts

    As JavaScript is single-threaded, we cannot capture request-level invocation contexts such (as current user, tenant, or locale) in what other languages like Java call thread-local variables. But luckily, starting with Node v12, means for so-called “Continuation-Local Storage (CLS)” were given to us. Basically, the equivalent of thread-local variables in the asynchronous continuations-based execution model of Node.js.

    cds.context cds.EventContext

    The current continuation’s event context. Usually this is set by inbound protocol adaptors or by the top-level service starting to process an event.

    The implementation is a getter/setter pair. The setter coerces values into valid instances of cds.EventContext. For example:

    cds.context = { tenant:'t1', user:'u2' }
    let ctx = cds.context
    ctx instanceof cds.EventContext  //> true
    ctx.user instanceof cds.User     //> true
    ctx.tenant === 't1'              //> true
    ctx.user.id === 'u2'             //> true
    

    If a transaction object is assigned, it’s tx.context will be used, hence cds.context = tx acts as a convenience shortcut for cds.context = tx.context:

    let tx = cds.context = cds.tx({ ... })
    tx.context === cds.context  //> true
    

    cds.spawn (options, fn)

    Runs the given function as detached continuation in a specified event context (not inheriting from the current one). Options every or after allow to run the function repeatedly or deferred. For example:

    cds.spawn ({ tenant:'t0', every: 1000 /* ms */ }, (tx) => {
      const mails = await SELECT.from('Outbox') 
      await MailServer.send(mails)
      await DELETE.from('Outbox').where (`ID in ${mails.map(m => m.ID)}`)
    })
    

    Arguments:

    • options is the same as the context argument for cds.tx(), plus:
      • every: <n> number of milliseconds to use in setInterval(fn,n)
      • after: <n> number of milliseconds to use in setTimeout(fn,n)
      • if non of both is given setImmediate(fn) is used to run the job
    • fn is a function representing the background task

    Returns:

    • The responses of setImmediate, setTimeout, or setInterval, which are used to execute the functions. For example, this allows to stop a regular running job:

      let job = cds.spawn({ every:111 }, ...)
      await sleep (11111)
      clearInterval (job) // stops the background job loop
      

    The implementation guarantees decoupled execution from request-handling threads/continuations, by…

    • constructing a new root transaction tx per run using cds.tx()
    • setting that as the background run’s continuation’s cds.context
    • invokes fn passing txas argument to it.

    Think of it as if each run happens in an own thread with own context, with automatic transaction management.

    Use argument options if you want to run the background thread with different user or tenant than the one you called cds.spawn() from.

    Context Propagation

    When creating new root transactions in calls to cds.tx(), all properties not specified in the context argument are inherited from cds.context, if set in the current continuation.

    In effect, this means the new transaction demarcates a new ACID boundary, while it inherits the event context properties unless overridden in the context argument to cds.tx(). The following applies:

    cds.context = { tenant:'t1', user:'u1' }
    cds.context.user.id === 'u1'          //> true
    let tx = cds.tx({ user:'u2' })
    tx.context !== cds.context            //> true
    tx.context.tenant === 't1'            //> true
    tx.context.user.id === 'u2'           //> true
    tx.context.user !== cds.context.user  //> true
    cds.context.user.id === 'u1'          //> true
    

    Exemplary Use Cases

    Setting Root Contexts in Inbound Adapters

    Understanding and applying context propagation is relevant, for example when implementing own protocol adapters. For example, you might implement new RestAdaptor as plain express.js middleware like so:

    const srv = cds.connect.to('SomeService')
    app.use (async function RestAdaptor (req, res, next) {
      const tenant = req.headers['x-tenant']
      const user = req.headers['x-user-id']
      cds.context = { tenant, user } //> set the root event context
      await srv.run (req.query)     //> auto-runs a tx within given context
      next()
    })
    

    The call to srv.run() in line 6 will automatically create and commit a new root transaction with the tenant and user preset for the current continuation in line 5 above.

    Root Transactions in Inbound Adapters

    Alternatively, you might want to not only set the root context but manage the root transaction in the adapter; for example, if you need to do multiple operations, possibly with different services:

    const srv1 = cds.connect.to('SomeService')
    const srv2 = cds.connect.to('AnotheService')
    
    app.use (async function RestAdaptor (req, res, next) {
      const tenant = req.headers['x-tenant']
      const user = req.headers['x-user-id']
      const tx = cds.context = cds.tx({ tenant, user })
      try { 
        //> runs in new managed root tx, with cds.context = tx.context
        await srv1.run (req.query)     //> runs in nested tx 
        await srv2.read ('whatever')   //> runs in nested tx 
        await tx.commit()
      } catch (e) {
        await tx.rollback(e)
      }
      next()
    })
    

    Note: The combination cds.context = cds.tx(...) ensures all transactions created automatically in the following continuation will be created as nested transactions.

    Using cds.spawn() in Inbound Adapters

    You could and should rewrite the previous code sample as follows:

    const srv1 = cds.connect.to('SomeService')
    const srv2 = cds.connect.to('AnotheService')
    
    app.use (async function RestAdaptor (req, res, next) {
      const tenant = req.headers['x-tenant']
      const user = req.headers['x-user-id']
      cds.spawn({ tenant, user }, (tx)=>{ 
        //> runs in new managed root tx, with cds.context = tx.context
        await srv1.run (req.query)     //> runs in nested tx 
        await srv2.read ('whatever')   //> runs in nested tx 
        next()
      })
    })
    

    See reference docs for cds.spawn()

    Using cds.spawn() for Background Jobs

    Always use cds.spawn() to run background jobs. One reason is that they frequently need to run not in context of a current user.

    const privileged = new cds.User.Priviledged
    // run in current tenant context but with privileged user 
    // and with a new database transactions each...
    cds.spawn ({ user: privileged, every: 1000 /* ms */ }, (tx) => {
      const mails = await SELECT.from('Outbox') 
      await MailServer.send(mails)
      await DELETE.from('Outbox').where (`ID in ${mails.map(m => m.ID)}`)
    })
    

    See reference docs for cds.spawn()

    Show/Hide Beta Features