Thursday, February 11, 2010

JPA rolled back objects…

Our project is online ecommerce web site. When an order is made, the inventory record should be updated for inventory allocation. Two customers may buy the same product at the same time, one customer should get the optimistic lock exception. We definitely don’t want the order cannot be made just because optimistic lock exception. We want to catch the optimistic lock exception and retry the “update inventory”. We use spring aop to catch the exception.

@Around("execution(* com.elasticpath.service.catalog.InventoryService.processInventoryUpdate(..))" )
public Object doConcurrentOperation(final ProceedingJoinPoint pjp)
throws Throwable {
int timesRetried = 0;
while (true) {
try {
timesRetried++;
return pjp.proceed();
} catch (JpaOptimisticLockingFailureException oe) {
if (timesRetried >= NUM_ATTEMPTS_INVENTORY_ASSIGNMENT) {
LOG.error("Failed to allocate inventory. Aborting checkout attempt");
//throw new InventoryAllocationException("Allocation failed", oe);
throw oe;
}
try {
Thread.sleep(WAIT_TIME_FOR_CONCURRENCY);
} catch (InterruptedException e) {
// nothing to do...
}
}
}
}


If the exception happens, we run pjp.proceed() again. But we may get the exception below



Caused by:  org.apache.renamed.openjpa.persistence.InvalidStateException: The generated value processing detected an existing value assigned to this field: com.elasticpath.domain.catalog.impl.InventoryAuditImpl.uidPk.  This existing value was either provided via an initializer or by calling the setter method.  You either need to remove the @GeneratedValue annotation or modify the code to remove the initializer processing.
at org.apache.renamed.openjpa.util.ApplicationIds.assign(ApplicationIds.java:446)
at org.apache.renamed.openjpa.util.ApplicationIds.assign(ApplicationIds.java:426)
at org.apache.renamed.openjpa.jdbc.kernel.JDBCStoreManager.assignObjectId(JDBCStoreManager.java:541)


The field uidPk is primary key and automatically generated by openjpa. If it does not exist in database before, the uidPk will be 0. If this field is populated not by openjpa, the exception above was thrown out. When the exception happens, the transaction is rolled back. We expect the object should also be rolled back (set uidpk to be 0 and the state for that field  should be never changed), but it not always happens. Based on JPA specification, you should not use rolled back objects.



An alternative way is to backup the parameter and use the restore it before retry.



@Around("processInventoryUpdatePointcut() && args(inventoryAudit)")
public Object doConcurrentOperationForInventoryUpdate(final ProceedingJoinPoint pjp, final InventoryAudit inventoryAudit)
throws Throwable {
int timesRetried = 0;
LOG.debug("enter the method... timesRetried: " + timesRetried);

//The rolled back domain object may contains populated primary key
//use this object again will get InvalidStateException
InventoryAudit backedupInventoryAudit = (InventoryAudit) BeanUtils.cloneBean(inventoryAudit);
InventoryAudit inventoryAuditUsed = inventoryAudit;

while (true) {
try {
timesRetried++;
LOG.debug("before method proceed... timesRetried: " + timesRetried);
return pjp.proceed(new Object[]{inventoryAuditUsed});
} catch (JpaOptimisticLockingFailureException oe) {
LOG.warn("Failed to create order. Attempt: " + timesRetried);
if (timesRetried >= NUM_ATTEMPTS_INVENTORY_ASSIGNMENT) {
LOG.error("Failed to allocate inventory. Aborting checkout attempt");
//throw new InventoryAllocationException("Allocation failed", oe);
throw oe;
}
try {
Thread.sleep(WAIT_TIME_FOR_CONCURRENCY);
} catch (InterruptedException e) {
// nothing to do...
}
inventoryAuditUsed = (InventoryAudit) BeanUtils.cloneBean(backedupInventoryAudit);
}
}
}

No comments:

Post a Comment