Loading TOC...
Java Application Developer's Guide (PDF)

Java Application Developer's Guide — Chapter 11

Transactions and Optimistic Locking

This chapter covers two different ways for locking documents during MarkLogic Server operations, multi-statement transactions and optimistic locking.

This chapter includes the following sections:

Multi-Statement Transactions

The following sections cover how to put multiple MarkLogic Server operations in a single multi-statement transaction. Specifically, you open a transaction, perform multiple operations in it, and then either rollback or commit the transaction. This section includes the following parts:

For detailed information about transactions in MarkLogic Server, see Understanding Transactions in MarkLogic Server in the Application Developer's Guide.

Transactions and the Java API

By default, most Java Client API interactions with MarkLogic happen in a single transaction. For example, if you use DocumentManager.write to insert a document into the database, the insertion happens as a single transaction that is automatically committed by MarkLogic before a response is sent back to the Java client application.

Requests without multi-statement transactions commit automatically and atomically and can be load balanced

You can use a multi-statement transaction to perform multiple interactions with MarkLogic Server in the context of a single transaction. A multi-statement transaction must be explicitly created and committed or rolled back. For example, you could use a multi-statement transaction to make several calls to DocumentManager.write, and then commit all the writes at once. None of the writes would be visible outside the transaction context unless or until you commit the transaction.

If at all possible, developers should avoid using multi-statement transactions, because 1 atomic network request is more efficient than 3 network requests to open, work, and commit, and because atomic requests can be retried, whereas multi-statement transactions cannot be retried. If you have a use case that requires multi-statement transactions (e.g., where multiple separate requests that mutate the database must complete together or not at all), consider using optimistic locking as a lighter-weight but safe alternative for reading before writing.

Database updates performed in a multi-statement transactions either all succeed or all roll back, depending on whether the transaction is committed or rolled back.

For example, suppose you open a transaction, create a document, and then try to perform a metadata operation on a different document that fails. If, in response to the failure, you roll back the transaction, then neither the document creation nor the metadata update is successful. If you commit the transaction instead, then the document creation succeeds.

Rollbacks do not take place automatically on operation failure. Your application must check for operation success or failure and explicitly rollback the transaction if that is the desired outcome. Failure can be detected by tests of your devising or by trapping and handling a related exception.

Transactions have an associated time limit. If a transaction is not committed before the time limit expires, it is automatically rolled back. The time limit is only a failsafe. You should not design your code with the expectation that a timeout will handle needed rollbacks. Leaving transactions open unnecessarily ties up server-side resources and holds locks on documents. The default time limit is the session time limit configured for your App Server. You can also specify a per transaction time limit when you create a multi-statement transaction; for details, see Starting A Transaction.

A multi-statement transaction must honor host affinity within your MarkLogic cluster. For example, all requests within the context of a transaction should be serviced by the same host. If you use multi-statement transactions in an environment where a load balancer sits between your client application and MarkLogic, then you might need to configure your load balancer to preserve session affinity. For more details, see Transaction Management When Using a Load Balancer.

Note that a document operation performed in the default single statement transaction context locks the document until that operation succeeds or fails. If MarkLogic detects a deadlock, then the transaction is automatically restarted until either it completes or an exception is thrown (for example, by reaching a time limit for the update transaction). This happens automatically, and you normally do not need to worry about it.

Transaction Interface

Use the com.marklogic.client.Transaction interface to manage a transaction. The following are the key operations for managing multi-statement transactions in the Java Client API:

  • Start a multi-statement transaction. For more details, see Starting A Transaction.
    DatabaseClient.openTransaction()
  • Commit a multi-statement transaction when it successfully finishes. For more details, see Committing A Transaction.
    Transaction.commit()
  • Rollback a multi-statement transaction, resetting any actions that previously took place in that transaction. For example, delete any created documents, restore any deleted documents, revert updates, etc. For more details, see Rolling Back A Transaction.
    Transaction.rollback()

You perform operations inside a given multi-statement transaction by passing the Transaction object returned by DatabaseClient.openTransaction into the operation. For details, see Operations Inside A Transaction

Use the Transaction.readStatus method to check whether or not a transaction is still open. That is, whether or not it has been committed or rolled back.

