diff options
author | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
---|---|---|
committer | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
commit | 72231250ed81e10d66bfe70701e64fa5fe50f712 (patch) | |
tree | 2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /vespajlib/src/main/java/com/yahoo/transaction |
Publish
Diffstat (limited to 'vespajlib/src/main/java/com/yahoo/transaction')
4 files changed, 290 insertions, 0 deletions
diff --git a/vespajlib/src/main/java/com/yahoo/transaction/Mutex.java b/vespajlib/src/main/java/com/yahoo/transaction/Mutex.java new file mode 100644 index 00000000000..3c4168b77f3 --- /dev/null +++ b/vespajlib/src/main/java/com/yahoo/transaction/Mutex.java @@ -0,0 +1,13 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.transaction; + +/** + * An auto closeable mutex + * + * @author bratseth + */ +public interface Mutex extends AutoCloseable { + + public void close(); + +} diff --git a/vespajlib/src/main/java/com/yahoo/transaction/NestedTransaction.java b/vespajlib/src/main/java/com/yahoo/transaction/NestedTransaction.java new file mode 100644 index 00000000000..4be0a32ffe8 --- /dev/null +++ b/vespajlib/src/main/java/com/yahoo/transaction/NestedTransaction.java @@ -0,0 +1,200 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.transaction; + +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +/** + * A transaction which may contain a list of transactions, typically to represent a distributed transaction + * over multiple systems. + * + * @author bratseth + */ +public final class NestedTransaction implements AutoCloseable { + + private static final Logger log = Logger.getLogger(NestedTransaction.class.getName()); + + /** Nested transactions with ordering constraints, in the order they are added */ + private final List<ConstrainedTransaction> transactions = new ArrayList<>(2); + + /** Transaction ordering pairs */ + //private final List<OrderingConstraint> transactionOrders = new ArrayList<>(2); + + /** A list of (non-transactional) operations to execute after this transaction has committed successfully */ + private final List<Runnable> onCommitted = new ArrayList<>(2); + + /** + * Adds a transaction to this. + * + * @param transaction the transaction to add + * @param before transaction classes which should commit after this, if present. It is beneficial + * to order transaction types from the least to most reliable. If conflicting ordering constraints are + * given this will not be detected at add time but the transaction will fail to commit + * @return this for convenience + */ + @SafeVarargs // don't warn on 'before' argument + @SuppressWarnings("varargs") // don't warn on passing 'before' to the nested class constructor + public final NestedTransaction add(Transaction transaction, Class<? extends Transaction> ... before) { + transactions.add(new ConstrainedTransaction(transaction, before)); + return this; + } + + /** Returns the transactions nested in this, as they will be committed. */ + public List<Transaction> transactions() { return organizeTransactions(transactions); } + + /** Perform a 2 phase commit */ + public void commit() { + List<Transaction> organizedTransactions = organizeTransactions(transactions); + + // First phase + for (Transaction transaction : organizedTransactions) + transaction.prepare(); + + // Second phase + for (ListIterator<Transaction> i = organizedTransactions.listIterator(); i.hasNext(); ) { + Transaction transaction = i.next(); + try { + transaction.commit(); + } + catch (Exception e) { + // Clean up committed part or log that we can't + i.previous(); + while (i.hasPrevious()) + i.previous().rollbackOrLog(); + throw new IllegalStateException("Transaction failed during commit", e); + } + } + + // After commit: Execute completion tasks + for (Runnable task : onCommitted) { + try { + task.run(); + } + catch (Exception e) { // Don't throw from here as that indicates transaction didn't complete + log.log(Level.WARNING, "A committed task in " + this + " caused an exception", e); + } + } + } + + public void onCommitted(Runnable runnable) { + onCommitted.add(runnable); + } + + /** Free up any temporary resources held by this */ + @Override + public void close() { + for (ConstrainedTransaction transaction : transactions) + transaction.transaction.close(); + } + + private List<Transaction> organizeTransactions(List<ConstrainedTransaction> transactions) { + return orderTransactions(combineTransactions(transactions), findOrderingConstraints(transactions)); + } + + /** Combines all transactions of the same type to one */ + private List<Transaction> combineTransactions(List<ConstrainedTransaction> transactions) { + List<Transaction> combinedTransactions = new ArrayList<>(transactions.size()); + for (List<Transaction> combinableTransactions : + transactions.stream().map(ConstrainedTransaction::transaction). + collect(Collectors.groupingBy(Transaction::getClass)).values()) { + Transaction combinedTransaction = combinableTransactions.get(0); + for (int i = 1; i < combinableTransactions.size(); i++) + combinedTransaction = combinedTransaction.add(combinableTransactions.get(i).operations()); + combinedTransactions.add(combinedTransaction); + } + return combinedTransactions; + } + + private List<OrderingConstraint> findOrderingConstraints(List<ConstrainedTransaction> transactions) { + List<OrderingConstraint> orderingConstraints = new ArrayList<>(1); + for (ConstrainedTransaction transaction : transactions) { + for (Class<? extends Transaction> afterThis : transaction.before()) + orderingConstraints.add(new OrderingConstraint(transaction.transaction().getClass(), afterThis)); + } + return orderingConstraints; + } + + /** Orders combined transactions consistent with the ordering constraints */ + private List<Transaction> orderTransactions(List<Transaction> transactions, List<OrderingConstraint> constraints) { + if (transactions.size() == 1) return transactions; + + List<Transaction> orderedTransactions = new ArrayList<>(); + for (Transaction transaction : transactions) + orderedTransactions.add(findSuitablePositionFor(transaction, orderedTransactions, constraints), transaction); + return orderedTransactions; + } + + private int findSuitablePositionFor(Transaction transaction, List<Transaction> orderedTransactions, + List<OrderingConstraint> constraints) { + for (int i = 0; i < orderedTransactions.size(); i++) { + Transaction candidateNextTransaction = orderedTransactions.get(i); + if ( ! mustBeAfter(candidateNextTransaction.getClass(), transaction.getClass(), constraints)) return i; + + // transaction must be after this: continue to next position + if (mustBeAfter(transaction.getClass(), candidateNextTransaction.getClass(), constraints)) // must be after && must be before + throw new IllegalStateException("Conflicting transaction ordering constraints between" + + transaction + " and " + candidateNextTransaction); + } + return orderedTransactions.size(); // add last as it must be after everything + } + + /** + * Returns whether transaction type B must be after type A according to the ordering constraints. + * This is the same as asking whether there is a path between node a and b in the bi-directional + * graph defined by the ordering constraints. + */ + private boolean mustBeAfter(Class<? extends Transaction> a, Class<? extends Transaction> b, + List<OrderingConstraint> constraints) { + for (OrderingConstraint fromA : findAllOrderingConstraintsFrom(a, constraints)) { + if (fromA.after().equals(b)) return true; + if (mustBeAfter(fromA.after(), b, constraints)) return true; + } + return false; + } + + private List<OrderingConstraint> findAllOrderingConstraintsFrom(Class<? extends Transaction> transactionType, + List<OrderingConstraint> constraints) { + return constraints.stream().filter(c -> c.before().equals(transactionType)).collect(Collectors.toList()); + } + + private static class ConstrainedTransaction { + + private final Transaction transaction; + + private final Class<? extends Transaction>[] before; + + public ConstrainedTransaction(Transaction transaction, Class<? extends Transaction>[] before) { + this.transaction = transaction; + this.before = before; + } + + public Transaction transaction() { return transaction; } + + /** Returns transaction types which should commit after this */ + public Class<? extends Transaction>[] before() { return before; } + + } + + private static class OrderingConstraint { + + private final Class<? extends Transaction> before, after; + + public OrderingConstraint(Class<? extends Transaction> before, Class<? extends Transaction> after) { + this.before = before; + this.after = after; + } + + public Class<? extends Transaction> before() { return before; } + + public Class<? extends Transaction> after() { return after; } + + @Override + public String toString() { return before + " -> " + after; } + + } + +} diff --git a/vespajlib/src/main/java/com/yahoo/transaction/Transaction.java b/vespajlib/src/main/java/com/yahoo/transaction/Transaction.java new file mode 100644 index 00000000000..642438dda0a --- /dev/null +++ b/vespajlib/src/main/java/com/yahoo/transaction/Transaction.java @@ -0,0 +1,72 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.transaction; + +import java.util.List; + +/** + * An interface for building a transaction and committing it. Implementations are required to atomically apply changes + * in the commit step or throw an exception if it fails. + * + * @author lulf + * @author bratseth + */ +public interface Transaction extends AutoCloseable { + + /** + * Adds an operation to this transaction. Return self for chaining. + * + * @param operation {@link Operation} to append + * @return self, for chaining + */ + Transaction add(Operation operation); + + /** + * Adds multiple operations to this transaction. Return self for chaining. + * + * @param operation {@link Operation} to append + * @return self, for chaining + */ + Transaction add(List<Operation> operation); + + /** + * Returns the operations of this. + * Ownership of the returned list is transferred to the caller. The ist may be ready only. + */ + List<Operation> operations(); + + /** + * Checks whether or not the transaction is able to commit in its current state and do any transient preparatory + * work to commit. + * + * @throws IllegalStateException if the transaction cannot be committed + */ + void prepare(); + + /** + * Commit this transaction. If this method returns, all operations in this transaction was committed + * successfully. Implementations of this must be exception safe or log a message of type severe if they partially + * alter state. + * + * @throws IllegalStateException if transaction failed. + */ + void commit(); + + /** + * This is called if the transaction should be rolled back after commit. If a rollback is not possible or + * supported. This must log a message of type severe with detailed information about the resulting state. + */ + void rollbackOrLog(); + + /** + * Closes and frees any resources allocated by this transaction. The transaction instance cannot be reused once + * closed. + */ + void close(); + + /** + * Operations that a transaction supports should implement this interface. + */ + public interface Operation { + } + +} diff --git a/vespajlib/src/main/java/com/yahoo/transaction/package-info.java b/vespajlib/src/main/java/com/yahoo/transaction/package-info.java new file mode 100644 index 00000000000..72ac10d13d0 --- /dev/null +++ b/vespajlib/src/main/java/com/yahoo/transaction/package-info.java @@ -0,0 +1,5 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +package com.yahoo.transaction; + +import com.yahoo.osgi.annotation.ExportPackage;
\ No newline at end of file |