15.1 Verification at Commit

With optimistic transactions, instances queried or read from the datastore are not treated as transactional unless they are modified, deleted, or marked by the application as transactional. At commit time, the transactional datastore context is used for verification of inserted, deleted, and updated datastore instances involved in the transaction.

The verification algorithm is not part of the JDO specification, although updates to the same field in the same instance by different transactions must cause a verification failure. The verification can be implemented by different strategies, based on the support provided by different datastores:

  • A JDO implementation might use a special timestamp field in each datastore instance and compare this field for verification. Some datastores provide a special timestamp type that automatically updates its value with every transaction that changes any value in the instance. If such a type is not available, an implementation might simply use an extra field, not visible to the application, to track these changes and manage the values itself.

  • An implementation might use an application-specific set of fields whose values are compared.

  • An implementation might allow your application to aggregate fields into groups and compare all of the values in each affected group to verify that no field in any group has changed.

  • An implementation might allow you to choose a different policy for each persistent class in your model.

Thus, it is possible for different optimistic transactions to perform updates to different fields of the same instance without resulting in an optimistic conflict. The JDO implementation provides a default policy for treating this situation and might allow some application control over the policy.

The JDO implementation verifies that the optimistic assumptions are true before permanently making changes to the datastore. For each transactional instance in the cache, the JDO implementation verifies that the values of the instances in the datastore match the assumed values of the optimistic transaction:

  • Unmodified instances that have been made transactional are verified against the current contents of the datastore. As noted earlier, the verification might be done by comparing timestamps or field values.

  • For application identity, new instances are verified in the datastore to ensure that they do not have the same identity as existing datastore instances. There is no such checking in the case of datastore identity, as this situation cannot occur.

  • Deleted instances are verified to ensure that they have not been deleted or modified by a concurrent transaction.

  • Updated instances are verified to ensure that they have not changed since being fetched into the cache.

If any instance fails verification, the JDO implementation throws a JDOOptimisticVerificationException, which contains an array of JDOExceptions, one for each instance that failed the verification. In this case, the optimistic transaction fails.

15.1.1 Recovery from a Failed Transaction

If an optimistic transaction fails verification at commit time, the transaction rolls back, just as if your application had called rollback( ). The changes made to cached instances revert to their pre-transaction state. Since the optimistic failure indicates that the cache is inconsistent with the state of the datastore, you should refresh the failed instances identified in the exception if you intend to continue to use the cache to retry the failed transaction or to perform new transactions.

After refreshing the cached instances, your application can report the failure to the user or it might attempt to replay the transaction. Replaying is only possible if your application has maintained a change list to reapply changes.

In order to replay the transaction, all instances involved in the transaction must be updated. After beginning a new optimistic transaction, the changes to each instance can be replayed:

  • Unmodified instances that failed verification can be reloaded from the datastore using PersistenceManager.refresh( ).

  • New instances that failed verification can be loaded from the datastore by performing a query or by getting the instance by its primary key.

  • New instances that did not fail verification can be made persistent again.

  • Deleted instances that failed verification because they were already deleted can simply be ignored.

  • Deleted instances that did not fail verification can be deleted again.

  • Updated instances that failed verification can be loaded from the datastore using PersistenceManager.refresh( ).

  • Updated instances that did not fail verification can be updated again.

Note that you must reapply inserts, updates, and deletes using application-consistency rules; otherwise, the consistency guarantees of the datastore are meaningless.

15.1.2 Setting Optimistic Transaction Behavior

Optimistic transactions are an optional feature of a JDO implementation. If an implementation does not support optimistic transactions, it will throw JDOUnsupportedOptionException when you attempt to set the value of the Optimistic property to true.

The Optimistic flag that activates optimistic transactions is a property of PersistenceManagerFactory and Transaction. You can set the property in the Properties instance used to create the PersistenceManagerFactory and access it via getOptimistic( ) and setOptimistic( ). The setting of the property in PersistenceManagerFactory is used as the default for all PersistenceManager instances obtained from it.

Setting the Optimistic flag to true changes the lifecycle-state transitions of persistent instances; therefore you can change the flag only when a transaction is not active. If you attempt to change the flag while a transaction is active, the implementation will throw JDOUserException.

15.1.3 Optimistic Example

To illustrate the programming techniques used in optimistic transactions, we'll modify the UpdateWebSite program to use optimistic transactions. First, we need to set the Optimistic property to true before beginning the transaction. We define executeOptimisticTransaction( ) to set the Optimistic property to true before calling execute( ). We return a boolean to indicate whether the transaction commits successfully:

public boolean executeOptimisticTransaction(  ) {
        try {
            tx.setOptimistic(true);
            tx.begin(  );
            execute(  );
            tx.commit(  );
            return true;
        } catch (JDOException exception){
            analyzeJDOException(exception, System.out);
            return false;
        } catch (Throwable throwable) {
            throwable.printStackTrace(System.out);
            return false;
        } finally {
            if (tx.isActive(  )) {
                try {
                    tx.rollback(  );
                } catch (Exception ex) {
                }
            }
        }
    }

When execute( ) locates the movie by title, the movie is not transactional. When the movie is updated in setWebSite( ), it transitions to transactional and the JDO implementation saves information about the movie to be used at commit:

public void execute(  )
    {
        Movie movie = PrototypeQueries.getMovie(pm, movieTitle);
        if( movie == null ){
            System.err.print("Could not access movie with title of ");
            System.err.println(movieTitle);
            return;
        }
        movie.setWebSite(newWebSite);
    }

At commit, the saved information is used to verify that the update did not conflict with a concurrent transaction; if the verification succeeds, the update is performed and the transaction completes.

Define the analyzeJDOException( ) method to analyze failed optimistic transactions:

public void analyzeJDOException(JDOException jdoException, PrintStream p) {
        p.println("JDOException thrown:");
        p.println(jdoException.toString(  ));
        Throwable[] nestedExceptions = jdoException.getNestedExceptions(  );
        int numberOfExceptions = nestedExceptions.length;
        p.println("Number of nested exceptions: " + numberOfExceptions);
        for (int i = 0; i < numberOfExceptions; ++i) {
            Throwable thrown = nestedExceptions[i];
            if (thrown instanceof JDOException) {
                JDOException instanceException = (JDOException)thrown;
                Object instance = instanceException.getFailedObject(  );
                Object objectId = JDOHelper.getObjectId(instance);
                p.println("Failed instance objectId: " + objectId);
            } else {
                p.println("Nested exception: " + thrown);
            }
        }
    }

We change main( ) to execute the optimistic transaction and, if it fails, retry once:

public static void main (String[] args) {
        String title = args[0];
        String website = args[1];
        UpdateWebSite update = new UpdateWebSite(title, website);
        if (!update.executeOptimisticTransaction(  )) {
            System.out.println("Optimistic transaction failed; retrying");
            if (!update.executeOptimisticTransaction(  )) {
                System.out.println("Failed again.");
            }
        }
    }

Figure 15-1 shows what happens during another example of an optimistic transaction, in which the application queries for movies, accesses the director of a movie, and then changes the web site of the movie. There is no datastore transactional context established at optimistic transaction begin( ). A short datastore transactional context is established in order to retrieve information to satisfy iterator.hasNext( ).

Figure 15-1. Optimistic transaction time line
figs/jdo_1501.gif

When the name of the director is accessed, another datastore transactional context is established. At commit time, the final datastore transactional context is extablished, in which the JDO implementation performs all verification and updates, and commits the changes to the datastore.