Loading TOC...
Node.js Application Developer's Guide (PDF)

Node.js Application Developer's Guide — Chapter 7

Managing Transactions

This chapter covers the following topics related to transaction management using the Node.js Client API.

Transaction Overview

This section gives a brief introduction to the MarkLogic Server transaction model as it applies to the Node.js Client API. For a full discussion of the MarkLogic transaction model, see Understanding Transactions in MarkLogic Server in the Application Developer's Guide..

By default, each operation on the database is equivalent to a single statement transaction. That is, the operation is evaluated as single transaction. For example, when you update one or more documents in the database using DatabaseClient.documents.write, the server-side handler effectively creates a new transaction, updates the document(s), commits the transaction, and then sends back a response. The updated documents are visible in the database and available to other operations once the write operation completes successfully. If an error occurs in during the update of one of the documents, the entire operation fails.

The Node.js Client API also enables your application to take direct control of transaction boundaries so that multiple operations can be evaluated in the same transaction context. This is equivalent to the multi-statement transactions described in Multi-Statement Transaction Concept Summary in the Application Developer's Guide.

Using multi-statement transactions, you can execute several operations and commit them as a single transaction, ensuring either all or none of the related updates appear in the database. The document manipulation and search capabilities of the Node.js Client API support multi-statement transactions through the DatabaseClient.transactions interface, plus the ability to pass a transaction object to most operations.

To use multi-statement transactions:

  1. Create a multi-statement transaction using DatabaseClient.transactions.open. This operation returns a transaction object. See Creating a Transaction.
  2. Perform one or more operations in the context of the transaction by including the transaction object for the txid parameter. See Associating a Transaction with an Operation.
  3. Commit the transaction using DatabaseClient.transactions.commit, or rollback the transaction using DatabaseClient.transactions.rollback. See Committing a Transaction and Rolling Back a Transaction.

If your application interacts with MarkLogic Server through a load balancer, you might need to include a HostId cookie in your requests to preserve session affinity. For details, see Managing Transactions When Using a Load Balancer.

When you explicitly create a transaction, you must explicitly commit it or roll it back. Failure to do so leaves the transaction open until the request or transaction timeout expires. Open transactions can hold locks and consume system resources, so it is important to close transactions when they are complete.

If the request or transaction timeout expires before a transaction is committed, the transaction is automatically rolled back and all updates are discarded. Configure the request timeout of the App Server using the Admin UI. Configure the timeout of a single transaction by setting the timeLimit request parameter during transaction creation.

Creating a Transaction

Use DatabaseClient.transactions.open to create a multi-statement transaction. For example:

const txObj = null;
db.transactions.open().result()
  .then(function(response) {
    txObj = response
  });

This call returns a transaction object that encapsulates state needed to preserve host affinity across the transaction, even in the presence of a load balancer.

Multi-statement transactions must be explicitly committed or rolled back. Failure to commit or rollback the transaction before the request timeout expires causes an automatic rollback. You can assign a shorter time limit to a transaction by supplying a time limit (in seconds) to open: For example, the following sets the time limit to tlimit and returns a stateful transaction object:

db.transactions.open({timeLimit: tlimit})

You should not depend on the time limit rolling back your transaction. The limit is only a failsafe. Instead, you should explicitly rollback your transaction when appropriate.

You can also provide a symbolic name when you create a transaction. You must still use the transaction object (or id) in all operations that accept a transaction parameter, but the name can be used with DatabaseClient.transactions.read and will show up in the Admin Interface and other transaction status displays.

For example, the following call provides both a time limit and a name, using an input call object with appropriate property names:

db.transactions.open({
  timeLimit: 45,
  transactionName: 'mySpecialTxn'
});

Associating a Transaction with an Operation

Once you create a transaction using DatabaseClient.transactions.open, you can pass the resulting transaction object (or id) to various operations to perform the operation in the context of a specific transaction.

For example, to update a document in the context of a specific multi-statement transaction, include a transaction id in the DatabaseClient.documents.write call:

