Skip to content

Commit 088a36c

Browse files
authored
Added ability to simulate activity timeouts in unit tests (#138)
* reduced visibility of DecisionContextImpl * Added ability to simulate activity timeouts in unit tests * Fixed serialization of DataConverter field needed for ActivityTimeoutException.
1 parent 0e059ab commit 088a36c

File tree

9 files changed

+189
-10
lines changed

9 files changed

+189
-10
lines changed

src/main/java/com/uber/cadence/converter/JsonDataConverter.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ public final class JsonDataConverter implements DataConverter {
7474
private static final DataConverter INSTANCE = new JsonDataConverter();
7575
private static final byte[] EMPTY_BLOB = new byte[0];
7676
private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
77+
public static final String TYPE_FIELD_NAME = "type";
78+
public static final String JSON_CONVERTER_TYPE = "JSON";
7779
private final Gson gson;
7880
private final JsonParser parser = new JsonParser();
7981

@@ -165,6 +167,34 @@ public Object[] fromDataArray(byte[] content, Class<?>... valueType)
165167
private static class ThrowableTypeAdapterFactory implements TypeAdapterFactory {
166168
@Override
167169
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
170+
// Special handling of fields of DataConverter type.
171+
// Needed to serialize exceptions like ActivityTimeoutException.
172+
if (DataConverter.class.isAssignableFrom(typeToken.getRawType())) {
173+
return new TypeAdapter<T>() {
174+
@Override
175+
public void write(JsonWriter out, T value) throws IOException {
176+
out.beginObject();
177+
out.name(TYPE_FIELD_NAME).value(JSON_CONVERTER_TYPE);
178+
out.endObject();
179+
}
180+
181+
@Override
182+
@SuppressWarnings("unchecked")
183+
public T read(JsonReader in) throws IOException {
184+
in.beginObject();
185+
if (!in.nextName().equals("type")) {
186+
throw new IOException("Cannot deserialize DataConverter. Missing type field");
187+
}
188+
String value = in.nextString();
189+
if (!"JSON".equals(value)) {
190+
throw new IOException(
191+
"Cannot deserialize DataConverter. Expected type is JSON. " + "Found " + value);
192+
}
193+
in.endObject();
194+
return (T) JsonDataConverter.getInstance();
195+
}
196+
};
197+
}
168198
if (!Throwable.class.isAssignableFrom(typeToken.getRawType())) {
169199
return null; // this class only serializes 'Throwable' and its subtypes
170200
}

src/main/java/com/uber/cadence/internal/replay/DecisionContextImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
import java.util.function.BiConsumer;
3131
import java.util.function.Consumer;
3232

33-
public final class DecisionContextImpl implements DecisionContext, HistoryEventHandler {
33+
final class DecisionContextImpl implements DecisionContext, HistoryEventHandler {
3434

3535
private final ActivityDecisionContext activityClient;
3636

src/main/java/com/uber/cadence/internal/replay/DecisionsHelper.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -527,8 +527,8 @@ private DecisionStateMachine getDecision(DecisionId decisionId) {
527527
throw new IllegalArgumentException(
528528
"Unknown "
529529
+ decisionId
530-
+ ". The possible causes are "
531-
+ "nondeterministic workflow definition code or incompatible change in the workflow definition.");
530+
+ ". The possible causes are a nondeterministic workflow definition code or an incompatible change in the workflow definition."
531+
+ "See the \"Workflow Implementation Constraints\" section from the github.com/uber-java/cadence-client README");
532532
}
533533
return result;
534534
}

src/main/java/com/uber/cadence/internal/sync/POJOActivityTaskHandler.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import com.uber.cadence.internal.common.InternalUtils;
3232
import com.uber.cadence.internal.worker.ActivityTaskHandler;
3333
import com.uber.cadence.serviceclient.IWorkflowService;
34+
import com.uber.cadence.testing.TestActivityTimeoutException;
3435
import java.lang.reflect.InvocationTargetException;
3536
import java.lang.reflect.Method;
3637
import java.util.Collections;
@@ -103,8 +104,16 @@ private ActivityTaskHandler.Result mapToActivityFailure(ActivityTask task, Throw
103104
if (failure instanceof ActivityCancelledException) {
104105
throw new CancellationException(failure.getMessage());
105106
}
107+
// Only expected during unit tests.
108+
if (failure instanceof TestActivityTimeoutException) {
109+
TestActivityTimeoutException timeoutException = (TestActivityTimeoutException) failure;
110+
failure =
111+
new TestActivityTimeoutExceptionInternal(
112+
timeoutException.getTimeoutType(),
113+
dataConverter.toData(timeoutException.getDetails()));
114+
}
106115
RespondActivityTaskFailedRequest result = new RespondActivityTaskFailedRequest();
107-
failure = CheckedExceptionWrapper.unwrap((Exception) failure);
116+
failure = CheckedExceptionWrapper.unwrap(failure);
108117
result.setReason(failure.getClass().getName());
109118
result.setDetails(dataConverter.toData(failure));
110119
return new ActivityTaskHandler.Result(null, result, null, null);

src/main/java/com/uber/cadence/internal/sync/SyncDecisionContext.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,18 @@ private RuntimeException mapActivityException(Exception failure) {
180180
} catch (Exception e) {
181181
cause = e;
182182
}
183+
if (cause instanceof TestActivityTimeoutExceptionInternal) {
184+
// This exception is thrown only in unit tests to mock the activity timeouts
185+
TestActivityTimeoutExceptionInternal testTimeout =
186+
(TestActivityTimeoutExceptionInternal) cause;
187+
return new ActivityTimeoutException(
188+
taskFailed.getEventId(),
189+
taskFailed.getActivityType(),
190+
taskFailed.getActivityId(),
191+
testTimeout.getTimeoutType(),
192+
testTimeout.getDetails(),
193+
getDataConverter());
194+
}
183195
return new ActivityFailureException(
184196
taskFailed.getEventId(), taskFailed.getActivityType(), taskFailed.getActivityId(), cause);
185197
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Modifications copyright (C) 2017 Uber Technologies, Inc.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License"). You may not
7+
* use this file except in compliance with the License. A copy of the License is
8+
* located at
9+
*
10+
* http://aws.amazon.com/apache2.0
11+
*
12+
* or in the "license" file accompanying this file. This file is distributed on
13+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
14+
* express or implied. See the License for the specific language governing
15+
* permissions and limitations under the License.
16+
*/
17+
18+
package com.uber.cadence.internal.sync;
19+
20+
import com.uber.cadence.TimeoutType;
21+
22+
/**
23+
* TestActivityTimeoutException is created from an TestActivityTimeoutException. The main difference
24+
* is that details are in a serialized form.
25+
*/
26+
final class TestActivityTimeoutExceptionInternal extends RuntimeException {
27+
28+
private final TimeoutType timeoutType;
29+
30+
private final byte[] details;
31+
32+
TestActivityTimeoutExceptionInternal(TimeoutType timeoutType, byte[] details) {
33+
this.timeoutType = timeoutType;
34+
this.details = details;
35+
}
36+
37+
TestActivityTimeoutExceptionInternal(TimeoutType timeoutType) {
38+
this.timeoutType = timeoutType;
39+
this.details = null;
40+
}
41+
42+
TimeoutType getTimeoutType() {
43+
return timeoutType;
44+
}
45+
46+
byte[] getDetails() {
47+
return details;
48+
}
49+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Modifications copyright (C) 2017 Uber Technologies, Inc.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License"). You may not
7+
* use this file except in compliance with the License. A copy of the License is
8+
* located at
9+
*
10+
* http://aws.amazon.com/apache2.0
11+
*
12+
* or in the "license" file accompanying this file. This file is distributed on
13+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
14+
* express or implied. See the License for the specific language governing
15+
* permissions and limitations under the License.
16+
*/
17+
18+
package com.uber.cadence.testing;
19+
20+
import com.uber.cadence.TimeoutType;
21+
22+
/**
23+
* TestActivityTimeoutException can be thrown from an activity implementation to simulate an
24+
* activity timeout. To be used only in unit tests. The workflow code is going to receive it as
25+
* {@link com.uber.cadence.workflow.ActivityTimeoutException}.
26+
*/
27+
public final class TestActivityTimeoutException extends RuntimeException {
28+
29+
private final TimeoutType timeoutType;
30+
31+
private final Object details;
32+
33+
public TestActivityTimeoutException(TimeoutType timeoutType, Object details) {
34+
this.timeoutType = timeoutType;
35+
this.details = details;
36+
}
37+
38+
public TestActivityTimeoutException(TimeoutType timeoutType) {
39+
this.timeoutType = timeoutType;
40+
this.details = null;
41+
}
42+
43+
public TimeoutType getTimeoutType() {
44+
return timeoutType;
45+
}
46+
47+
public Object getDetails() {
48+
return details;
49+
}
50+
}

src/main/java/com/uber/cadence/workflow/ActivityTimeoutException.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,20 @@
2020
import com.uber.cadence.ActivityType;
2121
import com.uber.cadence.TimeoutType;
2222
import com.uber.cadence.converter.DataConverter;
23+
import java.util.Objects;
2324

2425
/**
25-
* Exception that indicates Activity that activity timed out. If timeout type is {@link
26-
* TimeoutType#HEARTBEAT} then {@link #getDetails(Class)} returns the value passed to the latest
27-
* successful {@link com.uber.cadence.activity.Activity#heartbeat(Object)} call.
26+
* ActivityTimeoutException indicates that an activity has timed out. If the timeout type is a
27+
* {@link TimeoutType#HEARTBEAT} then the {@link #getDetails(Class)} returns a value passed to the
28+
* latest successful {@link com.uber.cadence.activity.Activity#heartbeat(Object)} call.
2829
*/
2930
@SuppressWarnings("serial")
3031
public final class ActivityTimeoutException extends ActivityException {
3132

3233
private final TimeoutType timeoutType;
3334

3435
private final byte[] details;
35-
private final transient DataConverter dataConverter;
36+
private final DataConverter dataConverter;
3637

3738
public ActivityTimeoutException(
3839
long eventId,
@@ -42,9 +43,9 @@ public ActivityTimeoutException(
4243
byte[] details,
4344
DataConverter dataConverter) {
4445
super("TimeoutType=" + String.valueOf(timeoutType), eventId, activityType, activityId);
45-
this.timeoutType = timeoutType;
46+
this.timeoutType = Objects.requireNonNull(timeoutType);
4647
this.details = details;
47-
this.dataConverter = dataConverter;
48+
this.dataConverter = Objects.requireNonNull(dataConverter);
4849
}
4950

5051
public TimeoutType getTimeoutType() {

src/test/java/com/uber/cadence/internal/testing/WorkflowTestingTest.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import com.uber.cadence.client.WorkflowStub;
3737
import com.uber.cadence.client.WorkflowTimedOutException;
3838
import com.uber.cadence.internal.common.WorkflowExecutionUtils;
39+
import com.uber.cadence.testing.TestActivityTimeoutException;
3940
import com.uber.cadence.testing.TestWorkflowEnvironment;
4041
import com.uber.cadence.worker.Worker;
4142
import com.uber.cadence.workflow.ActivityTimeoutException;
@@ -200,6 +201,33 @@ public void testActivityFailure() {
200201
}
201202
}
202203

204+
private static class SimulatedTimeoutActivityImpl implements TestActivity {
205+
206+
@Override
207+
public String activity1(String input) {
208+
throw new TestActivityTimeoutException(TimeoutType.HEARTBEAT, "progress1");
209+
}
210+
}
211+
212+
@Test
213+
public void testActivitySimulatedTimeout() {
214+
Worker worker = testEnvironment.newWorker(TASK_LIST);
215+
worker.registerWorkflowImplementationTypes(ActivityWorkflow.class);
216+
worker.registerActivitiesImplementations(new SimulatedTimeoutActivityImpl());
217+
worker.start();
218+
WorkflowClient client = testEnvironment.newWorkflowClient();
219+
TestWorkflow workflow = client.newWorkflowStub(TestWorkflow.class);
220+
try {
221+
workflow.workflow1("input1");
222+
fail("unreacheable");
223+
} catch (WorkflowException e) {
224+
assertTrue(e.getCause() instanceof ActivityTimeoutException);
225+
ActivityTimeoutException te = (ActivityTimeoutException) e.getCause();
226+
assertEquals(TimeoutType.HEARTBEAT, te.getTimeoutType());
227+
assertEquals("progress1", te.getDetails(String.class));
228+
}
229+
}
230+
203231
public interface TestActivityTimeoutWorkflow {
204232

205233
@WorkflowMethod(executionStartToCloseTimeoutSeconds = 3600 * 24, taskList = TASK_LIST)

0 commit comments

Comments
 (0)