/*
* Copyright 2015-2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.hawkular.inventory.impl.tinkerpop.spi;
import static org.hawkular.inventory.impl.tinkerpop.spi.Log.LOG;
import java.util.HashMap;
import java.util.Map;
import org.apache.tinkerpop.gremlin.structure.Graph;
import org.apache.tinkerpop.gremlin.structure.Transaction;
import org.hawkular.inventory.api.Configuration;
import org.hawkular.inventory.paths.CanonicalPath;
/**
* This is a service interface that the Tinkerpop implementation will use to get a configured and initialized instance
* of a blueprints graph.
* <p>
* <p>This level of indirection is needed because many graph databases provide configuration and management features
* that are not accessible through plain Blueprints API.
*
* @author Lukas Krejci
* @since 0.0.1
*/
public interface GraphProvider {
boolean isPreferringBigTransactions();
/**
* Some implementations need the pipeline to be fully drained in order to be able to reclaim backend resources.
*
* @return true if the underlying graph needs pipelines drained in order to clean up.
*/
boolean needsDraining();
/**
* @see org.hawkular.inventory.base.spi.InventoryBackend#isUniqueIndexSupported()
*/
boolean isUniqueIndexSupported();
/**
* Given provided configuration, tries to instantiate a graph to be used by the inventory.
*
* @param configuration the configuration of the graph
* @return a configured instance of the graph or null if not possible
*/
Graph instantiateGraph(Configuration configuration);
/**
* Makes sure all the indexes needed for good performance.
* <p>
* <p>The provided set of indexes is what the implementation thinks the indices should be. The graph provider
* is free to make more indexes if they choose so to support the "core" set of indices.
*
* @param graph the graph instance (coming from the
* {@link #instantiateGraph(Configuration)} call) to index
* @param indexSpecs the core set of indices to define
*/
void ensureIndices(Graph graph, IndexSpec... indexSpecs);
/**
* Initializes new transaction for use with given graph.
*
* @param graph the graph to start the transaction in
* @return a new transactional graph that is bound to a new transaction
*/
default Graph startTransaction(Graph graph) {
boolean tracing = LOG.isTraceEnabled();
Throwable previousTransactionAllocation = TransactionTracker.transactionStart.get().get(this);
if (previousTransactionAllocation != null) {
if (tracing) {
LOG.trace("FAILURE TO START THE TX.");
LOG.trace("THERE'S A TX ALREADY ACTIVE:", previousTransactionAllocation);
}
throw new IllegalStateException("Transaction already open.", previousTransactionAllocation);
}
boolean failed = false;
try {
graph.tx().open();
return graph;
} catch (Throwable t) {
failed = true;
throw t;
} finally {
if (!failed) {
if (tracing) {
TransactionTracker.transactionStart.get()
.put(this, new Exception());
LOG.trace("+++++++ TX STARTED ON GRAPH: " + graph);
} else {
TransactionTracker.transactionStart.get()
.put(this, NoRecordedStacktrace.INSTANCE);
}
}
}
}
/**
* Commits the transaction in the graph.
*
* <p>The default implementation merely calls {@link org.apache.tinkerpop.gremlin.structure.Transaction#commit()}.
*
* @param graph the graph to commit the transaction to
*/
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
default void commit(Graph graph) {
try {
Transaction tx = graph.tx();
tx.commit();
tx.close();
if (Log.LOG.isTraceEnabled()) {
Log.LOG.trace("------- TX COMMITTED ON GRAPH: " + graph);
}
} catch (Throwable t) {
if (Log.LOG.isTraceEnabled()) {
LOG.trace("FAILURE TO COMMIT TX:", t);
}
throw t;
} finally {
TransactionTracker.transactionStart.get().remove(this);
}
}
/**
* Rolls back the transaction in the graph.
* <p>
* <p>The default implementation merely calls {@link org.apache.tinkerpop.gremlin.structure.Transaction#rollback()}.
*
* @param graph the graph to rollback the transaction from
*/
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
default void rollback(Graph graph) {
try {
Transaction tx = graph.tx();
tx.rollback();
tx.close();
if (Log.LOG.isTraceEnabled()) {
Log.LOG.trace("------- TX ROLLED BACK ON GRAPH: " + graph);
}
} catch (Throwable t) {
if (Log.LOG.isTraceEnabled()) {
LOG.trace("FAILURE TO ROLLBACK TX:", t);
}
throw t;
} finally {
TransactionTracker.transactionStart.get().remove(this);
}
}
/**
* Translates the graph specific exception to an inventory exception.
* <p>
* <p>The default implementation is an identity function.</p>
*
* @param inputException an exception to convert
* @param affectedPath the canonical path of the entity affected by the exception
* @return converted exception
*/
default RuntimeException translateException(RuntimeException inputException, CanonicalPath affectedPath) {
return inputException;
}
/**
* Checks the exception thrown during the commit and returns true if the backend requires explicit rollback after
* such failure occured or false if the failure caused the transaction to close itself automatically.
*
* @param t the exception thrown during commit
* @return true to explictly roll back or false if the exception already caused the transaction to close
*/
default boolean requiresRollbackAfterFailure(Throwable t) {
return true;
}
}
/**
* We use the smaller overhead of a thread local hash map to ensure none of our codebase tries to spawn
* nested transactions. Concurrent transactions from different threads are OK.
*/
class TransactionTracker {
static ThreadLocal<Map<GraphProvider, Throwable>> transactionStart =
new ThreadLocal<Map<GraphProvider, Throwable>>() {
@Override protected Map<GraphProvider, Throwable> initialValue() {
//usually we don't have more than 1 graph provider active, so let's keep this small for starts
return new HashMap<>(1);
}
};
}
/**
* A trick to have an exception with no stacktrace. This is used in transaction tracker when the log level is not
* TRACE to save big time on execution time (because no stacktrace allocation needs to happen).
*/
class NoRecordedStacktrace extends Throwable {
@SuppressWarnings("ThrowableInstanceNeverThrown")
static final NoRecordedStacktrace INSTANCE = new NoRecordedStacktrace();
@Override public synchronized Throwable fillInStackTrace() {
return this;
}
}