Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions src/test/java/com/uber/cadence/FakeWorkflowServiceRule.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@
import com.uber.tchannel.api.handlers.ThriftRequestHandler;
import com.uber.tchannel.messages.ThriftRequest;
import com.uber.tchannel.messages.ThriftResponse;
import io.opentracing.mock.MockSpan;
import io.opentracing.mock.MockTracer;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Queue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
Expand All @@ -23,6 +27,8 @@
*/
public class FakeWorkflowServiceRule extends ExternalResource {

private static final Duration SPAN_TIMEOUT = Duration.ofSeconds(5);

private final Map<String, StubbedEndpoint> stubbedEndpoints = new ConcurrentHashMap<>();
private final MockTracer tracer = new MockTracer();
private TChannel tChannel;
Expand Down Expand Up @@ -89,6 +95,32 @@ public MockTracer getTracer() {
return tracer;
}

/**
* Returns the first finished span with the given operation name, waiting for it to appear.
* TChannel finishes its client span from a response callback which may run after the calling
* thread has been unblocked, so spans are not guaranteed to be visible once the call returns.
*/
public MockSpan awaitSpan(String operationName) {
long deadline = System.nanoTime() + SPAN_TIMEOUT.toNanos();
while (true) {
List<MockSpan> spans = tracer.finishedSpans();
Optional<MockSpan> span =
spans.stream().filter(s -> operationName.equals(s.operationName())).findFirst();
if (span.isPresent()) {
return span.get();
}
if (System.nanoTime() - deadline >= 0) {
throw new AssertionError("No span found for " + operationName + ", got: " + spans);
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new AssertionError("Interrupted waiting for span " + operationName, e);
}
}
}

public <V> CompletableFuture<V> stubSuccess(
String endpoint, Class<V> requestType, Object response) {
return stubEndpoint(endpoint, requestType, ResponseCode.OK, response);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,18 +116,9 @@ public void testEnqueueStart_includesTracing() {
stub.enqueueStart("input");

StartWorkflowExecutionAsyncRequest request = requestFuture.getNow(null).getStartRequest();
MockSpan mockSpan =
fakeService
.getTracer()
.finishedSpans()
.stream()
.filter(span -> "cadence-StartWorkflowExecutionAsync".equals(span.operationName()))
.findFirst()
.orElseThrow(
() ->
new AssertionError(
"No span found for StartWorkflowExecutionAsync:"
+ fakeService.getTracer().finishedSpans()));
// TChannel emits its own client span for the RPC in addition to the one the client creates
fakeService.awaitSpan("WorkflowService::StartWorkflowExecutionAsync");
MockSpan mockSpan = fakeService.awaitSpan("cadence-StartWorkflowExecutionAsync");
assertEquals(
mockSpan.context().toTraceId(),
Charsets.UTF_8
Expand Down Expand Up @@ -279,26 +270,9 @@ public void testEnqueueSignalWithStart_includesTracing() {

SignalWithStartWorkflowExecutionRequest request =
requestFuture.getNow(null).getSignalWithStartRequest().getRequest();
// TChannel finishes its own outbound span (in addition to the "cadence-..." span created
// explicitly by WorkflowServiceTChannel) from a future completion callback that can run on an
// I/O thread, so it may not yet be visible to this thread immediately after
// enqueueSignalWithStart() returns. Poll briefly instead of asserting immediately.
awaitFinishedSpansCount(2, Duration.ofSeconds(2));
assertEquals(2, fakeService.getTracer().finishedSpans().size());
MockSpan mockSpan =
fakeService
.getTracer()
.finishedSpans()
.stream()
.filter(
span ->
"cadence-SignalWithStartWorkflowExecutionAsync".equals(span.operationName()))
.findFirst()
.orElseThrow(
() ->
new AssertionError(
"No span found for SignalWithStartWorkflowExecutionAsync:"
+ fakeService.getTracer().finishedSpans()));
// TChannel emits its own client span for the RPC in addition to the one the client creates
fakeService.awaitSpan("WorkflowService::SignalWithStartWorkflowExecutionAsync");
MockSpan mockSpan = fakeService.awaitSpan("cadence-SignalWithStartWorkflowExecutionAsync");
assertEquals(
mockSpan.context().toTraceId(),
Charsets.UTF_8.decode(request.getHeader().getFields().get("traceid")).toString());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import org.junit.Assume;
import org.junit.Ignore;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -143,8 +144,14 @@ public Integer Double(Integer n) {
private static final Logger logger = LoggerFactory.getLogger(StartWorkflowTest.class);
private static final String DOMAIN = "test-domain";
private static final String TASK_LIST = "test-tasklist";
private static final Duration SPAN_TIMEOUT = Duration.ofSeconds(30);

@Test
@Ignore(
"TChannel sends the span context in $tracing$-prefixed application headers. A server running "
+ "yarpc 1.84.1 or newer only strips them when it has a tracer of its own, and otherwise "
+ "forwards them to history and matching over gRPC, which rejects the keys and fails the "
+ "request. See the tracer comment in WorkflowServiceTChannel.")
public void testStartWorkflowTchannel() {
Assume.assumeTrue(useDockerService);
MockTracer mockTracer = new MockTracer();
Expand Down Expand Up @@ -195,9 +202,10 @@ public void testStartMultipleWorkflowGRPC() {
worker.registerWorkflowImplementationTypes(TestWorkflowImpl.class, DoubleWorkflowImpl.class);
workerFactory.start();

int workflowCount = 100;
List<CompletableFuture<Void>> futures = new ArrayList<>();

for (int i = 0; i < 100; i++) {
for (int i = 0; i < workflowCount; i++) {
int finalI = i;
futures.add(
CompletableFuture.runAsync(
Expand All @@ -217,7 +225,12 @@ public void testStartMultipleWorkflowGRPC() {
// test debug log
StringBuilder sb = new StringBuilder();

List<MockSpan> spans = mockTracer.finishedSpans();
Map<String, Long> expectedSpans = new HashMap<>();
// each workflow runs an activity plus a child workflow which runs a local activity
expectedSpans.put("cadence-ExecuteWorkflow", 2L * workflowCount);
expectedSpans.put("cadence-ExecuteActivity", (long) workflowCount);
expectedSpans.put("cadence-ExecuteLocalActivity", (long) workflowCount);
List<MockSpan> spans = awaitSpans(mockTracer, expectedSpans);
spans.forEach(
span -> {
sb.append(span.toString()).append("\n");
Expand All @@ -244,6 +257,7 @@ public void testStartMultipleWorkflowGRPC() {
}

@Test
@Ignore("Skipped for the same reason as testStartWorkflowTchannel")
public void testSignalWithStartWorkflowTchannel() {
Assume.assumeTrue(useDockerService);
MockTracer mockTracer = new MockTracer();
Expand Down Expand Up @@ -337,7 +351,16 @@ private void testStartWorkflowHelper(
throw new AssertionError("workflow failure", e);
} finally {
rootSpan.finish();
List<MockSpan> spans = mockTracer.finishedSpans();
List<MockSpan> spans =
shouldPropagate
? awaitSpans(
mockTracer,
"cadence-StartWorkflowExecution",
"cadence-ExecuteWorkflow",
"cadence-ExecuteWorkflow",
"cadence-ExecuteActivity",
"cadence-ExecuteLocalActivity")
: mockTracer.finishedSpans();
spans.sort(
(o1, o2) -> {
if (o1.startMicros() < o2.startMicros()) {
Expand Down Expand Up @@ -431,7 +454,16 @@ private void testSignalWithStartWorkflowHelper(
throw new AssertionError("Workflow failure", e);
} finally {
rootSpan.finish();
List<MockSpan> spans = mockTracer.finishedSpans();
List<MockSpan> spans =
shouldPropagate
? awaitSpans(
mockTracer,
"cadence-SignalWithStartWorkflowExecution",
"cadence-ExecuteWorkflow",
"cadence-ExecuteWorkflow",
"cadence-ExecuteActivity",
"cadence-ExecuteLocalActivity")
: mockTracer.finishedSpans();
spans.sort(
(o1, o2) -> {
if (o1.startMicros() < o2.startMicros()) {
Expand Down Expand Up @@ -482,6 +514,49 @@ private void testSignalWithStartWorkflowHelper(
}
}

/**
* Returns the finished spans once every expected operation name is present, a repeated name
* requiring that many spans. Worker side spans are finished on worker threads after the client
* has already received the workflow result, so they aren't guaranteed to be visible as soon as
* the call returns.
*/
private List<MockSpan> awaitSpans(MockTracer tracer, String... expectedOperationNames) {
return awaitSpans(
tracer,
Arrays.stream(expectedOperationNames)
.collect(Collectors.groupingBy(name -> name, Collectors.counting())));
}

private List<MockSpan> awaitSpans(MockTracer tracer, Map<String, Long> expectedOperationCounts) {
long deadline = System.nanoTime() + SPAN_TIMEOUT.toNanos();
while (true) {
List<MockSpan> spans = tracer.finishedSpans();
Map<String, Long> actual =
spans
.stream()
.collect(Collectors.groupingBy(span -> span.operationName(), Collectors.counting()));
boolean complete =
expectedOperationCounts
.entrySet()
.stream()
.allMatch(
expected -> actual.getOrDefault(expected.getKey(), 0L) >= expected.getValue());
if (complete) {
return spans;
}
if (System.nanoTime() - deadline >= 0) {
throw new AssertionError(
"Timed out waiting for spans " + expectedOperationCounts + ", got: " + actual);
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new AssertionError("Interrupted waiting for spans " + expectedOperationCounts, e);
}
}
}

private MockSpan getFirstSpanByOperationName(List<MockSpan> spans, String operation) {
return spans
.stream()
Expand Down
Loading