Skip to content

Commit d69b39f

Browse files
Add support to the time skipping test server
1 parent 7b2a0c7 commit d69b39f

7 files changed

Lines changed: 363 additions & 31 deletions

File tree

temporal-sdk/src/main/java/io/temporal/internal/common/NexusUtil.java

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,15 @@ public static Failure temporalFailureToNexusFailure(
6767
.setDetails(ByteString.copyFromUtf8(e.getMessage()))
6868
.build();
6969
}
70-
return Failure.newBuilder()
71-
.setMessage(temporalFailure.getMessage())
72-
.setDetails(ByteString.copyFromUtf8(details))
73-
.putAllMetadata(NEXUS_FAILURE_METADATA)
74-
.build();
70+
Failure.Builder failureBuilder =
71+
Failure.newBuilder()
72+
.setMessage(temporalFailure.getMessage())
73+
.setDetails(ByteString.copyFromUtf8(details))
74+
.putAllMetadata(NEXUS_FAILURE_METADATA);
75+
if (!temporalFailure.getStackTrace().isEmpty()) {
76+
failureBuilder.setStackTrace(temporalFailure.getStackTrace());
77+
}
78+
return failureBuilder.build();
7579
}
7680

7781
public static io.temporal.api.failure.v1.Failure nexusFailureToAPIFailure(
@@ -102,9 +106,6 @@ public static io.temporal.api.failure.v1.Failure nexusFailureToAPIFailure(
102106

103107
// Ensure these always get written
104108
apiFailure.setMessage(failureInfo.getMessage());
105-
if (!failureInfo.getStackTrace().isEmpty()) {
106-
apiFailure.setStackTrace(failureInfo.getStackTrace());
107-
}
108109

109110
return apiFailure.build();
110111
}
@@ -165,6 +166,9 @@ public static HandlerError handlerErrorToNexusError(
165166
// TODO: check if this works on old server
166167
if (e.getCause() != null) {
167168
handlerError.setFailure(exceptionToNexusFailure(e.getCause(), dataConverter));
169+
} else if (e.getMessage() != null && !e.getMessage().isEmpty()) {
170+
// Include message even when there's no cause
171+
handlerError.setFailure(Failure.newBuilder().setMessage(e.getMessage()).build());
168172
}
169173
return handlerError.build();
170174
}

temporal-sdk/src/main/java/io/temporal/internal/worker/NexusWorker.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,13 @@ private void handleNexusTask(NexusTask task, Scope metricsScope) {
341341
// Check if the server supports using the Failure directly in responses
342342
boolean supportTemporalFailure =
343343
task.getResponse().getRequest().getCapabilities().getTemporalFailureResponses();
344+
345+
// Allow tests to force old format for backward compatibility testing
346+
String forceOldFormat = System.getProperty("temporal.nexus.forceOldFailureFormat");
347+
if ("true".equalsIgnoreCase(forceOldFormat)) {
348+
supportTemporalFailure = false;
349+
}
350+
344351
sendReply(taskToken, supportTemporalFailure, result, metricsScope);
345352
} catch (Exception e) {
346353
logExceptionDuringResultReporting(e, pollResponse, result);

temporal-sdk/src/test/java/io/temporal/internal/common/NexusUtilTest.java

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
11
package io.temporal.internal.common;
22

3+
import io.nexusrpc.FailureInfo;
4+
import io.nexusrpc.handler.HandlerException;
5+
import io.temporal.api.failure.v1.ApplicationFailureInfo;
6+
import io.temporal.api.failure.v1.CanceledFailureInfo;
7+
import io.temporal.api.failure.v1.Failure;
8+
import io.temporal.api.nexus.v1.HandlerError;
9+
import io.temporal.common.converter.DataConverter;
10+
import io.temporal.common.converter.DefaultDataConverter;
11+
import io.temporal.failure.ApplicationFailure;
312
import org.junit.Assert;
413
import org.junit.Test;
514

615
public class NexusUtilTest {
16+
private static final DataConverter DATA_CONVERTER = DefaultDataConverter.STANDARD_INSTANCE;
17+
718
@Test
819
public void testParseRequestTimeout() {
920
Assert.assertThrows(
@@ -15,4 +26,236 @@ public void testParseRequestTimeout() {
1526
Assert.assertEquals(java.time.Duration.ofMinutes(999), NexusUtil.parseRequestTimeout("999m"));
1627
Assert.assertEquals(java.time.Duration.ofMillis(1300), NexusUtil.parseRequestTimeout("1.3s"));
1728
}
29+
30+
@Test
31+
public void testTemporalFailureToNexusFailureRoundTrip() {
32+
// Create a Temporal failure with details
33+
Failure temporalFailure =
34+
Failure.newBuilder()
35+
.setMessage("test failure")
36+
.setStackTrace("at test.Class.method(Class.java:123)")
37+
.setApplicationFailureInfo(
38+
ApplicationFailureInfo.newBuilder()
39+
.setType("TestFailure")
40+
.setNonRetryable(true)
41+
.build())
42+
.build();
43+
44+
// Convert to Nexus failure
45+
io.temporal.api.nexus.v1.Failure nexusFailure =
46+
NexusUtil.temporalFailureToNexusFailure(temporalFailure);
47+
48+
// Verify message is preserved
49+
Assert.assertEquals("test failure", nexusFailure.getMessage());
50+
51+
// Verify metadata indicates this is a Temporal failure
52+
Assert.assertTrue(nexusFailure.getMetadataMap().containsKey("type"));
53+
Assert.assertEquals(
54+
"temporal.api.failure.v1.Failure", nexusFailure.getMetadataMap().get("type"));
55+
56+
// Convert back via FailureInfo
57+
FailureInfo.Builder failureInfoBuilder =
58+
FailureInfo.newBuilder()
59+
.setMessage(nexusFailure.getMessage())
60+
.setDetailsJson(nexusFailure.getDetails().toStringUtf8())
61+
.setStackTrace(nexusFailure.getStackTrace());
62+
// Add metadata entries individually
63+
for (String key : nexusFailure.getMetadataMap().keySet()) {
64+
failureInfoBuilder.putMetadata(key, nexusFailure.getMetadataMap().get(key));
65+
}
66+
FailureInfo failureInfo = failureInfoBuilder.build();
67+
68+
Failure reconstructed = NexusUtil.nexusFailureToAPIFailure(failureInfo, true);
69+
70+
// Verify round-trip preserves all fields
71+
Assert.assertEquals("test failure", reconstructed.getMessage());
72+
Assert.assertEquals("at test.Class.method(Class.java:123)", reconstructed.getStackTrace());
73+
Assert.assertTrue(reconstructed.hasApplicationFailureInfo());
74+
Assert.assertEquals("TestFailure", reconstructed.getApplicationFailureInfo().getType());
75+
Assert.assertTrue(reconstructed.getApplicationFailureInfo().getNonRetryable());
76+
}
77+
78+
@Test
79+
public void testTemporalFailureToNexusFailureInfoRoundTrip() {
80+
// Create a Temporal failure with nested cause
81+
Failure innerFailure =
82+
Failure.newBuilder()
83+
.setMessage("inner cause")
84+
.setApplicationFailureInfo(
85+
ApplicationFailureInfo.newBuilder().setType("InnerFailure").build())
86+
.build();
87+
88+
Failure outerFailure =
89+
Failure.newBuilder()
90+
.setMessage("outer failure")
91+
.setCause(innerFailure)
92+
.setApplicationFailureInfo(
93+
ApplicationFailureInfo.newBuilder().setType("OuterFailure").build())
94+
.build();
95+
96+
// Convert to FailureInfo and back
97+
FailureInfo failureInfo = NexusUtil.temporalFailureToNexusFailureInfo(outerFailure);
98+
Failure reconstructed = NexusUtil.nexusFailureToAPIFailure(failureInfo, false);
99+
100+
// Verify nested structure is preserved
101+
Assert.assertEquals("outer failure", reconstructed.getMessage());
102+
Assert.assertEquals("OuterFailure", reconstructed.getApplicationFailureInfo().getType());
103+
Assert.assertTrue(reconstructed.hasCause());
104+
Assert.assertEquals("inner cause", reconstructed.getCause().getMessage());
105+
Assert.assertEquals(
106+
"InnerFailure", reconstructed.getCause().getApplicationFailureInfo().getType());
107+
}
108+
109+
@Test
110+
public void testHandlerErrorToNexusErrorWithCause() {
111+
ApplicationFailure cause = ApplicationFailure.newFailure("test error", "TestType", "detail");
112+
HandlerException exception =
113+
new HandlerException(HandlerException.ErrorType.BAD_REQUEST, cause);
114+
115+
HandlerError nexusError = NexusUtil.handlerErrorToNexusError(exception, DATA_CONVERTER);
116+
117+
Assert.assertEquals("BAD_REQUEST", nexusError.getErrorType());
118+
Assert.assertTrue(nexusError.hasFailure());
119+
Assert.assertEquals("test error", nexusError.getFailure().getMessage());
120+
}
121+
122+
@Test
123+
public void testHandlerErrorToNexusErrorWithoutCause() {
124+
HandlerException exception =
125+
new HandlerException(
126+
HandlerException.ErrorType.BAD_REQUEST, "handler message", (Throwable) null);
127+
128+
HandlerError nexusError = NexusUtil.handlerErrorToNexusError(exception, DATA_CONVERTER);
129+
130+
Assert.assertEquals("BAD_REQUEST", nexusError.getErrorType());
131+
Assert.assertTrue(nexusError.hasFailure());
132+
Assert.assertEquals("handler message", nexusError.getFailure().getMessage());
133+
}
134+
135+
@Test
136+
public void testHandlerErrorToNexusErrorWithEmptyMessage() {
137+
HandlerException exception =
138+
new HandlerException(HandlerException.ErrorType.INTERNAL, "", (Throwable) null);
139+
140+
HandlerError nexusError = NexusUtil.handlerErrorToNexusError(exception, DATA_CONVERTER);
141+
142+
Assert.assertEquals("INTERNAL", nexusError.getErrorType());
143+
// Should not have failure when message is empty
144+
Assert.assertFalse(nexusError.hasFailure());
145+
}
146+
147+
@Test
148+
public void testNexusFailureWithStackTracePreservation() {
149+
String stackTrace =
150+
"at io.temporal.test.Method1(Test.java:100)\n"
151+
+ "at io.temporal.test.Method2(Test.java:200)\n"
152+
+ "at io.temporal.test.Method3(Test.java:300)";
153+
154+
Failure failure =
155+
Failure.newBuilder()
156+
.setMessage("failure with stack")
157+
.setStackTrace(stackTrace)
158+
.setApplicationFailureInfo(
159+
ApplicationFailureInfo.newBuilder().setType("TestFailure").build())
160+
.build();
161+
162+
io.temporal.api.nexus.v1.Failure nexusFailure =
163+
NexusUtil.temporalFailureToNexusFailure(failure);
164+
Assert.assertEquals(stackTrace, nexusFailure.getStackTrace());
165+
166+
// Convert back
167+
FailureInfo.Builder failureInfoBuilder =
168+
FailureInfo.newBuilder()
169+
.setMessage(nexusFailure.getMessage())
170+
.setDetailsJson(nexusFailure.getDetails().toStringUtf8())
171+
.setStackTrace(nexusFailure.getStackTrace());
172+
// Add metadata entries individually
173+
for (String key : nexusFailure.getMetadataMap().keySet()) {
174+
failureInfoBuilder.putMetadata(key, nexusFailure.getMetadataMap().get(key));
175+
}
176+
FailureInfo failureInfo = failureInfoBuilder.build();
177+
178+
Failure reconstructed = NexusUtil.nexusFailureToAPIFailure(failureInfo, true);
179+
Assert.assertEquals(stackTrace, reconstructed.getStackTrace());
180+
}
181+
182+
@Test
183+
public void testNexusFailureWithoutTemporalMetadata() {
184+
// Test handling of non-Temporal Nexus failures
185+
FailureInfo failureInfo =
186+
FailureInfo.newBuilder()
187+
.setMessage("generic nexus failure")
188+
.putMetadata("custom-key", "custom-value")
189+
.setDetailsJson("{\"detail\":\"some data\"}")
190+
.build();
191+
192+
Failure apiFailure = NexusUtil.nexusFailureToAPIFailure(failureInfo, true);
193+
194+
// Should be wrapped as NexusFailure type
195+
Assert.assertEquals("generic nexus failure", apiFailure.getMessage());
196+
Assert.assertTrue(apiFailure.hasApplicationFailureInfo());
197+
Assert.assertEquals("NexusFailure", apiFailure.getApplicationFailureInfo().getType());
198+
Assert.assertFalse(apiFailure.getApplicationFailureInfo().getNonRetryable());
199+
}
200+
201+
@Test
202+
public void testDeeplyNestedFailureCauses() {
203+
// Test 4 levels of nesting
204+
Failure level4 =
205+
Failure.newBuilder()
206+
.setMessage("level 4")
207+
.setApplicationFailureInfo(
208+
ApplicationFailureInfo.newBuilder().setType("Level4").build())
209+
.build();
210+
211+
Failure level3 =
212+
Failure.newBuilder()
213+
.setMessage("level 3")
214+
.setCause(level4)
215+
.setApplicationFailureInfo(
216+
ApplicationFailureInfo.newBuilder().setType("Level3").build())
217+
.build();
218+
219+
Failure level2 =
220+
Failure.newBuilder()
221+
.setMessage("level 2")
222+
.setCause(level3)
223+
.setApplicationFailureInfo(
224+
ApplicationFailureInfo.newBuilder().setType("Level2").build())
225+
.build();
226+
227+
Failure level1 =
228+
Failure.newBuilder()
229+
.setMessage("level 1")
230+
.setCause(level2)
231+
.setApplicationFailureInfo(
232+
ApplicationFailureInfo.newBuilder().setType("Level1").build())
233+
.build();
234+
235+
// Convert through Nexus format and back
236+
io.temporal.api.nexus.v1.Failure nexusFailure = NexusUtil.temporalFailureToNexusFailure(level1);
237+
FailureInfo failureInfo = NexusUtil.temporalFailureToNexusFailureInfo(level1);
238+
Failure reconstructed = NexusUtil.nexusFailureToAPIFailure(failureInfo, true);
239+
240+
// Verify all levels are preserved
241+
Assert.assertEquals("level 1", reconstructed.getMessage());
242+
Assert.assertEquals("level 2", reconstructed.getCause().getMessage());
243+
Assert.assertEquals("level 3", reconstructed.getCause().getCause().getMessage());
244+
Assert.assertEquals("level 4", reconstructed.getCause().getCause().getCause().getMessage());
245+
}
246+
247+
@Test
248+
public void testCanceledFailureConversion() {
249+
Failure canceledFailure =
250+
Failure.newBuilder()
251+
.setMessage("operation canceled")
252+
.setCanceledFailureInfo(CanceledFailureInfo.newBuilder().build())
253+
.build();
254+
255+
FailureInfo failureInfo = NexusUtil.temporalFailureToNexusFailureInfo(canceledFailure);
256+
Failure reconstructed = NexusUtil.nexusFailureToAPIFailure(failureInfo, true);
257+
258+
Assert.assertEquals("operation canceled", reconstructed.getMessage());
259+
Assert.assertTrue(reconstructed.hasCanceledFailureInfo());
260+
}
18261
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package io.temporal.workflow.nexus;
2+
3+
import org.junit.AfterClass;
4+
import org.junit.BeforeClass;
5+
import org.junit.runner.RunWith;
6+
import org.junit.runners.Suite;
7+
8+
/**
9+
* Runs the OperationFailMetric suite with the old failure format forced via system property. This
10+
* verifies that the test server correctly handles the old format (UnsuccessfulOperationError and
11+
* HandlerError) even though it advertises support for the new format.
12+
*
13+
* <p>The system property "temporal.nexus.forceOldFailureFormat=true" makes the SDK send old format
14+
* responses regardless of server capabilities.
15+
*/
16+
@RunWith(Suite.class)
17+
@Suite.SuiteClasses({OperationFailMetricTest.class})
18+
public class NexusFailureOldFormatTest {
19+
private static String originalValue;
20+
21+
@BeforeClass
22+
public static void setUpClass() {
23+
// Save original value if it exists
24+
originalValue = System.getProperty("temporal.nexus.forceOldFailureFormat");
25+
// Force old format for all tests in this suite
26+
System.setProperty("temporal.nexus.forceOldFailureFormat", "true");
27+
}
28+
29+
@AfterClass
30+
public static void tearDownClass() {
31+
// Restore original value
32+
if (originalValue != null) {
33+
System.setProperty("temporal.nexus.forceOldFailureFormat", originalValue);
34+
} else {
35+
System.clearProperty("temporal.nexus.forceOldFailureFormat");
36+
}
37+
}
38+
}

0 commit comments

Comments
 (0)