/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.apache.tinkerpop.gremlin.structure; import org.apache.tinkerpop.gremlin.structure.util.AbstractTransaction; import java.util.Collections; import java.util.Set; import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; /** * A set of methods that allow for control of transactional behavior of a {@link Graph} instance. Providers may * consider using {@link AbstractTransaction} as a base implementation that provides default features for most of * these methods. * <p/> * It is expected that this interface be implemented by providers in a {@link ThreadLocal} fashion. In other words * transactions are bound to the current thread, which means that any graph operation executed by the thread occurs * in the context of that transaction and that there may only be one thread executing in a single transaction. * <p/> * It is important to realize that this class is not a "transaction object". It is a class that holds transaction * related methods thus hiding them from the {@link Graph} interface. This object is not meant to be passed around * as a transactional context. * * @author Marko A. Rodriguez (http://markorodriguez.com) * @author Stephen Mallette (http://stephen.genoprime.com) * @author TinkerPop Community (http://tinkerpop.apache.org) */ public interface Transaction extends AutoCloseable { /** * Opens a transaction. */ public void open(); /** * Commits a transaction. */ public void commit(); /** * Rolls back a transaction. */ public void rollback(); /** * Submit a unit of work that represents a transaction returning a {@link Workload} that can be automatically * retried in the event of failure. */ public <R> Workload<R> submit(final Function<Graph, R> work); /** * Creates a transaction that can be executed across multiple threads. The {@link Graph} returned from this * method is not meant to represent some form of child transaction that can be committed from this object. * A threaded transaction is a {@link Graph} instance that has a transaction context that enables multiple * threads to collaborate on the same transaction. A standard transactional context tied to a {@link Graph} * that supports transactions will typically bind a transaction to a single thread via {@link ThreadLocal}. */ public <G extends Graph> G createThreadedTx(); /** * Determines if a transaction is currently open. */ public boolean isOpen(); /** * An internal function that signals a read or a write has occurred - not meant to be called directly by end users. */ public void readWrite(); /** * Closes the transaction where the default close behavior defined by {{@link #onClose(Consumer)}} will be * executed. */ @Override public void close(); /** * Describes how a transaction is started when a read or a write occurs. This value can be set using standard * behaviors defined in {@link READ_WRITE_BEHAVIOR} or a mapper {@link Consumer} function. */ public Transaction onReadWrite(final Consumer<Transaction> consumer); /** * Describes what happens to a transaction on a call to {@link Graph#close()}. This value can be set using * standard behavior defined in {@link CLOSE_BEHAVIOR} or a mapper {@link Consumer} function. */ public Transaction onClose(final Consumer<Transaction> consumer); /** * Adds a listener that is called back with a status when a commit or rollback is successful. It is expected * that listeners be bound to the current thread as is standard for transactions. Therefore a listener registered * in the current thread will not get callback events from a commit or rollback call in a different thread. */ public void addTransactionListener(final Consumer<Status> listener); /** * Removes a transaction listener. */ public void removeTransactionListener(final Consumer<Status> listener); /** * Removes all transaction listeners. */ public void clearTransactionListeners(); /** * A status provided to transaction listeners to inform whether a transaction was successfully committed * or rolled back. */ public enum Status { COMMIT, ROLLBACK } public static class Exceptions { private Exceptions() { } public static IllegalStateException transactionAlreadyOpen() { return new IllegalStateException("Stop the current transaction before opening another"); } public static IllegalStateException transactionMustBeOpenToReadWrite() { return new IllegalStateException("Open a transaction before attempting to read/write the transaction"); } public static IllegalStateException openTransactionsOnClose() { return new IllegalStateException("Commit or rollback all outstanding transactions before closing the transaction"); } public static UnsupportedOperationException threadedTransactionsNotSupported() { return new UnsupportedOperationException("Graph does not support threaded transactions"); } public static IllegalArgumentException onCloseBehaviorCannotBeNull() { return new IllegalArgumentException("Transaction behavior for onClose cannot be null"); } public static IllegalArgumentException onReadWriteBehaviorCannotBeNull() { return new IllegalArgumentException("Transaction behavior for onReadWrite cannot be null"); } } /** * Behaviors to supply to the {@link #onClose(Consumer)}. The semantics of these behaviors must be examined in * the context of the implementation. In most cases, these behaviors will be applied as {{@link ThreadLocal}}. */ public enum CLOSE_BEHAVIOR implements Consumer<Transaction> { /** * Commit the transaction when {@link #close()} is called. */ COMMIT { @Override public void accept(final Transaction transaction) { if (transaction.isOpen()) transaction.commit(); } }, /** * Rollback the transaction when {@link #close()} is called. */ ROLLBACK { @Override public void accept(final Transaction transaction) { if (transaction.isOpen()) transaction.rollback(); } }, /** * Throw an exception if the current transaction is open when {@link #close()} is called. */ MANUAL { @Override public void accept(final Transaction transaction) { if (transaction.isOpen()) throw Exceptions.openTransactionsOnClose(); } } } /** * Behaviors to supply to the {@link #onReadWrite(Consumer)}. */ public enum READ_WRITE_BEHAVIOR implements Consumer<Transaction> { /** * Transactions are automatically started when a read or a write occurs. */ AUTO { @Override public void accept(final Transaction transaction) { if (!transaction.isOpen()) transaction.open(); } }, /** * Transactions must be explicitly opened for operations to occur on the graph. */ MANUAL { @Override public void accept(final Transaction transaction) { if (!transaction.isOpen()) throw Exceptions.transactionMustBeOpenToReadWrite(); } } } /** * A {@link Workload} represents a unit of work constructed by the {@link Workload#submit(Function)} method on * the {@link Transaction} interface. The unit of work is a {@link Function} typically containing mutations to * the {@link Graph}. The {@link Workload} is responsible for executing the unit of work in the context of a * retry strategy, such that a failed unit of work is rolled back and executed again until retries are exhausted * or the unit of work succeeds with a commit operation. * * @param <R> The type of the result from the unit of work. */ public static class Workload<R> { public static final long DEFAULT_DELAY_MS = 20; public static final int DEFAULT_TRIES = 8; private final Function<Graph, R> workToDo; private final Graph g; /** * Creates a new {@link Workload} that will be tried to be executed within a transaction. * * @param g The {@link Graph} instance on which the work will be performed. * @param work The work to be executed on the Graph instance which will optionally return a value. */ public Workload(final Graph g, final Function<Graph, R> work) { this.g = g; this.workToDo = work; } /** * Try to execute a {@link Workload} with a mapper retry strategy. * * @param retryStrategy The first argument to this function is the Graph instance and the second is * the encapsulated work to be performed. The function should ultimately return the * result of the encapsulated work function. * @return The result of the encapsulated work. */ public R attempt(final BiFunction<Graph, Function<Graph, R>, R> retryStrategy) { return retryStrategy.apply(g, workToDo); } /** * Executes the {@link Workload} committing if possible and rolling back on failure. On failure, an exception * is reported. */ public R oneAndDone() { return attempt((g, w) -> { try { R result = w.apply(g); g.tx().commit(); return result; } catch (Throwable t) { g.tx().rollback(); throw new RuntimeException(t); } }); } /** * Executes the {@link Workload} committing if possible and rolling back on failure. On failure no exception * is reported. */ public R fireAndForget() { return attempt((g, w) -> { R result = null; try { result = w.apply(g); g.tx().commit(); } catch (Throwable t) { g.tx().rollback(); } return result; }); } /** * Executes the {@link Workload} with the default number of retries and with the default number of * milliseconds delay between each try. */ public R retry() { return retry(DEFAULT_TRIES); } /** * Executes the {@link Workload} with a number of retries and with the default number of milliseconds delay * between each try. */ public R retry(final int tries) { return retry(tries, DEFAULT_DELAY_MS); } /** * Executes the {@link Workload} with a number of retries and with a number of milliseconds delay between * each try. */ public R retry(final int tries, final long delay) { return retry(tries, delay, Collections.emptySet()); } /** * Executes the {@link Workload} with a number of retries and with a number of milliseconds delay between each * try and will only retry on the set of supplied exceptions. Exceptions outside of that set will generate a * RuntimeException and immediately fail. */ public R retry(final int tries, final long delay, final Set<Class> exceptionsToRetryOn) { return attempt(retry(tries, exceptionsToRetryOn, i -> delay)); } /** * Executes the {@link Workload} with the default number of retries and with a exponentially increasing * number of milliseconds between each retry using the default retry delay. */ public R exponentialBackoff() { return exponentialBackoff(DEFAULT_TRIES); } /** * Executes the {@link Workload} with a number of retries and with a exponentially increasing number of * milliseconds between each retry using the default retry delay. */ public R exponentialBackoff(final int tries) { return exponentialBackoff(tries, DEFAULT_DELAY_MS); } /** * Executes the {@link Workload} with a number of retries and with a exponentially increasing number of * milliseconds between each retry. */ public R exponentialBackoff(final int tries, final long initialDelay) { return exponentialBackoff(tries, initialDelay, Collections.emptySet()); } /** * Executes the {@link Workload} with a number of retries and with a exponentially increasing number of * milliseconds between each retry. It will only retry on the set of supplied exceptions. Exceptions outside * of that set will generate a {@link RuntimeException} and immediately fail. */ public R exponentialBackoff(final int tries, final long initialDelay, final Set<Class> exceptionsToRetryOn) { return attempt(retry(tries, exceptionsToRetryOn, retryCount -> (long) (initialDelay * Math.pow(2, retryCount)))); } /** * Creates a generic retry function to be passed to the {@link Workload#attempt(java.util.function.BiFunction)} * method. */ private static <R> BiFunction<Graph, Function<Graph, R>, R> retry(final int tries, final Set<Class> exceptionsToRetryOn, final Function<Integer, Long> delay) { return (g, w) -> { R returnValue; // this is the default exception...it may get reassgined during retries Exception previousException = new RuntimeException("Exception initialized when trying commit"); // try to commit a few times for (int ix = 0; ix < tries; ix++) { // increase time after each failed attempt though there is no delay on the first try if (ix > 0) try { Thread.sleep(delay.apply(ix)); } catch (InterruptedException ignored) { } try { // ensure that a transaction is open for this try. even if there was an open transaction // from the first try it would be rolled back on failure and unless automatic transactions // are used, the transaction would be closed on the next retry. if (!g.tx().isOpen()) g.tx().open(); returnValue = w.apply(g); g.tx().commit(); // need to exit the function here so that retries don't happen return returnValue; } catch (Exception ex) { g.tx().rollback(); // retry if this is an allowed exception otherwise, just throw and go boolean retry = false; if (exceptionsToRetryOn.size() == 0) retry = true; else { for (Class exceptionToRetryOn : exceptionsToRetryOn) { if (ex.getClass().equals(exceptionToRetryOn)) { retry = true; break; } } } if (!retry) { throw new RuntimeException(ex); } previousException = ex; } } // the exception just won't go away after all the retries throw new RuntimeException(previousException); }; } } }