const txnObj = null;
db.transactions.open(true).result()
  .then(function(response) {
     txnObj = response;
     return db.documents.write({
       uri: '/my/documents.json',
       content: {some: 'content'},
       contentType: 'application/json,
       txid: txnObj
     }).result;
  ...

Updates associated with a multi-statement transaction are visible to subsequent operations using the same transaction, but they are not visible outside the transaction until the transaction is committed.

You can have multiple transactions open at the same time, and/or other users can be using the same database concurrently. To prevent conflicts, whenever an update occurs in a transaction, the document is locked until the transaction either commits or rolls back. Therefore, you should commit or roll back your transactions as soon as possible to avoid resource contention.

The database context in which you perform an operation must be the same as the database context in which the transaction was created. Consistency is assured if you're only using a single DatabaseClient configuration.

You can intermix operations that are not part of a transaction with operations that are. Any operation without a txid parameter or call object property is not part of a multi-statement transaction. However, you usually group operations in the same transaction together so you can commit or roll back the transaction in a timely fashion.

Committing a Transaction

Use DatabaseClient.transactions.commit to commit a multi-statement transaction. Supply the transaction object (or id) from DatabaseClient.transactions.open in your commit call. For example:

db.transactions.commit(transactionObj);

Once a transaction is committed, it cannot be rolled back, and the transaction object (or id) can no longer be used. To perform another transaction, obtain a new transaction by calling open.

The database context in which you commit or roll back a transaction must be the same as the database context in which the transaction was created. Consistency is assured if you're only using a single DatabaseClient configuration.

Rolling Back a Transaction

In case of an error or exception, you can roll back an open transaction using DatabaseClient.transactions.rollback.

db.transactions.rollback(transactionObj);

Calling rollback cancels the remainder of the transactions and reverts the database to its state prior to the transaction start. It is better to explicitly roll back a transaction than wait for a timeout.

You must have the rest-writer or rest-admin role or equivalent privileges to roll back a transaction.

The database context in which you commit or roll back a transaction must be the same as the database context in which the transaction was created. Consistency is assured if you're only using a single DatabaseClient configuration.

When working with multi-statement transactions, you should ensure your transaction is rolled back explicitly in the event of an error by including a catch clause that calls rollback. For example:

const txnObj = null;
db.transactions.open(true).result().
  then(function(response) {
    txnObj = response;
    return db.documents.read({uris: oldUri, txid: txnObj}).result();
    }).
  then(...).
  catch(function() {
    db.transactions.rollback(txnObj);
    });

Example: Using Promises With a Multi-Statement Transaction

The following function demonstrates how you can use the Promise pattern to synchronize operations within a multi-statement transaction. To learn more about Promises, see Promise Result Handling Pattern.

This function moves a document by reading the contents from the initial URI, inserting the contents into the database with the new URI, and then removing the original document. The function initially creates a transaction, then executes the read, write, and remove operations in the context of that transaction. When these operations complete, the transaction is committed. If an error occurs, the transaction is rolled back.

function transactionalMove(oldUri, newUri) {
  const txnObj = null;
  db.transactions.open(true).result().
    then(function(response) {
      txnObj = response;
      return db.documents.read({uris: oldUri, txid: txnObj}).result();
      }).
    then(function(documents) {
      documents[0].uri = newUri;
      return db.documents.write(
        {documents: documents, txid: txnObj}).result();
      }).
    then(function(response) {
      return db.documents.remove({uri: oldUri, txid: txnObj}).result();
      }).
    then(function(response) {
      return db.transactions.commit(txnObj).result();
      }).
    catch(function(error) {
      console.log('ERROR: ' + JSON.stringify(error));
      db.transactions.rollback(txnObj);
      });
}

Checking Transaction Status

Use DatabaseClient.transactions.read to query the status of a transaction. For example:

db.transactions.read(transactionObj)

Managing Transactions When Using a Load Balancer

This section applies only to client applications that use multi-statement transactions and interact with a MarkLogic Server cluster through a load balancer.

When you use a load balancer, it is possible for requests from your application to MarkLogic Server to be routed to different hosts, even within the same session. This has no effect on most interactions with MarkLogic Server, but operations that are part of the same multi-statement transaction need to be routed to the same host within your MarkLogic cluster. This consistent routing through a load balancer is called session affinity.

Most load balancers provide a mechanism that supports session affinity. This usually takes the form of a session cookie that originates on the load balancer. The client acquires the cookie from the load balancer, and passes it on any requests that belong to the session. The exact steps required to configure a load balancer to generate session cookies depends on the load balancer. Consult your load balancer documentation for details.

To the load balancer, a session corresponds to a browser session, as defined in RFC 2109 (https://www.ietf.org/rfc/rfc2109.txt). However, in the context of a Node.js Client API application using multi-statement transactions, a session corresponds to a single multi-statement transaction.

To properly preserve session affinity, you must call DatabaseClient.transactions.open in a way that returns a transaction object, rather than a simple string transaction id. That is, you must ensure the withState parameter (or call object property) is not explicitly set to false. The transactions.open function returns an object by default.

The Node.js Client API leverages a session cookie to preserve host affinity across operations in a multi-statement transaction in the following way. This process is transparent to your application; the information is provided to illustrate the expected load balancer behavior.

  1. When you create a transaction using DatabaseClient.transactions.open, the Node.js Client API receives a transaction id from MarkLogic and, if the load balancer is properly configured, a session cookie from the load balancer. This information is cached in the returned Transaction object.
  2. Each time you perform a Node.js API operation that includes a Transaction object, the Node.js Client API attaches the transaction id and the session cookie to the request(s) it sends to MarkLogic. The session cookie causes the load balancer to route the request to the same host in your MarkLogic cluster that created the transaction.
  3. When MarkLogic receives a request, it discards the session cookie (if present), but uses the transaction id to ensure the operation is part of the requested transaction. When MarkLogic responds, the load balancer again adds a session cookie, which the Node.js Client API caches on the Transaction object.
  4. When you commit or roll back a transaction, any cookies returned by the load balancer are discarded since the transaction is no longer valid. This effectively ends the session from the load balancer's perspective because the Node.js Client API will no longer pass the session cookie around.

Any Node.js Client API operation that does not include a Transaction object will not include a session cookie (or transaction id) in the request to MarkLogic, so the load balancer is free to route the request to any host in your MarkLogic cluster.

« Previous chapter
Next chapter »