All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.exist.test.TransactionTestDSL Maven / Gradle / Ivy

There is a newer version: 6.3.0
Show newest version
/*
 * eXist-db Open Source Native XML Database
 * Copyright (C) 2001 The eXist-db Authors
 *
 * [email protected]
 * http://www.exist-db.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
package org.exist.test;

import com.evolvedbinary.j8fu.Either;
import com.evolvedbinary.j8fu.function.QuadFunction7E;
import com.evolvedbinary.j8fu.tuple.Tuple2;
import org.exist.EXistException;
import org.exist.collections.Collection;
import org.exist.collections.triggers.TriggerException;
import org.exist.dom.persistent.DocumentImpl;
import org.exist.dom.persistent.NewArrayNodeSet;
import org.exist.dom.persistent.NodeProxy;
import org.exist.dom.persistent.NodeSet;
import org.exist.security.PermissionDeniedException;
import org.exist.storage.BrokerPool;
import org.exist.storage.DBBroker;
import org.exist.storage.lock.Lock;
import org.exist.storage.txn.Txn;
import org.exist.test.TransactionTestDSL.BiTransactionScheduleBuilder.BiTransactionScheduleBuilderOperation;
import org.exist.test.TransactionTestDSL.TransactionScheduleBuilder.NilOperation;
import org.exist.util.LockException;
import org.exist.xmldb.XmldbURI;
import org.exist.xquery.XPathException;
import org.exist.xquery.XQuery;

import java.io.IOException;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.*;
import java.util.function.Function;

import static com.evolvedbinary.j8fu.Either.Left;
import static com.evolvedbinary.j8fu.Either.Right;
import static org.exist.dom.persistent.DocumentImpl.XML_FILE;
import static org.exist.util.ThreadUtils.nameInstanceThread;
import static org.exist.util.ThreadUtils.newInstanceSubThreadGroup;

/**
 * A DSL for describing a schedule of
 * transaction operations upon the database.
 *
 * A type-safe builder pattern is provided
 * for constructing the schedule. Once
 * the schedule is build a scheduler
 * can execute it upon the database
 * and return the results.
 *
 * The DSL uses recursive types
 * in a similar way to a typed heterogeneous
 * list (such as Shapeless's HList) to ensure
 * that the each operation in the schedule
 * receives the correct input type, i.e.
 * the output type of the previous operation.
 * At the cost of complexity in implementing
 * the DSL, the recursive typing makes use of
 * the DSL by the user much simpler and safer.
 *
 * The recursive type implementation was
 * inspired by https://apocalisp.wordpress.com/2008/10/23/heterogeneous-lists-and-the-limits-of-the-java-type-system/.
 *
 * Example usage for creating a schedule of
 * two transactions, where each will execute in
 * its own thread but operationally linear
 * according to the schedule:
 *
 * 
 *
 * import static org.exist.test.TransactionTestDSL.TransactionOperation.*;
 * import static org.exist.test.TransactionTestDSL.TransactionScheduleBuilder.biSchedule;
 *
 * {@code @Test}
 * public void getDocuments() throws ExecutionException, InterruptedException {
 *   final String documentUri = "/db/test/hamlet.xml";
 *
 *   final Tuple2{@code } result = biSchedule()
 *       .firstT1(getDocument(documentUri))
 *                                            .andThenT2(getDocument(documentUri))
 *       .andThenT1(commit())
 *                                            .andThenT2(commit())
 *       .build()
 *   .execute(existEmbeddedServer.getBrokerPool());
 *
 *   assertNotNull(result);
 *   assertNotNull(result._1);
 *   assertNotNull(result._2);
 *
 *   assertEquals(documentUri, result._1.getURI().getCollectionPath());
 *   assertEquals(documentUri, result._2.getURI().getCollectionPath());
 * }
 *
 * 
* * @author Adam Retter */ public interface TransactionTestDSL { /** * A Transaction Schedule builder. * * Enables us to build a schedule of operations to be executed * within one or more transactions. * * @param A recursive type, holds the type of the previously scheduled * operation(s). */ interface TransactionScheduleBuilder> { NilOperation nilOperation = new NilOperation(); static NilOperation nil() { return nilOperation; } final class NilOperation implements TransactionScheduleBuilder { private NilOperation() {} } /** * Creates a Schedule Builder factory for two transactions T1 and T2. * * @return a Schedule Builder factory for two transactions */ static BiTransactionScheduleBuilderFactory biSchedule() { return BiTransactionScheduleBuilderFactory.getInstance(); } } /** * A schedule builder factory for two transactions T1 and T2. * * Responsible for creating a Schedule Builder which is initialized * to the first transaction state. */ class BiTransactionScheduleBuilderFactory { private static final BiTransactionScheduleBuilderFactory INSTANCE = new BiTransactionScheduleBuilderFactory(); private BiTransactionScheduleBuilderFactory() { } private static BiTransactionScheduleBuilderFactory getInstance() { return INSTANCE; } /** * Constructs a Schedule Builder for two transactions T1 and T2, * whose first schedule is an operation with T1. * * @param The state returned by the first operation with T1. * * @param stateTransform The initial state for T1. * * @return the schedule builder. */ public BiTransactionScheduleBuilderOperation firstT1(final TransactionOperation stateTransform) { return BiTransactionScheduleBuilderOperation.first(Left(stateTransform)); } /** * Constructs a Schedule Builder for two transactions T1 and T2, * whose first schedule is an operation with T2. * * @param The state returned by the first operation with T2. * * @param stateTransform The initial state for T2. * * @return the schedule builder. */ public BiTransactionScheduleBuilderOperation firstT2(final TransactionOperation stateTransform) { return BiTransactionScheduleBuilderOperation.first(Right(stateTransform)); } } /** * A Schedule for two transactions T1 and T2. * * @param A recursive type, holds the type of the previously scheduled * operation(s). */ abstract class BiTransactionScheduleBuilder > implements TransactionScheduleBuilder { /** * Describes a scheduled operation on transaction T1 and/or T2. * * @param The type of the state held for T1 before the * operation has executed * @param The type of the state held for T1 after the * operation has executed * @param The type of the state held for T2 before the * operation has executed * @param The type of the state held for T2 after the * operation has executed * * @param A recursive type, holds the type of the previously scheduled * operation(s). */ public static class BiTransactionScheduleBuilderOperation> extends BiTransactionScheduleBuilder> { // was this created by a transformation on T1 (if not then T2) private final boolean operationOnT1; // the previous schedule builder operation private final B previous; // transformations on the transactions private final TransactionOperation t1_state; private final TransactionOperation t2_state; // latches, used to switch scheduling between t1 and t2 private final NamedCountDownLatch t1WaitLatch; private final NamedCountDownLatch t2WaitLatch; // just a counter to help us name our latches for debugging private static int countDownLatchNum = 0; /** * Constructs an initial schedule builder. * * @param The type of the state held for T1 before the *
operation
has executed * @param The type of the state held for T1 after the *
operation
has executed * @param The type of the state held for T2 before the *
operation
has executed * @param The type of the state held for T2 after the *
operation
has executed * * @param operation An operation on either Transaction T1 or T2 * * @return the builder */ public static BiTransactionScheduleBuilderOperation first(final Either, TransactionOperation> operation) { if(operation.isLeft()) { final TransactionOperation initial_t1_state = (broker, txn, listener, t1) -> { listener.event("Initialized T1: Starting schedule execution with T1..."); return t1; }; // as we start with t1, we add a function that pauses t2, awaiting countdown latch signalled from t1 final NamedCountDownLatch t2WaitForT1 = new NamedCountDownLatch("t2WaitForT1-" + (++countDownLatchNum), 1); final TransactionOperation initial_t2_state = (broker, txn, listener, t2) -> { // instruct t2 to wait listener.event("Initialized T2: Instructing T2 to wait for T1 (" + t2WaitForT1.getName() + ")..."); t2WaitForT1.await(); return null; }; return new BiTransactionScheduleBuilderOperation<>(true, TransactionScheduleBuilder.nil(), initial_t1_state.andThen(operation.left().get()), initial_t2_state, null, t2WaitForT1); } else { final TransactionOperation initial_t2_state = (broker, txn, listener, t2) -> { listener.event("Initialized T2: Starting schedule execution with T2..."); return t2; }; // as we start with t2, we add a function that pauses t1, awaiting countdown latch signalled from t2 final NamedCountDownLatch t1WaitForT2 = new NamedCountDownLatch("t1WaitForT2-" + (++countDownLatchNum), 1); final TransactionOperation initial_t1_state = (broker, txn, listener, t1) -> { // instruct t1 to wait listener.event("Initialized T1: Instructing T1 to wait for T2 (" + t1WaitForT2.getName() + ")..."); t1WaitForT2.await(); return null; }; return new BiTransactionScheduleBuilderOperation<>(false, TransactionScheduleBuilder.nil(), initial_t1_state, initial_t2_state.andThen(operation.right().get()), t1WaitForT2, null); } } private BiTransactionScheduleBuilderOperation(final boolean operationOnT1, final B previous, final TransactionOperation t1_state, final TransactionOperation t2_state) { this(operationOnT1, previous, t1_state, t2_state, null, null); } private BiTransactionScheduleBuilderOperation(final boolean operationOnT1, final B previous, final TransactionOperation t1_state, final TransactionOperation t2_state, final NamedCountDownLatch t1WaitLatch, final NamedCountDownLatch t2WaitLatch) { this.operationOnT1 = operationOnT1; this.previous = previous; this.t1_state = t1_state; this.t2_state = t2_state; this.t1WaitLatch = t1WaitLatch; this.t2WaitLatch = t2WaitLatch; } /** * Utility getter to access the previous state. * * @return The previous schedule builder operation, or null * if there was no previous operation. */ public B previous() { return previous; } /** * Schedules the next operation on Transaction T1. * * @param The type of the state held for T1 after the *
t1Transformation
has executed * * @param t1Transformation An operation to perform with T1 * on the current state held for T1, which yields a * new state of type
V1
* * @return the schedule builder. */ public BiTransactionScheduleBuilderOperation> andThenT1(final TransactionOperation t1Transformation) { if(operationOnT1) { //continue executing t1 return new BiTransactionScheduleBuilderOperation<>(true, this, t1_state.andThen(t1Transformation), t2_state, t1WaitLatch, t2WaitLatch); } else { // switch execution from t2 to t1 // we add a function that pauses t2 awaiting countdown latch signalled from t1 final NamedCountDownLatch t2WaitForT1 = new NamedCountDownLatch("t2WaitForT1-" + (++countDownLatchNum), 1); final TransactionOperation next_t2_state = t2_state .andThen((broker, txn, listener, t2) -> { // resume t1, by counting down the latch if(t1WaitLatch != null) { listener.event("Releasing T1 from wait (" + t1WaitLatch.getName() + ")..."); t1WaitLatch.countDown(); } return t2; }) .andThen((broker, txn, listener, t2) -> { // instruct t2 to wait listener.event("Instructing T2 to wait for T1 (" + t2WaitForT1.getName() + ")..."); t2WaitForT1.await(); return t2; }); return new BiTransactionScheduleBuilderOperation<>(true, this, t1_state.andThen(t1Transformation), next_t2_state, t1WaitLatch, t2WaitForT1); } } /** * Schedules the next operation on Transaction T2. * * @param The type of the state held for T2 after the *
t2Transformation
has executed * * @param t2Transformation An operation to perform with T2 * on the current state held for T2, which yields a * new state of type
V2
* * @return the schedule builder. */ public BiTransactionScheduleBuilderOperation> andThenT2(final TransactionOperation t2Transformation) { if(!operationOnT1) { //continue executing t2 return new BiTransactionScheduleBuilderOperation<>(false, this, t1_state, t2_state.andThen(t2Transformation), t1WaitLatch, t2WaitLatch); } else { // switch execution from t1 to t2 // we add a function that pauses t1 awaiting countdown latch signalled from t2 final NamedCountDownLatch t1WaitForT2 = new NamedCountDownLatch("t1WaitForT2-" + (++countDownLatchNum), 1); final TransactionOperation next_t1_state = t1_state .andThen((broker, txn, listener, t1) -> { // resume t2, by counting down the latch if(t2WaitLatch != null) { listener.event("Releasing T2 from wait (" + t2WaitLatch.getName() + ")..."); t2WaitLatch.countDown(); } return t1; }) .andThen((broker, txn, listener, t1) -> { // instruct t1 to wait listener.event("Instructing T1 to wait for T2 (" + t1WaitForT2.getName() + ")..."); t1WaitForT2.await(); return t1; }); return new BiTransactionScheduleBuilderOperation<>(false, this, next_t1_state, t2_state.andThen(t2Transformation), t1WaitForT2, t2WaitLatch); } } /** * Constructs the final Transaction Schedule * from the builder. * * @return the transaction schedule. */ public BiTransactionSchedule> build() { // we must compose a final operation to countdown any last remaining latches between t1->t2, or t2->t1 scheduling transitions final TransactionOperation final_t1_state = t1_state.andThen((broker, txn, listener, t1) -> { if(t2WaitLatch != null && t2WaitLatch.getCount() == 1) { listener.event("Final release of T2 from wait (" + t2WaitLatch.getName() + ")..."); t2WaitLatch.countDown(); } return t1; }); final TransactionOperation final_t2_state = t2_state.andThen((broker, txn, listener, t2) -> { if(t1WaitLatch != null && t1WaitLatch.getCount() == 1) { listener.event("Final release of T1 from wait (" + t1WaitLatch.getName() + ")..."); t1WaitLatch.countDown(); } return t2; }); final BiTransactionScheduleBuilderOperation> finalOperation = new BiTransactionScheduleBuilderOperation<>(false /* value of this parameter here is not significant */, this, final_t1_state, final_t2_state, t1WaitLatch, t2WaitLatch); return new BiTransactionSchedule<>(finalOperation); } } } /** * A schedule of one or more transactions that may * be executed upon the database * * @param The type of the result from executing the schedule. */ interface TransactionSchedule { /** * Execute the schedule on the database. * * @param brokerPool The database * * @return The result of executing the schedule. * * @throws ExecutionException if the execution fails. * @throws InterruptedException if the execution is interrupted. */ default U execute(final BrokerPool brokerPool) throws ExecutionException, InterruptedException { return execute(brokerPool, NULL_SCHEDULE_LISTENER); } /** * Execute the schedule on the database. * * @param brokerPool The database * @param executionListener A listener which receives execution events. * * @return The result of executing the schedule. * * @throws ExecutionException if the execution fails. * @throws InterruptedException if the execution is interrupted. */ U execute(final BrokerPool brokerPool, final ExecutionListener executionListener) throws ExecutionException, InterruptedException; } /** * A schedule of two transactions T1 and T2. * * Which are executed concurrently, each in their * own thread, but linearly according to the schedule. * * @param The initial type of the state held for T1 before the * schedule is executed * @param The final type of the state held for T1 after the * schedule has executed * @param The initial type of the state held for T2 before the * schedule is executed * @param The final type of the state held for T2 after the * schedule has executed * * @param A recursive type, which enforces the types of all operations which * which make up the schedule. */ class BiTransactionSchedule> implements TransactionSchedule> { private final BiTransactionScheduleBuilderOperation lastOperation; private BiTransactionSchedule(final BiTransactionScheduleBuilderOperation lastOperation) { this.lastOperation = lastOperation; } // TODO(AR) enable the creation of transactions to be specified in the DSL just like in Granite, so we can show isolation (or not)! @Override public Tuple2 execute(final BrokerPool brokerPool, final ExecutionListener executionListener) throws ExecutionException, InterruptedException { Objects.requireNonNull(brokerPool); Objects.requireNonNull(executionListener); final ThreadGroup transactionsThreadGroup = newInstanceSubThreadGroup(brokerPool, "transactionTestDSL"); // submit t1 final ExecutorService t1ExecutorService = Executors.newSingleThreadExecutor(r -> new Thread(transactionsThreadGroup, r, nameInstanceThread(brokerPool, "transaction-test-dsl.transaction-1-schedule"))); final Future t1Result = t1ExecutorService.submit(() -> { try (final DBBroker broker = brokerPool.get(Optional.of(brokerPool.getSecurityManager().getSystemSubject())); final Txn txn = brokerPool.getTransactionManager().beginTransaction()) { final U1 result = lastOperation.t1_state.apply(broker, txn, executionListener, null); txn.commit(); return result; } }); // submit t2 final ExecutorService t2ExecutorService = Executors.newSingleThreadExecutor(r -> new Thread(transactionsThreadGroup, r, nameInstanceThread(brokerPool, "transaction-test-dsl.transaction-2-schedule"))); final Future t2Result = t2ExecutorService.submit(() -> { try (final DBBroker broker = brokerPool.get(Optional.of(brokerPool.getSecurityManager().getSystemSubject())); final Txn txn = brokerPool.getTransactionManager().beginTransaction()) { final U2 result = lastOperation.t2_state.apply(broker, txn, executionListener, null); txn.commit(); return result; } }); try { U1 u1 = null; U2 u2 = null; while (true) { if (t1Result.isDone()) { u1 = t1Result.get(); } if (t2Result.isDone()) { u2 = t2Result.get(); } if (t1Result.isDone() && t2Result.isDone()) { return new Tuple2<>(u1, u2); } Thread.sleep(50); } } catch (final ExecutionException | InterruptedException e) { // if we get to here then t1Result or t2Result has thrown an exception // force shutdown of transaction threads t2ExecutorService.shutdownNow(); t1ExecutorService.shutdownNow(); //TODO(AR) rather than working with exceptions, it would be better to encapsulate them in a similar way to working on an empty sequence, e.g. could use Either??? throw e; } } } /** * A function which describes an operation on the database with a Transaction. * * You can think of this as a function
f(T) -> U
* where the database and transaction are available to the * function
f
. * * @param The initial state before the transaction operation. * @param The state after the transaction operation. */ @FunctionalInterface interface TransactionOperation extends QuadFunction7E { /** * Get a document from the database. * * @param The type of the state held for the transaction * before this operation executes. * * @param uri The uri of the document. * * @return an operation which will retrieve the document from the database. */ static TransactionOperation getDocument(final String uri) { return (broker, txn, listener, t) -> { listener.event("Getting document: " + uri); return (DocumentImpl)broker.getXMLResource(XmldbURI.create(uri)); }; } /** * Delete a document from the database. * * @param The type of the document held for the transaction * before this operation executes. * * @return an operation which will delete a document from the database. */ static TransactionOperation deleteDocument() { return (broker, transaction, listener, doc) -> { listener.event("Deleting document: " + doc.getDocumentURI()); final XmldbURI collectionUri = doc.getURI().removeLastSegment(); try(final Collection collection = broker.openCollection(collectionUri, Lock.LockMode.WRITE_LOCK)) { if (collection == null) { throw new EXistException("No such collection: " + collectionUri); } if (XML_FILE == doc.getResourceType()) { collection.removeXMLResource(transaction, broker, doc.getFileURI()); } else { collection.removeBinaryResource(transaction, broker, doc.getFileURI()); } } return null; }; } /** * Update a document in the database. * * @param The type of the document held for the transaction * before this operation executes. * * @param xqueryUpdate The XQuery Update to execute on the document. * * @return an operation which will update a document in the database. */ static TransactionOperation updateDocument(final String xqueryUpdate) { return (broker, transaction, listener, doc) -> { listener.event("Updating document: " + doc.getDocumentURI() + ", with: " + xqueryUpdate); final XQuery xquery = broker.getBrokerPool().getXQueryService(); final NodeSet nodeSet = new NewArrayNodeSet(); nodeSet.add(new NodeProxy(doc)); xquery.execute(broker, xqueryUpdate, nodeSet); return null; }; } /** * Query a document in the database. * * @param The type of the document held for the transaction * before this operation executes. * * @param query The XQuery to execute against the document. * * @return an operation which will update a document in the database. */ static TransactionOperation queryDocument(final String query) { return (broker, transaction, listener, doc) -> { listener.event("Querying document: " + doc.getDocumentURI() + ", with: " + query); final XQuery xquery = broker.getBrokerPool().getXQueryService(); final NodeSet nodeSet = new NewArrayNodeSet(); nodeSet.add(new NodeProxy(doc)); xquery.execute(broker, query, nodeSet); return null; }; } /** * Commit the transaction. * * @param The type of the state held for the transaction * before this operation executes. * * @return an operation which will commit the transaction * and return the input unchanged. */ static TransactionOperation commit() { return (broker, transaction, listener, t) -> { listener.event("Committing Transaction"); transaction.commit(); return t; }; } /** * Abort the transaction. * * @param The type of the state held for the transaction * before this operation executes. * * @return an operation which will abort the transaction * and return the input unchanged. */ static TransactionOperation abort() { return (broker, transaction, listener, t) -> { listener.event("Aborting Transaction"); transaction.abort(); return t; }; } /** * Executes this, and then the other Transaction Operation * on the input type {@code } and returns * the results as a tuple. * * e.g.
Tuple2(f(T) -> U, other(T) -> U2)
* * @param thr result of the other operation. * * @param other another transaction operation which also operates on T * * @return The tuple of results. */ default TransactionOperation> with(final TransactionOperation other) { return (broker, txn, listener, t) -> new Tuple2<>(apply(broker, txn, listener, t), other.apply(broker, txn, listener, t)); } /** * Returns a composed function that first applies this function to * its input, and then applies the {@code after} function to the result. * * See {@link Function#andThen(Function)} * * @param the result of the after operation. * * @param after the after function * * @return the composed function */ default TransactionOperation andThen(final TransactionOperation after) { return (broker, txn, listener, t) -> after.apply(broker, txn, listener, apply(broker, txn, listener, t)); } /** * Returns a composed function that first applies the {@code before} * function to its input, and then applies this function to the result. * * See {@link Function#compose(Function)} * * @param the input type of the before operation. * * @param before the before function. * * @return the composed function */ default TransactionOperation compose(final TransactionOperation before) { Objects.requireNonNull(before); return (broker, txn, listener, v) -> apply(broker, txn, listener, before.apply(broker, txn, listener, v)); } /** * Returns a function that always returns its input argument. * * See {@link Function#identity()} * * @param the result of the identity operation. * * @return the identity operation */ static TransactionOperation identity() { return (broker, transaction, listener, t) -> t; } } /** * A simple extension of {@link CountDownLatch} * which also provides a name for the latch. * * Useful for debugging latching ordering in * transaction schedules. */ class NamedCountDownLatch extends CountDownLatch { private final String name; public NamedCountDownLatch(final String name, final int count) { super(count); this.name = name; } public final String getName() { return name; } } /** * A listener that receives events * from the scheduler during execution * of the schedule. */ @FunctionalInterface interface ExecutionListener { /** * Called when a execution event occurs. * * @param message a message describing the event. */ void event(final String message); } /** * Discards {@link ExecutionListener} Events */ ExecutionListener NULL_SCHEDULE_LISTENER = message -> {}; /** * Just writes {@link ExecutionListener} Events to Standard Out */ ExecutionListener STD_OUT_SCHEDULE_LISTENER = message -> System.out.println("[" + System.nanoTime() + "]: " + Thread.currentThread().getName() + ": " + message); }