Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Modifications copyright (C) 2017 Uber Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"). You may not
* use this file except in compliance with the License. A copy of the License is
* located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file 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.uber.cadence.internal.common;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.uber.cadence.FeatureFlags;
import java.lang.reflect.Modifier;

/** Serializes {@link FeatureFlags} into the value of the cadence-client-feature-flags header. */
public final class FeatureFlagsHeader {

// The server deserializes this header with a protobuf JSON unmarshaller that fails on any field
// it doesn't know, and silently falls back to all flags disabled when it does. Thrift declares
// the IDL fields public and keeps its own bookkeeping, such as __isset_bitfield, private, so
// excluding private fields leaves exactly the fields the server expects. Field names are sent as
// Thrift declares them because the proto IDL pins json_name to those names.
private static final Gson GSON =
new GsonBuilder()
.excludeFieldsWithModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.TRANSIENT)
.create();

public static String serialize(FeatureFlags featureFlags) {
return GSON.toJson(featureFlags);
}

private FeatureFlagsHeader() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@
package com.uber.cadence.internal.compatibility.proto.serviceclient;

import com.google.common.base.Strings;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.uber.cadence.api.v1.DomainAPIGrpc;
import com.uber.cadence.api.v1.MetaAPIGrpc;
import com.uber.cadence.api.v1.MetaAPIGrpc.MetaAPIBlockingStub;
Expand All @@ -32,6 +30,7 @@
import com.uber.cadence.api.v1.WorkflowAPIGrpc.WorkflowAPIBlockingStub;
import com.uber.cadence.api.v1.WorkflowAPIGrpc.WorkflowAPIFutureStub;
import com.uber.cadence.internal.Version;
import com.uber.cadence.internal.common.FeatureFlagsHeader;
import com.uber.cadence.serviceclient.ClientOptions;
import com.uber.cadence.serviceclient.auth.IAuthorizationProvider;
import io.grpc.*;
Expand Down Expand Up @@ -114,10 +113,8 @@ final class GrpcServiceStubs implements IGrpcServiceStubs {
headers.put(ISOLATION_GROUP_HEADER_KEY, options.getIsolationGroup());
}
if (options.getFeatureFlags() != null) {
GsonBuilder gsonBuilder = new GsonBuilder();
Gson gson = gsonBuilder.create();
String serialized = gson.toJson(options.getFeatureFlags());
headers.put(CLIENT_FEATURE_FLAGS_HEADER_KEY, serialized);
headers.put(
CLIENT_FEATURE_FLAGS_HEADER_KEY, FeatureFlagsHeader.serialize(options.getFeatureFlags()));
}
mergeHeaders(headers, options.getHeaders());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,11 @@

import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.uber.cadence.*;
import com.uber.cadence.WorkflowService.GetWorkflowExecutionHistory_result;
import com.uber.cadence.internal.Version;
import com.uber.cadence.internal.common.CheckedExceptionWrapper;
import com.uber.cadence.internal.common.FeatureFlagsHeader;
import com.uber.cadence.internal.common.InternalUtils;
import com.uber.cadence.internal.metrics.MetricsTag;
import com.uber.cadence.internal.metrics.MetricsType;
Expand Down Expand Up @@ -157,10 +156,8 @@ private static Map<String, String> getThriftHeaders(ClientOptions options) {
}

if (options.getFeatureFlags() != null) {
GsonBuilder gsonBuilder = new GsonBuilder();
Gson gson = gsonBuilder.create();
String serialized = gson.toJson(options.getFeatureFlags());
builder.put("cadence-client-feature-flags", serialized);
builder.put(
"cadence-client-feature-flags", FeatureFlagsHeader.serialize(options.getFeatureFlags()));
}

if (!Strings.isNullOrEmpty(options.getIsolationGroup())) {
Expand Down
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
@@ -0,0 +1,43 @@
/*
* Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Modifications copyright (C) 2017 Uber Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"). You may not
* use this file except in compliance with the License. A copy of the License is
* located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file 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.uber.cadence.internal.common;

import static org.junit.Assert.assertEquals;

import com.uber.cadence.FeatureFlags;
import org.junit.Test;

public class FeatureFlagsHeaderTest {

// The server rejects the whole header when it carries a field the IDL doesn't define, so these
// assertions are on the exact wire format rather than on the parsed flags.
@Test
public void testSerializeEnabledFlag() {
assertEquals(
"{\"WorkflowExecutionAlreadyCompletedErrorEnabled\":true}",
FeatureFlagsHeader.serialize(
new FeatureFlags().setWorkflowExecutionAlreadyCompletedErrorEnabled(true)));
}

@Test
public void testSerializeUnsetFlags() {
assertEquals(
"{\"WorkflowExecutionAlreadyCompletedErrorEnabled\":false}",
FeatureFlagsHeader.serialize(new FeatureFlags()));
}
}
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