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

Java Application Developer's Guide — Chapter 10

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

A multi-statement transaction lets you ensure that all operations finish successfully or roll back the transaction such that the system's state is the same as it was before you opened the transaction. For example, you open a transaction, successfully create a document, and then try to perform a metadata operation on a different document that fails. Responding to the failure, you can rollback the document creation.

Since it was in a rolled back transaction, the metadata operation is not the only one that fails; all operations in the transaction are rolled back, resetting the program's and database's states such that the document was never created. However, if you commit the transaction, then the new document will exist.

A key point about MarkLogic Server multi-statement transactions is that rollbacks do not take place automatically on operation failure. It is up to the Java API developer to write code that checks if operations in a transaction succeed or fail, and, in the event of failure, have the rollback method called. Failure could be detected either by tests of your devising, or detecting that an exception has been thrown.

While you can read and search documents during transactions, most transaction operations are writes and deletes, which alter the database's state. A transaction's purpose is to ensure that either all or none of multiple changes to the database are made.

You can also specify a time limit, so if the transaction does not commit before time expires, it is automatically rolled back. However, you should never write code with the expectation that a timeout will roll back a transaction. The time limit serves as a failsafe, not as a programming tool.

Finally, you should be aware that ordinary Java API MarkLogic operations are automatically in a single operation transaction, and whenever an operation touches a document, it locks the document until that operation succeeds or fails. If the 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 all happens automatically, and you normally do not need to worry about it. The material in the following sections all deals with multi-statement transactions.

Transaction Class

Transactions are straightforward, once you understand they amount to a wrapper around a series of actions to guarantee either all of those actions are successful, or none of them ever happened. There are only three main operations, opening, committing, and rolling back, each of which will be covered in the next few sections.

Transaction is defined in the com.marklogic.client package.

  • Start a transaction:
    DatabaseClient.openTransaction()
  • Commit a transaction when it successfully finishes:
    Transaction.commit()
  • Rollback a multi-statement transaction to reset any actions that have already occured in that transaction; for example, delete any created items, restore any deleted items, revert back any edits, etc.
    Transaction.rollback()

Finally, there is the readStatus() method, which lets you check if the transaction is still open (in other words, it has been opened, but you have not committed it or rolled it back yet).

Starting A Transaction

To start a transaction and obtain a Transaction object, call the openTransaction() method on a DatabaseClient object (since the transaction controls if database changes are made). 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, which is rarely used, 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

Once created and opened, you pass the Transaction object to a document manager's read(), write(), or delete() methods, or a query manager's search() method to perform operations within the multi-statement transaction. 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);

Of course, you could have several different transactions happening at once, and/or other users could 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 almost always want to group all commands 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 with an explicit transaction id 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 transaction id created by one DatabaseClient with a DatabaseClient with a different configuration.

Rolling Back A Transaction

In case of an error or exception, call a transaction's rollback() method. The rollback() method cancels the remainder of the transaction, and reverts the database to its state prior to the transaction's start. With respect to server load, this is better than timing out the transaction. To roll back a transaction, your application must authenticate as rest-writer or rest-admin. Then just call:

transaction.rollback()

Committing A Transaction

Once all of a transaction's actions have successfully completed, you need to commit the transaction so that the database is actually changed by those actions. To commit, your application must authenticate as rest-writer or rest-admin. Then just call:

transaction.commit();

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

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.

When you create a transaction, a HostId cookie is cached on the Transaction object. Whenever your application passes the Transaction object to a method that sends a request to MarkLogic Server, the Java API includes the HostId cookie in the request. You can configure your load balancer to use the HostId cookie to preserve session affinity.

The exact steps required to configure your load balancer to use the HostId cookie for session affinity depend upon your load balancer. Consult your load balancer documentation for details.

If a request is not routed through a load balancer, the HostId cookie is ignored. The Java API does not persist the HostId cookie. The cookie does not include any session state. The cookie value is not used by the Java API.

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 »
Powered by MarkLogic Server 7.0-4.1 and rundmc | Terms of Use | Privacy Policy