/*
* Copyright 2012 LinkedIn, Inc
*
* 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 com.linkedin.parseq.trace;
import static com.linkedin.parseq.Task.value;
import static org.testng.AssertJUnit.*;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.testng.annotations.Test;
import com.linkedin.parseq.BaseEngineTest;
import com.linkedin.parseq.BaseTask;
import com.linkedin.parseq.Context;
import com.linkedin.parseq.Exceptions;
import com.linkedin.parseq.Task;
import com.linkedin.parseq.Tasks;
import com.linkedin.parseq.promise.Promise;
import com.linkedin.parseq.promise.Promises;
import com.linkedin.parseq.promise.SettablePromise;
/**
* @author Chris Pettitt
* @author Chi Chan
* @author Jaroslaw Odzga (jodzga@linkedin.com)
*/
public class TestTaskToTrace extends BaseEngineTest {
@Test
public void testUnstartedTrace() {
final Task<?> task = value("taskName", "value");
// We don't run the task
verifyShallowTrace(task);
}
@Test
public void testSuccessfulTrace() throws InterruptedException {
final Task<String> task = value("taskName", "value");
runAndWait("TestTaskToTrace.testSuccessfulTrace", task);
verifyShallowTrace(task);
}
@Test
public void testSuccessfulTraceWithNullValue() throws InterruptedException {
final Task<String> task = value("taskName", null);
runAndWait("TestTaskToTrace.testSuccessfulTraceWithNullValue", task);
verifyShallowTrace(task);
}
@Test
public void testErrorTrace() throws InterruptedException {
final Exception exception = new Exception("error message");
final Task<?> task = Task.failure("taskName", exception);
try {
runAndWait("TestTaskToTrace.testErrorTrace", task);
fail("task should finish with Exception");
} catch (Throwable t) {
assertEquals(exception, task.getError());
}
verifyShallowTrace(task);
}
@Test
public void testNotHiddenTrace() throws InterruptedException {
final Task<String> task1 = new BaseTask<String>() {
@Override
protected Promise<? extends String> run(Context context) throws Exception {
return Promises.value("task1");
}
};
final Task<String> task2 = new BaseTask<String>() {
@Override
protected Promise<? extends String> run(Context context) throws Exception {
return Promises.value("task2");
}
};
final Task<?> par1 = Task.par(task1, task2);
runAndWait("TestTaskToTrace.testNotHiddenTrace", par1);
assertFalse(par1.getShallowTrace().getHidden());
assertFalse(task1.getShallowTrace().getHidden());
assertFalse(task1.getShallowTrace().getHidden());
}
@Test
public void testUserHiddenTrace() throws InterruptedException {
final Task<String> task1 = new BaseTask<String>() {
@Override
protected Promise<? extends String> run(Context context) throws Exception {
return Promises.value("task1");
}
@Override
public ShallowTrace getShallowTrace() {
_shallowTraceBuilder.setHidden(true);
return super.getShallowTrace();
}
};
final Task<String> task2 = new BaseTask<String>() {
@Override
protected Promise<? extends String> run(Context context) throws Exception {
return Promises.value("task2");
}
@Override
public ShallowTrace getShallowTrace() {
_shallowTraceBuilder.setHidden(true);
return super.getShallowTrace();
}
};
final Task<?> par1 = Task.par(task1, task2);
runAndWait("TestTaskToTrace.testUserHiddenTrace", par1);
assertFalse(par1.getShallowTrace().getHidden());
assertTrue(task1.getShallowTrace().getHidden());
assertTrue(task1.getShallowTrace().getHidden());
}
@Test
public void testUnfinishedTrace() throws InterruptedException {
// Used to ensure that the task has started running
final CountDownLatch cdl = new CountDownLatch(1);
final SettablePromise<Void> promise = Promises.settable();
final Task<Void> task = new BaseTask<Void>() {
@Override
public Promise<Void> run(final Context context) throws Exception {
cdl.countDown();
// Return a promise that won't be satisfied until after out test
return promise;
}
};
getEngine().run(task);
assertTrue(cdl.await(5, TimeUnit.SECONDS));
logTracingResults("TestTaskToTrace.testUnfinishedTrace", task);
verifyShallowTrace(task);
// Finish task
promise.done(null);
}
private Set<TraceRelationship> getRelationships(Trace trace, long id) {
Set<TraceRelationship> result = new HashSet<>();
for (TraceRelationship rel : trace.getRelationships()) {
if (rel.getFrom() == id || rel.getTo() == id) {
result.add(rel);
}
}
return result;
}
@Test
public void testTraceWithPredecessorTrace() throws InterruptedException {
final Task<String> predecessor = value("predecessor", "predecessorValue");
final Task<String> successor = value("successor", "successorValue");
final Task<?> seq = predecessor.andThen(successor);
runAndWait("TestTaskToTrace.testTraceWithPredecessorTrace", seq);
verifyShallowTrace(successor);
verifyShallowTrace(predecessor);
assertEquals(predecessor.getTrace(), successor.getTrace());
//expected relationship: PARENT_OF and SUCCESSOR_OF
assertEquals(2, getRelationships(successor.getTrace(), successor.getId()).size());
assertTrue(successor.getTrace().getRelationships()
.contains(new TraceRelationship(successor.getShallowTraceBuilder(),
predecessor.getShallowTraceBuilder(), Relationship.SUCCESSOR_OF)));
}
@Test
public void testSideEffectsPredecessorTrace() throws InterruptedException, IOException {
final Task<String> baseTask = value("base", "baseValue");
final Task<String> sideEffect = value("sideEffect", "sideEffectValue");
final Task<String> withSideEffect = baseTask.withSideEffect(x -> sideEffect);
runAndWait("TestTaskToTrace.testSideEffectsPredecessorTrace", withSideEffect);
assertTrue(sideEffect.await(5, TimeUnit.SECONDS));
assertEquals(2, getRelationships(withSideEffect.getTrace(), withSideEffect.getId()).size());
verifyShallowTrace(sideEffect);
verifyShallowTrace(baseTask);
assertTrue(withSideEffect.getTrace().getRelationships().toString(), withSideEffect.getTrace().getRelationships()
.contains(new TraceRelationship(withSideEffect.getShallowTraceBuilder(),
baseTask.getShallowTraceBuilder(), Relationship.PARENT_OF)));
}
@SuppressWarnings("deprecation")
@Test
public void testTraceWithSuccessChild() throws InterruptedException {
final Task<String> task = value("taskName", "value");
final Task<?> seq = Tasks.seq(Arrays.asList(task));
runAndWait("TestTaskToTrace.testTraceWithSuccessChild", seq);
verifyShallowTrace(task);
verifyShallowTrace(seq);
assertEquals(1, getRelationships(seq.getTrace(), seq.getId()).size());
assertTrue(seq.getTrace().getRelationships()
.contains(new TraceRelationship(seq.getShallowTraceBuilder(),
task.getShallowTraceBuilder(), Relationship.PARENT_OF)));
}
@Test
public void testTraceWithEarlyFinish() throws InterruptedException {
final Task<String> innerTask = value("xyz");
final Task<String> task = new BaseTask<String>() {
@Override
protected Promise<? extends String> run(final Context context) throws Exception {
// We kick off a task that won't finish before the containing task
// (this task) is finished.
context.run(innerTask);
return Promises.value("value");
}
};
runAndWait("TestTaskToTrace.testTraceWithEarlyFinish", task);
assertEquals(1, getRelationships(task.getTrace(), task.getId()).size());
assertTrue(task.getTrace().getRelationships()
.contains(new TraceRelationship(task.getShallowTraceBuilder(),
innerTask.getShallowTraceBuilder(), Relationship.POTENTIAL_PARENT_OF)));
assertEquals(ResultType.EARLY_FINISH, task.getTrace().getTraceMap().get(innerTask.getId()).getResultType());
}
@Test
public void testTraceIsAddedBeforeAwaitCompletes() throws InterruptedException {
for (int i = 0; i < 100; i++) {
final Task<String> innerTask = value("xyz");
final Task<String> task = new BaseTask<String>() {
@Override
protected Promise<? extends String> run(final Context context) throws Exception {
// We kick off a task that won't finish before the containing task
// (this task) is finished.
context.run(innerTask);
return Promises.value("value");
}
};
runAndWait("TestTaskToTrace.testTraceIsAddedBeforeAwaitCompletes", task);
assertTrue(task.getTrace().getRelationships().size() > 0);
}
}
@Test
public void testTraceWithMultiplePotentialParentsPar() throws InterruptedException {
final Task<String> innerTask = value("xyz");
final Task<String> task1 = new BaseTask<String>("task1") {
@Override
protected Promise<? extends String> run(final Context context) throws Exception {
context.run(innerTask);
return Promises.value("value1");
}
};
final Task<String> task2 = new BaseTask<String>("task2") {
@Override
protected Promise<? extends String> run(final Context context) throws Exception {
context.run(innerTask);
return Promises.value("value2");
}
};
Task<?> par = Task.par(task1, task2);
runAndWait("TestTaskToTrace.testTraceWithMultiplePotentialParentsPar", par);
Set<Long> tasksWithParent = new HashSet<>();
Map<Long, Integer> tasksWithPotentialParent = new HashMap<>();
assertAndFindParent(par.getTrace(), tasksWithParent, tasksWithPotentialParent);
assertEquals(2, tasksWithParent.size());
assertEquals((Integer) 2, tasksWithPotentialParent.get(innerTask.getId()));
assertEquals(1, tasksWithPotentialParent.size());
assertTrue(tasksWithParent.contains(task1.getId()));
assertTrue(tasksWithParent.contains(task2.getId()));
verifyShallowTrace(task1);
verifyShallowTrace(task2);
verifyShallowTrace(innerTask);
assertEquals(ResultType.EARLY_FINISH, innerTask.getTrace().getTraceMap().get(innerTask.getId()).getResultType());
}
@Test
public void testTraceWithMultiplePotentialParentsSeq() throws InterruptedException {
final SettablePromise<String> promise1 = Promises.settable();
final Task<String> innerTask = new BaseTask<String>("innerTask") {
@Override
protected Promise<? extends String> run(Context context) throws Exception {
promise1.done("inner");
return promise1;
}
};
final Task<String> task1 = new BaseTask<String>("task1") {
@Override
protected Promise<? extends String> run(final Context context) throws Exception {
context.run(innerTask);
return Promises.value("value1");
}
};
final Task<String> task2 = new BaseTask<String>("task2") {
@Override
protected Promise<? extends String> run(final Context context) throws Exception {
context.run(innerTask);
return Promises.value("value2");
}
};
final Task<String> task3 = new BaseTask<String>("task2") {
@Override
protected Promise<? extends String> run(Context context) throws Exception {
context.run(innerTask);
return Promises.value("value3");
}
};
Task<?> seq = task1.andThen(task2).andThen(task3);
runAndWait("TestTaskToTrace.testTraceWithMultiplePotentialParentsSeq", seq);
Set<Long> tasksWithParent = new HashSet<>();
Map<Long, Integer> tasksWithPotentialParent = new HashMap<>();
assertAndFindParent(seq.getTrace(), tasksWithParent, tasksWithPotentialParent);
assertEquals(4, tasksWithParent.size());
assertEquals((Integer) 3, tasksWithPotentialParent.get(innerTask.getId()));
assertEquals(1, tasksWithPotentialParent.size());
assertTrue(tasksWithParent.contains(task1.getId()));
assertTrue(tasksWithParent.contains(task2.getId()));
assertTrue(tasksWithParent.contains(task3.getId()));
verifyShallowTrace(task1);
verifyShallowTrace(task2);
verifyShallowTrace(task3);
verifyShallowTrace(innerTask);
assertEquals(ResultType.EARLY_FINISH, innerTask.getTrace().getTraceMap().get(innerTask.getId()).getResultType());
}
@Test
public void testTraceWithDiamond() throws InterruptedException {
final Task<String> a = value("valueA");
final Task<String> b = value("valueB");
final Task<String> c = value("valueC");
final Task<String> d = value("valueD");
final Task<String> parent = new BaseTask<String>() {
@Override
protected Promise<? extends String> run(final Context context) throws Exception {
context.after(a).run(b);
context.after(a).run(c);
context.after(b, c).run(d);
context.run(a);
return d;
}
};
runAndWait("TestTaskToTrace.testTraceWithDiamond", parent);
verifyShallowTrace(parent);
verifyShallowTrace(a);
verifyShallowTrace(b);
verifyShallowTrace(c);
verifyShallowTrace(d);
assertTrue(parent.getTrace().getRelationships().contains(
new TraceRelationship(parent.getShallowTraceBuilder(), a.getShallowTraceBuilder(), Relationship.PARENT_OF)));
assertTrue(parent.getTrace().getRelationships().contains(
new TraceRelationship(parent.getShallowTraceBuilder(), b.getShallowTraceBuilder(), Relationship.PARENT_OF)));
assertTrue(parent.getTrace().getRelationships().contains(
new TraceRelationship(parent.getShallowTraceBuilder(), c.getShallowTraceBuilder(), Relationship.PARENT_OF)));
assertTrue(parent.getTrace().getRelationships().contains(
new TraceRelationship(parent.getShallowTraceBuilder(), d.getShallowTraceBuilder(), Relationship.PARENT_OF)));
assertTrue(parent.getTrace().getRelationships().contains(
new TraceRelationship(d.getShallowTraceBuilder(), b.getShallowTraceBuilder(), Relationship.SUCCESSOR_OF)));
assertTrue(parent.getTrace().getRelationships().contains(
new TraceRelationship(d.getShallowTraceBuilder(), c.getShallowTraceBuilder(), Relationship.SUCCESSOR_OF)));
assertTrue(parent.getTrace().getRelationships().contains(
new TraceRelationship(b.getShallowTraceBuilder(), a.getShallowTraceBuilder(), Relationship.SUCCESSOR_OF)));
assertTrue(parent.getTrace().getRelationships().contains(
new TraceRelationship(c.getShallowTraceBuilder(), a.getShallowTraceBuilder(), Relationship.SUCCESSOR_OF)));
}
/**
* Populates sets with ids of tasks that have parent and ids of tasks that have potential
* parents with number of potential parents.
* Validates that task can have only one parent but many potential parents.
*/
private void assertAndFindParent(Trace trace, Set<Long> tasksWithParent,
Map<Long, Integer> tasksWithPotentialParent) {
for (TraceRelationship rel : trace.getRelationships()) {
if (rel.getRelationhsip() == Relationship.PARENT_OF) {
assertFalse(tasksWithParent.contains(rel.getTo()));
tasksWithParent.add(rel.getTo());
} else if (rel.getRelationhsip() == Relationship.POTENTIAL_PARENT_OF) {
if (!tasksWithPotentialParent.containsKey(rel.getTo())) {
tasksWithPotentialParent.put(rel.getTo(), 0);
}
tasksWithPotentialParent.put(rel.getTo(), tasksWithPotentialParent.get(rel.getTo()) + 1);
}
}
}
private Long getChild(TraceRelationship rel, Long parent) {
if ((rel.getRelationhsip() == Relationship.PARENT_OF || rel.getRelationhsip() == Relationship.POTENTIAL_PARENT_OF) &&
rel.getFrom().equals(parent)) {
return rel.getTo();
}
if (rel.getRelationhsip() == Relationship.POTENTIAL_CHILD_OF && rel.getTo().equals(parent)) {
return rel.getFrom();
}
return null;
}
private ShallowTrace findPossiblyFusedTrace(final Task<?> task) {
final ShallowTrace main = task.getShallowTrace();
if (main.getName().equals("fused")) {
final Trace trace = task.getTrace();
Optional<Long> child = trace.getRelationships().stream()
.map(rel -> getChild(rel, main.getId()))
.filter(id-> id != null)
.findFirst();
if (child.isPresent()) {
return trace.getTraceMap().get(child.get());
}
}
return main;
}
private void verifyShallowTrace(final Task<?> task) {
final ShallowTrace trace = findPossiblyFusedTrace(task);
assertEquals(task.getName(), trace.getName());
assertEquals(ResultType.fromTask(task), trace.getResultType());
// If the task has not been started then we expect the endNanos to be null.
// If the task has started but has not been finished then endNanos is set
// to the time that the trace was taken. If the task was finished then the
// task end time and trace end time should match.
if (trace.getResultType().equals(ResultType.UNFINISHED)) {
if (trace.getStartNanos() == null) {
assertNull(trace.getEndNanos());
} else {
// Trace will have the end time set to the time the trace was taken
assertTrue(trace.getEndNanos() <= System.nanoTime());
// We assume that the end time will always be at least one nanosecond
// greater than the start time.
assertTrue(trace.getEndNanos() > trace.getStartNanos());
}
}
if (task.isDone()) {
if (task.isFailed() && !(Exceptions.isEarlyFinish(task.getError()))) {
assertNotNull(trace.getValue());
} else {
assertNull(trace.getValue());
}
} else {
assertNull(trace.getValue());
}
}
}