start(
return OperationStartResult.async(id);
}
- @Override
- public String fetchResult(OperationContext context, OperationFetchResultDetails details)
- throws OperationStillRunningException {
- throw new UnsupportedOperationException("Not implemented");
- }
-
- @Override
- public OperationInfo fetchInfo(OperationContext context, OperationFetchInfoDetails details) {
- throw new UnsupportedOperationException("Not implemented");
- }
-
@Override
public void cancel(OperationContext context, OperationCancelDetails details) {
throw new UnsupportedOperationException("Not implemented");
diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/nexus/NexusFailureOldFormatTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/NexusFailureOldFormatTest.java
new file mode 100644
index 0000000000..2ef9a11b7c
--- /dev/null
+++ b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/NexusFailureOldFormatTest.java
@@ -0,0 +1,38 @@
+package io.temporal.workflow.nexus;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+/**
+ * Runs the OperationFailMetric suite with the old failure format forced via system property. This
+ * verifies that the test server correctly handles the old format (UnsuccessfulOperationError and
+ * HandlerError) even though it advertises support for the new format.
+ *
+ * The system property "temporal.nexus.forceOldFailureFormat=true" makes the SDK send old format
+ * responses regardless of server capabilities.
+ */
+@RunWith(Suite.class)
+@Suite.SuiteClasses({OperationFailMetricTest.class})
+public class NexusFailureOldFormatTest {
+ private static String originalValue;
+
+ @BeforeClass
+ public static void setUpClass() {
+ // Save original value if it exists
+ originalValue = System.getProperty("temporal.nexus.forceOldFailureFormat");
+ // Force old format for all tests in this suite
+ System.setProperty("temporal.nexus.forceOldFailureFormat", "true");
+ }
+
+ @AfterClass
+ public static void tearDownClass() {
+ // Restore original value
+ if (originalValue != null) {
+ System.setProperty("temporal.nexus.forceOldFailureFormat", originalValue);
+ } else {
+ System.clearProperty("temporal.nexus.forceOldFailureFormat");
+ }
+ }
+}
diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/nexus/OperationFailMetricTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/OperationFailMetricTest.java
index 1ed8252be9..b5f5dd4375 100644
--- a/temporal-sdk/src/test/java/io/temporal/workflow/nexus/OperationFailMetricTest.java
+++ b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/OperationFailMetricTest.java
@@ -14,7 +14,9 @@
import io.temporal.client.WorkflowFailedException;
import io.temporal.common.reporter.TestStatsReporter;
import io.temporal.failure.ApplicationFailure;
+import io.temporal.failure.CanceledFailure;
import io.temporal.failure.NexusOperationFailure;
+import io.temporal.failure.TemporalFailure;
import io.temporal.serviceclient.MetricsTag;
import io.temporal.testUtils.Eventually;
import io.temporal.testing.internal.SDKTestWorkflowRule;
@@ -31,7 +33,7 @@
import org.junit.Test;
public class OperationFailMetricTest {
- private static Map invocationCount = new ConcurrentHashMap<>();
+ private static final Map invocationCount = new ConcurrentHashMap<>();
private final TestStatsReporter reporter = new TestStatsReporter();
@@ -46,6 +48,11 @@ public class OperationFailMetricTest {
.reportEvery(com.uber.m3.util.Duration.ofMillis(10)))
.build();
+ // Check if we're forcing old format via system property
+ private static boolean isUsingNewFormat() {
+ return !("true".equalsIgnoreCase(System.getProperty("temporal.nexus.forceOldFailureFormat")));
+ }
+
private ImmutableMap.Builder getBaseTags() {
return ImmutableMap.builder()
.putAll(MetricsTag.defaultTags(NAMESPACE))
@@ -84,6 +91,12 @@ public void failOperationMetrics() {
assertNoRetries("fail");
ApplicationFailure applicationFailure =
assertNexusOperationFailure(ApplicationFailure.class, workflowException);
+ if (isUsingNewFormat()) {
+ Assert.assertEquals(
+ "java.lang.RuntimeException: intentional failure",
+ applicationFailure.getOriginalMessage());
+ applicationFailure = (ApplicationFailure) applicationFailure.getCause();
+ }
Assert.assertEquals("intentional failure", applicationFailure.getOriginalMessage());
Map execFailedTags =
@@ -101,6 +114,43 @@ public void failOperationMetrics() {
});
}
+ @Test
+ public void cancelOperationMetrics() {
+ TestWorkflow1 workflowStub =
+ testWorkflowRule.newWorkflowStubTimeoutOptions(TestWorkflow1.class);
+
+ WorkflowFailedException workflowException =
+ Assert.assertThrows(WorkflowFailedException.class, () -> workflowStub.execute("cancel"));
+ assertNoRetries("cancel");
+ CanceledFailure canceledFailure =
+ assertNexusOperationFailure(CanceledFailure.class, workflowException);
+ TemporalFailure temporalFailure = canceledFailure;
+ if (isUsingNewFormat()) {
+ Assert.assertEquals(
+ "java.lang.RuntimeException: intentional cancel", temporalFailure.getOriginalMessage());
+ Assert.assertNotNull(canceledFailure.getCause());
+ Assert.assertTrue(canceledFailure.getCause() instanceof ApplicationFailure);
+ temporalFailure = (TemporalFailure) temporalFailure.getCause();
+ }
+ Assert.assertEquals("intentional cancel", temporalFailure.getOriginalMessage());
+
+ Map execFailedTags =
+ getOperationTags()
+ .put(MetricsTag.TASK_FAILURE_TYPE, "operation_canceled")
+ .buildKeepingLast();
+ Eventually.assertEventually(
+ Duration.ofSeconds(3),
+ () -> {
+ reporter.assertTimer(
+ MetricsType.NEXUS_SCHEDULE_TO_START_LATENCY, getBaseTags().buildKeepingLast());
+ reporter.assertTimer(
+ MetricsType.NEXUS_EXEC_LATENCY, getOperationTags().buildKeepingLast());
+ reporter.assertTimer(
+ MetricsType.NEXUS_TASK_E2E_LATENCY, getOperationTags().buildKeepingLast());
+ reporter.assertCounter(MetricsType.NEXUS_EXEC_FAILED_COUNTER, execFailedTags, 1);
+ });
+ }
+
@Test
public void failOperationApplicationErrorMetrics() {
TestWorkflow1 workflowStub =
@@ -111,6 +161,93 @@ public void failOperationApplicationErrorMetrics() {
assertNoRetries("fail-app");
ApplicationFailure applicationFailure =
assertNexusOperationFailure(ApplicationFailure.class, workflowException);
+ if (isUsingNewFormat()) {
+ Assert.assertEquals(
+ "io.temporal.failure.ApplicationFailure: message='intentional failure', type='TestFailure', nonRetryable=false",
+ applicationFailure.getOriginalMessage());
+ Assert.assertEquals("OperationError", applicationFailure.getType());
+ Assert.assertNotNull(applicationFailure.getCause());
+ applicationFailure = (ApplicationFailure) applicationFailure.getCause();
+ }
+ Assert.assertEquals("intentional failure", applicationFailure.getOriginalMessage());
+ Assert.assertEquals("TestFailure", applicationFailure.getType());
+ Assert.assertEquals("foo", applicationFailure.getDetails().get(String.class));
+
+ Map execFailedTags =
+ getOperationTags().put(MetricsTag.TASK_FAILURE_TYPE, "operation_failed").buildKeepingLast();
+ Eventually.assertEventually(
+ Duration.ofSeconds(3),
+ () -> {
+ reporter.assertTimer(
+ MetricsType.NEXUS_SCHEDULE_TO_START_LATENCY, getBaseTags().buildKeepingLast());
+ reporter.assertTimer(
+ MetricsType.NEXUS_EXEC_LATENCY, getOperationTags().buildKeepingLast());
+ reporter.assertTimer(
+ MetricsType.NEXUS_TASK_E2E_LATENCY, getOperationTags().buildKeepingLast());
+ reporter.assertCounter(MetricsType.NEXUS_EXEC_FAILED_COUNTER, execFailedTags, 1);
+ });
+ }
+
+ @Test
+ public void cancelOperationApplicationErrorMetrics() {
+ TestWorkflow1 workflowStub =
+ testWorkflowRule.newWorkflowStubTimeoutOptions(TestWorkflow1.class);
+
+ WorkflowFailedException workflowException =
+ Assert.assertThrows(
+ WorkflowFailedException.class, () -> workflowStub.execute("cancel-app"));
+ assertNoRetries("cancel-app");
+ CanceledFailure canceledFailure =
+ assertNexusOperationFailure(CanceledFailure.class, workflowException);
+ if (isUsingNewFormat()) {
+ Assert.assertEquals(
+ "io.temporal.failure.ApplicationFailure: message='intentional cancel', type='TestFailure', nonRetryable=false",
+ canceledFailure.getOriginalMessage());
+ Assert.assertEquals(0, canceledFailure.getDetails().getSize());
+ Assert.assertNotNull(canceledFailure.getCause());
+ Assert.assertTrue(canceledFailure.getCause() instanceof ApplicationFailure);
+ ApplicationFailure applicationFailure = (ApplicationFailure) canceledFailure.getCause();
+ Assert.assertEquals("TestFailure", applicationFailure.getType());
+ Assert.assertEquals("intentional cancel", applicationFailure.getOriginalMessage());
+ Assert.assertEquals("foo", applicationFailure.getDetails().get(String.class));
+ } else {
+ Assert.assertEquals("intentional cancel", canceledFailure.getOriginalMessage());
+ Assert.assertEquals(1, canceledFailure.getDetails().getSize());
+ }
+
+ Map execFailedTags =
+ getOperationTags()
+ .put(MetricsTag.TASK_FAILURE_TYPE, "operation_canceled")
+ .buildKeepingLast();
+ Eventually.assertEventually(
+ Duration.ofSeconds(3),
+ () -> {
+ reporter.assertTimer(
+ MetricsType.NEXUS_SCHEDULE_TO_START_LATENCY, getBaseTags().buildKeepingLast());
+ reporter.assertTimer(
+ MetricsType.NEXUS_EXEC_LATENCY, getOperationTags().buildKeepingLast());
+ reporter.assertTimer(
+ MetricsType.NEXUS_TASK_E2E_LATENCY, getOperationTags().buildKeepingLast());
+ reporter.assertCounter(MetricsType.NEXUS_EXEC_FAILED_COUNTER, execFailedTags, 1);
+ });
+ }
+
+ @Test
+ public void failOperationMessageApplicationErrorMetrics() {
+ TestWorkflow1 workflowStub =
+ testWorkflowRule.newWorkflowStubTimeoutOptions(TestWorkflow1.class);
+
+ WorkflowFailedException workflowException =
+ Assert.assertThrows(
+ WorkflowFailedException.class, () -> workflowStub.execute("fail-msg-app"));
+ assertNoRetries("fail-msg-app");
+ ApplicationFailure applicationFailure =
+ assertNexusOperationFailure(ApplicationFailure.class, workflowException);
+ if (isUsingNewFormat()) {
+ Assert.assertEquals("failure message", applicationFailure.getOriginalMessage());
+ Assert.assertEquals("OperationError", applicationFailure.getType());
+ applicationFailure = (ApplicationFailure) applicationFailure.getCause();
+ }
Assert.assertEquals("intentional failure", applicationFailure.getOriginalMessage());
Assert.assertEquals("TestFailure", applicationFailure.getType());
Assert.assertEquals("foo", applicationFailure.getDetails().get(String.class));
@@ -162,6 +299,39 @@ public void failHandlerBadRequestMetrics() {
});
}
+ @Test
+ public void failHandlerBadRequestNoCauseMetrics() {
+ TestWorkflow1 workflowStub =
+ testWorkflowRule.newWorkflowStubTimeoutOptions(TestWorkflow1.class);
+ WorkflowFailedException workflowException =
+ Assert.assertThrows(
+ WorkflowFailedException.class, () -> workflowStub.execute("handlererror-no-cause"));
+ assertNoRetries("handlererror-no-cause");
+ HandlerException handlerException =
+ assertNexusOperationFailure(HandlerException.class, workflowException);
+ Assert.assertEquals(HandlerException.ErrorType.BAD_REQUEST, handlerException.getErrorType());
+ if (isUsingNewFormat()) {
+ Assert.assertEquals("handler failure message", handlerException.getMessage());
+ Assert.assertNull(handlerException.getCause());
+ }
+
+ Map execFailedTags =
+ getOperationTags()
+ .put(MetricsTag.TASK_FAILURE_TYPE, "handler_error_BAD_REQUEST")
+ .buildKeepingLast();
+ Eventually.assertEventually(
+ Duration.ofSeconds(3),
+ () -> {
+ reporter.assertTimer(
+ MetricsType.NEXUS_SCHEDULE_TO_START_LATENCY, getBaseTags().buildKeepingLast());
+ reporter.assertTimer(
+ MetricsType.NEXUS_EXEC_LATENCY, getOperationTags().buildKeepingLast());
+ reporter.assertTimer(
+ MetricsType.NEXUS_TASK_E2E_LATENCY, getOperationTags().buildKeepingLast());
+ reporter.assertCounter(MetricsType.NEXUS_EXEC_FAILED_COUNTER, execFailedTags, 1);
+ });
+ }
+
@Test
public void failHandlerAppBadRequestMetrics() {
TestWorkflow1 workflowStub =
@@ -196,6 +366,45 @@ public void failHandlerAppBadRequestMetrics() {
});
}
+ @Test
+ public void failHandlerMessageAppBadRequestMetrics() {
+ TestWorkflow1 workflowStub =
+ testWorkflowRule.newWorkflowStubTimeoutOptions(TestWorkflow1.class);
+ WorkflowFailedException workflowException =
+ Assert.assertThrows(
+ WorkflowFailedException.class, () -> workflowStub.execute("handlererror-msg-app"));
+ assertNoRetries("handlererror-msg-app");
+ HandlerException handlerException =
+ assertNexusOperationFailure(HandlerException.class, workflowException);
+ Assert.assertEquals(HandlerException.ErrorType.BAD_REQUEST, handlerException.getErrorType());
+ if (isUsingNewFormat()) {
+ Assert.assertEquals("handler failure message", handlerException.getMessage());
+ } else {
+ Assert.assertEquals("intentional failure", handlerException.getMessage());
+ }
+ Assert.assertTrue(handlerException.getCause() instanceof ApplicationFailure);
+ ApplicationFailure applicationFailure = (ApplicationFailure) handlerException.getCause();
+ Assert.assertEquals("intentional failure", applicationFailure.getOriginalMessage());
+ Assert.assertEquals("TestFailure", applicationFailure.getType());
+ Assert.assertEquals("foo", applicationFailure.getDetails().get(String.class));
+
+ Map execFailedTags =
+ getOperationTags()
+ .put(MetricsTag.TASK_FAILURE_TYPE, "handler_error_BAD_REQUEST")
+ .buildKeepingLast();
+ Eventually.assertEventually(
+ Duration.ofSeconds(3),
+ () -> {
+ reporter.assertTimer(
+ MetricsType.NEXUS_SCHEDULE_TO_START_LATENCY, getBaseTags().buildKeepingLast());
+ reporter.assertTimer(
+ MetricsType.NEXUS_EXEC_LATENCY, getOperationTags().buildKeepingLast());
+ reporter.assertTimer(
+ MetricsType.NEXUS_TASK_E2E_LATENCY, getOperationTags().buildKeepingLast());
+ reporter.assertCounter(MetricsType.NEXUS_EXEC_FAILED_COUNTER, execFailedTags, 1);
+ });
+ }
+
@Test
public void failHandlerAlreadyStartedMetrics() {
TestWorkflow1 workflowStub =
@@ -267,10 +476,18 @@ public void failHandlerNonRetryableApplicationFailureMetrics() {
assertNexusOperationFailure(HandlerException.class, workflowException);
assertNoRetries("non-retryable-application-failure");
- Assert.assertTrue(handlerFailure.getMessage().contains("intentional failure"));
- Assert.assertEquals(HandlerException.ErrorType.INTERNAL, handlerFailure.getErrorType());
- Assert.assertEquals(
- HandlerException.RetryBehavior.NON_RETRYABLE, handlerFailure.getRetryBehavior());
+ Exception failure = handlerFailure;
+ if (isUsingNewFormat()) {
+ Assert.assertEquals(
+ "Handler failed with non-retryable application error", handlerFailure.getMessage());
+ Assert.assertEquals(HandlerException.ErrorType.INTERNAL, handlerFailure.getErrorType());
+ Assert.assertEquals(
+ HandlerException.RetryBehavior.NON_RETRYABLE, handlerFailure.getRetryBehavior());
+ Assert.assertNotNull(failure.getCause());
+ failure = (Exception) failure.getCause();
+ }
+
+ Assert.assertTrue(failure.getMessage().contains("intentional failure"));
Map execFailedTags =
getOperationTags()
@@ -405,17 +622,41 @@ public OperationHandler operation() {
case "fail-app":
throw OperationException.failure(
ApplicationFailure.newFailure("intentional failure", "TestFailure", "foo"));
+ case "fail-msg-app":
+ throw OperationException.failure(
+ "failure message",
+ ApplicationFailure.newFailure("intentional failure", "TestFailure", "foo"));
+ case "cancel":
+ throw OperationException.canceled(new RuntimeException("intentional cancel"));
+ case "cancel-app":
+ throw OperationException.canceled(
+ ApplicationFailure.newFailure("intentional cancel", "TestFailure", "foo"));
+ case "cancel-msg-app":
+ throw OperationException.canceled(
+ "failure message",
+ ApplicationFailure.newFailure("intentional cancel", "TestFailure", "foo"));
case "handlererror":
- throw new HandlerException(HandlerException.ErrorType.BAD_REQUEST, "handlererror");
+ throw new HandlerException(
+ HandlerException.ErrorType.BAD_REQUEST, new RuntimeException("handlererror"));
case "handlererror-app":
throw new HandlerException(
HandlerException.ErrorType.BAD_REQUEST,
ApplicationFailure.newFailure("intentional failure", "TestFailure", "foo"));
+ case "handlererror-msg-app":
+ throw new HandlerException(
+ HandlerException.ErrorType.BAD_REQUEST,
+ "handler failure message",
+ ApplicationFailure.newFailure("intentional failure", "TestFailure", "foo"));
case "handlererror-nonretryable":
throw new HandlerException(
HandlerException.ErrorType.INTERNAL,
ApplicationFailure.newNonRetryableFailure("intentional failure", "TestFailure"),
HandlerException.RetryBehavior.NON_RETRYABLE);
+ case "handlererror-no-cause":
+ throw new HandlerException(
+ HandlerException.ErrorType.BAD_REQUEST,
+ "handler failure message",
+ (Throwable) null);
case "already-started":
throw new WorkflowExecutionAlreadyStarted(
WorkflowExecution.getDefaultInstance(), "TestWorkflowType", null);
diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/nexus/OperationFailureConversionTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/OperationFailureConversionTest.java
index c260cfab21..1f8d4e550f 100644
--- a/temporal-sdk/src/test/java/io/temporal/workflow/nexus/OperationFailureConversionTest.java
+++ b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/OperationFailureConversionTest.java
@@ -41,8 +41,9 @@ public void nexusOperationApplicationFailureNonRetryableFailureConversion() {
NexusOperationFailure nexusFailure = (NexusOperationFailure) exception.getCause();
Assert.assertTrue(nexusFailure.getCause() instanceof HandlerException);
HandlerException handlerException = (HandlerException) nexusFailure.getCause();
- Assert.assertTrue(handlerException.getMessage().contains("failed to call operation"));
Assert.assertEquals(HandlerException.ErrorType.INTERNAL, handlerException.getErrorType());
+ Assert.assertTrue(
+ handlerException.getCause().getMessage().contains("failed to call operation"));
}
@Test
@@ -56,8 +57,8 @@ public void nexusOperationApplicationFailureFailureConversion() {
NexusOperationFailure nexusFailure = (NexusOperationFailure) exception.getCause();
Assert.assertTrue(nexusFailure.getCause() instanceof HandlerException);
HandlerException handlerFailure = (HandlerException) nexusFailure.getCause();
- Assert.assertTrue(handlerFailure.getMessage().contains("exceeded invocation count"));
Assert.assertEquals(HandlerException.ErrorType.INTERNAL, handlerFailure.getErrorType());
+ Assert.assertTrue(handlerFailure.getCause().getMessage().contains("exceeded invocation count"));
}
@Test
diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/nexus/SyncOperationCancelledTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/SyncOperationCancelledTest.java
index 4a0af9d26c..a6bb29daaa 100644
--- a/temporal-sdk/src/test/java/io/temporal/workflow/nexus/SyncOperationCancelledTest.java
+++ b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/SyncOperationCancelledTest.java
@@ -88,8 +88,7 @@ public OperationHandler operation() {
return OperationHandler.sync(
(ctx, details, input) -> {
if (input.equals("cancel-in-handler")) {
- throw OperationException.canceled(
- new RuntimeException("operation canceled in handler"));
+ throw OperationException.canceled("operation canceled in handler");
}
throw new RuntimeException("failed to call operation");
});
diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/nexus/SyncOperationFailTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/SyncOperationFailTest.java
index a96c157259..3f27de61da 100644
--- a/temporal-sdk/src/test/java/io/temporal/workflow/nexus/SyncOperationFailTest.java
+++ b/temporal-sdk/src/test/java/io/temporal/workflow/nexus/SyncOperationFailTest.java
@@ -133,7 +133,7 @@ public OperationHandler operation() {
// Implemented inline
return OperationHandler.sync(
(ctx, details, name) -> {
- throw OperationException.failure(new RuntimeException("failed to call operation"));
+ throw OperationException.failure("failed to call operation");
});
}
}
diff --git a/temporal-sdk/src/test/java/io/temporal/workflow/updateTest/UpdateWithStartTest.java b/temporal-sdk/src/test/java/io/temporal/workflow/updateTest/UpdateWithStartTest.java
index fa2b0ca40b..b1c62931c0 100644
--- a/temporal-sdk/src/test/java/io/temporal/workflow/updateTest/UpdateWithStartTest.java
+++ b/temporal-sdk/src/test/java/io/temporal/workflow/updateTest/UpdateWithStartTest.java
@@ -646,6 +646,7 @@ public void failWhenUpdateNamesDoNotMatch() {
}
@Test
+ @SuppressWarnings("deprecation")
public void failServerSideWhenStartIsInvalid() {
WorkflowClient workflowClient = testWorkflowRule.getWorkflowClient();
diff --git a/temporal-serviceclient/src/main/proto b/temporal-serviceclient/src/main/proto
index 1ae5b673d6..188e309dee 160000
--- a/temporal-serviceclient/src/main/proto
+++ b/temporal-serviceclient/src/main/proto
@@ -1 +1 @@
-Subproject commit 1ae5b673d66b0a94f6131c3eb06bc7173ae2c326
+Subproject commit 188e309dee0acb3e3c84363d2d9f11be32df3bb8
diff --git a/temporal-test-server/src/main/java/io/temporal/internal/testservice/StateMachines.java b/temporal-test-server/src/main/java/io/temporal/internal/testservice/StateMachines.java
index ae20d22858..a8e42a9d3c 100644
--- a/temporal-test-server/src/main/java/io/temporal/internal/testservice/StateMachines.java
+++ b/temporal-test-server/src/main/java/io/temporal/internal/testservice/StateMachines.java
@@ -703,6 +703,9 @@ private static void scheduleNexusOperation(
.setRequest(
io.temporal.api.nexus.v1.Request.newBuilder()
.setScheduledTime(ctx.currentTime())
+ .setCapabilities(
+ io.temporal.api.nexus.v1.Request.Capabilities.newBuilder()
+ .setTemporalFailureResponses(true))
.putAllHeader(attr.getNexusHeaderMap())
.putHeader(
io.nexusrpc.Header.OPERATION_TIMEOUT.toLowerCase(),
diff --git a/temporal-test-server/src/main/java/io/temporal/internal/testservice/TestWorkflowService.java b/temporal-test-server/src/main/java/io/temporal/internal/testservice/TestWorkflowService.java
index 469fde42df..2c043c83ec 100644
--- a/temporal-test-server/src/main/java/io/temporal/internal/testservice/TestWorkflowService.java
+++ b/temporal-test-server/src/main/java/io/temporal/internal/testservice/TestWorkflowService.java
@@ -69,7 +69,7 @@
public final class TestWorkflowService extends WorkflowServiceGrpc.WorkflowServiceImplBase
implements Closeable {
private static final Logger log = LoggerFactory.getLogger(TestWorkflowService.class);
- private static final JsonFormat.Parser JSON_PARSER = JsonFormat.parser();
+ private static final JsonFormat.Parser JSON_PARSER = JsonFormat.parser().ignoringUnknownFields();
private static final String FAILURE_TYPE_STRING = Failure.getDescriptor().getFullName();
@@ -310,6 +310,7 @@ public void startWorkflowExecution(
}
}
+ @SuppressWarnings("deprecation")
StartWorkflowExecutionResponse startWorkflowExecutionImpl(
StartWorkflowExecutionRequest startRequest,
Duration backoffStartInterval,
@@ -475,6 +476,7 @@ private StartWorkflowExecutionResponse throwDuplicatedWorkflow(
WorkflowExecutionAlreadyStartedFailure.getDescriptor());
}
+ @SuppressWarnings("deprecation")
private void validateWorkflowIdReusePolicy(
WorkflowIdReusePolicy reusePolicy, WorkflowIdConflictPolicy conflictPolicy) {
if (conflictPolicy != WorkflowIdConflictPolicy.WORKFLOW_ID_CONFLICT_POLICY_UNSPECIFIED
@@ -988,7 +990,17 @@ public void respondNexusTaskCompleted(
mutableState.cancelNexusOperationRequestAcknowledge(tt.getOperationRef());
} else if (request.getResponse().hasStartOperation()) {
StartOperationResponse startResp = request.getResponse().getStartOperation();
- if (startResp.hasOperationError()) {
+ if (startResp.hasFailure()) {
+ // New format: Failure directly contains ApplicationFailureInfo or CanceledFailureInfo
+ Failure failure = startResp.getFailure();
+ if (failure.hasCanceledFailureInfo()) {
+ mutableState.cancelNexusOperation(tt.getOperationRef(), failure);
+ } else {
+ mutableState.failNexusOperation(
+ tt.getOperationRef(), wrapNexusOperationFailure(failure));
+ }
+ } else if (startResp.hasOperationError()) {
+ // Old format: UnsuccessfulOperationError with Nexus Failure
UnsuccessfulOperationError opError = startResp.getOperationError();
Failure.Builder b = Failure.newBuilder().setMessage(opError.getFailure().getMessage());
@@ -1013,7 +1025,7 @@ public void respondNexusTaskCompleted(
tt.getOperationRef(), startResp.getSyncSuccess().getPayload());
} else {
throw Status.INVALID_ARGUMENT
- .withDescription("Expected success or OperationError to be set on request.")
+ .withDescription("Expected success, Failure, or OperationError to be set on request.")
.asRuntimeException();
}
} else {
@@ -1028,21 +1040,30 @@ public void respondNexusTaskCompleted(
}
}
+ @SuppressWarnings("deprecation")
@Override
public void respondNexusTaskFailed(
RespondNexusTaskFailedRequest request,
StreamObserver responseObserver) {
try {
- if (!request.hasError()) {
+ Failure failure;
+ if (request.hasFailure()) {
+ // New format: Failure directly contains the handler error with NexusHandlerFailureInfo
+ // Don't wrap with NexusOperationFailureInfo - the state machine will do that if needed
+ failure = request.getFailure();
+ } else if (request.hasError()) {
+ // Old format: HandlerError needs to be converted
+ failure = handlerErrorToFailure(request.getError());
+ } else {
throw Status.INVALID_ARGUMENT
- .withDescription("Nexus handler error not set on RespondNexusTaskFailedRequest")
+ .withDescription("Neither Failure nor Error set on RespondNexusTaskFailedRequest")
.asRuntimeException();
}
+
NexusTaskToken tt = NexusTaskToken.fromBytes(request.getTaskToken());
TestWorkflowMutableState mutableState =
getMutableState(tt.getOperationRef().getExecutionId());
if (mutableState.validateOperationTaskToken(tt)) {
- Failure failure = handlerErrorToFailure(request.getError());
mutableState.failNexusOperation(tt.getOperationRef(), failure);
}
responseObserver.onNext(RespondNexusTaskFailedResponse.getDefaultInstance());
@@ -1115,15 +1136,27 @@ public void completeNexusOperation(
}
private static Failure handlerErrorToFailure(HandlerError err) {
- return Failure.newBuilder()
- .setMessage(err.getFailure().getMessage())
- .setNexusHandlerFailureInfo(
- NexusHandlerFailureInfo.newBuilder()
- .setType(err.getErrorType())
- .setRetryBehavior(err.getRetryBehavior())
- .build())
- .setCause(nexusFailureToAPIFailure(err.getFailure(), false))
- .build();
+ Failure.Builder failureBuilder =
+ Failure.newBuilder()
+ .setMessage(err.getFailure().getMessage())
+ .setNexusHandlerFailureInfo(
+ NexusHandlerFailureInfo.newBuilder()
+ .setType(err.getErrorType())
+ .setRetryBehavior(err.getRetryBehavior())
+ .build());
+ // Only set cause if the failure has meaningful content beyond just a message
+ if (err.hasFailure() && hasFailureContent(err.getFailure())) {
+ failureBuilder.setCause(nexusFailureToAPIFailure(err.getFailure(), false));
+ }
+ return failureBuilder.build();
+ }
+
+ private static boolean hasFailureContent(io.temporal.api.nexus.v1.Failure failure) {
+ // Check if the failure has content beyond just a message
+ return !failure.getDetails().isEmpty()
+ || !failure.getMetadataMap().isEmpty()
+ || !failure.getStackTrace().isEmpty()
+ || failure.hasCause();
}
/**
@@ -1149,7 +1182,11 @@ private static Failure nexusFailureToAPIFailure(
applicationFailureInfo.setNonRetryable(!retryable);
apiFailure.setApplicationFailureInfo(applicationFailureInfo.build());
}
+ // Ensure these always get written
apiFailure.setMessage(failure.getMessage());
+ if (!failure.getStackTrace().isEmpty()) {
+ apiFailure.setStackTrace(failure.getStackTrace());
+ }
return apiFailure.build();
}
diff --git a/temporal-test-server/src/test/java/io/temporal/testserver/functional/NexusWorkflowTest.java b/temporal-test-server/src/test/java/io/temporal/testserver/functional/NexusWorkflowTest.java
index 3553ee71e6..58106ce849 100644
--- a/temporal-test-server/src/test/java/io/temporal/testserver/functional/NexusWorkflowTest.java
+++ b/temporal-test-server/src/test/java/io/temporal/testserver/functional/NexusWorkflowTest.java
@@ -1102,6 +1102,7 @@ private CompletableFuture completeNexusTask(
});
}
+ @SuppressWarnings("deprecation")
private CompletableFuture failNexusTask(
ByteString taskToken, HandlerError err) {
return CompletableFuture.supplyAsync(
diff --git a/temporal-test-server/src/test/java/io/temporal/testserver/functional/WorkflowIdReusePolicyTest.java b/temporal-test-server/src/test/java/io/temporal/testserver/functional/WorkflowIdReusePolicyTest.java
index 989dd81500..b0b697a76f 100644
--- a/temporal-test-server/src/test/java/io/temporal/testserver/functional/WorkflowIdReusePolicyTest.java
+++ b/temporal-test-server/src/test/java/io/temporal/testserver/functional/WorkflowIdReusePolicyTest.java
@@ -82,6 +82,7 @@ public void alreadyRunningWorkflowBlocksSecondEvenWithAllowDuplicate() {
@Test
public void secondWorkflowTerminatesFirst() {
String workflowId = "terminate-if-running-1";
+ @SuppressWarnings("deprecation")
WorkflowOptions options =
WorkflowOptions.newBuilder()
.setWorkflowId(workflowId)