Starting A Transaction

To create a multi-statement transaction and obtain a Transaction object, call the openTransaction() method on a DatabaseClient object. To call openTransaction(), an application must authenticate as rest-writer or rest-admin. For example:

Transaction transaction = client.openTransaction();

You can also include a transaction name and time limit arguments. The timeLimit value is the number of seconds the transaction has to finish and commit before it is automatically rolled back. As previously noted, you should not depend on the time limit rolling back your transaction; it is only meant as a failsafe to end the transaction if all else fails.

Transaction transaction1 = client.openTransaction(MyTrans, 10);

Operations Inside A Transaction

To perform an operation within the context of a multi-statement transaction, pass the Transaction object returned by DatabaseClient.openTransaction into the operation. For example, pass a Transaction object into DocumentManager.read, DocumentManager.write, or QueryManager.search. For example:

// read a document inside a transaction
docMgr.read(myDocId1, handle, myTransaction);
// write a document inside a transaction
docMgr.write(myDocId1, handle, myTransaction);
// delete a document inside a transaction
docMgr.delete(myDocId2, myTransaction);

You can have more than one transaction open at once. Other users can also be running transactions on or sending requests to the same database. To prevent conflicts, whenever the server does something to a document while in a transaction, the database locks the document until that transaction either commits or rolls back. Because of this, you should commit or roll back your transactions as soon as possible to avoid slowing down your and possibly others' applications.

You can intermix commands which are not part of a transaction with transaction commands. Any command without a Transaction object argument is not part of a multi-statement transaction. However, you usually group all operations for a given transaction together without interruption so you can commit or roll it back as fast as possible.

The database context in which you perform an operation in a multi-statement transaction must be the same as the database context in which the transaction was created. The database is set when you create a DatabaseClient, so consistency is assured as long as you do not attempt to use a Transaction object created by one DatabaseClient with an operation performed through a DatabaseClient with a different configuration.

Rolling Back A Transaction

In case of an error or exception, call a transaction's rollback() method:

transaction.rollback()

The rollback() method cancels the remainder of the transaction, and reverts the database to its state prior to the transaction start. Proactively rolling back a transaction puts less load on MarkLogic than waiting for the transaction to time out.

To roll back a transaction, your application must authenticate as rest-writer or rest-admin.

Committing A Transaction

Once your application successfully completes all operations associated with a multi-statement transaction, commit the transaction so that the actions are reflected in the database. Commit a transaction by calling Transaction.commit:

transaction.commit();

To commit a multi-statement transaction, your application must authenticate as rest-writer or rest-admin.

Once a transaction has been committed, it cannot be rolled back and the Transaction object is no longer available for use. To perform another transaction, you must create a new Transaction object.

Cookbook: Multistatement Transaction

See com.marklogic.client.example.cookbook.MultiStatementTransaction for a full example of how to use multi-statement transactions. The Cookbook examples are in the Java API distribution in the following directory:

example/com/marklogic/client/example/cookbook

Transaction Management 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. For additional general-purpose load balancer guidelines, see Connecting 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 Java Client API application using multi-statement transactions, a session corresponds to a single multi-statement transaction.

The Java 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.openTransaction, the Java 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 Transaction object.
  2. Each time you perform a Java Client API operation that includes a Transaction object, the Java 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 ignores 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 Java 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 Java Client API will no longer pass the session cookie around.

Any Java 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.

Optimistic Locking

An application under optimistic locking creates a document only when the document does not exist and updates or deletes a document only when the document has not changed since this application last changed it. However, optimistic locking does not actually involve placing a lock on an object.

Optimistic locking is useful in environments where integrity is important, but contention is rare enough that it is useful to minimize server load by avoiding unnecessary multi-statement transactions.

This section includes the following sub-sections:

Activating Optimistic Locking

Optimistic locking relies on an opaque numeric identifier that is associated with the state of the document's content at a point of time. By default, the REST Server to which the Java API connects does not keep track of this identifier, but you can enable it for use by setting a property, and make it optional or required.

To expand, there is a number associated with every document. Whenever a document's content changes, the value of its number changes. By comparing the stored value of that number at a point in time with the current value, the REST Server can determine if a document's content has changed since the time the stored value was stored.

