/*
* Hibernate OGM, Domain model persistence for NoSQL datastores
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.ogm.backendtck.compensation;
import static org.fest.assertions.Assertions.assertThat;
import static org.fest.assertions.Fail.fail;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
import org.hibernate.StaleObjectStateException;
import org.hibernate.Transaction;
import org.hibernate.ogm.OgmSession;
import org.hibernate.ogm.OgmSessionFactory;
import org.hibernate.ogm.cfg.OgmProperties;
import org.hibernate.ogm.compensation.ErrorHandler.FailedGridDialectOperationContext;
import org.hibernate.ogm.compensation.ErrorHandler.RollbackContext;
import org.hibernate.ogm.compensation.operation.CreateTupleWithKey;
import org.hibernate.ogm.compensation.operation.ExecuteBatch;
import org.hibernate.ogm.compensation.operation.GridDialectOperation;
import org.hibernate.ogm.compensation.operation.InsertOrUpdateTuple;
import org.hibernate.ogm.compensation.operation.UpdateTupleWithOptimisticLock;
import org.hibernate.ogm.dialect.batch.spi.BatchableGridDialect;
import org.hibernate.ogm.dialect.batch.spi.GroupingByEntityDialect;
import org.hibernate.ogm.dialect.impl.GridDialects;
import org.hibernate.ogm.dialect.optimisticlock.spi.OptimisticLockingAwareGridDialect;
import org.hibernate.ogm.dialect.spi.DuplicateInsertPreventionStrategy;
import org.hibernate.ogm.dialect.spi.GridDialect;
import org.hibernate.ogm.dialect.spi.TupleAlreadyExistsException;
import org.hibernate.ogm.model.impl.DefaultEntityKeyMetadata;
import org.hibernate.ogm.utils.GridDialectType;
import org.hibernate.ogm.utils.OgmTestCase;
import org.hibernate.ogm.utils.SkipByGridDialect;
import org.hibernate.ogm.utils.TestHelper;
import org.hibernate.resource.transaction.spi.TransactionStatus;
import org.junit.After;
import org.junit.BeforeClass;
import org.junit.Test;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
/**
* Tests around the error compensation SPI.
*
* @author Gunnar Morling
*/
public class CompensationSpiTest extends OgmTestCase {
private static ExecutorService executor;
@BeforeClass
public static void setUpExecutor() {
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat( "ogm-test-thread-%d" ).build();
executor = Executors.newSingleThreadExecutor( threadFactory );
}
@Test
public void onRollbackPresentsAppliedInsertOperations() {
OgmSession session = openSession();
session.getTransaction().begin();
// given two inserted records
session.persist( new Shipment( "shipment-1", "INITIAL" ) );
session.persist( new Shipment( "shipment-2", "INITIAL" ) );
session.flush();
session.clear();
try {
// when provoking a duplicate-key exception
session.persist( new Shipment( "shipment-1", "INITIAL" ) );
session.getTransaction().commit();
}
catch (Exception e) {
rollbackTransactionIfActive( session.getTransaction() );
}
// then expect the ops for inserting the two records
Iterator<RollbackContext> onRollbackInvocations = InvocationTrackingHandler.INSTANCE.getOnRollbackInvocations().iterator();
Iterator<GridDialectOperation> appliedOperations = onRollbackInvocations.next().getAppliedGridDialectOperations().iterator();
assertThat( onRollbackInvocations.hasNext() ).isFalse();
if ( currentDialectHasFacet( BatchableGridDialect.class ) ||
currentDialectHasFacet( GroupingByEntityDialect.class ) ) {
assertThat( appliedOperations.next() ).isInstanceOf( CreateTupleWithKey.class );
assertThat( appliedOperations.next() ).isInstanceOf( CreateTupleWithKey.class );
GridDialectOperation operation = appliedOperations.next();
assertThat( operation ).isInstanceOf( ExecuteBatch.class );
ExecuteBatch batch = operation.as( ExecuteBatch.class );
Iterator<GridDialectOperation> batchedOperations = batch.getOperations().iterator();
assertThat( batchedOperations.next() ).isInstanceOf( InsertOrUpdateTuple.class );
assertThat( batchedOperations.next() ).isInstanceOf( InsertOrUpdateTuple.class );
assertThat( batchedOperations.hasNext() ).isFalse();
}
else {
assertThat( appliedOperations.next() ).isInstanceOf( CreateTupleWithKey.class );
assertThat( appliedOperations.next() ).isInstanceOf( InsertOrUpdateTuple.class );
assertThat( appliedOperations.next() ).isInstanceOf( CreateTupleWithKey.class );
assertThat( appliedOperations.next() ).isInstanceOf( InsertOrUpdateTuple.class );
}
// If LOOK_UP is used for duplicate prevention, the duplicated id will be detected prior to the actual insert
// itself; otherwise, the CreateTuple call will succeed, and only the insert call will fail
if ( currentDialectUsesLookupDuplicatePreventionStrategy() ) {
assertThat( appliedOperations.hasNext() ).isFalse();
}
else {
assertThat( appliedOperations.next() ).isInstanceOf( CreateTupleWithKey.class );
}
session.close();
}
@Test
public void onRollbackPresentsAppliedInsertOperationsForSave() {
OgmSession session = openSession();
session.getTransaction().begin();
// given two inserted records
session.persist( new Shipment( "shipment-1", "INITIAL" ) );
session.persist( new Shipment( "shipment-2", "INITIAL" ) );
session.flush();
session.clear();
try {
// when provoking a duplicate-key exception
session.save( new Shipment( "shipment-1", "INITIAL" ) );
session.getTransaction().commit();
}
catch (Exception e) {
rollbackTransactionIfActive( session.getTransaction() );
}
// then expect the ops for inserting the two records
Iterator<RollbackContext> onRollbackInvocations = InvocationTrackingHandler.INSTANCE.getOnRollbackInvocations().iterator();
Iterator<GridDialectOperation> appliedOperations = onRollbackInvocations.next().getAppliedGridDialectOperations().iterator();
assertThat( onRollbackInvocations.hasNext() ).isFalse();
if ( currentDialectHasFacet( BatchableGridDialect.class )
|| currentDialectHasFacet( GroupingByEntityDialect.class ) ) {
assertThat( appliedOperations.next() ).isInstanceOf( CreateTupleWithKey.class );
assertThat( appliedOperations.next() ).isInstanceOf( CreateTupleWithKey.class );
assertThat( appliedOperations.next() ).isInstanceOf( ExecuteBatch.class );
}
else {
assertThat( appliedOperations.next() ).isInstanceOf( CreateTupleWithKey.class );
assertThat( appliedOperations.next() ).isInstanceOf( InsertOrUpdateTuple.class );
assertThat( appliedOperations.next() ).isInstanceOf( CreateTupleWithKey.class );
assertThat( appliedOperations.next() ).isInstanceOf( InsertOrUpdateTuple.class );
}
// If LOOK_UP is used for duplicate prevention, the duplicated id will be detected prior to the actual insert
// itself; otherwise, the CreateTuple call will succeed, and only the insert call will fail
if ( currentDialectUsesLookupDuplicatePreventionStrategy() ) {
assertThat( appliedOperations.hasNext() ).isFalse();
}
else {
assertThat( appliedOperations.next() ).isInstanceOf( CreateTupleWithKey.class );
}
session.close();
}
@Test
public void onRollbackPresentsAppliedUpdateOperations() throws Exception {
OgmSession session = openSession();
session.getTransaction().begin();
Shipment shipment1 = new Shipment( "shipment-1", "INITIAL" );
session.persist( shipment1 );
Shipment shipment2 = new Shipment( "shipment-2", "INITIAL" );
session.persist( shipment2 );
session.getTransaction().commit();
session.clear();
session.getTransaction().begin();
try {
Shipment loadedShipment1 = (Shipment) session.get( Shipment.class, "shipment-1" );
Shipment loadedShipment2 = (Shipment) session.get( Shipment.class, "shipment-2" );
// do an update in parallel and wait until its done
Future<?> future = updateShipmentInConcurrentThread( "shipment-2", "PROCESSING" );
future.get();
loadedShipment1.setState( "PROCESSING" );
loadedShipment2.setState( "PROCESSING" );
session.getTransaction().commit();
fail( "expected exception was not raised" );
}
catch (StaleObjectStateException sose) {
// Expected
}
finally {
rollbackTransactionIfActive( session.getTransaction() );
session.close();
}
Iterator<RollbackContext> onRollbackInvocations = InvocationTrackingHandler.INSTANCE.getOnRollbackInvocations().iterator();
Iterator<GridDialectOperation> appliedOperations = onRollbackInvocations.next().getAppliedGridDialectOperations().iterator();
assertThat( onRollbackInvocations.hasNext() ).isFalse();
if ( currentDialectHasFacet( OptimisticLockingAwareGridDialect.class ) ) {
GridDialectOperation appliedOperation = appliedOperations.next();
assertThat( appliedOperation ).isInstanceOf( UpdateTupleWithOptimisticLock.class );
UpdateTupleWithOptimisticLock updateTupleWithOptimisticLock = appliedOperation.as( UpdateTupleWithOptimisticLock.class );
assertThat( updateTupleWithOptimisticLock.getEntityKey().getTable() ).isEqualTo( "Shipment" );
assertThat( updateTupleWithOptimisticLock.getEntityKey().getColumnValues() ).isEqualTo( new Object[] { "shipment-1" } );
}
else if ( currentDialectHasFacet( GroupingByEntityDialect.class ) ) {
GridDialectOperation operation = appliedOperations.next();
assertThat( operation ).isInstanceOf( ExecuteBatch.class );
ExecuteBatch batch = operation.as( ExecuteBatch.class );
Iterator<GridDialectOperation> batchedOperations = batch.getOperations().iterator();
InsertOrUpdateTuple insertOrUpdate = batchedOperations.next().as( InsertOrUpdateTuple.class );
assertThat( insertOrUpdate.getEntityKey().getTable() ).isEqualTo( "Shipment" );
assertThat( insertOrUpdate.getEntityKey().getColumnValues() ).isEqualTo( new Object[] { "shipment-1" } );
assertThat( batchedOperations.hasNext() ).isFalse();
}
else {
GridDialectOperation appliedOperation = appliedOperations.next();
assertThat( appliedOperation ).isInstanceOf( InsertOrUpdateTuple.class );
InsertOrUpdateTuple insertOrUpdate = appliedOperation.as( InsertOrUpdateTuple.class );
assertThat( insertOrUpdate.getEntityKey().getTable() ).isEqualTo( "Shipment" );
assertThat( insertOrUpdate.getEntityKey().getColumnValues() ).isEqualTo( new Object[] { "shipment-1" } );
}
assertThat( appliedOperations.hasNext() ).isFalse();
}
@Test
@SkipByGridDialect(
value = { GridDialectType.NEO4J_EMBEDDED, GridDialectType.NEO4J_REMOTE, GridDialectType.INFINISPAN },
comment = "Can use parallel local TX not with JTA"
)
public void appliedOperationsPassedToErrorHandlerAreSeparatedByTransaction() throws Exception {
OgmSession session = openSession();
session.getTransaction().begin();
session.persist( new Shipment( "shipment-1", "INITIAL" ) );
session.persist( new Shipment( "shipment-2", "INITIAL" ) );
session.persist( new Shipment( "shipment-3", "INITIAL" ) );
session.getTransaction().commit();
session.close();
OgmSession sessionA = openSession();
sessionA.getTransaction().begin();
OgmSession sessionB = openSession();
sessionB.getTransaction().begin();
try {
Shipment loadedShipment1A = (Shipment) sessionA.get( Shipment.class, "shipment-1" );
Shipment loadedShipment2B = (Shipment) sessionB.get( Shipment.class, "shipment-2" );
Shipment loadedShipment3B = (Shipment) sessionB.get( Shipment.class, "shipment-3" );
// do an update in parallel which will cause the rollback of TX B and wait until its done
Future<?> future = updateShipmentInConcurrentThread( "shipment-3", "PROCESSING" );
future.get();
loadedShipment1A.setState( "PROCESSING" );
sessionA.flush();
loadedShipment2B.setState( "PROCESSING" );
loadedShipment3B.setState( "PROCESSING" );
sessionA.getTransaction().commit();
sessionB.getTransaction().commit();
fail( "expected exception was not raised" );
}
catch (StaleObjectStateException sose) {
// Expected
}
finally {
rollbackTransactionIfActive( sessionA.getTransaction() );
rollbackTransactionIfActive( sessionB.getTransaction() );
sessionA.close();
sessionB.close();
}
// The update to shipment-1 is done by TX A, so only the update to shipment-2 is expected in the applied ops by TX B
// upon rollback due to the failure of the update to shipment-3
Iterator<RollbackContext> onRollbackInvocations = InvocationTrackingHandler.INSTANCE.getOnRollbackInvocations().iterator();
Iterator<GridDialectOperation> appliedOperations = onRollbackInvocations.next().getAppliedGridDialectOperations().iterator();
assertThat( onRollbackInvocations.hasNext() ).isFalse();
if ( currentDialectHasFacet( OptimisticLockingAwareGridDialect.class ) ) {
GridDialectOperation appliedOperation = appliedOperations.next();
assertThat( appliedOperation ).isInstanceOf( UpdateTupleWithOptimisticLock.class );
UpdateTupleWithOptimisticLock updateTupleWithOptimisticLock = appliedOperation.as( UpdateTupleWithOptimisticLock.class );
assertThat( updateTupleWithOptimisticLock.getEntityKey().getTable() ).isEqualTo( "Shipment" );
assertThat( updateTupleWithOptimisticLock.getEntityKey().getColumnValues() ).isEqualTo( new Object[] { "shipment-2" } );
}
else if ( currentDialectHasFacet( GroupingByEntityDialect.class ) ) {
GridDialectOperation operation = appliedOperations.next();
assertThat( operation ).isInstanceOf( ExecuteBatch.class );
ExecuteBatch batch = operation.as( ExecuteBatch.class );
Iterator<GridDialectOperation> batchedOperations = batch.getOperations().iterator();
InsertOrUpdateTuple insertOrUpdate = batchedOperations.next().as( InsertOrUpdateTuple.class );
assertThat( insertOrUpdate.getEntityKey().getTable() ).isEqualTo( "Shipment" );
assertThat( insertOrUpdate.getEntityKey().getColumnValues() ).isEqualTo( new Object[] { "shipment-2" } );
assertThat( batchedOperations.hasNext() ).isFalse();
}
else {
GridDialectOperation appliedOperation = appliedOperations.next();
assertThat( appliedOperation ).isInstanceOf( InsertOrUpdateTuple.class );
InsertOrUpdateTuple insertOrUpdate = appliedOperation.as( InsertOrUpdateTuple.class );
assertThat( insertOrUpdate.getEntityKey().getTable() ).isEqualTo( "Shipment" );
assertThat( insertOrUpdate.getEntityKey().getColumnValues() ).isEqualTo( new Object[] { "shipment-2" } );
}
}
@Test
public void onFailedOperationPresentsFailedAndAppliedOperationsAndException() {
OgmSession session = openSession();
session.getTransaction().begin();
// given two inserted records
session.persist( new Shipment( "shipment-1", "INITIAL" ) );
session.persist( new Shipment( "shipment-2", "INITIAL" ) );
session.flush();
session.clear();
try {
// when provoking a duplicate-key exception
session.persist( new Shipment( "shipment-1", "INITIAL" ) );
session.getTransaction().commit();
fail( "Expected exception was not raised" );
}
catch (Exception e) {
rollbackTransactionIfActive( session.getTransaction() );
}
Iterator<FailedGridDialectOperationContext> onFailedOperationInvocations = InvocationTrackingHandler.INSTANCE.getOnFailedOperationInvocations().iterator();
FailedGridDialectOperationContext invocation = onFailedOperationInvocations.next();
assertThat( onFailedOperationInvocations.hasNext() ).isFalse();
// then expect the failed op
if ( (currentDialectHasFacet( BatchableGridDialect.class ) || currentDialectHasFacet( GroupingByEntityDialect.class )) &&
!currentDialectUsesLookupDuplicatePreventionStrategy() ) {
assertThat( invocation.getFailedOperation() ).isInstanceOf( ExecuteBatch.class );
}
else {
assertThat( invocation.getFailedOperation() ).isInstanceOf( InsertOrUpdateTuple.class );
}
// and the exception
assertThat( invocation.getException() ).isExactlyInstanceOf( TupleAlreadyExistsException.class );
// and the applied ops
Iterator<GridDialectOperation> appliedOperations = invocation.getAppliedGridDialectOperations().iterator();
if ( currentDialectHasFacet( BatchableGridDialect.class ) ||
currentDialectHasFacet( GroupingByEntityDialect.class ) ) {
assertThat( appliedOperations.next() ).isInstanceOf( CreateTupleWithKey.class );
assertThat( appliedOperations.next() ).isInstanceOf( CreateTupleWithKey.class );
GridDialectOperation operation = appliedOperations.next();
assertThat( operation ).isInstanceOf( ExecuteBatch.class );
ExecuteBatch batch = operation.as( ExecuteBatch.class );
Iterator<GridDialectOperation> batchedOperations = batch.getOperations().iterator();
assertThat( batchedOperations.next() ).isInstanceOf( InsertOrUpdateTuple.class );
assertThat( batchedOperations.next() ).isInstanceOf( InsertOrUpdateTuple.class );
assertThat( batchedOperations.hasNext() ).isFalse();
}
else {
assertThat( appliedOperations.next() ).isInstanceOf( CreateTupleWithKey.class );
assertThat( appliedOperations.next() ).isInstanceOf( InsertOrUpdateTuple.class );
assertThat( appliedOperations.next() ).isInstanceOf( CreateTupleWithKey.class );
assertThat( appliedOperations.next() ).isInstanceOf( InsertOrUpdateTuple.class );
}
if ( currentDialectUsesLookupDuplicatePreventionStrategy() ) {
assertThat( appliedOperations.hasNext() ).isFalse();
}
else {
assertThat( appliedOperations.next() ).isInstanceOf( CreateTupleWithKey.class );
}
session.close();
}
@Test
@SkipByGridDialect(
value = { GridDialectType.NEO4J_EMBEDDED, GridDialectType.NEO4J_REMOTE },
comment = "Transaction cannot be committed when continuing after an exception "
)
public void subsequentOperationsArePerformedForErrorHandlingStrategyContinue() {
OgmSessionFactory sessionFactory = TestHelper.getDefaultTestSessionFactory(
Collections.<String, Object>singletonMap( OgmProperties.ERROR_HANDLER, ContinuingErrorHandler.INSTANCE ),
getAnnotatedClasses()
);
OgmSession session = sessionFactory.openSession();
session.getTransaction().begin();
session.persist( new Shipment( "shipment-1", "INITIAL" ) );
session.persist( new Shipment( "shipment-2", "INITIAL" ) );
// TODO: without flush/clear ORM itself would detect the duplicate entity; should we relay this exception to the
// handler prior to rollback?
session.flush();
session.clear();
// when provoking a duplicate-key exception
session.persist( new Shipment( "shipment-1", "INITIAL" ) );
// TODO without the flush we'll batch this and the next insert; we cannot continue with remaining elements of a batch
session.flush();
session.persist( new Shipment( "shipment-3", "INITIAL" ) );
session.getTransaction().commit();
session.close();
session = sessionFactory.openSession();
session.getTransaction().begin();
// then expect all previously and subsequent operations applied
Shipment loadedShipment = (Shipment) session.get( Shipment.class, "shipment-1" );
assertThat( loadedShipment ).isNotNull();
loadedShipment = (Shipment) session.get( Shipment.class, "shipment-2" );
assertThat( loadedShipment ).isNotNull();
loadedShipment = (Shipment) session.get( Shipment.class, "shipment-3" );
assertThat( loadedShipment ).isNotNull();
session.getTransaction().commit();
session.close();
}
private Future<?> updateShipmentInConcurrentThread(final String id, final String newState) {
return executor.submit( new Runnable() {
@Override
public void run() {
OgmSession session = openSession();
session.getTransaction().begin();
Shipment shipment = (Shipment) session.get( Shipment.class, id );
shipment.setState( newState );
session.getTransaction().commit();
session.close();
}
} );
}
@After
public void deleteTestDataAndResetErrorHandler() {
OgmSession session = openSession();
session.getTransaction().begin();
Shipment shipment = (Shipment) session.get( Shipment.class, "shipment-1" );
if ( shipment != null ) {
session.delete( shipment );
}
shipment = (Shipment) session.get( Shipment.class, "shipment-2" );
if ( shipment != null ) {
session.delete( shipment );
}
shipment = (Shipment) session.get( Shipment.class, "shipment-3" );
if ( shipment != null ) {
session.delete( shipment );
}
shipment = (Shipment) session.get( Shipment.class, "shipment-4" );
if ( shipment != null ) {
session.delete( shipment );
}
session.getTransaction().commit();
session.close();
InvocationTrackingHandler.INSTANCE.clear();
}
@Override
protected void configure(Map<String, Object> settings) {
settings.put( OgmProperties.ERROR_HANDLER, InvocationTrackingHandler.INSTANCE );
}
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class<?>[] { Shipment.class };
}
private boolean currentDialectHasFacet(Class<? extends GridDialect> facet) {
GridDialect gridDialect = getSessionFactory().getServiceRegistry().getService( GridDialect.class );
return GridDialects.hasFacet( gridDialect, facet );
}
private boolean currentDialectUsesLookupDuplicatePreventionStrategy() {
GridDialect gridDialect = getSessionFactory().getServiceRegistry().getService( GridDialect.class );
DefaultEntityKeyMetadata ekm = new DefaultEntityKeyMetadata( "Shipment", new String[]{"id"} );
return gridDialect.getDuplicateInsertPreventionStrategy( ekm ) == DuplicateInsertPreventionStrategy.LOOK_UP;
}
/**
* In JTA the failed commit attempt will have done the rollback already. The TX is NOT_ACTIVE in this case.
*/
private void rollbackTransactionIfActive(Transaction transaction) {
if ( transaction.getStatus() == TransactionStatus.ACTIVE ) {
transaction.rollback();
}
}
}