diff --git a/java/com.sap.sse.datamining.test/src/com/sap/sse/datamining/impl/TestAbortingHeavyLoadQuery.java b/java/com.sap.sse.datamining.test/src/com/sap/sse/datamining/impl/TestAbortingHeavyLoadQuery.java
index 27db549e027..2aa3e6ada0e 100644
--- a/java/com.sap.sse.datamining.test/src/com/sap/sse/datamining/impl/TestAbortingHeavyLoadQuery.java
+++ b/java/com.sap.sse.datamining.test/src/com/sap/sse/datamining/impl/TestAbortingHeavyLoadQuery.java
@@ -1,14 +1,20 @@
package com.sap.sse.datamining.impl;
import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
+import java.lang.reflect.Method;
+import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.Date;
import java.util.HashSet;
import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
@@ -17,6 +23,7 @@ import java.util.concurrent.RunnableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -37,25 +44,63 @@ import com.sap.sse.datamining.shared.GroupKey;
import com.sap.sse.datamining.shared.data.QueryResultState;
import com.sap.sse.datamining.shared.impl.GenericGroupKey;
import com.sap.sse.datamining.test.util.components.StatefulBlockingInstruction;
+import com.sap.sse.datamining.test.util.components.StatefulProcessorInstruction;
+/**
+ * Integration test aborting a query with simulated heavy load instructions. Uses a highly customized processor chain
+ * (based on the processors/instructions used in production) that allows detailed introspection and logging of the
+ * execution process. The test is used to verify the behavior of processors, if such a heavy load query is aborted
+ * during the process. For example that no instructions are scheduled after the query was aborted and the behavior of
+ * already scheduled instructions that may or may not have been started.
+ *
+ * The test is configurable regarding the number of threads for the executor, the number of data elements/heavy load
+ * instructions, the duration of heavy load instructions and the time to wait before the query is aborted. Note that
+ * the test can fail for some configurations, due to race conditions that would be very hard to check for.
+ *
+ * The test optionally records the process in order of the execution (using a concurrent message queue), which is
+ * printed to the console before assertions are performed. This isn't used by the assertions, but can be used to get
+ * a better understanding of what happens during the execution.
+ *
+ * See {@link #initialize()} for more details about the processor chain.
+ *
+ * @author Lennart Hensler
+ */
public class TestAbortingHeavyLoadQuery {
- private static final int ExecutorPoolSize = Math.min(3, Runtime.getRuntime().availableProcessors() - 2);
+ // Test Configuration ----------------------------------------------------------------------------------------
+ /**
+ * Number of threads in the executor. Determines the number of elements (and thus heavy load instructions)
+ * retrieved for each group. This means that each group should have a runtime of {@value #HeavyLoadInstructionDuration}ms,
+ * since the single instructions are executed concurrently.
+ */
+ private static final int ExecutorPoolSize = Math.max(3, Runtime.getRuntime().availableProcessors());
+ /** The number of groups contained in the initial data source. */
private static final int DataSourceSize = 2000;
private static final String GroupKeyPrefix = "G";
+ /** The time a heavy load instruction blocks the executing thread using {@link Thread#sleep(long)}. */
private static final long HeavyLoadInstructionDuration = 500;
- private static final long AbortQueryDelay = (long) (HeavyLoadInstructionDuration * 1.1);
+ /** The number of milliseconds to wait before {@link Query#abort()} is called. */
+ private static final long AbortQueryDelay = (long) (HeavyLoadInstructionDuration * 5.45);
+ //------------------------------------------------------------------------------------------------------------
+ // Execution Recording Configuration -------------------------------------------------------------------------
+ /** Enables concurrent logging of the query execution and prints the current record before assertions. */
private static final boolean RecordExecution = true;
+ private static final SimpleDateFormat DateFormatter = new SimpleDateFormat("HH:mm:ss.SSS");
private ConcurrentLinkedQueue executionRecord;
+ //------------------------------------------------------------------------------------------------------------
+ // Test State - Initialized in initialize() ------------------------------------------------------------------
private ExecutorService executor;
private Query> query;
private Collection> processors;
+ private Set> unfinishedInstructions;
+ //------------------------------------------------------------------------------------------------------------
@Test
public void testAbortingHeavyLoadQuery() throws InterruptedException, ExecutionException {
- RunnableFuture>> queryTask = new FutureTask<>(() -> {
+ // Executing query in separate thread
+ RunnableFuture>> queryTask = new FutureTask<>(() -> {
logExecution("Starting query execution");
long start = System.currentTimeMillis();
QueryResult> result = query.run();
@@ -69,46 +114,133 @@ public class TestAbortingHeavyLoadQuery {
Thread.sleep(10);
} while (query.getState() != QueryState.RUNNING);
+ // Aborting query and waiting for completion
Thread.sleep(AbortQueryDelay);
logExecution("Aborting query");
query.abort();
QueryResult> result = queryTask.get();
+
+ // The query execution returned, so all processors are aborted (checked below).
+ // The number of unfinished instructions mustn't change after this point
+ int unfinishedInstructionsCount = unfinishedInstructions.size();
+ boolean unfinishedInstructionWasFinished = false;
+ Set> runningInstructions = new HashSet<>();
+ Set> notStartedInstructions = new HashSet<>();
+ for (StatefulProcessorInstruction> instruction : unfinishedInstructions) {
+ if (instruction.runWasCalled()) {
+ runningInstructions.add(instruction);
+ } else {
+ notStartedInstructions.add(instruction);
+ }
+ unfinishedInstructionWasFinished |= instruction.computeResultWasFinished();
+ }
+ logExecution(unfinishedInstructionsCount + " unfinished instructions left - " +
+ runningInstructions.size() + " running, " + notStartedInstructions.size() + " not started");
+ printExecutionRecord();
+ assertThat("Number of unfinished instructions changed", runningInstructions.size() + notStartedInstructions.size(), is(unfinishedInstructionsCount));
+ assertFalse("Unfinished instructions expected, but at least one was completed", unfinishedInstructionWasFinished);
+
+ // Checking query, result and processor chain state
+ assertThat(query.getState(), is(QueryState.ABORTED));
assertThat(result.getState(), is(QueryResultState.ABORTED));
assertTrue("The result is not empty", result.isEmpty());
for (Processor, ?> processor : processors) {
assertTrue("Processor wasn't aborted", processor.isAborted());
}
- // TODO Ensure that no instructions are scheduled after the query has been aborted
- // TODO More detailed assessment of the leftover instructions
-
+ // Execution of unfinished instructions
executor.shutdown();
boolean terminated = executor.awaitTermination((long) (HeavyLoadInstructionDuration * 1.5), TimeUnit.MILLISECONDS);
-
- if (RecordExecution) {
- for (String string : executionRecord) {
- System.out.println(string);
- }
- }
+ printExecutionRecord();
assertTrue("The executor didn't terminate in the given time", terminated);
+
+ // Verify that the collection of unfinished instructions didn't change
+ assertThat("Number of unfinished instructions changed", unfinishedInstructions.size(), is(unfinishedInstructionsCount));
+ for (StatefulProcessorInstruction> instruction : unfinishedInstructions) {
+ boolean previouslyContained = runningInstructions.contains(instruction) || notStartedInstructions.contains(instruction);
+ assertTrue("A new instruction has been scheduled after aborting the query", previouslyContained);
+ }
+ for (StatefulProcessorInstruction> instruction : runningInstructions) {
+ assertTrue("A previously running instruction was removed from unfinished instructions", unfinishedInstructions.contains(instruction));
+ }
+ for (StatefulProcessorInstruction> instruction : notStartedInstructions) {
+ assertTrue("A previously unstarted instruction was removed from unfinished instructions", unfinishedInstructions.contains(instruction));
+ }
+
+ // Checking state of unfinished instructions
+ for (StatefulProcessorInstruction> instruction : runningInstructions) {
+ assertTrue("computeResult() of a running unfinished instruction wasn't called", instruction.computeResultWasCalled());
+ assertTrue("computeResult() of a running unfinished instruction didn't finish", instruction.computeResultWasFinished());
+ }
+ for (StatefulProcessorInstruction> instruction : notStartedInstructions) {
+ assertTrue("run() of an unstarted unfinished instruction wasn't called", instruction.runWasCalled());
+
+ assertFalse("computeResult() of an unstarted unfinished instruction was called", instruction.computeResultWasCalled());
+ assertFalse("computeResult() of an unstarted unfinished instruction was finished", instruction.computeResultWasFinished());
+ }
+ }
+
+ private void printExecutionRecord() {
+ if (RecordExecution) {
+ Collection snapshot = new ArrayList<>(executionRecord);
+ executionRecord.clear();
+ for (String string : snapshot) {
+ System.out.println(string);
+ }
+ }
}
private void logExecution(String message) {
if (RecordExecution) {
- executionRecord.add(Thread.currentThread().getName() + ": " + message);
+ String timeString = DateFormatter.format(new Date());
+ executionRecord.add(timeString + " - " + Thread.currentThread().getName() + ": " + message);
}
}
-
+
+ /**
+ * Initializes the {@link ExecutorService}, the execution record, helper collections (e.g. the processors in the
+ * chain or the unfinished instructions) and most importantly the processor chain for the query, which works as
+ * follows:
+ *
+ * -
+ * The initial data source is a collection of strings from {@value #GroupKeyPrefix}
n to
+ * {@value #GroupKeyPrefix}n-1, denoting a group. n is specified in {@link #DataSourceSize}.
+ *
+ * -
+ *
x (specified by {@link #ExecutorPoolSize}) {@link Element elements} are retrieved for each
+ * group, where the elements name is set to the received string and the elements value is the current value
+ * of x.
+ *
+ * -
+ * A heavy load instruction for each element is scheduled, which blocks the running thread for
+ * {@value #HeavyLoadInstructionDuration}ms.
+ *
+ * -
+ * Each element is grouped by its name and its value is used as value for the {@link GroupedDataEntry}.
+ *
+ * - The grouped data is collected as set.
+ *
+ * This results in n * x data elements (and heavy load instructions), with x instructions
+ * executed concurrently.
+ *
+ * Each processor creates {@link StatefulProcessorInstruction} or uses {@link StatefulInstructionWrapper}, which allows
+ * to verify if instruction.run() was called, its computation started and its computation finished.
+ *
+ * A concurrent set ({@link ConcurrentHashMap#newKeySet()}) is used to track the unfinished instructions. An instruction
+ * is added to the set upon its construction and is removed when the instruction finished callback method is called.
+ */
@Before
@SuppressWarnings("unchecked")
public void initialize() {
- executor = new DataMiningExecutorService(ExecutorPoolSize);
-// executor = new ThreadPoolExecutor(ExecutorPoolSize, ExecutorPoolSize, 0, TimeUnit.MILLISECONDS, new PriorityBlockingQueue<>());
-// executor = new ThreadPoolExecutor(ExecutorPoolSize, ExecutorPoolSize, 0, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
if (RecordExecution) {
executionRecord = new ConcurrentLinkedQueue<>();
}
+
+ executor = new DataMiningExecutorService(ExecutorPoolSize);
+// executor = new ThreadPoolExecutor(ExecutorPoolSize, ExecutorPoolSize, 0, TimeUnit.MILLISECONDS, new PriorityBlockingQueue<>());
+// executor = new ThreadPoolExecutor(ExecutorPoolSize, ExecutorPoolSize, 0, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
processors = new ArrayList<>();
+ unfinishedInstructions = ConcurrentHashMap.newKeySet();
Class> dataSourceType = (Class>)(Class>) Iterable.class;
Class> groupedType = (Class>)(Class>) GroupedDataEntry.class;
@@ -118,29 +250,51 @@ public class TestAbortingHeavyLoadQuery {
for (int i = 0; i < DataSourceSize; i++) {
dataSource.add(GroupKeyPrefix + i);
}
-
query = new ProcessorQuery, Iterable>(dataSource, resultType) {
@Override
protected Processor, ?> createChainAndReturnFirstProcessor(Processor