While this numeric identifier lets you compare state, and uses a numeric value to do so, this is not document versioning. The numeric identifier only indicates that a document has been changed, nothing more. It does not store multiple versions of the document, nor does it keep track of what the changes are to a document, only that it has been changed at some point. You cannot use this for change-tracking or archiving previous versions of a document.

Since this App Server configuration parameter applies either to all documents or none, it is implemented in the REST Server. This means it is part of the overall server configuration, and must be turned on and off via a ServerConfigurationManager object and thus requires rest-admin privileges. For more about server configuration management, see REST Server Configuration.

To activate optimistic locking, do the following:

// if not already done, create a database client
DatabaseClient client = DatabaseClientFactory.newClient(...);

// create server configuration manager
ServerConfigurationManager configMgr =
                           client.newServerConfigManager();

// read the server configuration from the database
configMgr.readConfiguration();

// require content versions for updates and deletes
// use UpdatePolicy.VERSION_OPTIONAL to allow but not 
// require identifier use. Use UpdatePolicy.MERGE_METADATA
// (the default) to deactive identifier use
configMgr.setUpdatePolicy(UpdatePolicy.VERSION_REQUIRED);

// write the server configuration to the database
configMgr.writeConfiguration();

// release the client
client.release();

Allowed values for UpdatePolicy are in the Enum ServerConfigurationManager.UpdatePolicy.

DocumentDescriptors

To work with a document's change identifier, you must create a DocumentDescriptor for the document. A DocumentDescriptor describes exactly one document and is created via use of an appropriately typed method for the document. For more information on document managers, see Document Managers.

// create a descriptor for versions of the document
DocumentDescriptor desc = docMgr.newDescriptor(docId);

You can also get a document's DocumentDescriptor by checking to see if the document exists. This code returns the specified document's DocumentDesciptor or, if the document does not exist, null:

DocumentDescriptor desc = docMgr.exists(docId);

Using Optimistic Locking

Each read(), write(), and delete() method for DocumentManager has both a version that uses a URI string parameter to identify the document to be read, written, or deleted, and an identical version that uses a DocumentDescriptor object instead. The descriptor is only populated with state when you read a document or when you check for a document's existence. When you write, the state changes, but is not reflected in the descriptor.

When UpdatePolicy is set to VERSION_REQUIRED, you must use the DocumentDescriptor versions of the write() (when modifying a document) and delete() methods. If the change identifier has not changed, the write or delete operation succeeds. If someone else has changed the document so that a new version has been created, the operation fails by throwing an exception.

There is no general notification when UpdatePolicy changes to VERSION_REQUIRED. If the policy changes to required and an application uses the URI string version of read(), etc., such requests will now fail and throw exceptions.

If you are creating a document under VERSION_REQUIRED, you either must not supply a descriptor, or if you do pass in a descriptor it must not have state. A descriptor is stateless if it is created through a DocumentManager and has not yet been populated with state by a read() or exists() method. If the document does not exist, the operation succeeds. If the document exists, the operation fails and throws an exception.

When UpdatePolicy is set to VERSION_OPTIONAL, if you do not supply an identifier value via the descriptor and use the docId versions of write() and delete(), the operation always succeeds. If you do supply an identifier value by using the DocumentDescriptor versions of write() and delete(), the same rules apply as above when the update policy is VERSION_REQUIRED.

The identifier value always changes on the server when a document's content changes there.

The optimistic part of optimistic locking comes from this not being an actual lock, but rather a means of checking if another application has changed a document since you last accessed it. If another application does try to modify the document, the Server does not even try to stop it from doing so. It just changes the document's identifier value.

So, the next time your application accesses the document, it compares the number it stored for that document with its current number. If they are different, your application knows the document has been changed since it last accessed the document. It could have been changed once, twice, a hundred times; it does not matter. All that matters is that it has been changed. If the numbers are the same, the document has not been changed since you last accessed it.

Cookbook: Version Control and Optimistic Locking

See com.marklogic.client.example.OptimisticLocking in the Cookbook for a full example of how to use and optimistic locking. The Cookbook examples are in the Java API distribution in the following directory:

example/com/marklogic/client/example/cookbook
« Previous chapter
Next chapter »