Search code examples
arangodbarangojs

Arango beginTransaction() not rolling back with trx.abort()


I'm having some difficulty with arangodb.beginTransaction(). In my tests, creating a transaction, calling a function via trx.run() API and then aborting the trx does not roll back the data changes on the database. I'm unclear what is happening and the documentation is extremely sparse. There is this documentation which is quite ominous, but also very vague:

If the given function contains asynchronous logic, only the synchronous part of the function will be run in the transaction. E.g. when using async/await only the code up to the first await will run in the transaction.

What about nested async/await inside the async function being called? For example, if I have this:

async runQuery(query) {
    try {
      const cursor = await this.arangodb.query(query)
      return cursor.all()
    } catch (error) {
      logger.error('ArangoDB query failed', { stack: error })
      throw error
    }
}

async touchDocument(id, revision) {
    const type = await this.getObjectTypeFromId(id)
    const collection = await this.getCollection(type, false, true)
    const idCollection = await this.getCollection(ID_COLLECTION, false, true)
    const touchAql = aql`
      LET permanentDocId = DOCUMENT(${idCollection}, ${id}).docId
      LET permanentDoc = MERGE( DOCUMENT(permanentDocId),  { _rev : ${revision} })
      UPDATE permanentDoc WITH permanentDoc in ${collection} OPTIONS { ignoreRevs: false } 
      RETURN NEW
    `
    return this.runQuery(touchAql)
}

trx.run(() => this.touchDocument(parentId, parentRevision))

Will this.arangodb.query() execute inside the transaction or not?


Solution

  • I'm the author of arangojs. For posterity I'd like to clarify that this answer is about arangojs 6, which is the current release of arangojs at the time of this writing and the version that first added support for streaming transactions. The API may change in future versions although there are currently no plans to do so.

    Because of how transactions work (as of arangojs 6), you need to pay special attention to the caveat mentioned in the documentation for the run method:

    If the given function contains asynchronous logic, only the synchronous part of the function will be run in the transaction. E.g. when using async/await only the code up to the first await will run in the transaction. Pay attention to the examples below.

    In other words, async functions will likely not behave correctly if you just wrap them in trx.run. I'd recommend passing the transaction object to each function and wrapping the method calls in those functions in trx.run.

    For example:

    async runQuery(query, trx) {
        try {
          const cursor = await trx.run(() => this.arangodb.query(query))
          return trx.run(() => cursor.all())
        } catch (error) {
          logger.error('ArangoDB query failed', { stack: error })
          throw error
        }
    }
    
    async touchDocument(id, revision, trx) {
        const type = await this.getObjectTypeFromId(id, trx)
        const collection = await this.getCollection(type, false, true, trx)
        const idCollection = await this.getCollection(ID_COLLECTION, false, true, trx)
        const touchAql = aql`
          LET permanentDocId = DOCUMENT(${idCollection}, ${id}).docId
          LET permanentDoc = MERGE( DOCUMENT(permanentDocId),  { _rev : ${revision} })
          UPDATE permanentDoc WITH permanentDoc in ${collection} OPTIONS { ignoreRevs: false } 
          RETURN NEW
        `
        return this.runQuery(touchAql, trx)
    }
    
    this.touchDocument(parentId, parentRevision, trx)
    

    The reason behind this is that trx.run sets the entire driver into "transaction mode", executes the given function and then disables "transaction mode" after executing it so unrelated code doesn't accidentally run in the transaction.

    The drawback of this approach is that if the function is async and contains multiple await statements, only the code leading up to and including the first await will be run in the transaction. In your code that means "transaction mode" is disabled after this.getObjectTypeFromId(id) returns. If that method itself contains multiple await expressions, again only the first one will be part of the transaction (and so forth).

    I hope this clears up some of the confusion.