/*
* 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.datastore.neo4j.remote.http.transaction.impl;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import javax.transaction.Status;
import org.hibernate.HibernateException;
import org.hibernate.TransactionException;
import org.hibernate.engine.transaction.spi.IsolationDelegate;
import org.hibernate.engine.transaction.spi.TransactionObserver;
import org.hibernate.jdbc.WorkExecutor;
import org.hibernate.jdbc.WorkExecutorVisitable;
import org.hibernate.ogm.datastore.neo4j.logging.impl.Log;
import org.hibernate.ogm.datastore.neo4j.logging.impl.LoggerFactory;
import org.hibernate.ogm.datastore.neo4j.remote.http.impl.HttpNeo4jClient;
import org.hibernate.ogm.datastore.neo4j.remote.http.impl.HttpNeo4jDatastoreProvider;
import org.hibernate.ogm.dialect.impl.IdentifiableDriver;
import org.hibernate.resource.transaction.SynchronizationRegistry;
import org.hibernate.resource.transaction.TransactionCoordinator;
import org.hibernate.resource.transaction.TransactionCoordinatorBuilder;
import org.hibernate.resource.transaction.internal.SynchronizationRegistryStandardImpl;
import org.hibernate.resource.transaction.spi.TransactionCoordinatorOwner;
import org.hibernate.resource.transaction.spi.TransactionStatus;
/**
* An implementation of TransactionCoordinator based on managing a transaction through a Neo4j Connection.
* <p>
* This is inspired by {@code org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImp}.
*
* @author Davide D'Alto
*/
public class HttpNeo4jResourceLocalTransactionCoordinator implements TransactionCoordinator {
private static final Log log = LoggerFactory.getLogger();
private final TransactionCoordinatorBuilder transactionCoordinatorBuilder;
private final TransactionCoordinatorOwner owner;
private final SynchronizationRegistryStandardImpl synchronizationRegistry = new SynchronizationRegistryStandardImpl();
private Neo4jTransactionDriver physicalTransactionDelegate;
private int timeOut = -1;
private final transient List<TransactionObserver> observers;
private final HttpNeo4jDatastoreProvider provider;
/**
* Construct a {@link HttpNeo4jResourceLocalTransactionCoordinator} instance. package-protected to ensure access goes through
* builder.
*
* @param owner The transactionCoordinatorOwner
*/
HttpNeo4jResourceLocalTransactionCoordinator(
TransactionCoordinatorBuilder transactionCoordinatorBuilder,
TransactionCoordinatorOwner owner,
HttpNeo4jDatastoreProvider provider) {
this.provider = provider;
this.observers = new ArrayList<>();
this.transactionCoordinatorBuilder = transactionCoordinatorBuilder;
this.owner = owner;
}
@Override
public TransactionDriver getTransactionDriverControl() {
// Again, this PhysicalTransactionDelegate will act as the bridge from the local transaction back into the
// coordinator. We lazily build it as we invalidate each delegate after each transaction (a delegate is
// valid for just one transaction)
if ( physicalTransactionDelegate == null ) {
physicalTransactionDelegate = new Neo4jTransactionDriver( provider );
}
return physicalTransactionDelegate;
}
@Override
public void explicitJoin() {
// nothing to do here, but log a warning
log.callingJoinTransactionOnNonJtaEntityManager();
}
@Override
public boolean isJoined() {
return physicalTransactionDelegate != null && physicalTransactionDelegate.getStatus() == TransactionStatus.ACTIVE;
}
@Override
public void pulse() {
getTransactionDriverControl();
}
@Override
public SynchronizationRegistry getLocalSynchronizations() {
return synchronizationRegistry;
}
@Override
public boolean isActive() {
return owner.isActive();
}
@Override
public IsolationDelegate createIsolationDelegate() {
return new Neo4jIsolationDelegate( provider );
}
private class Neo4jIsolationDelegate implements IsolationDelegate {
private final HttpNeo4jDatastoreProvider provider;
public Neo4jIsolationDelegate(HttpNeo4jDatastoreProvider provider) {
this.provider = provider;
}
@Override
public <T> T delegateWork(WorkExecutorVisitable<T> work, boolean transacted) throws HibernateException {
HttpNeo4jTransaction tx = null;
try {
if ( !transacted ) {
log.cannotExecuteWorkOutsideIsolatedTransaction();
}
HttpNeo4jClient dataBase = provider.getClient();
tx = dataBase.beginTx();
// Neo4j does not have a connection object, I'm not sure what it is best to do in this case.
// In this scenario I expect the visitable object to already have a way to connect to the db.
Connection connection = null;
T result = work.accept( new WorkExecutor<T>(), connection );
tx.commit();
return result;
}
catch (Exception e) {
try {
tx.rollback();
}
catch (Exception re) {
log.unableToRollbackTransaction( re );
}
if ( e instanceof HibernateException ) {
throw (HibernateException) e;
}
else {
throw log.unableToPerformIsolatedWork( e );
}
}
finally {
if ( tx != null ) {
tx.close();
tx = null;
}
}
}
@Override
public <T> T delegateCallable(Callable<T> callable, boolean transacted) throws HibernateException {
throw new UnsupportedOperationException( "Not implemented yet" );
}
}
@Override
public TransactionCoordinatorBuilder getTransactionCoordinatorBuilder() {
return this.transactionCoordinatorBuilder;
}
@Override
public void setTimeOut(int seconds) {
this.timeOut = seconds;
}
@Override
public int getTimeOut() {
return this.timeOut;
}
// PhysicalTransactionDelegate ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
private void afterBeginCallback() {
if ( this.timeOut > 0 ) {
owner.setTransactionTimeOut( this.timeOut );
}
owner.afterTransactionBegin();
for ( TransactionObserver observer : observers ) {
observer.afterBegin();
}
log.trace( "ResourceLocalTransactionCoordinatorImpl#afterBeginCallback" );
}
private void beforeCompletionCallback() {
log.trace( "ResourceLocalTransactionCoordinatorImpl#beforeCompletionCallback" );
try {
owner.beforeTransactionCompletion();
synchronizationRegistry.notifySynchronizationsBeforeTransactionCompletion();
for ( TransactionObserver observer : observers ) {
observer.beforeCompletion();
}
}
catch (RuntimeException e) {
if ( physicalTransactionDelegate != null ) {
// should never happen that the physicalTransactionDelegate is null, but to be safe
physicalTransactionDelegate.markRollbackOnly();
}
throw e;
}
}
private void afterCompletionCallback(boolean successful) {
log.tracef( "ResourceLocalTransactionCoordinatorImpl#afterCompletionCallback(%s)", successful );
final int statusToSend = successful ? Status.STATUS_COMMITTED : Status.STATUS_UNKNOWN;
synchronizationRegistry.notifySynchronizationsAfterTransactionCompletion( statusToSend );
owner.afterTransactionCompletion( successful, false );
for ( TransactionObserver observer : observers ) {
observer.afterCompletion( successful, false );
}
invalidateDelegate();
}
private void invalidateDelegate() {
if ( physicalTransactionDelegate == null ) {
throw new IllegalStateException( "Physical-transaction delegate not known on attempt to invalidate" );
}
physicalTransactionDelegate.invalidate();
physicalTransactionDelegate = null;
}
@Override
public void addObserver(TransactionObserver observer) {
observers.add( observer );
}
@Override
public void removeObserver(TransactionObserver observer) {
observers.remove( observer );
}
/**
* The delegate bridging between the local (application facing) transaction and the "physical" notion of a
* transaction via the JDBC Connection.
*/
public class Neo4jTransactionDriver implements IdentifiableDriver {
private final HttpNeo4jClient client;
private TransactionStatus status;
private HttpNeo4jTransaction tx;
private boolean invalid;
private boolean rollbackOnly = false;
public Neo4jTransactionDriver(HttpNeo4jDatastoreProvider provider) {
this.client = provider.getClient();
}
protected void invalidate() {
invalid = true;
}
@Override
public void begin() {
errorIfInvalid();
tx = client.beginTx();
status = TransactionStatus.ACTIVE;
HttpNeo4jResourceLocalTransactionCoordinator.this.afterBeginCallback();
}
protected void errorIfInvalid() {
if ( invalid ) {
throw new IllegalStateException( "Physical-transaction delegate is no longer valid" );
}
}
@Override
public void commit() {
try {
if ( rollbackOnly ) {
throw new TransactionException( "Transaction was marked for rollback only; cannot commit" );
}
HttpNeo4jResourceLocalTransactionCoordinator.this.beforeCompletionCallback();
tx.commit();
close();
status = TransactionStatus.NOT_ACTIVE;
HttpNeo4jResourceLocalTransactionCoordinator.this.afterCompletionCallback( true );
}
catch (RuntimeException e) {
try {
rollback();
}
catch (RuntimeException e2) {
log.debug( "Encountered failure rolling back failed commit", e2 );
}
throw e;
}
}
private void close() {
try {
tx.close();
}
finally {
tx = null;
}
}
@Override
public void rollback() {
if ( rollbackOnly || getStatus() == TransactionStatus.ACTIVE ) {
rollbackOnly = false;
tx.rollback();
status = TransactionStatus.NOT_ACTIVE;
close();
HttpNeo4jResourceLocalTransactionCoordinator.this.afterCompletionCallback( false );
}
// no-op otherwise.
}
@Override
public TransactionStatus getStatus() {
return rollbackOnly ? TransactionStatus.MARKED_ROLLBACK : status;
}
@Override
public void markRollbackOnly() {
if ( log.isDebugEnabled() ) {
log.debug(
"Neo4j transaction marked for rollback-only (exception provided for stack trace)",
new Exception( "exception just for purpose of providing stack trace" ) );
}
rollbackOnly = true;
}
@Override
public Object getTransactionId() {
return tx.getId();
}
}
}